This repository has been archived on 2023-08-30. You can view files and clone it, but cannot push or open issues/pull-requests.
bytedo
/
wcui
Archived
1
0
Fork 0

优化layer组件tips的定位;重构meditor的插件机制

old
宇天 2018-05-18 22:52:08 +08:00
parent d256f145cf
commit fead12b0d2
7 changed files with 878 additions and 867 deletions

View File

@ -22,6 +22,7 @@ let defconf = {
background: '#fff',
mask: true, // 遮罩
maskClose: false, // 遮罩点击关闭弹窗
maskColor: null, // 遮罩背景色
radius: '0px', // 弹窗圆角半径
area: ['auto', 'auto'],
title: '提示', // 弹窗主标题(在工具栏上的)
@ -64,6 +65,7 @@ const close = function(id) {
}, 200)
} catch (err) {}
}
document.body.style.overflow = ''
}
const repeat = function(str, num) {
@ -208,6 +210,9 @@ class __layer__ {
if (state.mask) {
outerBox.classList.add('mask')
}
if (state.maskColor) {
outerBox.style.background = state.maskColor
}
layBox.classList.add('layer-box')
layBox.classList.add('skin-' + state.skin)
@ -373,40 +378,49 @@ class __layer__ {
style.color = state.color
style.opacity = 1
let $container = Anot(container)
let $doc = Anot(document)
let $arrow = $container[0].querySelector('.arrow')
let cw = $container.innerWidth()
let ch = $container.innerHeight()
let ol = $container.offset().left - document.body.scrollLeft
let ot = $container.offset().top - document.body.scrollTop
let ol = $container.offset().left - $doc.scrollLeft()
let ot = $container.offset().top - $doc.scrollTop()
let layw = parseInt(css.width)
let layh = parseInt(css.height)
let arrowOffset = ['top']
Anot(layerDom[$id][1]).css(style)
$container.bind('mouseenter', ev => {
let tmpStyle = { visibility: 'visible' }
ol = $container.offset().left - $doc.scrollLeft()
ot = $container.offset().top - $doc.scrollTop()
if (ot + 18 < layh) {
arrowOffset[0] = 'bottom'
$arrow.style.borderBottomColor = state.background
style.top = ot + ch + 8
tmpStyle.top = ot + ch + 8
} else {
$arrow.style.borderTopColor = state.background
style.top = ot - layh - 8
tmpStyle.top = ot - layh - 8
}
if (ol + cw * 0.7 + layw > window.innerWidth) {
style.left = ol + cw * 0.3 - layw
tmpStyle.left = ol + cw * 0.3 - layw
arrowOffset[1] = 'left'
} else {
style.left = ol + cw * 0.7
tmpStyle.left = ol + cw * 0.7
}
$arrow.classList.add('offset-' + arrowOffset.join('-'))
Anot(layerDom[$id][1]).css(style)
$container.bind('mouseenter', ev => {
layerDom[$id][1].style.visibility = 'visible'
Anot(layerDom[$id][1]).css(tmpStyle)
})
$container.bind('mouseleave', () => {
setTimeout(() => {
$arrow.classList.remove('offset-' + arrowOffset.join('-'))
arrowOffset = ['top']
$arrow.style.borderBottomColor = ''
$arrow.style.borderTopColor = ''
layerDom[$id][1].style.visibility = 'hidden'
}, 100)
})
@ -436,6 +450,7 @@ class __layer__ {
Anot(layerDom[$id][1]).css(style)
setTimeout(() => {
document.body.style.overflow = 'hidden'
layerDom[$id][1].classList.add('shift')
setTimeout(_ => {
Anot(layerDom[$id][1]).css(offsetStyle)

View File

@ -75,7 +75,7 @@
/* tips类弹层(type 5) */
&.type-5 {visibility:hidden;z-index:65534;min-width:75px;max-width:600px;line-height:1.5;color:#fff;background:rgba(0,0,0,.5);opacity:0;box-shadow:none;-webkit-transition:opacity .3s ease-in-out;-moz-transition:opacity .3s ease-in-out;transition:opacity .3s ease-in-out;
&.type-5 {visibility:hidden;position:fixed;z-index:65534;min-width:75px;max-width:600px;line-height:1.5;color:#fff;background:rgba(0,0,0,.5);opacity:0;box-shadow:none;-webkit-transition:opacity .3s ease-in-out;-moz-transition:opacity .3s ease-in-out;transition:opacity .3s ease-in-out;
// &.active {visibility:visible;opacity:1;}
i.arrow {position:absolute;width:0;height:0;border:6px solid transparent;content: ""}
@ -190,7 +190,6 @@
&.shift {transition: all .5s ease-out;}
.layer-box {position:absolute;}
}
&:active {z-index:65536;}
}

View File

@ -7,110 +7,64 @@
'use strict'
define(['lib/layer/base', 'css!./attach'], function() {
var Uploader = function(url) {
this.url = url || ''
this.init()
}
import 'layer/index'
import './attach.scss'
Uploader.prototype = {
init: function() {
const $doc = Anot(document)
class Uploader {
constructor(url) {
this.url = url
this.xhr = new XMLHttpRequest()
this.form = new FormData()
return this
},
field: function(key, val) {
if (typeof key === 'object') {
for (var i in key) {
this.form.append(i, key[i])
}
} else {
field(key, val) {
this.form.append(key, val)
}
return this
},
start: function() {
var _this = this
}
onProgress(fn) {
this.progress = fn
return this
}
then(cb) {
if (!this.url) {
Anot.error('invalid upload url')
}
let defer = Promise.defer()
this.xhr.open('POST', this.url, true)
var startTime = Date.now()
this.xhr.upload.addEventListener(
'progress',
function(evt) {
if (evt.lengthComputable && _this.progress) {
var res = Math.round(evt.loaded * 100 / evt.total)
_this.progress(res)
evt => {
if (evt.lengthComputable && this.progress) {
let res = Math.round(evt.loaded * 100 / evt.total)
this.progress(res)
}
},
false
)
this.xhr.onreadystatechange = function() {
if (_this.xhr.readyState === 4) {
if (_this.xhr.status >= 200 && _this.xhr.status < 205) {
var res = _this.xhr.responseText
this.xhr.onreadystatechange = () => {
if (this.xhr.readyState === 4) {
if (this.xhr.status >= 200 && this.xhr.status < 205) {
let res = this.xhr.responseText
try {
res = JSON.parse(res)
} catch (err) {}
_this.end && _this.end(res)
defer.resolve(cb(res))
} else {
console.error(_this.xhr)
defer.reject(this.xhr)
}
}
}
this.xhr.send(this.form)
},
onProgress: function(fn) {
this.progress = fn
return this
},
onEnd: function(fn) {
this.end = fn
return this
}
}
function uploadScreenshot(vm, blob) {
var upload = new Uploader(vm.uploadUrl || ME.uploadUrl)
if (ME.beforeUpload) {
ME.beforeUpload(Date.now().toString(16) + '.jpg', function(qn) {
upload
.field('file', blob)
.field('token', qn.token)
.field('key', qn.key)
.onEnd(function(json) {
ME.insert(vm.$editor, '![截图](' + qn.url + ')')
})
.start()
})
} else {
upload
.field('file', blob)
.onEnd(function(json) {
ME.insert(vm.$editor, '![截图](' + json.data.url + ')')
})
.start()
}
}
var $init = function(vm) {
if (!vm.uploadUrl && !ME.uploadUrl) {
console.error(
'使用附件上传,必须先设置uploadUrl;\n可以给vm增加uploadUrl属性,也可以通过ME.uploadUrl设置'
)
}
if (!vm.manageUrl && !ME.manageUrl) {
console.error(
'使用附件管理功能,必须先设置manageUrl;\n可以给vm增加manageUrl属性,也可以通过ME.manageUrl设置'
)
}
if (!ME.maxSize) {
ME.maxSize = 4194304
return defer.promise
}
}
var $init = function(vm) {
vm.$editor.addEventListener('paste', function(ev) {
var txt = ev.clipboardData.getData('text/plain').trim(),
html = ev.clipboardData.getData('text/html').trim()
@ -175,135 +129,193 @@ define(['lib/layer/base', 'css!./attach'], function() {
}
ev.preventDefault()
})
},
lang = {
image: ['远程图片', '图片管理', '图片描述', '图片地址'],
file: ['远程附件', '附件管理', '附件描述', '附件地址']
},
opened = false, //记录是否已经打开
openType = 'image', //打开类型, 图片/附件
cache = {
}
let cache = {
//缓存附件列表
image: [],
file: []
},
fixCont = function() {
return (
'<dl class="do-meditor-attach do-meditor-font">' +
'<dt class="tab-box" :drag="do-layer" data-limit="window">' +
'<span class="item" :class="active:tab === 1" :click="$switch(1)">' +
lang[openType][0] +
'</span>' +
'<span class="item" :class="active:tab === 2" :click="$switch(2)">本地上传</span>' +
'<span class="item" :class="active:tab === 3" :click="$switch(3)">' +
lang[openType][1] +
'</span>' +
'<a href="javascript:;" :click="no" class="action-close do-ui-font"></a>' +
'</dt>' +
'<dt class="cont-box">' +
'<div class="remote" :visible="tab === 1">' +
'<section class="section do-fn-cl input"><span class="label">' +
lang[openType][2] +
'</span>' +
'<input class="txt" :duplex="attachAlt" />' +
'</section>' +
'<section class="section do-fn-cl input"><span class="label">' +
lang[openType][3] +
'</span>' +
'<input class="txt" :duplex="attach" />' +
'</section>' +
'<section class="section do-fn-cl">' +
'<a href="javascript:;" class="submit" :click="$confirm">确定</a>' +
'</section>' +
'</div>' +
'<div class="local" :visible="tab === 2">' +
'<div class="select-file"><input id="meditor-attch" multiple :change="$change" type="file" class="hide" /><span class="file" :click="$select">选择文件</span><span class="tips">(上传大小限制:单文件最大 ' +
((ME.maxSize / 1048576).toFixed(2) - 0) +
'MB)</span></div>' +
'<ul class="upload-box">' +
'<li class="tr thead"><span class="td name">文件名</span><span class="td progress">上传进度</span><span class="td option">操作</span></li>' +
'<li class="tr" :repeat="uploadFile">' +
'<span class="td name" :text="el.name"></span>' +
'<span class="td progress" :html="el.progress"></span>' +
'<span class="td option"><a href="javascript:;" :click="$insert(el)">插入</a></span>' +
'</li>' +
'</ul>' +
'</div>' +
'<ul class="manager" :visible="tab === 3">' +
'<li class="item" :repeat="attachList" :click="$insert(el)">' +
'<span class="thumb" :html="el.thumb"></span>' +
'<p class="name" :attr-title="el.name" :text="el.name"></p>' +
'</li>' +
'</ul>' +
'</dt>' +
'</dl>'
)
}
}
/**
const LANG = {
image: ['远程图片', '图片管理', '图片描述', '图片地址'],
file: ['远程附件', '附件管理', '附件描述', '附件地址']
}
const fixCont = function(vm, tool) {
let limit = false
if (vm.props.uploadSizeLimit) {
limit = (vm.props.uploadSizeLimit / (1024 * 1024)).toFixed(2)
}
return `
<dl class="do-meditor-attach do-meditor__font">
<dt :click="close" class="do-icon-close close"></dt>
<dt class="tab-box" :drag="do-layer" data-limit="window">
<span class="item" :class="active:tab === 1" :click="switchTab(1)">
${LANG[tool][0]}
</span>
<span class="item" :class="active:tab === 2" :click="switchTab(2)">本地上传</span>
<span class="item" :class="active:tab === 3" :click="switchTab(3)">
${LANG[tool][1]}
</span>
</dt>
<dd class="cont-box">
<div class="remote" :visible="tab === 1">
<section class="section do-fn-cl">
<input
class="txt"
:duplex="attachAlt"
placeholder="${LANG[tool][2]}" />
</section>
<section class="section do-fn-cl">
<input
class="txt"
:duplex="attach"
placeholder="${LANG[tool][3]}" />
</section>
<section class="section do-fn-cl">
<a
href="javascript:;"
class="do-meditor__button submit"
:click="$confirm">确定</a>
</section>
</div>
<div class="local" :visible="tab === 2">
<div class="select-file">
<input ref="attach" multiple :change="change" type="file" class="hide" />
<span class="file" :click="select">选择文件</span>
${
limit
? `<span class="tips">(上传大小限制:单文件最大${limit} MB)</span>`
: ''
}
</div>
<ul class="upload-box">
<li class="thead">
<span class="col">文件名</span>
<span class="col">上传进度</span>
<span class="col">操作</span>
</li>
<li class="tbody">
<p :repeat="uploadQueue">
<span
class="col do-fn-ell"
:text="el.name"
:layer-tips="el.name"></span>
<span class="col" :html="el.progress"></span>
<span class="col"><a class="insert" :click="insert(el)">插入</a></span>
</p>
</li>
</ul>
</div>
<ul class="manager" :visible="tab === 3">
<li class="item" :repeat="attachList" :click="$insert(el)">
<span class="thumb" :html="el.thumb"></span>
<p class="name" :attr-title="el.name" :text="el.name"></p>
</li>
</ul>
</dd>
</dl>`
}
/**
* [uploadFile 文件上传]
* @param {[type]} files [文件列表]
* @param {[type]} vm [vm对象]
* @param {[type]} type [image/file]
* @param {[type]} tool [image/file]
*/
function uploadFile(files, vm) {
for (var i = 0, it; (it = files[i++]); ) {
if (
openType === 'image' &&
!/\.(jpg|jpeg|png|gif|bmp|webp|ico)$/.test(it.name)
) {
vm.uploadFile.push({
function uploadFile(vm, tool) {
for (let it of this.files) {
let ext = it.name.slice(it.name.lastIndexOf('.'))
if (tool === 'image' && !/^\.(jpg|jpeg|png|gif|bmp|webp|ico)$/.test(ext)) {
this.uploadQueue.push({
name: it.name,
progress: '<span class="red">0%(失败,不允许的文件类型)</span>',
progress: '<span class="red">0%(文件类型错误)</span>',
url: ''
})
continue
}
if (ME.maxSize > 0 && it.size > ME.maxSize) {
vm.uploadFile.push({
if (vm.props.uploadSizeLimit && it.size > vm.props.uploadSizeLimit) {
this.uploadQueue.push({
name: it.name,
progress: '<span class="red">0%(文件体积过大)</span>',
url: ''
})
continue
}
var fixName =
Date.now().toString(16) + it.name.slice(it.name.lastIndexOf('.'))
let idx = this.uploadQueue.length
let fixName = new Date().format('YmdHis') + ext
let attach = { name: it.name, fixName, progress: '0%', url: '' }
let upload = new Uploader(vm.props.uploadUrl).field('file', it)
var idx = vm.uploadFile.length,
upload = new Uploader(vm.uploadUrl || ME.uploadUrl)
this.uploadQueue.push(attach)
vm.uploadFile.push({ name: it.name, progress: '0%', url: '' })
upload.field('file', it)
if (ME.beforeUpload) {
ME.beforeUpload(fixName, function(qn) {
upload
.field('token', qn.token)
.field('key', qn.key)
.onProgress(function(val) {
vm.uploadFile[idx].progress = val + '%'
if (vm.props.beforeUpload) {
vm.props
.beforeUpload(attach, upload)
.then(next => {
if (!next) {
return Promise.reject('something wrong with beforeUpload')
}
return upload
.onProgress(val => {
this.uploadQueue[idx].progress = val + '%'
})
.onEnd(function(json) {
vm.uploadFile[idx].url = qn.url
.then(res => {
return res.data
})
.start()
})
.then(data => {
this.uploadQueue[idx].url = data.url
})
.catch(err => {
Anot.error(err)
})
} else {
upload
.onProgress(function(val) {
vm.uploadFile[idx].progress = val + '%'
.onProgress(val => {
this.uploadQueue[idx].progress = val + '%'
})
.onEnd(function(json) {
vm.uploadFile[idx].url = json.data.url
.then(res => {
return res.data
})
.then(data => {
this.uploadQueue[idx].url = data.url
})
.catch(err => {
Anot.error(err)
})
.start()
}
}
}
}
function getAttach(vm, cb) {
function uploadScreenshot(vm, blob) {
let name = new Date().format('YmdHis') + '.jpg'
let attach = { name, url: '' }
let upload = new Uploader(vm.props.uploadUrl).field('file', blob)
if (vm.props.beforeUpload) {
vm.props
.beforeUpload(attach, upload)
.then(upload => {
return upload.then(res => {
return res.data
})
})
.then(data => {
vm.insert(`![截图](${data.url})`)
})
} else {
upload
.then(res => {
return res.data
})
.then(data => {
vm.insert(`![截图](${data.url})`)
})
}
}
function getAttach(vm, cb) {
var xhr = new XMLHttpRequest(),
url = vm.manageUrl || ME.manageUrl
@ -332,107 +344,82 @@ define(['lib/layer/base', 'css!./attach'], function() {
}
}
xhr.send()
}
}
function showDialog(elem, vm) {
opened = true
var offset = yua(elem).offset(),
layid = layer.open({
function showDialog(elem, vm, tool) {
let offset = Anot(elem).offset()
layer.open({
type: 7,
menubar: false,
shade: false,
fixed: true,
offset: [offset.top + 37 - ME.doc.scrollTop()],
offset: [offset.top + 37 - $doc.scrollTop()],
tab: 2,
attach: '',
attachAlt: '',
uploadFile: [], //当前上传的列表
uploadQueue: [], //当前上传的列表
attachList: [], //附件管理列表
$switch: function(id) {
var lvm = yua.vmodels[layid]
lvm.tab = id
switchTab: function(id) {
this.tab = id
if (id === 3) {
lvm.attachList.clear()
if (cache[openType].length) {
lvm.attachList = cache[openType]
this.attachList.clear()
if (cache[tool].length) {
this.attachList = cache[tool]
} else {
getAttach(vm, function(json) {
if (json) {
cache[openType] = json.data.list.map(function(it) {
cache[tool] = json.data.list.map(function(it) {
it.thumb =
openType === 'image'
tool === 'image'
? '<img src="' + it.url + '"/>'
: '<em class="attach-icon">&#xe73e;</em>'
return it
})
lvm.attachList = json.data.list
this.attachList = json.data.list
}
})
}
}
},
$select: yua.noop,
$change: yua.noop,
$insert: function(it) {
select() {
let ev = document.createEvent('MouseEvent')
ev.initEvent('click', false, false)
this.$refs.attach.dispatchEvent(ev)
},
change(ev) {
this.files = ev.target.files
uploadFile.call(this, vm, tool)
},
insert: function(it) {
if (!it.url) {
return
}
var val =
(openType === 'image' ? '!' : '') +
'[' +
it.name +
'](' +
it.url +
')'
ME.insert(vm.$editor, val)
let val = `\n${tool === 'image' ? '!' : ''}[${it.name}](${it.url})`
vm.insert(val)
},
$confirm: function() {
var lvm = yua.vmodels[layid]
if (!lvm.attach || !lvm.attachAlt) {
return layer.alert('描述和地址不能为空')
confirm: function() {
if (!this.attach || !this.attachAlt) {
return layer.toast('描述和地址不能为空', 'error')
}
var val = '![' + lvm.attachAlt + '](' + lvm.attach + ')'
let val = `\n${tool === 'image' ? '!' : ''}[${this.attachAlt}](${
this.attach
})`
ME.insert(vm.$editor, val)
lvm.no()
vm.insert(val)
this.close()
},
success: function(id) {
var _this = yua.vmodels[id],
$file = document.body.querySelector('#meditor-attch')
_this.no = function() {
layer.close(id)
opened = false
}
_this.$select = function() {
var ev = document.createEvent('MouseEvent')
ev.initEvent('click', false, false)
$file.dispatchEvent(ev)
}
_this.$change = function() {
uploadFile(this.files, _this)
}
},
content: fixCont()
content: fixCont(vm, tool)
})
}
}
ME.addon.image = function(elem, vm) {
if (opened) {
return
}
openType = 'image'
showDialog(elem, vm)
const addon = {
attach(elem) {
showDialog(elem, this, 'file')
},
image(elem) {
showDialog(elem, this, 'image')
}
}
ME.addon.file = function(elem, vm) {
if (opened) {
return
}
openType = 'file'
showDialog(elem, vm)
}
return $init
})
export default addon

View File

@ -5,50 +5,68 @@
* @date 2017-04-20 19:13:24
*
*/
@import 'var.scss';
.do-meditor-attach {width:630px;height:auto;background:#f7f7f7;cursor:default;
.do-meditor-attach {width:630px;height:300px;cursor:default;color:nth($cgr, 1);
dt.close {position:absolute;z-index:65539;top:-8px;right:-8px;width:40px;height:40px;line-height:40px;font-size:20px;text-align:center;cursor:pointer;
.tab-box {width:100%;height:50px;line-height:49px;border-bottom:1px solid #e2e2e2;text-align:center;
.item {position:relative;float:left;width:100px;height:49px;border-right:1px solid #ddd;cursor:pointer;}
.item.active {background:#fff;}
.item.active::after {position:absolute;left:0;bottom:-1px;width:100%;height:1px;background:#fff;content:""}
a.action-close {top:5px;width:40px;height:40px;line-height:40px;font-size:20px;}
a.action-close:hover {line-height:40px;border:0;}
&:hover {color:nth($ct, 1);font-size:28px;}
}
.cont-box {position:relative;width:100%;height:auto;min-height:200px;background:#fff;
.tab-box {float:left;width:130px;height:300px;padding:10px 5px;text-align:center;background:nth($cp, 2);border-radius:5px;
.item {display:block;width:100%;height:40px;line-height:40px;border-radius:3px;cursor:pointer;
&.active {background:#fff;}
}
}
.cont-box {position:relative;float:right;width:480px;height:auto;min-height:200px;
.remote,
.local {position:relative;width:60%;height:auto;margin:0 auto;padding:15px 0 30px;}
.local {width:96%;}
.local {position:relative;width:92%;height:auto;margin:0 auto;}
.remote {padding:30px 0;}
.hide {display:none;}
.section {display:block;width:100%;height:auto;margin:15px 0;line-height:35px;
&.input {line-height:33px;border:1px solid #e9e9e9;}
.label {float: left;width:30%;text-align:center;background:#f7f7f7;}
.txt {float: left;width:70%;height:33px;padding:0 8px;border:0;border-left:1px solid #e9e9e9;background:#fff;color:#666;}
.submit {float:right;width:30%;height:35px;background:#ddd;color:#666;text-align:center;}
.txt {width:100%;height:45px;padding:0 10px;border:0;border-radius:5px;background:nth($cp, 2);color:nth($cgr, 1);font-size:14px;}
.submit {float:right;width:30%;height:45px;line-height:45px;}
}
.select-file {width:100%;height:35px;line-height:35px;
.file {float:left;width:100px;height:35px;border-radius:3px;background:nth($cp, 1);text-align:center;cursor:pointer;
&:hover {background:nth($cp, 2);}
&:active {background:nth($cp, 3);}
}
.tips {display:inline-block;padding:0 10px;}
}
.upload-box {width:100%;height:auto;min-height:255px;padding-top:10px;
.thead {width:100%;height:35px;line-height:35px;background:nth($cp, 2);}
.thead .col {text-align:center;}
.col {float:left;height:30px;padding:0 5px;}
.col:nth-child(1) {width:50%}
.col:nth-child(2) {width:35%}
.col:nth-child(3) {width:15%}
.tbody {overflow:hidden;overflow-y:auto;width:100%;height:220px;
p {display:block;width:100%;height:30px;line-height:30px;}
}
.insert {display:inline-block;padding:3px 5px;line-height:13px;background:nth($cp, 2);color:nth($cgr, 1);cursor:pointer;}
.red {color:#f30;}
}
.select-file {width:100%;height:35px;line-height:33px}
.select-file span.file {float:left;width:100px;height:35px;border:1px solid #ddd;background:#f7f7f7;color:#666;text-align:center;cursor:pointer;}
.select-file span.tips {display:inline-block;padding:0 10px;line-height:35px;color:#666;}
.upload-box {width:100%;height:auto;min-height:190px;margin:10px 0;border:1px solid #e2e2e2;}
.upload-box .tr {width:100%;height:35px;line-height:35px;text-align:center;}
.upload-box .tr:hover {background:#fafafa}
.upload-box .thead {line-height:34px;border-bottom:1px solid #e2e2e2;background:#f7f7f7;}
.upload-box .td {float:left;}
.upload-box .td.name {width:45%;}
.upload-box .td.progress {width:40%;}
.upload-box .td.option {width:15%;}
.upload-box .td.option a {display:inline-block;padding:3px 5px;line-height:13px;border:1px solid #e2e2e2;background:#f7f7f7;color:#666;}
.upload-box .td .red {color:#f30;}
.manager {overflow:hidden;overflow-y:auto;width:100%;height:320px;padding:10px;}
.manager .item {float:left;width:22%;height:130px;margin:10px 1.5%;padding:5px;}

View File

@ -20,32 +20,33 @@ function trim(str, sign) {
return str.replace(new RegExp('^' + sign + '|' + sign + '$', 'g'), '')
}
const $doc = Anot(document)
const addon = {
h1: function(elem, vm) {
h1: function(elem) {
let that = this
let editor = vm.$refs.editor
let offset = Anot(elem).offset()
let wrap = this.selection(editor, true) || '在此输入文本'
let wrap = this.selection(true) || '在此输入文本'
layer.open({
type: 7,
menubar: false,
maskClose: true,
maskColor: 'rgba(255,255,255,0)',
fixed: true,
insert: function(level) {
wrap = wrap.replace(/^#{1,6} /, '')
wrap = that.repeat('#', level) + ' ' + wrap
that.insert(editor, wrap, true)
wrap = wrap.replace(/^(#{1,6} )?/, '#'.repeat(level) + ' ')
that.insert(wrap, true)
this.close()
},
offset: [
offset.top + 40 - that.doc.scrollTop(),
offset.top + 40 - $doc.scrollTop(),
'auto',
'auto',
offset.left - that.doc.scrollLeft()
offset.left - $doc.scrollLeft()
],
shift: {
top: offset.top - that.doc.scrollTop(),
left: offset.left - that.doc.scrollLeft()
top: offset.top - $doc.scrollTop(),
left: offset.left - $doc.scrollLeft()
},
content: `
<ul class="do-meditor-h1 do-fn-noselect do-meditor__font">
@ -58,60 +59,61 @@ const addon = {
</ul>`
})
},
quote: function(elem, vm) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本'
quote: function(elem) {
let wrap = this.selection() || '在此输入文本'
wrap = '> ' + wrap
this.insert(vm.$refs.editor, wrap, true)
this.insert(wrap, true)
},
bold: function(elem, vm) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本'
bold: function(elem) {
let wrap = this.selection() || '在此输入文本'
let wraped = trim(wrap, '\\*\\*')
wrap = wrap === wraped ? '**' + wrap + '**' : wraped
this.insert(vm.$refs.editor, wrap, true)
this.insert(wrap, true)
},
italic: function(elem, vm) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本'
italic: function(elem) {
let wrap = this.selection() || '在此输入文本'
let wraped = trim(wrap, '_')
wrap = wrap === wraped ? '_' + wrap + '_' : wraped
this.insert(vm.$refs.editor, wrap, true)
this.insert(wrap, true)
},
through: function(elem, vm) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本'
through: function(elem) {
let wrap = this.selection() || '在此输入文本'
let wraped = trim(wrap, '~~')
wrap = wrap === wraped ? '~~' + wrap + '~~' : wraped
this.insert(vm.$refs.editor, wrap, true)
this.insert(wrap, true)
},
unordered: function(elem, vm) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本'
unordered: function(elem) {
let wrap = this.selection() || '在此输入文本'
wrap = '* ' + wrap
this.insert(vm.$refs.editor, wrap, false)
this.insert(wrap, false)
},
ordered: function(elem, vm) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本'
ordered: function(elem) {
let wrap = this.selection() || '在此输入文本'
wrap = '1. ' + wrap
this.insert(vm.$refs.editor, wrap, false)
this.insert(wrap, false)
},
hr: function(elem, vm) {
this.insert(vm.$refs.editor, '\n\n---\n\n', false)
hr: function(elem) {
this.insert('\n\n---\n\n', false)
},
link: function(elem, vm) {
link: function(elem) {
let that = this
let offset = Anot(elem).offset()
let wrap = this.selection(vm.$refs.editor) || ''
let wrap = this.selection() || ''
layer.open({
type: 7,
menubar: false,
maskClose: true,
maskColor: 'rgba(255,255,255,0)',
fixed: true,
link: '',
linkName: wrap,
@ -124,29 +126,29 @@ const addon = {
this.linkTarget === 1 ? ' "target=_blank"' : ''
})`
that.insert(vm.$refs.editor, val, false)
that.insert(val, false)
this.close()
},
offset: [
offset.top + 40 - that.doc.scrollTop(),
offset.top + 40 - $doc.scrollTop(),
'auto',
'auto',
offset.left - that.doc.scrollLeft()
offset.left - $doc.scrollLeft()
],
shift: {
top: offset.top - that.doc.scrollTop(),
left: offset.left - that.doc.scrollLeft()
top: offset.top - $doc.scrollTop(),
left: offset.left - $doc.scrollLeft()
},
content: `
<div class="do-meditor-common do-meditor__font">
<section class="input">
<section>
<input class="txt" :duplex="linkName" placeholder="链接文字"/>
</section>
<section class="input">
<section>
<input class="txt" :duplex="link" placeholder="链接地址"/>
</section>
<section>
<label>
<label class="label">
<input
name="link"
type="radio"
@ -155,7 +157,7 @@ const addon = {
value="1"/>
新窗口打开
</label>
<label>
<label class="label">
<input
name="link"
type="radio"
@ -174,10 +176,10 @@ const addon = {
</div>`
})
},
time: function(elem, vm) {
this.insert(vm.$refs.editor, new Date().format(), false)
time: function(elem) {
this.insert(new Date().format(), false)
},
face: function(elem, vm) {
face: function(elem) {
let that = this
let offset = Anot(elem).offset()
@ -186,6 +188,7 @@ const addon = {
title: '插入表情',
fixed: true,
maskClose: true,
maskColor: 'rgba(255,255,255,0)',
arr: [
'😀',
'😅',
@ -225,14 +228,14 @@ const addon = {
'🙏'
],
offset: [
offset.top + 40 - that.doc.scrollTop(),
offset.top + 40 - $doc.scrollTop(),
'auto',
'auto',
offset.left - that.doc.scrollLeft()
offset.left - $doc.scrollLeft()
],
shift: {
top: offset.top - that.doc.scrollTop(),
left: offset.left - that.doc.scrollLeft()
top: offset.top - $doc.scrollTop(),
left: offset.left - $doc.scrollLeft()
},
content: `
<ul class="do-meditor-face">
@ -241,12 +244,12 @@ const addon = {
</li>
</ul>`,
insert: function(val) {
that.insert(vm.$refs.editor, val, false)
that.insert(val, false)
this.close()
}
})
},
table: function(elem, vm) {
table: function(elem) {
let that = this
let offset = Anot(elem).offset()
@ -255,15 +258,16 @@ const addon = {
title: '0行 x 0列',
fixed: true,
maskClose: true,
maskColor: 'rgba(255,255,255,0)',
offset: [
offset.top + 40 - that.doc.scrollTop(),
offset.top + 40 - $doc.scrollTop(),
'auto',
'auto',
offset.left - that.doc.scrollLeft()
offset.left - $doc.scrollLeft()
],
shift: {
top: offset.top - that.doc.scrollTop(),
left: offset.left - that.doc.scrollLeft()
top: offset.top - $doc.scrollTop(),
left: offset.left - $doc.scrollLeft()
},
matrix: objArr(10).map(function() {
return objArr(10)
@ -313,26 +317,27 @@ const addon = {
let x = ev.target.dataset.x - 0 + 1
let y = ev.target.dataset.y - 0 + 1
let thead = `\n\n${that.repeat('| 表头 ', x)}|\n`
let pipe = `${that.repeat('| -- ', x)}|\n`
let tbody = that.repeat(that.repeat('| ', x) + '|\n', y)
let thead = `\n\n${'| 表头 '.repeat(x)}|\n`
let pipe = `${'| -- '.repeat(x)}|\n`
let tbody = ('| '.repeat(x) + '|\n').repeat(y)
that.insert(vm.$refs.editor, thead + pipe + tbody, false)
that.insert(thead + pipe + tbody, false)
this.close()
}
})
}
})
},
image: function(elem, vm) {
image: function(elem) {
let that = this
let offset = Anot(elem).offset()
let wrap = this.selection(vm.$refs.editor) || ''
let wrap = this.selection() || ''
layer.open({
type: 7,
menubar: false,
maskClose: true,
maskColor: 'rgba(255,255,255,0)',
fixed: true,
img: '',
imgAlt: wrap,
@ -342,25 +347,25 @@ const addon = {
}
let val = `![${this.imgAlt}](${this.img})`
that.insert(vm.$refs.editor, val, false)
that.insert(val, false)
this.close()
},
offset: [
offset.top + 40 - that.doc.scrollTop(),
offset.top + 40 - $doc.scrollTop(),
'auto',
'auto',
offset.left - that.doc.scrollLeft()
offset.left - $doc.scrollLeft()
],
shift: {
top: offset.top - that.doc.scrollTop(),
left: offset.left - that.doc.scrollLeft()
top: offset.top - $doc.scrollTop(),
left: offset.left - $doc.scrollLeft()
},
content: `
<div class="do-meditor-common do-meditor__font">
<section class="input">
<section>
<input class="txt" :duplex="imgAlt" placeholder="图片描述"/>
</section>
<section class="input">
<section>
<input class="txt" :duplex="img" placeholder="图片地址"/>
</section>
<section>
@ -373,17 +378,17 @@ const addon = {
`
})
},
attach: function(elem, vm) {
this.addon.link.call(this, elem, vm, false)
attach: function(elem) {
this.addon.link.call(this, elem)
},
inlinecode: function(elem, vm) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本'
inlinecode: function(elem) {
let wrap = this.selection() || '在此输入文本'
let wraped = trim(wrap, '`')
wrap = wrap === wraped ? '`' + wrap + '`' : wraped
this.insert(vm.$refs.editor, wrap, true)
this.insert(wrap, true)
},
blockcode: function(elem, vm) {
blockcode: function(elem) {
let that = this
layer.open({
type: 7,
@ -426,48 +431,55 @@ const addon = {
],
lang: 'javascript',
code: '',
$confirm: function() {
var lvm = Anot.vmodels[layid]
var val =
'\n```' + lvm.lang + '\n' + (lvm.code || '//在此输入代码') + '\n```\n'
that.insert(vm.$refs.editor, val, false)
layer.close(layid)
maskClose: true,
insert: function() {
let val = `\n\`\`\`${this.lang}\n${this.code ||
'// 在此输入代码'}\n\`\`\`\n`
that.insert(val, false)
this.close()
},
content:
'<div class="do-meditor-codeblock do-meditor__font">' +
'<section class="do-fn-cl"><span class="label">语言类型</span>' +
'<select :duplex="lang">' +
'<option :repeat="$lang" :attr-value="el.id">{{el.name || el.id}}</option>' +
'</select>' +
'</section>' +
'<section>' +
'<textarea :duplex="code" placeholder="在这里输入/粘贴代码"></textarea>' +
'</section>' +
'<section class="do-fn-cl">' +
'<a href="javascript:;" class="submit" :click="$confirm">确定</a>' +
'</section>' +
'</div>'
content: `
<div class="do-meditor-codeblock do-meditor__font">
<section class="do-fn-cl">
<span class="label">语言类型</span>
<select :duplex="lang">
<option :repeat="$lang" :attr-value="el.id">{{el.name || el.id}}</option>
</select>
</section>
<section>
<textarea :duplex="code" placeholder="在这里输入/粘贴代码"></textarea>
</section>
<section class="do-fn-cl">
<a
href="javascript:;"
class="do-meditor__button submit"
:click="insert">确定</a>
</section>
</div>
`
})
},
preview: function(elem, vm) {
vm.preview = !vm.preview
if (vm.preview) {
vm.htmlTxt = vm.$htmlTxt
preview: function() {
this.preview = !this.preview
if (this.preview) {
this.htmlTxt = this.__tmp__
}
},
fullscreen: function(elem, vm) {
vm.fullscreen = !vm.fullscreen
vm.$onFullscreen(vm.fullscreen)
fullscreen: function() {
this.fullscreen = !this.fullscreen
if (typeof this.props.onFullscreen === 'function') {
this.props.onFullscreen(this.fullscreen)
}
},
about: function(elem) {
var offset = Anot(elem).offset()
let offset = Anot(elem).offset()
layer.open({
type: 7,
title: '关于编辑器',
maskClose: true,
offset: [offset.top + 37 - this.doc.scrollTop()],
shift: { top: offset.top - this.doc.scrollTop() },
maskColor: 'rgba(255,255,255,0)',
offset: [offset.top + 37 - $doc.scrollTop()],
shift: { top: offset.top - $doc.scrollTop() },
content:
'<div class="do-meditor-about do-meditor__font">' +
'<pre>' +
@ -477,7 +489,7 @@ const addon = {
'| | | | |__| (_| | | || (_) | |\n' +
'|_| |_|_____\\__,_|_|\\__\\___/|_| ' +
'v' +
this.version +
Anot.ui.meditor +
'</pre>' +
'<p>开源在线Markdown编辑器</p>' +
'<p><a target="_blank" href="https://doui.cc/product/meditor">https://doui.cc/product/meditor</a></p>' +

View File

@ -17,14 +17,21 @@ marked.setOptions({
return Prism.highlight(code, Prism.languages[lang])
}
})
let editorVM = []
if (!String.prototype.repeat) {
String.prototype.repeat = function(num) {
let result = ''
while (num > 0) {
result += this
num--
}
return result
}
}
Anot.ui.meditor = '1.0.0'
const log = console.log
//存放编辑器公共静态资源
window.ME = {
version: Anot.ui.meditor,
// 工具栏title
toolbar: {
// 工具栏title
const TOOLBAR = {
pipe: '',
h1: '标题',
quote: '引用文本',
@ -45,130 +52,36 @@ window.ME = {
preview: '预览',
fullscreen: '全屏',
about: '关于编辑器'
},
addon, // 已有插件
// 往文本框中插入内容
insert: function(dom, val, isSelect) {
if (document.selection) {
dom.focus()
let range = document.selection.createRange()
range.text = val
dom.focus()
range.moveStart('character', -1)
} else if (dom.selectionStart || dom.selectionStart === 0) {
let startPos = dom.selectionStart
let endPos = dom.selectionEnd
let scrollTop = dom.scrollTop
dom.value =
dom.value.slice(0, startPos) +
val +
dom.value.slice(endPos, dom.value.length)
dom.selectionStart = isSelect ? startPos : startPos + val.length
dom.selectionEnd = startPos + val.length
dom.scrollTop = scrollTop
dom.focus()
} else {
dom.value += val
dom.focus()
}
},
/**
* [selection 获取选中的文本]
* @param {[type]} dom [要操作的元素]
* @param {[type]} line [是否强制选取整行]
*/
selection: function(dom, line) {
if (document.selection) {
return document.selection.createRange().text
} else {
let startPos = dom.selectionStart
let endPos = dom.selectionEnd
if (endPos) {
//强制选择整行
if (line) {
startPos = dom.value.slice(0, startPos).lastIndexOf('\n')
let tmpEnd = dom.value.slice(endPos).indexOf('\n')
tmpEnd = tmpEnd < 0 ? 0 : tmpEnd
startPos += 1 //把\n加上
endPos += tmpEnd
dom.selectionStart = startPos
dom.selectionEnd = endPos
}
} else {
//强制选择整行
if (line) {
endPos = dom.value.indexOf('\n')
endPos = endPos < 0 ? dom.value.length : endPos
dom.selectionEnd = endPos
}
}
return dom.value.slice(startPos, endPos)
}
},
repeat: function(str, num) {
if (String.prototype.repeat) {
return str.repeat(num)
} else {
var result = ''
while (num > 0) {
result += str
num--
}
return result
}
},
get: function(id) {
if (id === void 0) {
id = editorVM.length - 1
}
var vm = editorVM[id]
if (vm) {
return {
id: vm.$id,
getVal: function() {
return vm.plainTxt.trim()
},
getHtml: function() {
return vm.$htmlTxt
},
setVal: function(txt) {
vm.plainTxt = txt || ''
},
show: function() {
vm.editorVisible = true
},
hide: function() {
vm.editorVisible = false
}
}
}
return null
},
doc: Anot(document)
}
//获取真实的引用路径,避免因为不同的目录结构导致加载失败的情况
for (var i in Anot.modules) {
if (/meditor/.test(i)) {
ME.path = i.slice(0, i.lastIndexOf('/'))
break
}
}
var elems = {
p: function(str, attr, inner) {
return inner ? '\n' + inner + '\n' : ''
},
br: '\n',
'h([1-6])': function(str, level, attr, inner) {
var h = ME.repeat('#', level)
return '\n' + h + ' ' + inner + '\n'
},
hr: '\n\n___\n\n',
const DEFAULT_TOOLBAR = [
'h1',
'quote',
'|',
'bold',
'italic',
'through',
'|',
'unordered',
'ordered',
'|',
'hr',
'link',
'time',
'face',
'|',
'table',
'image',
'attach',
'inlinecode',
'blockcode',
'|',
'preview',
'fullscreen',
'|',
'about'
]
const ELEMS = {
a: function(str, attr, inner) {
let href = attr.match(attrExp('href'))
let title = attr.match(attrExp('title'))
@ -212,18 +125,27 @@ var elems = {
alt = (alt && alt[1]) || ''
return '![' + alt + '](' + src + ')'
}
},
p: function(str, attr, inner) {
return inner ? '\n' + inner : ''
},
br: '\n',
'h([1-6])': function(str, level, attr, inner) {
let h = '#'.repeat(level)
return '\n' + h + ' ' + inner + '\n'
},
hr: '\n\n___\n\n'
}
function attrExp(field) {
return new RegExp(field + '\\s?=\\s?["\']?([^"\']*)["\']?', 'i')
function attrExp(field, flag = 'i') {
return new RegExp(field + '\\s?=\\s?["\']?([^"\']*)["\']?', flag)
}
function tagExp(tag, open) {
var exp = ''
if (['br', 'hr', 'img'].indexOf(tag) > -1) {
exp = '<' + tag + '([^>]*)\\/?>'
exp = '<' + tag + '([^>]*?)\\/?>'
} else {
exp = '<' + tag + '([^>]*)>([\\s\\S]*?)<\\/' + tag + '>'
exp = '<' + tag + '([^>]*?)>([\\s\\S]*?)<\\/' + tag + '>'
}
return new RegExp(exp, 'gi')
}
@ -233,14 +155,20 @@ function html2md(str) {
} catch (err) {}
str = str.replace(/\t/g, ' ').replace(/<meta [^>]*>/, '')
str = str.replace(
/<(div|span|dl|dd|dt|table|tr|td|thead|tbody|i|em|strong|h[1-6]|ul|ol|li) [^>]*>/g,
str = str
.replace(attrExp('class', 'g'), '')
.replace(attrExp('style', 'g'), '')
str = str
.replace(
/<(div|span|header|footer|nav|dl|dd|dt|table|tr|td|thead|tbody|i|em|b|strong|h[1-6]|ul|ol|li|p|pre) [^>]*>/g,
'<$1>'
)
.replace(/<svg [^>]*>.*?<\/svg>/g, '{invalid image}')
for (var i in elems) {
var cb = elems[i],
exp = tagExp(i)
// log(str)
for (let i in ELEMS) {
let cb = ELEMS[i]
let exp = tagExp(i)
if (i === 'blockquote') {
while (str.match(exp)) {
@ -250,6 +178,11 @@ function html2md(str) {
str = str.replace(exp, cb)
}
// 对另外3种同类标签做一次处理
if (i === 'p') {
exp = tagExp('div')
str = str.replace(exp, cb)
}
if (i === 'em') {
exp = tagExp('i')
str = str.replace(exp, cb)
@ -259,23 +192,23 @@ function html2md(str) {
str = str.replace(exp, cb)
}
}
var liExp = /<(ul|ol)[^>]*>(?:(?!<ul|<ol)[\s\S])*?<\/\1>/gi
let liExp = /<(ul|ol)>(?:(?!<ul|<ol)[\s\S])*?<\/\1>/gi
while (str.match(liExp)) {
str = str.replace(liExp, function(match) {
match = match.replace(/<(ul|ol)[^>]*>([\s\S]*?)<\/\1>/gi, function(
match = match.replace(/<(ul|ol)>([\s\S]*?)<\/\1>/gi, function(
m,
t,
inner
) {
var li = inner.split('</li>')
let li = inner.split('</li>')
li.pop()
for (var i = 0, len = li.length; i < len; i++) {
var pre = t === 'ol' ? i + 1 + '. ' : '* '
for (let i = 0, len = li.length; i < len; i++) {
let pre = t === 'ol' ? i + 1 + '. ' : '* '
li[i] =
pre +
li[i]
.replace(/\s*<li[^>]*>([\s\S]*)/i, function(m, n) {
.replace(/\s*<li>([\s\S]*)/i, function(m, n) {
n = n.trim().replace(/\n/g, '\n ')
return n
})
@ -287,6 +220,7 @@ function html2md(str) {
})
}
str = str
.replace(/<[\/]?[\w]*[^>]*>/g, '')
.replace(/```([\w\W]*)```/g, function(str, inner) {
inner = inner
@ -298,52 +232,50 @@ function html2md(str) {
return str
}
var defaultToolbar = [
'h1',
'quote',
'|',
'bold',
'italic',
'through',
'|',
'unordered',
'ordered',
'|',
'hr',
'link',
'time',
'face',
'|',
'table',
'image',
'attach',
'inlinecode',
'blockcode',
'|',
'preview',
'fullscreen',
'|',
'about'
],
extraAddons = []
function tool(name) {
name = (name + '').trim().toLowerCase()
name = '|' === name ? 'pipe' : name
return (
'<span title="' +
ME.toolbar[name] +
'" class="do-meditor__icon icon-' +
name +
'" ' +
(name !== 'pipe' ? ':click="onToolClick(\'' + name + '\', $event)"' : '') +
'></span>'
)
let event = name === 'pipe' ? '' : `:click="onToolClick('${name}', $event)"`
let title = TOOLBAR[name]
return `
<span title="${title}" class="do-meditor__icon icon-${name}" ${event}></span>`
}
class MEObject {
constructor(vm) {
this.vm = vm
this.id = vm.$id
}
getVal() {
return this.vm.plainTxt.trim()
}
getHtml() {
return this.vm.__tmp__
}
setVal(txt) {
this.vm.plainTxt = txt || ''
}
show() {
this.vm.editorVisible = true
}
hide() {
this.vm.editorVisible = false
}
extends(addon) {
Object.assign(this.vm.addon, addon)
}
}
Anot.component('meditor', {
construct: function(props, state) {
if (props.hasOwnProperty('$show')) {
state.editorVisible = props.$show
delete props.$show
}
},
render: function() {
let toolbar = (this.toolbar || defaultToolbar).map(it => tool(it)).join('')
let toolbar = (this.toolbar || DEFAULT_TOOLBAR).map(it => tool(it)).join('')
delete this.toolbar
@ -361,97 +293,150 @@ Anot.component('meditor', {
:duplex="plainTxt"
:on-paste="onPaste($event)"></textarea>
<content
ref="preview"
class="md-preview do-marked-theme"
:visible="preview"
:html="htmlTxt"></content>
</div>
`
},
construct: function(props, state) {
// Anot.mix(base, opt, attr)
// if (base.$addons && Array.isArray(base.$addons)) {
// extraAddons = base.$addons.map(function(name) {
// return ME.path + '/addon/' + name
// })
// delete base.$addons
// }
if (props.hasOwnProperty('$show')) {
state.editorVisible = props.$show
delete props.$show
}
},
componentWillMount: function(vm) {},
componentDidMount: function(vm, elem) {
console.log(this)
// vm.$editor = elem.children[1]
let $editor = Anot(this.$refs.editor)
let preview = this.$refs.preview
$editor.bind('keydown', ev => {
let wrap = this.selection() || ''
let select = !!wrap
//tab键改为插入2个空格,阻止默认事件,防止焦点失去
if (ev.keyCode === 9) {
ev.preventDefault()
wrap = wrap
.split('\n')
.map(function(it) {
return ev.shiftKey ? it.replace(/^\s\s/, '') : ' ' + it
})
.join('\n')
this.insert(wrap, select)
}
//修复按退格键删除选中文本时,选中的状态不更新的bug
if (ev.keyCode === 8) {
if (select) {
ev.preventDefault()
this.insert('', select)
}
}
})
// editorVM.push(vm)
// //自动加载额外的插件
// require(extraAddons, function() {
// var args = Array.prototype.slice.call(arguments, 0)
// args.forEach(function(addon) {
// addon && addon(vm)
// })
// })
$editor.bind('scroll', ev => {
let syncTop =
ev.target.scrollTop / ev.target.scrollHeight * preview.scrollHeight
// Anot(vm.$editor).bind('keydown', function(ev) {
// var wrap = ME.selection(vm.$editor) || '',
// select = !!wrap
// //tab键改为插入2个空格,阻止默认事件,防止焦点失去
// if (ev.keyCode === 9) {
// wrap = wrap
// .split('\n')
// .map(function(it) {
// return ev.shiftKey ? it.replace(/^\s\s/, '') : ' ' + it
// })
// .join('\n')
// ME.insert(this, wrap, select)
// ev.preventDefault()
// }
// //修复按退格键删除选中文本时,选中的状态不更新的bug
// if (ev.keyCode === 8) {
// if (select) {
// ME.insert(this, '', select)
// ev.preventDefault()
// }
// }
// })
// //编辑器成功加载的回调
// vm.$onSuccess(ME.get(), vm)
preview.scrollTop = syncTop
})
//编辑器成功加载的回调
if (typeof this.props.onCreated === 'function') {
this.props.onCreated(new MEObject(this))
}
},
watch: {
plainTxt: function(val) {
this.compile()
//只有开启实时预览,才会赋值给htmlTxt
if (this.preview) {
this.htmlTxt = this.$htmlTxt
this.htmlTxt = this.__tmp__
}
if (typeof this.props.onUpdate === 'function') {
this.props.onUpdate(this.plainTxt, this.$htmlTxt)
this.props.onUpdate(this.plainTxt, this.__tmp__)
}
}
},
state: {
disabled: false, //禁用编辑器
fullscreen: false, //是否全屏
preview: false, //是否显示预览
// $editor: null, //编辑器元素
preview: true, //是否显示预览
editorVisible: true,
$htmlTxt: '', //临时储存html文本
htmlTxt: '', //用于预览渲染
plainTxt: '', //纯md文本
$safelyCompile: true
addon // 已有插件
},
props: {
safelyCompile: true,
onSuccess: Anot.PropsTypes.isFunction(),
onUpdate: Anot.PropsTypes.isFunction(),
onFullscreen: Anot.PropsTypes.isFunction()
},
skip: ['addon', 'insert', 'selection'],
methods: {
// 往文本框中插入内容
insert(val, isSelect) {
let dom = this.$refs.editor
if (document.selection) {
dom.focus()
let range = document.selection.createRange()
range.text = val
dom.focus()
range.moveStart('character', -1)
} else if (dom.selectionStart || dom.selectionStart === 0) {
let startPos = dom.selectionStart
let endPos = dom.selectionEnd
let scrollTop = dom.scrollTop
dom.value =
dom.value.slice(0, startPos) +
val +
dom.value.slice(endPos, dom.value.length)
dom.selectionStart = isSelect ? startPos : startPos + val.length
dom.selectionEnd = startPos + val.length
dom.scrollTop = scrollTop
dom.focus()
} else {
dom.value += val
dom.focus()
}
this.plainTxt = dom.value
},
/**
* [selection 获取选中的文本]
* @param {[type]} dom [要操作的元素]
* @param {[type]} forceHoleLine [是否强制光标所在的整行文本]
*/
selection(forceHoleLine) {
let dom = this.$refs.editor
if (document.selection) {
return document.selection.createRange().text
} else {
let startPos = dom.selectionStart
let endPos = dom.selectionEnd
if (endPos) {
//强制选择整行
if (forceHoleLine) {
startPos = dom.value.slice(0, startPos).lastIndexOf('\n')
let tmpEnd = dom.value.slice(endPos).indexOf('\n')
tmpEnd = tmpEnd < 0 ? dom.value.slice(endPos).length : tmpEnd
startPos += 1 // 把\n加上
endPos += tmpEnd
dom.selectionStart = startPos
dom.selectionEnd = endPos
}
} else {
//强制选择整行
if (forceHoleLine) {
endPos = dom.value.indexOf('\n')
endPos = endPos < 0 ? dom.value.length : endPos
dom.selectionEnd = endPos
}
}
dom.focus()
return dom.value.slice(startPos, endPos)
}
},
onToolClick: function(name, ev) {
if (ME.addon[name]) {
ME.addon[name].call(ME, ev.target, this)
if (this.addon[name]) {
this.addon[name].call(this, ev.target)
} else {
console.log('%c没有对应的插件%c[%s]', 'color:#f00;', '', name)
}
@ -464,24 +449,22 @@ Anot.component('meditor', {
html = html2md(html)
if (html) {
ME.insert(ev.target, html)
this.insert(html)
} else if (txt) {
ME.insert(ev.target, txt)
this.insert(txt)
}
log(ev.target.value)
this.plainTxt = this.$refs.editor.value
},
compile: function() {
log(this)
var txt = this.plainTxt.trim()
let txt = this.plainTxt.trim()
if (this.$safelyCompile) {
if (this.props.safelyCompile) {
txt = txt
.replace(/<script([^>]*?)>/g, '&lt;script$1&gt;')
.replace(/<\/script>/g, '&lt;/script&gt;')
}
//只解析,不渲染
this.$htmlTxt = marked(txt)
this.__tmp__ = marked(txt)
}
}
})

View File

@ -14,11 +14,11 @@
.do-meditor {position:relative;width:100%;height:100%;min-height:180px;padding-top:41px;border:1px solid nth($cp, 3);background:#fff;color:#666;box-shadow: 0 8px 40px rgba(0, 0, 0, 0.15);
::-webkit-scrollbar {width:5px;height:5px;background:#ebeeec;}
::-webkit-scrollbar:hover {background:rgba(0,0,0,.05);}
::-webkit-scrollbar {width:5px;height:5px;background:nth($cp, 2);}
::-webkit-scrollbar:hover {background:nth($cp, 1);}
::-webkit-scrollbar-button {display:none;}
::-webkit-scrollbar-thumb {background:nth($ct, 1);}
::-webkit-scrollbar-thumb:hover {background:nth($ct, 3);}
::-webkit-scrollbar-thumb {background:nth($cgr, 2);}
::-webkit-scrollbar-thumb:hover {background:nth($cgr, 1);}
@ -31,9 +31,9 @@
}
}
.editor-body{overflow:hidden;overflow-y:auto;float:left;width:100%;height:100%;padding:5px 5px 50px;border:0;outline:none;resize:none;color:#666;background:#f7f8fb;font-size:14px;}
.editor-body{overflow:hidden;overflow-y:auto;float:left;width:100%;height:100%;padding:5px 5px 50px;line-height:2;border:0;outline:none;resize:none;color:#666;background:#fff;font-size:14px;}
.md-preview {float:right;overflow:hidden;overflow-y:auto;display:block;width:50%;height:100%;padding:10px 10px 50px;line-height:2;border-left:1px solid #ddd;color:#666;font-size:14px;background:#ff0;}
.md-preview {float:right;overflow:hidden;overflow-y:auto;display:block;width:50%;height:100%;padding:10px 10px 50px;line-height:2;border-left:1px solid #ddd;color:#666;font-size:14px;background:#fff;}
@ -76,7 +76,7 @@
.do-meditor__button {overflow:hidden;position:relative;display:inline-block;width:auto;min-width:60px;height:40px;margin-left:5px;padding:0 10px;color:nth($ct, 1);text-align:center;
.do-meditor__button {overflow:hidden;position:relative;display:inline-block;width:auto;min-width:60px;height:40px;margin-left:5px;padding:0 10px;line-height:40px;color:nth($ct, 1);text-align:center;
&::before {position:absolute;left:-50%;top:-50%;z-index:-1;display:block;width:200%;height:200%;border-radius:50%;background:nth($cp, 2); content:"";opacity:0;transform: scale(0, .0); transition:opacity 1.3s cubic-bezier(0.23, 1, 0.32, 1),transform 1.3s cubic-bezier(0.23, 1, 0.32, 1);}
&:hover {
@ -126,30 +126,27 @@
section {width:100%;height: 40px;margin:10px 0;line-height:40px;
&.input {border-radius:5px;background:nth($cp, 2);
.txt {width:100%;height:40px;padding:0 10px;border:0;background:none;color:nth($cgr, 1);font-size:14px;}
}
.label {float: left;width:30%;text-align:center;background:#f7f7f7;}
label {float: left;width:50%;}
.txt {width:100%;height:40px;padding:0 10px;border:0;border-radius:5px;background:nth($cp, 2);color:nth($cgr, 1);font-size:14px;}
.label {float: left;width:50%;}
.submit {float:right;width:30%;}
}
}
.do-meditor-codeblock {width:780px;height:auto;padding:15px 20px;background:#fafafa;
.do-meditor-codeblock {width:780px;height:auto;
section {display:block;width:100%;height:auto;margin:10px 0;line-height:35px;
.label {float: left;width:80px;}
select {float:left;width:200px;height:35px;padding:0 30px 0 10px;border:0;border-bottom:1px solid #e7e7e7;background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAMAAABV0m3JAAAADFBMVEUAAAD///+Pj4+JiYkxcGihAAAABHRSTlMAABBwqVQF9wAAADNJREFUeNqlzjEOACAMw8DQ/v/PSE5FFhaEx5usdekBuzRVH0RtCqJYELUFrVjQigX/5jdvzgDh9izlMQAAAABJRU5ErkJggg==) no-repeat right 8px;color:nth($cgr, 1);outline:none;-webkit-appearance:none;-moz-appearance: none;@include ts;
select {float:left;width:200px;height:35px;padding:0 30px 0 10px;border:0;border-radius:0;border-bottom:1px solid nth($cp, 3);background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAMAAABV0m3JAAAADFBMVEUAAAD///+Pj4+JiYkxcGihAAAABHRSTlMAABBwqVQF9wAAADNJREFUeNqlzjEOACAMw8DQ/v/PSE5FFhaEx5usdekBuzRVH0RtCqJYELUFrVjQigX/5jdvzgDh9izlMQAAAABJRU5ErkJggg==) no-repeat right 12px;color:nth($cgr, 1);outline:none;-webkit-appearance:none;-moz-appearance: none;@include ts;
&::-ms-expand {display:none;}
&:focus {border-color:nth($ct, 1);}
&:focus {box-shadow:0 0 5px nth($ct, 2)}
}
textarea {width:100%;height:300px;padding:5px 10px;border:1px solid #ddd;background:#fff;resize:none;outline:none;}
.submit {float:right;width:80px;height:35px;background:#ddd;color:#666;text-align:center;}
textarea {width:100%;height:300px;padding:5px 10px;border:0;border-radius:5px;background:nth($cp, 2);font-size:14px;resize:none;outline:none;
&:focus {box-shadow:0 0 5px nth($ct, 2)}
}
.submit {float:right;width:80px;}
}
}