Merge pull request 'Extend Markdown Editor with Toolbar' (#134) from monofox/base:feature/ticket_90 into dev
Reviewed-on: https://codeberg.org/lerntools/base/pulls/134webpack5
commit
14cc9c1e65
@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="markdown-editor-wrapper form-group">
|
||||
<div class="markdown-editor-toolbar">
|
||||
<ul>
|
||||
<li @click="handleButton($event, 'bold')"><format-bold-icon /></li>
|
||||
<li @click="handleButton($event, 'italic')"><format-italic-icon /></li>
|
||||
<li @click="handleButton($event, 'underline')"><format-underline-icon /></li>
|
||||
<li @click="handleButton($event, 'strikethrough')"><format-strike-icon /></li>
|
||||
<li @click="handleButton($event, 'quote')"><format-quote-icon /></li>
|
||||
<li class="divider">|</li>
|
||||
<li @click="handleButton($event, 'h1')"><span>H1</span></li>
|
||||
<li @click="handleButton($event, 'h2')"><span>H2</span></li>
|
||||
<li @click="handleButton($event, 'h3')"><span>H3</span></li>
|
||||
<li @click="handleButton($event, 'h4')"><span>H4</span></li>
|
||||
<li class="divider">|</li>
|
||||
<li @click="handleButton($event, 'list')"><format-list-icon /></li>
|
||||
<li @click="handleButton($event, 'list-numbered')"><format-list-numbered-icon /></li>
|
||||
<li class="divider">|</li>
|
||||
<li @click="handleButton($event, 'insert-link')"><insert-link-icon /></li>
|
||||
<li @click="handleButton($event, 'insert-image')"><insert-image-icon /></li>
|
||||
<li @click="handleButton($event, 'insert-code')"><insert-code-icon /></li>
|
||||
<li class="divider">|</li>
|
||||
<li @click="toggleFullscreen($event)"><fullscreen-icon v-if="!isFullscreen" /><fullscreen-exit-icon v-if="isFullscreen" /></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="markdown-editor-pane">
|
||||
<textarea class="form-control" :value="value" rows="15" @input="handleInput($event)"
|
||||
v-on:change="contentChanged"></textarea>
|
||||
<div class="markdown-preview" v-html="renderedMd"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import md from "Main/js/markdown.js";
|
||||
import MarkdownEditorFunctions from 'Main/js/markdown-editor.js';
|
||||
|
||||
import FormatBoldIcon from 'vue-material-design-icons/FormatBold.vue';
|
||||
import FormatItalicIcon from 'vue-material-design-icons/FormatItalic.vue';
|
||||
import FormatUnderlineIcon from 'vue-material-design-icons/FormatUnderline.vue';
|
||||
import FormatStrikeIcon from 'vue-material-design-icons/FormatStrikethroughVariant.vue';
|
||||
import FormatQuoteIcon from 'vue-material-design-icons/FormatQuoteClose.vue';
|
||||
import FormatListIcon from 'vue-material-design-icons/FormatListBulleted.vue';
|
||||
import FormatListNumberedIcon from 'vue-material-design-icons/FormatListNumbered.vue';
|
||||
import InsertLinkIcon from 'vue-material-design-icons/Link.vue'
|
||||
import InsertImageIcon from 'vue-material-design-icons/ImagePlus.vue'
|
||||
import InsertCodeIcon from 'vue-material-design-icons/CodeTags.vue'
|
||||
import FullscreenIcon from 'vue-material-design-icons/Fullscreen.vue'
|
||||
import FullscreenExitIcon from 'vue-material-design-icons/FullscreenExit.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormatBoldIcon,FormatItalicIcon,FormatUnderlineIcon,
|
||||
FormatListIcon, FormatListNumberedIcon, InsertLinkIcon,
|
||||
InsertImageIcon, MarkdownEditorFunctions, InsertCodeIcon,
|
||||
FormatStrikeIcon, FormatQuoteIcon,
|
||||
FullscreenIcon, FullscreenExitIcon
|
||||
},
|
||||
name: 'markdown-editor',
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
content: this.value,
|
||||
renderedMd: '',
|
||||
mde: undefined,
|
||||
isFullscreen: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: function() {
|
||||
this.updateRender();
|
||||
this.contentChanged();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateRender() {
|
||||
this.renderedMd = md.renderFull(this.value);
|
||||
},
|
||||
handleInput (e) {
|
||||
this.content = e.target.value;
|
||||
this.$emit('input', this.content)
|
||||
},
|
||||
contentChanged() {
|
||||
this.$emit('modified');
|
||||
},
|
||||
toolboxActionDone(newValue) {
|
||||
this.content = newValue;
|
||||
this.$el.querySelector('textarea').focus();
|
||||
this.$emit('input', this.content)
|
||||
},
|
||||
toggleFullscreen(e) {
|
||||
var elem = this.$el.classList;
|
||||
if (this.isFullscreen) {
|
||||
elem.remove('markdown-editor-fullscreen');
|
||||
this.isFullscreen = false;
|
||||
} else {
|
||||
elem.add('markdown-editor-fullscreen');
|
||||
this.isFullscreen = true;
|
||||
}
|
||||
},
|
||||
handleButton(e, btnType) {
|
||||
if (this.mde === undefined) {
|
||||
this.mde = new MarkdownEditorFunctions.toolbox(
|
||||
this.$el.querySelector('textarea'),
|
||||
this.toolboxActionDone
|
||||
);
|
||||
}
|
||||
if (!this.mde) {
|
||||
return;
|
||||
}
|
||||
this.mde.execute(btnType);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateRender();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -0,0 +1,203 @@
|
||||
export default {
|
||||
toolbox: class {
|
||||
constructor(textarea, callback=undefined) {
|
||||
this.textarea = textarea;
|
||||
this.callback = callback;
|
||||
};
|
||||
|
||||
execute(btnType) {
|
||||
switch(btnType) {
|
||||
case 'bold':
|
||||
this.bold();
|
||||
break;
|
||||
case 'italic':
|
||||
this.italic();
|
||||
break;
|
||||
case 'underline':
|
||||
this.underline();
|
||||
break;
|
||||
case 'strikethrough':
|
||||
this.strikethrough();
|
||||
break;
|
||||
case 'quote':
|
||||
this.quote();
|
||||
break;
|
||||
case 'h1':
|
||||
this.headline(1);
|
||||
break;
|
||||
case 'h2':
|
||||
this.headline(2);
|
||||
break;
|
||||
case 'h3':
|
||||
this.headline(3);
|
||||
break;
|
||||
case 'h4':
|
||||
this.headline(4);
|
||||
break;
|
||||
case 'list':
|
||||
this.list();
|
||||
break;
|
||||
case 'list-numbered':
|
||||
this.listNumbered();
|
||||
break;
|
||||
case 'insert-link':
|
||||
this.link();
|
||||
break;
|
||||
case 'insert-image':
|
||||
this.image();
|
||||
break;
|
||||
case 'insert-code':
|
||||
this.code();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
bold() {
|
||||
this._format('**');
|
||||
};
|
||||
italic() {
|
||||
this._format('*');
|
||||
};
|
||||
underline() {
|
||||
this._format('_');
|
||||
};
|
||||
strikethrough() {
|
||||
this._format('~~');
|
||||
};
|
||||
quote() {
|
||||
this._prependLine('> ');
|
||||
};
|
||||
headline(num = 1) {
|
||||
this._prependLine('#'.repeat(num) + ' ');
|
||||
};
|
||||
list() {
|
||||
this._prependLine('- ');
|
||||
}
|
||||
listNumbered() {
|
||||
this._prependLine('1. ');
|
||||
}
|
||||
code() {
|
||||
this._format('`');
|
||||
}
|
||||
link(prepend='') {
|
||||
var startPos = this.textarea.selectionStart;
|
||||
var endPos = this.textarea.selectionEnd;
|
||||
var data = this.textarea.value.substring(startPos, endPos);
|
||||
var url = 'https://';
|
||||
var validUrl = this.isUrl(data);
|
||||
if (validUrl) {
|
||||
url = data.replace('(', '\\(').replace(')', '\\)');
|
||||
} else if (data.length <= 0) {
|
||||
data = 'Link';
|
||||
}
|
||||
data = data.replace('[', '\\[').replace(']', '\\]');
|
||||
var wrapper = prepend + '[' + data + ']' + '(' + url + ')';
|
||||
this.textarea.value =
|
||||
this.textarea.value.substring(0, startPos) +
|
||||
wrapper +
|
||||
this.textarea.value.substring(endPos);
|
||||
|
||||
if (startPos == endPos) {
|
||||
// Position at url input
|
||||
startPos = startPos + prepend.length + 3 + data.length + url.length;
|
||||
endPos = startPos;
|
||||
} else if (validUrl) {
|
||||
// Select data
|
||||
startPos = startPos + prepend.length + 1;
|
||||
endPos = startPos + data.length;
|
||||
} else {
|
||||
// Select URL
|
||||
startPos = startPos + prepend.length + 1 + data.length + 2;
|
||||
endPos = startPos + url.length;
|
||||
}
|
||||
// set cursor
|
||||
this.textarea.focus();
|
||||
this.textarea.selectionStart = startPos;
|
||||
this.textarea.selectionEnd = endPos;
|
||||
|
||||
// callback
|
||||
if (this.callback !== undefined) {
|
||||
this.callback(this.textarea.value);
|
||||
}
|
||||
}
|
||||
isUrl(url) {
|
||||
const reg = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/;
|
||||
return reg.test(url);
|
||||
}
|
||||
image() {
|
||||
this.link('!');
|
||||
}
|
||||
|
||||
_format(wrapper) {
|
||||
var startPos = this.textarea.selectionStart;
|
||||
var endPos = this.textarea.selectionEnd;
|
||||
this.textarea.value =
|
||||
this.textarea.value.substring(0, startPos) +
|
||||
wrapper +
|
||||
this.textarea.value.substring(startPos, endPos) +
|
||||
wrapper +
|
||||
this.textarea.value.substring(endPos);
|
||||
|
||||
// set cursor
|
||||
this.textarea.focus();
|
||||
this.textarea.selectionStart = startPos + wrapper.length;
|
||||
this.textarea.selectionEnd = endPos + wrapper.length;
|
||||
|
||||
// callback
|
||||
if (this.callback !== undefined) {
|
||||
this.callback(this.textarea.value);
|
||||
}
|
||||
};
|
||||
|
||||
_prependLine(wrapper) {
|
||||
var startPos = this.textarea.selectionStart;
|
||||
var endPos = this.textarea.selectionEnd;
|
||||
var positionNewLine = this.textarea.value.substring(0, startPos).lastIndexOf('\n');
|
||||
var toAddStart = 0;
|
||||
var toAddEnd = 0;
|
||||
var selectionContent = this.textarea.value.substring(
|
||||
startPos, endPos
|
||||
);
|
||||
var dataAfter = this.textarea.value.substring(endPos);
|
||||
var prependData = '';
|
||||
var appendData = '';
|
||||
|
||||
if (dataAfter.length > 0) {
|
||||
dataAfter = '\n\n';
|
||||
} else {
|
||||
dataAfter = '\n';
|
||||
}
|
||||
|
||||
if (positionNewLine < 0) {
|
||||
// check if there is any content in front.
|
||||
if (this.textarea.value.substring(0, startPos).trim().length > 0) {
|
||||
prependData = prependData += '\n';
|
||||
}
|
||||
}
|
||||
if (positionNewLine >= 0) {
|
||||
// check if there is any content in front.
|
||||
if (this.textarea.value.substring(positionNewLine, startPos).trim().length > 0) {
|
||||
prependData = prependData += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
this.textarea.value =
|
||||
this.textarea.value.substring(0, startPos) +
|
||||
prependData + wrapper + selectionContent +
|
||||
dataAfter +
|
||||
this.textarea.value.substring(endPos);
|
||||
toAddStart = toAddStart + prependData.length +
|
||||
wrapper.length;
|
||||
toAddEnd = toAddStart + appendData.length;
|
||||
|
||||
this.textarea.focus();
|
||||
this.textarea.selectionStart = startPos + toAddStart;
|
||||
this.textarea.selectionEnd = endPos + toAddEnd;
|
||||
|
||||
// callback
|
||||
if (this.callback !== undefined) {
|
||||
this.callback(this.textarea.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue