TransFlow/node_modules/mavon-editor/src/mavon-editor.vue

733 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div :class="[{ 'fullscreen': s_fullScreen, 'shadow': boxShadow }]" class="v-note-wrapper markdown-body" :style="{'box-shadow': boxShadow ? boxShadowStyle : ''}">
<!--工具栏-->
<div class="v-note-op" v-show="toolbarsFlag" :style="{'background': toolbarsBackground}">
<v-md-toolbar-left ref="toolbar_left" :editable="editable" :transition="transition" :d_words="d_words"
@toolbar_left_click="toolbar_left_click" @toolbar_left_addlink="toolbar_left_addlink" :toolbars="toolbars"
@imgAdd="$imgAdd" @imgDel="$imgDel" @imgTouch="$imgTouch" :image_filter="imageFilter"
:class="{'transition': transition}">
<slot name="left-toolbar-before" slot="left-toolbar-before" />
<slot name="left-toolbar-after" slot="left-toolbar-after" />
</v-md-toolbar-left>
<v-md-toolbar-right ref="toolbar_right" :d_words="d_words" @toolbar_right_click="toolbar_right_click"
:toolbars="toolbars"
:s_subfield="s_subfield"
:s_preview_switch="s_preview_switch" :s_fullScreen="s_fullScreen"
:s_html_code="s_html_code"
:s_navigation="s_navigation"
:class="{'transition': transition}">
<slot name="right-toolbar-before" slot="right-toolbar-before" />
<slot name="right-toolbar-after" slot="right-toolbar-after" />
</v-md-toolbar-right>
</div>
<!--编辑展示区域-->
<div class="v-note-panel">
<!--编辑区-->
<div ref="vNoteEdit" @scroll="$v_edit_scroll" class="v-note-edit divarea-wrapper"
:class="{'scroll-style': s_scrollStyle, 'scroll-style-border-radius': s_scrollStyle && !s_preview_switch && !s_html_code, 'single-edit': !s_preview_switch && !s_html_code, 'single-show': (!s_subfield && s_preview_switch) || (!s_subfield && s_html_code), 'transition': transition}"
@click="textAreaFocus">
<div class="content-input-wrapper" :style="{'background-color': editorBackground}">
<!-- 双栏 -->
<v-autoTextarea ref="vNoteTextarea" :placeholder="placeholder ? placeholder : d_words.start_editor"
class="content-input" :fontSize="fontSize"
lineHeight="1.5" v-model="d_value" fullHeight
:style="{'background-color': editorBackground}"></v-autoTextarea>
</div>
</div>
<!--展示区-->
<div :class="{'single-show': (!s_subfield && s_preview_switch) || (!s_subfield && s_html_code)}"
v-show="s_preview_switch || s_html_code" class="v-note-show">
<div ref="vShowContent" v-html="d_render" v-show="!s_html_code"
:class="{'scroll-style': s_scrollStyle, 'scroll-style-border-radius': s_scrollStyle}" class="v-show-content"
:style="{'background-color': previewBackground}">
</div>
<div v-show="s_html_code" :class="{'scroll-style': s_scrollStyle, 'scroll-style-border-radius': s_scrollStyle}" class="v-show-content-html"
:style="{'background-color': previewBackground}">
{{d_render}}
</div>
</div>
<!--标题导航-->
<transition name="slideTop">
<div v-show="s_navigation" class="v-note-navigation-wrapper" :class="{'transition': transition}">
<div class="v-note-navigation-title">
{{d_words.navigation_title}}<i @click="toolbar_right_click('navigation')"
class="fa fa-mavon-times v-note-navigation-close"
aria-hidden="true"></i>
</div>
<div ref="navigationContent" class="v-note-navigation-content" :class="{'scroll-style': s_scrollStyle}">
</div>
</div>
</transition>
</div>
<!--帮助文档-->
<transition name="fade">
<div ref="help">
<div @click="toolbar_right_click('help')" class="v-note-help-wrapper" v-if="s_help">
<div class="v-note-help-content markdown-body" :class="{'shadow': boxShadow}">
<i @click.stop.prevent="toolbar_right_click('help')" class="fa fa-mavon-times"
aria-hidden="true"></i>
<div class="scroll-style v-note-help-show" v-html="d_help"></div>
</div>
</div>
</div>
</transition>
<!-- 预览图片 -->
<transition name="fade">
<div @click="d_preview_imgsrc=null" class="v-note-img-wrapper" v-if="d_preview_imgsrc">
<img :src="d_preview_imgsrc" alt="none">
</div>
</transition>
<!--阅读模式-->
<div :class="{'show': s_readmodel}" class="v-note-read-model scroll-style" ref="vReadModel">
<div ref="vNoteReadContent" class="v-note-read-content" v-html="d_render">
</div>
</div>
</div>
</template>
<script>
// import tomarkdown from './lib/core/to-markdown.js'
import {autoTextarea} from 'auto-textarea'
import {keydownListen} from './lib/core/keydown-listen.js'
import hljsCss from './lib/core/hljs/lang.hljs.css.js'
import hljsLangs from './lib/core/hljs/lang.hljs.js'
const xss = require('xss');
import {
fullscreenchange,
/* windowResize, */
scrollLink,
insertTextAtCaret,
getNavigation,
insertTab,
unInsertTab,
insertOl,
insertUl,
insertEnter,
removeLine,
loadLink,
loadScript,
ImagePreviewListener
} from './lib/core/extra-function.js'
import {p_ObjectCopy_DEEP, stopEvent} from './lib/util.js'
import {toolbar_left_click, toolbar_left_addlink} from './lib/toolbar_left_click.js'
import {toolbar_right_click} from './lib/toolbar_right_click.js'
import {CONFIG} from './lib/config.js'
import hljs from './lib/core/highlight.js'
import markdown from './lib/mixins/markdown.js'
import md_toolbar_left from './components/md-toolbar-left.vue'
import md_toolbar_right from './components/md-toolbar-right.vue'
import "./lib/font/css/fontello.css"
import './lib/css/md.css'
export default {
mixins: [markdown],
props: {
scrollStyle: { // 是否渲染滚动条样式(webkit)
type: Boolean,
default: true
},
boxShadow: { // 是否添加阴影
type: Boolean,
default: true
},
transition: { // 是否开启动画过渡
type: Boolean,
default: true
},
autofocus: { // 是否自动获取焦点
type: Boolean,
default: true
},
fontSize: { // 字体大小
type: String,
default: '14px'
},
toolbarsBackground: { // 工具栏背景色
type: String,
default: '#ffffff'
},
editorBackground: { // TODO: 编辑栏背景色
type: String,
default: '#ffffff'
},
previewBackground: { // 预览栏背景色
type: String,
default: '#fbfbfb'
},
boxShadowStyle: { // 阴影样式
type: String,
default: '0 2px 12px 0 rgba(0, 0, 0, 0.1)'
},
help: {
type: String,
default: null
},
value: { // 初始 value
type: String,
default: ''
},
language: { // 初始语言
type: String,
default: 'zh-CN'
},
subfield: {
type: Boolean,
default: true
},
navigation: {
type: Boolean,
default: false
},
defaultOpen: {
type: String,
default: null
},
editable: { // 是否开启编辑
type: Boolean,
default: true
},
toolbarsFlag: { // 是否开启工具栏
type: Boolean,
default: true
},
toolbars: { // 工具栏
type: Object,
default() {
return CONFIG.toolbars
}
},
xssOptions: { // 工具栏
type: Object,
default() {
return null
}
},
codeStyle: { // <code></code> 样式
type: String,
default() {
return 'github';
}
},
placeholder: { // 编辑器默认内容
type: String,
default: null
},
ishljs: {
type: Boolean,
default: true
},
externalLink: {
type: [Object, Boolean],
default: true
},
imageFilter: {
type: Function,
default: null
},
imageClick: {
type: Function,
default: null
},
tabSize: {
type: Number,
default: 0
},
shortCut:{
type: Boolean,
default: true
}
},
data() {
return {
s_right_click_menu_show: false,
right_click_menu_top: 0,
right_click_menu_left: 0,
s_subfield: (() => {
return this.subfield;
})(),
s_autofocus: true,
// 标题导航
s_navigation: (() => {
return this.navigation;
})(),
s_scrollStyle: (() => {
return this.scrollStyle
})(),// props 是否渲染滚动条样式
d_value: '',// props 文本内容
d_render: '',// props 文本内容render
s_preview_switch: (() => {
let default_open_ = this.defaultOpen;
if (!default_open_) {
default_open_ = this.subfield ? 'preview' : 'edit';
}
return default_open_ === 'preview' ? true : false;
})(), // props true 展示编辑 false展示预览
s_fullScreen: false,// 全屏编辑标志
s_help: false,// markdown帮助
s_html_code: false,// 分栏情况下查看html
d_help: null,
d_words: null,
edit_scroll_height: -1,
s_readmodel: false,
s_table_enter: false, // 回车事件是否在表格中执行
d_history: (() => {
let temp_array = []
temp_array.push(this.value)
return temp_array;
})(), // 编辑记录
d_history_index: 0, // 编辑记录索引
currentTimeout: '',
d_image_file: [],
d_preview_imgsrc: null, // 图片预览地址
s_external_link: {
markdown_css: function() {
return 'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/2.9.0/github-markdown.min.css';
},
hljs_js: function() {
return 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js';
},
hljs_lang: function(lang) {
return 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/' + lang + '.min.js';
},
hljs_css: function(css) {
if (hljsCss[css]) {
return 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/' + css + '.min.css';
}
return '';
},
katex_js: function() {
return 'https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.8.3/katex.min.js';
},
katex_css: function() {
return 'https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.8.3/katex.min.css';
}
},
p_external_link: {},
textarea_selectionEnd: 0,
textarea_selectionEnds: [0],
};
},
created() {
var $vm = this;
// 初始化语言
this.initLanguage();
this.initExternalFuc();
this.$nextTick(() => {
// 初始化Textarea编辑开关
$vm.editableTextarea();
})
},
mounted() {
var $vm = this;
this.$el.addEventListener('paste', function (e) {
$vm.$paste(e);
})
this.$el.addEventListener('drop', function (e) {
$vm.$drag(e);
})
// 浏览器siz大小
/* windowResize(this); */
keydownListen(this);
// 图片预览事件监听
ImagePreviewListener(this);
// 设置默认焦点
if (this.autofocus) {
this.getTextareaDom().focus();
}
// fullscreen事件
fullscreenchange(this);
this.d_value = this.value;
// 将help添加到末尾
document.body.appendChild(this.$refs.help);
this.loadExternalLink('markdown_css', 'css');
this.loadExternalLink('katex_css', 'css')
this.loadExternalLink('katex_js', 'js', function() {
$vm.iRender(true);
})
this.loadExternalLink('hljs_js', 'js', function() {
$vm.iRender(true);
})
if (!(typeof $vm.externalLink === 'object' && typeof $vm.externalLink['markdown_css'] === 'function')) {
// 没有外部文件要来接管markdown样式可以更改markdown样式。
$vm.codeStyleChange($vm.codeStyle, true)
}
},
beforeDestroy() {
document.body.removeChild(this.$refs.help);
},
getMarkdownIt() {
return this.mixins[0].data().markdownIt
},
methods: {
loadExternalLink(name, type, callback) {
if (typeof this.p_external_link[name] !== 'function') {
if (this.p_external_link[name] != false) {
console.error('external_link.' + name, 'is not a function, if you want to disabled this error log, set external_link.' + name, 'to function or false');
}
return;
}
var _obj = {
'css': loadLink,
'js': loadScript
};
if (_obj.hasOwnProperty(type)) {
_obj[type](this.p_external_link[name](), callback);
}
},
initExternalFuc() {
var $vm = this;
var _external_ = ['markdown_css', 'hljs_js', 'hljs_css', 'hljs_lang', 'katex_js', 'katex_css'];
var _type_ = typeof $vm.externalLink;
var _is_object = (_type_ === 'object');
var _is_boolean = (_type_ === 'boolean');
for (var i = 0; i < _external_.length; i++) {
if ((_is_boolean && !$vm.externalLink) || (_is_object && $vm.externalLink[_external_[i]] === false)) {
$vm.p_external_link[_external_[i]] = false;
} else if (_is_object && typeof $vm.externalLink[_external_[i]] === 'function') {
$vm.p_external_link[_external_[i]] = $vm.externalLink[_external_[i]];
} else {
$vm.p_external_link[_external_[i]] = $vm.s_external_link[_external_[i]];
}
}
},
textAreaFocus() {
this.$refs.vNoteTextarea.$refs.vTextarea.focus();
},
$drag($e) {
var dataTransfer = $e.dataTransfer;
if (dataTransfer) {
var files = dataTransfer.files;
if (files.length > 0) {
$e.preventDefault();
this.$refs.toolbar_left.$imgFilesAdd(files);
}
}
},
$paste($e) {
var clipboardData = $e.clipboardData;
if (clipboardData) {
var items = clipboardData.items;
if (!items) return;
var types = clipboardData.types || [];
var item = null;
for (var i = 0; i < types.length; i++) {
if (types[i] === 'Files') {
item = items[i];
break;
}
}
if (item && item.kind === 'file') {
// prevent filename being pasted parallel along
// with the image pasting process
stopEvent($e)
var oFile = item.getAsFile();
this.$refs.toolbar_left.$imgFilesAdd([oFile]);
}
}
},
$imgTouch(file) {
var $vm = this;
// TODO 跳转到图片位置
},
$imgDel(file) {
this.markdownIt.image_del(file[1]);
// 删除所有markdown中的图片
let fileReg = file[0]
let reg = new RegExp(`\\!\\[${file[1]._name}\\]\\(${fileReg}\\)`, "g")
this.d_value = this.d_value.replace(reg, '');
this.iRender();
this.$emit('imgDel', file);
},
$imgAdd(pos, $file, isinsert) {
if (isinsert === undefined) isinsert = true;
var $vm = this;
if (this.__rFilter == null) {
// this.__rFilter = /^(?:image\/bmp|image\/cis\-cod|image\/gif|image\/ief|image\/jpeg|image\/jpeg|image\/jpeg|image\/pipeg|image\/png|image\/svg\+xml|image\/tiff|image\/x\-cmu\-raster|image\/x\-cmx|image\/x\-icon|image\/x\-portable\-anymap|image\/x\-portable\-bitmap|image\/x\-portable\-graymap|image\/x\-portable\-pixmap|image\/x\-rgb|image\/x\-xbitmap|image\/x\-xpixmap|image\/x\-xwindowdump)$/i;
this.__rFilter = /^image\//i;
}
this.__oFReader = new FileReader();
this.__oFReader.onload = function (oFREvent) {
$vm.markdownIt.image_add(pos, oFREvent.target.result);
$file.miniurl = oFREvent.target.result;
if (isinsert === true) {
// 去除特殊字符
$file._name = $file.name.replace(/[\[\]\(\)\+\{\}&\|\\\*^%$#@\-]/g, '');
$vm.insertText($vm.getTextareaDom(),
{
prefix: '![' + $file._name + '](' + pos + ')',
subfix: '',
str: ''
});
$vm.$nextTick(function () {
$vm.$emit('imgAdd', pos, $file);
})
}
}
if ($file) {
var oFile = $file;
if (this.__rFilter.test(oFile.type)) {
this.__oFReader.readAsDataURL(oFile);
}
}
},
$imgUpdateByUrl(pos, url) {
var $vm = this;
this.markdownIt.image_add(pos, url);
this.$nextTick(function () {
$vm.d_render = this.markdownIt.render(this.d_value);
})
},
$imgAddByUrl(pos, url) {
if (this.$refs.toolbar_left.$imgAddByUrl(pos, url)) {
this.$imgUpdateByUrl(pos, url);
return true;
}
return false;
},
$img2Url(fileIndex, url) {
// x.replace(/(\[[^\[]*?\](?=\())\(\s*(\.\/2)\s*\)/g, "$1(http://path/to/png.png)")
var reg_str = "/(!\\[\[^\\[\]*?\\]\(?=\\(\)\)\\(\\s*\(" + fileIndex + "\)\\s*\\)/g"
var reg = eval(reg_str);
this.d_value = this.d_value.replace(reg, "$1(" + url + ")")
this.$refs.toolbar_left.$changeUrl(fileIndex, url)
this.iRender()
},
$imglst2Url(imglst) {
if (imglst instanceof Array) {
for (var i = 0; i < imglst.length; i++) {
this.$img2Url(imglst[i][0], imglst[i][1]);
}
}
},
toolbar_left_click(_type) {
toolbar_left_click(_type, this);
},
toolbar_left_addlink(_type, text, link) {
toolbar_left_addlink(_type, text, link, this);
},
toolbar_right_click(_type) {
toolbar_right_click(_type, this);
},
getNavigation($vm, full) {
return getNavigation($vm, full);
},
// @event
// 修改数据触发 val val_render
change(val, render) {
this.$emit('change', val, render)
},
// 切换全屏触发 status , val
fullscreen(status, val) {
this.$emit('fullScreen', status, val)
},
// 打开阅读模式触发status , val
readmodel(status, val) {
this.$emit('readModel', status, val)
},
// 切换阅读编辑触发 status , val
previewtoggle(status, val) {
this.$emit('previewToggle', status, val)
},
// 切换分栏触发 status , val
subfieldtoggle(status, val) {
this.$emit('subfieldToggle', status, val)
},
// 切换htmlcode触发 status , val
htmlcode(status, val) {
this.$emit('htmlCode', status, val)
},
// 打开 , 关闭 help触发 status , val
helptoggle(status, val) {
this.$emit('helpToggle', status, val)
},
// 监听ctrl + s
save(val, render) {
this.$emit('save', val, render)
},
// 导航栏切换
navigationtoggle(status, val) {
this.$emit('navigationToggle', status, val)
},
$toolbar_right_read_change_status() {
this.s_readmodel = !this.s_readmodel
if (this.readmodel) {
this.readmodel(this.s_readmodel, this.d_value)
}
if (this.s_readmodel && this.toolbars.navigation) {
this.getNavigation(this, true)
}
},
// ---------------------------------------
// 滚动条联动
$v_edit_scroll($event) {
scrollLink($event, this);
},
// 获取textarea dom节点
getTextareaDom() {
return this.$refs.vNoteTextarea.$refs.vTextarea;
},
// 工具栏插入内容
insertText(obj, {prefix, subfix, str, type}) {
// if (this.s_preview_switch) {
insertTextAtCaret(obj, {prefix, subfix, str, type}, this);
},
insertTab() {
insertTab(this, this.tabSize)
},
insertOl() {
insertOl(this)
},
removeLine() {
removeLine(this)
},
insertUl() {
insertUl(this)
},
unInsertTab() {
unInsertTab(this, this.tabSize)
},
insertEnter(event) {
insertEnter(this, event)
},
saveHistory() {
this.d_history.splice(this.d_history_index + 1, this.d_history.length)
this.d_history.push(this.d_value)
this.textarea_selectionEnds.splice(this.d_history_index + 1, this.textarea_selectionEnds.length)
this.textarea_selectionEnds.push(this.textarea_selectionEnd)
this.d_history_index = this.d_history.length - 1
},
saveSelectionEndsHistory() {
const textarea = this.$refs.vNoteTextarea && this.$refs.vNoteTextarea.$el.querySelector('textarea');
this.textarea_selectionEnd = textarea ? textarea.selectionEnd : this.textarea_selectionEnd;
},
initLanguage() {
let lang = CONFIG.langList.indexOf(this.language) >= 0 ? this.language : 'zh-CN';
var $vm = this;
$vm.$render(CONFIG[`help_${lang}`], function(res) {
$vm.d_help = res;
})
this.d_words = CONFIG[`words_${lang}`];
},
// 编辑开关
editableTextarea() {
let text_dom = this.$refs.vNoteTextarea.$refs.vTextarea;
if (this.editable) {
text_dom.removeAttribute('disabled');
} else {
text_dom.setAttribute('disabled', 'disabled');
}
},
codeStyleChange(val, isInit) {
isInit = isInit ? isInit : false;
if (typeof this.p_external_link.hljs_css !== 'function') {
if (this.p_external_link.hljs_css != false)
{ console.error('external_link.hljs_css is not a function, if you want to disabled this error log, set external_link.hljs_css to function or false'); }
return;
}
var url = this.p_external_link.hljs_css(val);
if (url.length === 0 && isInit) {
console.warn('hljs color scheme', val, 'do not exist, loading default github');
url = this.p_external_link.hljs_css('github')
}
if (url.length > 0) {
loadLink(url)
} else {
console.warn('hljs color scheme', val, 'do not exist, hljs color scheme will not change');
}
},
iRender(toggleChange) {
var $vm = this;
this.$render($vm.d_value, function(res) {
// render
$vm.d_render = res;
// change回调 toggleChange == false 时候触发change回调
if (!toggleChange)
{
if ($vm.change) $vm.change($vm.d_value, $vm.d_render);
}
// 改变标题导航
if ($vm.s_navigation) getNavigation($vm, false);
// v-model 语法糖
$vm.$emit('input', $vm.d_value)
// 塞入编辑记录数组
if ($vm.d_value === $vm.d_history[$vm.d_history_index]) return
window.clearTimeout($vm.currentTimeout)
$vm.currentTimeout = setTimeout(() => {
$vm.saveHistory();
}, 500);
})
},
// 清空上一步 下一步缓存
$emptyHistory() {
this.d_history = [this.d_value] // 编辑记录
this.d_history_index = 0 // 编辑记录索引
}
},
watch: {
d_value: function (val, oldVal) {
this.saveSelectionEndsHistory();
this.iRender();
},
value: function (val, oldVal) {
// Escaping all XSS characters
// escapeHtml (html) {
// return html
// }
if (this.xssOptions) {
val = xss(val, this.xssOptions);
}
if (val !== this.d_value) {
this.d_value = val
}
},
subfield: function (val, oldVal) {
this.s_subfield = val
},
d_history_index() {
if (this.d_history_index > 20) {
this.d_history.shift()
this.d_history_index = this.d_history_index - 1
}
this.d_value = this.d_history[this.d_history_index]
},
language: function (val) {
this.initLanguage();
},
editable: function () {
this.editableTextarea();
},
defaultOpen: function (val) {
let default_open_ = val;
if (!default_open_) {
default_open_ = this.subfield ? 'preview' : 'edit';
}
return this.s_preview_switch = default_open_ === 'preview' ? true : false;
},
codeStyle: function (val) {
this.codeStyleChange(val)
}
},
components: {
'v-autoTextarea': autoTextarea,
'v-md-toolbar-left': md_toolbar_left,
'v-md-toolbar-right': md_toolbar_right
}
};
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import "lib/css/scroll.styl"
@import "lib/css/mavon-editor.styl"
</style>
<style lang="css" scoped>
.auto-textarea-wrapper {
height: 100%;
}
</style>