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

View File

@ -75,7 +75,7 @@
/* tips类弹层(type 5) */ /* 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;} // &.active {visibility:visible;opacity:1;}
i.arrow {position:absolute;width:0;height:0;border:6px solid transparent;content: ""} i.arrow {position:absolute;width:0;height:0;border:6px solid transparent;content: ""}
@ -190,7 +190,6 @@
&.shift {transition: all .5s ease-out;} &.shift {transition: all .5s ease-out;}
.layer-box {position:absolute;}
} }
&:active {z-index:65536;} &:active {z-index:65536;}
} }

View File

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

View File

@ -5,50 +5,68 @@
* @date 2017-04-20 19:13:24 * @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; &:hover {color:nth($ct, 1);font-size:28px;}
.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;}
} }
.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, .remote,
.local {position:relative;width:60%;height:auto;margin:0 auto;padding:15px 0 30px;} .local {position:relative;width:92%;height:auto;margin:0 auto;}
.local {width:96%;}
.remote {padding:30px 0;}
.hide {display:none;} .hide {display:none;}
.section {display:block;width:100%;height:auto;margin:15px 0;line-height:35px; .section {display:block;width:100%;height:auto;margin:15px 0;line-height:35px;
&.input {line-height:33px;border:1px solid #e9e9e9;} .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;}
.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;}
.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 {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;} .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'), '') return str.replace(new RegExp('^' + sign + '|' + sign + '$', 'g'), '')
} }
const $doc = Anot(document)
const addon = { const addon = {
h1: function(elem, vm) { h1: function(elem) {
let that = this let that = this
let editor = vm.$refs.editor
let offset = Anot(elem).offset() let offset = Anot(elem).offset()
let wrap = this.selection(editor, true) || '在此输入文本' let wrap = this.selection(true) || '在此输入文本'
layer.open({ layer.open({
type: 7, type: 7,
menubar: false, menubar: false,
maskClose: true, maskClose: true,
maskColor: 'rgba(255,255,255,0)',
fixed: true, fixed: true,
insert: function(level) { insert: function(level) {
wrap = wrap.replace(/^#{1,6} /, '') wrap = wrap.replace(/^(#{1,6} )?/, '#'.repeat(level) + ' ')
wrap = that.repeat('#', level) + ' ' + wrap that.insert(wrap, true)
that.insert(editor, wrap, true)
this.close() this.close()
}, },
offset: [ offset: [
offset.top + 40 - that.doc.scrollTop(), offset.top + 40 - $doc.scrollTop(),
'auto', 'auto',
'auto', 'auto',
offset.left - that.doc.scrollLeft() offset.left - $doc.scrollLeft()
], ],
shift: { shift: {
top: offset.top - that.doc.scrollTop(), top: offset.top - $doc.scrollTop(),
left: offset.left - that.doc.scrollLeft() left: offset.left - $doc.scrollLeft()
}, },
content: ` content: `
<ul class="do-meditor-h1 do-fn-noselect do-meditor__font"> <ul class="do-meditor-h1 do-fn-noselect do-meditor__font">
@ -58,60 +59,61 @@ const addon = {
</ul>` </ul>`
}) })
}, },
quote: function(elem, vm) { quote: function(elem) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本' let wrap = this.selection() || '在此输入文本'
wrap = '> ' + wrap wrap = '> ' + wrap
this.insert(vm.$refs.editor, wrap, true) this.insert(wrap, true)
}, },
bold: function(elem, vm) { bold: function(elem) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本' let wrap = this.selection() || '在此输入文本'
let wraped = trim(wrap, '\\*\\*') let wraped = trim(wrap, '\\*\\*')
wrap = wrap === wraped ? '**' + wrap + '**' : wraped wrap = wrap === wraped ? '**' + wrap + '**' : wraped
this.insert(vm.$refs.editor, wrap, true) this.insert(wrap, true)
}, },
italic: function(elem, vm) { italic: function(elem) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本' let wrap = this.selection() || '在此输入文本'
let wraped = trim(wrap, '_') let wraped = trim(wrap, '_')
wrap = wrap === wraped ? '_' + wrap + '_' : wraped wrap = wrap === wraped ? '_' + wrap + '_' : wraped
this.insert(vm.$refs.editor, wrap, true) this.insert(wrap, true)
}, },
through: function(elem, vm) { through: function(elem) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本' let wrap = this.selection() || '在此输入文本'
let wraped = trim(wrap, '~~') let wraped = trim(wrap, '~~')
wrap = wrap === wraped ? '~~' + wrap + '~~' : wraped wrap = wrap === wraped ? '~~' + wrap + '~~' : wraped
this.insert(vm.$refs.editor, wrap, true) this.insert(wrap, true)
}, },
unordered: function(elem, vm) { unordered: function(elem) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本' let wrap = this.selection() || '在此输入文本'
wrap = '* ' + wrap wrap = '* ' + wrap
this.insert(vm.$refs.editor, wrap, false) this.insert(wrap, false)
}, },
ordered: function(elem, vm) { ordered: function(elem) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本' let wrap = this.selection() || '在此输入文本'
wrap = '1. ' + wrap wrap = '1. ' + wrap
this.insert(vm.$refs.editor, wrap, false) this.insert(wrap, false)
}, },
hr: function(elem, vm) { hr: function(elem) {
this.insert(vm.$refs.editor, '\n\n---\n\n', false) this.insert('\n\n---\n\n', false)
}, },
link: function(elem, vm) { link: function(elem) {
let that = this let that = this
let offset = Anot(elem).offset() let offset = Anot(elem).offset()
let wrap = this.selection(vm.$refs.editor) || '' let wrap = this.selection() || ''
layer.open({ layer.open({
type: 7, type: 7,
menubar: false, menubar: false,
maskClose: true, maskClose: true,
maskColor: 'rgba(255,255,255,0)',
fixed: true, fixed: true,
link: '', link: '',
linkName: wrap, linkName: wrap,
@ -124,29 +126,29 @@ const addon = {
this.linkTarget === 1 ? ' "target=_blank"' : '' this.linkTarget === 1 ? ' "target=_blank"' : ''
})` })`
that.insert(vm.$refs.editor, val, false) that.insert(val, false)
this.close() this.close()
}, },
offset: [ offset: [
offset.top + 40 - that.doc.scrollTop(), offset.top + 40 - $doc.scrollTop(),
'auto', 'auto',
'auto', 'auto',
offset.left - that.doc.scrollLeft() offset.left - $doc.scrollLeft()
], ],
shift: { shift: {
top: offset.top - that.doc.scrollTop(), top: offset.top - $doc.scrollTop(),
left: offset.left - that.doc.scrollLeft() left: offset.left - $doc.scrollLeft()
}, },
content: ` content: `
<div class="do-meditor-common do-meditor__font"> <div class="do-meditor-common do-meditor__font">
<section class="input"> <section>
<input class="txt" :duplex="linkName" placeholder="链接文字"/> <input class="txt" :duplex="linkName" placeholder="链接文字"/>
</section> </section>
<section class="input"> <section>
<input class="txt" :duplex="link" placeholder="链接地址"/> <input class="txt" :duplex="link" placeholder="链接地址"/>
</section> </section>
<section> <section>
<label> <label class="label">
<input <input
name="link" name="link"
type="radio" type="radio"
@ -155,7 +157,7 @@ const addon = {
value="1"/> value="1"/>
新窗口打开 新窗口打开
</label> </label>
<label> <label class="label">
<input <input
name="link" name="link"
type="radio" type="radio"
@ -174,10 +176,10 @@ const addon = {
</div>` </div>`
}) })
}, },
time: function(elem, vm) { time: function(elem) {
this.insert(vm.$refs.editor, new Date().format(), false) this.insert(new Date().format(), false)
}, },
face: function(elem, vm) { face: function(elem) {
let that = this let that = this
let offset = Anot(elem).offset() let offset = Anot(elem).offset()
@ -186,6 +188,7 @@ const addon = {
title: '插入表情', title: '插入表情',
fixed: true, fixed: true,
maskClose: true, maskClose: true,
maskColor: 'rgba(255,255,255,0)',
arr: [ arr: [
'😀', '😀',
'😅', '😅',
@ -225,14 +228,14 @@ const addon = {
'🙏' '🙏'
], ],
offset: [ offset: [
offset.top + 40 - that.doc.scrollTop(), offset.top + 40 - $doc.scrollTop(),
'auto', 'auto',
'auto', 'auto',
offset.left - that.doc.scrollLeft() offset.left - $doc.scrollLeft()
], ],
shift: { shift: {
top: offset.top - that.doc.scrollTop(), top: offset.top - $doc.scrollTop(),
left: offset.left - that.doc.scrollLeft() left: offset.left - $doc.scrollLeft()
}, },
content: ` content: `
<ul class="do-meditor-face"> <ul class="do-meditor-face">
@ -241,12 +244,12 @@ const addon = {
</li> </li>
</ul>`, </ul>`,
insert: function(val) { insert: function(val) {
that.insert(vm.$refs.editor, val, false) that.insert(val, false)
this.close() this.close()
} }
}) })
}, },
table: function(elem, vm) { table: function(elem) {
let that = this let that = this
let offset = Anot(elem).offset() let offset = Anot(elem).offset()
@ -255,15 +258,16 @@ const addon = {
title: '0行 x 0列', title: '0行 x 0列',
fixed: true, fixed: true,
maskClose: true, maskClose: true,
maskColor: 'rgba(255,255,255,0)',
offset: [ offset: [
offset.top + 40 - that.doc.scrollTop(), offset.top + 40 - $doc.scrollTop(),
'auto', 'auto',
'auto', 'auto',
offset.left - that.doc.scrollLeft() offset.left - $doc.scrollLeft()
], ],
shift: { shift: {
top: offset.top - that.doc.scrollTop(), top: offset.top - $doc.scrollTop(),
left: offset.left - that.doc.scrollLeft() left: offset.left - $doc.scrollLeft()
}, },
matrix: objArr(10).map(function() { matrix: objArr(10).map(function() {
return objArr(10) return objArr(10)
@ -313,26 +317,27 @@ const addon = {
let x = ev.target.dataset.x - 0 + 1 let x = ev.target.dataset.x - 0 + 1
let y = ev.target.dataset.y - 0 + 1 let y = ev.target.dataset.y - 0 + 1
let thead = `\n\n${that.repeat('| 表头 ', x)}|\n` let thead = `\n\n${'| 表头 '.repeat(x)}|\n`
let pipe = `${that.repeat('| -- ', x)}|\n` let pipe = `${'| -- '.repeat(x)}|\n`
let tbody = that.repeat(that.repeat('| ', x) + '|\n', y) let tbody = ('| '.repeat(x) + '|\n').repeat(y)
that.insert(vm.$refs.editor, thead + pipe + tbody, false) that.insert(thead + pipe + tbody, false)
this.close() this.close()
} }
}) })
} }
}) })
}, },
image: function(elem, vm) { image: function(elem) {
let that = this let that = this
let offset = Anot(elem).offset() let offset = Anot(elem).offset()
let wrap = this.selection(vm.$refs.editor) || '' let wrap = this.selection() || ''
layer.open({ layer.open({
type: 7, type: 7,
menubar: false, menubar: false,
maskClose: true, maskClose: true,
maskColor: 'rgba(255,255,255,0)',
fixed: true, fixed: true,
img: '', img: '',
imgAlt: wrap, imgAlt: wrap,
@ -342,25 +347,25 @@ const addon = {
} }
let val = `![${this.imgAlt}](${this.img})` let val = `![${this.imgAlt}](${this.img})`
that.insert(vm.$refs.editor, val, false) that.insert(val, false)
this.close() this.close()
}, },
offset: [ offset: [
offset.top + 40 - that.doc.scrollTop(), offset.top + 40 - $doc.scrollTop(),
'auto', 'auto',
'auto', 'auto',
offset.left - that.doc.scrollLeft() offset.left - $doc.scrollLeft()
], ],
shift: { shift: {
top: offset.top - that.doc.scrollTop(), top: offset.top - $doc.scrollTop(),
left: offset.left - that.doc.scrollLeft() left: offset.left - $doc.scrollLeft()
}, },
content: ` content: `
<div class="do-meditor-common do-meditor__font"> <div class="do-meditor-common do-meditor__font">
<section class="input"> <section>
<input class="txt" :duplex="imgAlt" placeholder="图片描述"/> <input class="txt" :duplex="imgAlt" placeholder="图片描述"/>
</section> </section>
<section class="input"> <section>
<input class="txt" :duplex="img" placeholder="图片地址"/> <input class="txt" :duplex="img" placeholder="图片地址"/>
</section> </section>
<section> <section>
@ -373,17 +378,17 @@ const addon = {
` `
}) })
}, },
attach: function(elem, vm) { attach: function(elem) {
this.addon.link.call(this, elem, vm, false) this.addon.link.call(this, elem)
}, },
inlinecode: function(elem, vm) { inlinecode: function(elem) {
let wrap = this.selection(vm.$refs.editor) || '在此输入文本' let wrap = this.selection() || '在此输入文本'
let wraped = trim(wrap, '`') let wraped = trim(wrap, '`')
wrap = wrap === wraped ? '`' + wrap + '`' : wraped 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 let that = this
layer.open({ layer.open({
type: 7, type: 7,
@ -426,48 +431,55 @@ const addon = {
], ],
lang: 'javascript', lang: 'javascript',
code: '', code: '',
$confirm: function() { maskClose: true,
var lvm = Anot.vmodels[layid] insert: function() {
var val = let val = `\n\`\`\`${this.lang}\n${this.code ||
'\n```' + lvm.lang + '\n' + (lvm.code || '//在此输入代码') + '\n```\n' '// 在此输入代码'}\n\`\`\`\n`
that.insert(val, false)
that.insert(vm.$refs.editor, val, false) this.close()
layer.close(layid)
}, },
content: content: `
'<div class="do-meditor-codeblock do-meditor__font">' + <div class="do-meditor-codeblock do-meditor__font">
'<section class="do-fn-cl"><span class="label">语言类型</span>' + <section class="do-fn-cl">
'<select :duplex="lang">' + <span class="label">语言类型</span>
'<option :repeat="$lang" :attr-value="el.id">{{el.name || el.id}}</option>' + <select :duplex="lang">
'</select>' + <option :repeat="$lang" :attr-value="el.id">{{el.name || el.id}}</option>
'</section>' + </select>
'<section>' + </section>
'<textarea :duplex="code" placeholder="在这里输入/粘贴代码"></textarea>' + <section>
'</section>' + <textarea :duplex="code" placeholder="在这里输入/粘贴代码"></textarea>
'<section class="do-fn-cl">' + </section>
'<a href="javascript:;" class="submit" :click="$confirm">确定</a>' + <section class="do-fn-cl">
'</section>' + <a
'</div>' href="javascript:;"
class="do-meditor__button submit"
:click="insert">确定</a>
</section>
</div>
`
}) })
}, },
preview: function(elem, vm) { preview: function() {
vm.preview = !vm.preview this.preview = !this.preview
if (vm.preview) { if (this.preview) {
vm.htmlTxt = vm.$htmlTxt this.htmlTxt = this.__tmp__
} }
}, },
fullscreen: function(elem, vm) { fullscreen: function() {
vm.fullscreen = !vm.fullscreen this.fullscreen = !this.fullscreen
vm.$onFullscreen(vm.fullscreen) if (typeof this.props.onFullscreen === 'function') {
this.props.onFullscreen(this.fullscreen)
}
}, },
about: function(elem) { about: function(elem) {
var offset = Anot(elem).offset() let offset = Anot(elem).offset()
layer.open({ layer.open({
type: 7, type: 7,
title: '关于编辑器', title: '关于编辑器',
maskClose: true, maskClose: true,
offset: [offset.top + 37 - this.doc.scrollTop()], maskColor: 'rgba(255,255,255,0)',
shift: { top: offset.top - this.doc.scrollTop() }, offset: [offset.top + 37 - $doc.scrollTop()],
shift: { top: offset.top - $doc.scrollTop() },
content: content:
'<div class="do-meditor-about do-meditor__font">' + '<div class="do-meditor-about do-meditor__font">' +
'<pre>' + '<pre>' +
@ -477,7 +489,7 @@ const addon = {
'| | | | |__| (_| | | || (_) | |\n' + '| | | | |__| (_| | | || (_) | |\n' +
'|_| |_|_____\\__,_|_|\\__\\___/|_| ' + '|_| |_|_____\\__,_|_|\\__\\___/|_| ' +
'v' + 'v' +
this.version + Anot.ui.meditor +
'</pre>' + '</pre>' +
'<p>开源在线Markdown编辑器</p>' + '<p>开源在线Markdown编辑器</p>' +
'<p><a target="_blank" href="https://doui.cc/product/meditor">https://doui.cc/product/meditor</a></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]) 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' Anot.ui.meditor = '1.0.0'
const log = console.log const log = console.log
//存放编辑器公共静态资源
window.ME = {
version: Anot.ui.meditor,
// 工具栏title // 工具栏title
toolbar: { const TOOLBAR = {
pipe: '', pipe: '',
h1: '标题', h1: '标题',
quote: '引用文本', quote: '引用文本',
@ -45,130 +52,36 @@ window.ME = {
preview: '预览', preview: '预览',
fullscreen: '全屏', fullscreen: '全屏',
about: '关于编辑器' about: '关于编辑器'
}, }
addon, // 已有插件 const DEFAULT_TOOLBAR = [
// 往文本框中插入内容 'h1',
insert: function(dom, val, isSelect) { 'quote',
if (document.selection) { '|',
dom.focus() 'bold',
let range = document.selection.createRange() 'italic',
range.text = val 'through',
dom.focus() '|',
range.moveStart('character', -1) 'unordered',
} else if (dom.selectionStart || dom.selectionStart === 0) { 'ordered',
let startPos = dom.selectionStart '|',
let endPos = dom.selectionEnd 'hr',
let scrollTop = dom.scrollTop 'link',
'time',
'face',
'|',
'table',
'image',
'attach',
'inlinecode',
'blockcode',
'|',
'preview',
'fullscreen',
'|',
'about'
]
dom.value = const ELEMS = {
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',
a: function(str, attr, inner) { a: function(str, attr, inner) {
let href = attr.match(attrExp('href')) let href = attr.match(attrExp('href'))
let title = attr.match(attrExp('title')) let title = attr.match(attrExp('title'))
@ -212,18 +125,27 @@ var elems = {
alt = (alt && alt[1]) || '' alt = (alt && alt[1]) || ''
return '![' + alt + '](' + src + ')' 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) { function attrExp(field, flag = 'i') {
return new RegExp(field + '\\s?=\\s?["\']?([^"\']*)["\']?', 'i') return new RegExp(field + '\\s?=\\s?["\']?([^"\']*)["\']?', flag)
} }
function tagExp(tag, open) { function tagExp(tag, open) {
var exp = '' var exp = ''
if (['br', 'hr', 'img'].indexOf(tag) > -1) { if (['br', 'hr', 'img'].indexOf(tag) > -1) {
exp = '<' + tag + '([^>]*)\\/?>' exp = '<' + tag + '([^>]*?)\\/?>'
} else { } else {
exp = '<' + tag + '([^>]*)>([\\s\\S]*?)<\\/' + tag + '>' exp = '<' + tag + '([^>]*?)>([\\s\\S]*?)<\\/' + tag + '>'
} }
return new RegExp(exp, 'gi') return new RegExp(exp, 'gi')
} }
@ -233,14 +155,20 @@ function html2md(str) {
} catch (err) {} } catch (err) {}
str = str.replace(/\t/g, ' ').replace(/<meta [^>]*>/, '') str = str.replace(/\t/g, ' ').replace(/<meta [^>]*>/, '')
str = str.replace( str = str
/<(div|span|dl|dd|dt|table|tr|td|thead|tbody|i|em|strong|h[1-6]|ul|ol|li) [^>]*>/g, .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>' '<$1>'
) )
.replace(/<svg [^>]*>.*?<\/svg>/g, '{invalid image}')
for (var i in elems) { // log(str)
var cb = elems[i], for (let i in ELEMS) {
exp = tagExp(i) let cb = ELEMS[i]
let exp = tagExp(i)
if (i === 'blockquote') { if (i === 'blockquote') {
while (str.match(exp)) { while (str.match(exp)) {
@ -250,6 +178,11 @@ function html2md(str) {
str = str.replace(exp, cb) str = str.replace(exp, cb)
} }
// 对另外3种同类标签做一次处理
if (i === 'p') {
exp = tagExp('div')
str = str.replace(exp, cb)
}
if (i === 'em') { if (i === 'em') {
exp = tagExp('i') exp = tagExp('i')
str = str.replace(exp, cb) str = str.replace(exp, cb)
@ -259,23 +192,23 @@ function html2md(str) {
str = str.replace(exp, cb) 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)) { while (str.match(liExp)) {
str = str.replace(liExp, function(match) { 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, m,
t, t,
inner inner
) { ) {
var li = inner.split('</li>') let li = inner.split('</li>')
li.pop() li.pop()
for (var i = 0, len = li.length; i < len; i++) { for (let i = 0, len = li.length; i < len; i++) {
var pre = t === 'ol' ? i + 1 + '. ' : '* ' let pre = t === 'ol' ? i + 1 + '. ' : '* '
li[i] = li[i] =
pre + pre +
li[i] 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 ') n = n.trim().replace(/\n/g, '\n ')
return n return n
}) })
@ -287,6 +220,7 @@ function html2md(str) {
}) })
} }
str = str str = str
.replace(/<[\/]?[\w]*[^>]*>/g, '') .replace(/<[\/]?[\w]*[^>]*>/g, '')
.replace(/```([\w\W]*)```/g, function(str, inner) { .replace(/```([\w\W]*)```/g, function(str, inner) {
inner = inner inner = inner
@ -298,52 +232,50 @@ function html2md(str) {
return 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) { function tool(name) {
name = (name + '').trim().toLowerCase() name = (name + '').trim().toLowerCase()
name = '|' === name ? 'pipe' : name name = '|' === name ? 'pipe' : name
return ( let event = name === 'pipe' ? '' : `:click="onToolClick('${name}', $event)"`
'<span title="' + let title = TOOLBAR[name]
ME.toolbar[name] + return `
'" class="do-meditor__icon icon-' + <span title="${title}" class="do-meditor__icon icon-${name}" ${event}></span>`
name + }
'" ' +
(name !== 'pipe' ? ':click="onToolClick(\'' + name + '\', $event)"' : '') + class MEObject {
'></span>' 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', { Anot.component('meditor', {
construct: function(props, state) {
if (props.hasOwnProperty('$show')) {
state.editorVisible = props.$show
delete props.$show
}
},
render: function() { 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 delete this.toolbar
@ -361,97 +293,150 @@ Anot.component('meditor', {
:duplex="plainTxt" :duplex="plainTxt"
:on-paste="onPaste($event)"></textarea> :on-paste="onPaste($event)"></textarea>
<content <content
ref="preview"
class="md-preview do-marked-theme" class="md-preview do-marked-theme"
:visible="preview" :visible="preview"
:html="htmlTxt"></content> :html="htmlTxt"></content>
</div> </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) { componentDidMount: function(vm, elem) {
console.log(this) let $editor = Anot(this.$refs.editor)
// vm.$editor = elem.children[1] 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) $editor.bind('scroll', ev => {
// //自动加载额外的插件 let syncTop =
// require(extraAddons, function() { ev.target.scrollTop / ev.target.scrollHeight * preview.scrollHeight
// var args = Array.prototype.slice.call(arguments, 0)
// args.forEach(function(addon) {
// addon && addon(vm)
// })
// })
// Anot(vm.$editor).bind('keydown', function(ev) { preview.scrollTop = syncTop
// var wrap = ME.selection(vm.$editor) || '', })
// select = !!wrap //编辑器成功加载的回调
// //tab键改为插入2个空格,阻止默认事件,防止焦点失去 if (typeof this.props.onCreated === 'function') {
// if (ev.keyCode === 9) { this.props.onCreated(new MEObject(this))
// 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)
}, },
watch: { watch: {
plainTxt: function(val) { plainTxt: function(val) {
this.compile() this.compile()
//只有开启实时预览,才会赋值给htmlTxt //只有开启实时预览,才会赋值给htmlTxt
if (this.preview) { if (this.preview) {
this.htmlTxt = this.$htmlTxt this.htmlTxt = this.__tmp__
} }
if (typeof this.props.onUpdate === 'function') { if (typeof this.props.onUpdate === 'function') {
this.props.onUpdate(this.plainTxt, this.$htmlTxt) this.props.onUpdate(this.plainTxt, this.__tmp__)
} }
} }
}, },
state: { state: {
disabled: false, //禁用编辑器 disabled: false, //禁用编辑器
fullscreen: false, //是否全屏 fullscreen: false, //是否全屏
preview: false, //是否显示预览 preview: true, //是否显示预览
// $editor: null, //编辑器元素
editorVisible: true, editorVisible: true,
$htmlTxt: '', //临时储存html文本
htmlTxt: '', //用于预览渲染 htmlTxt: '', //用于预览渲染
plainTxt: '', //纯md文本 plainTxt: '', //纯md文本
$safelyCompile: true addon // 已有插件
}, },
props: { props: {
safelyCompile: true,
onSuccess: Anot.PropsTypes.isFunction(), onSuccess: Anot.PropsTypes.isFunction(),
onUpdate: Anot.PropsTypes.isFunction(), onUpdate: Anot.PropsTypes.isFunction(),
onFullscreen: Anot.PropsTypes.isFunction() onFullscreen: Anot.PropsTypes.isFunction()
}, },
skip: ['addon', 'insert', 'selection'],
methods: { 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) { onToolClick: function(name, ev) {
if (ME.addon[name]) { if (this.addon[name]) {
ME.addon[name].call(ME, ev.target, this) this.addon[name].call(this, ev.target)
} else { } else {
console.log('%c没有对应的插件%c[%s]', 'color:#f00;', '', name) console.log('%c没有对应的插件%c[%s]', 'color:#f00;', '', name)
} }
@ -464,24 +449,22 @@ Anot.component('meditor', {
html = html2md(html) html = html2md(html)
if (html) { if (html) {
ME.insert(ev.target, html) this.insert(html)
} else if (txt) { } else if (txt) {
ME.insert(ev.target, txt) this.insert(txt)
} }
log(ev.target.value)
this.plainTxt = this.$refs.editor.value this.plainTxt = this.$refs.editor.value
}, },
compile: function() { compile: function() {
log(this) let txt = this.plainTxt.trim()
var txt = this.plainTxt.trim()
if (this.$safelyCompile) { if (this.props.safelyCompile) {
txt = txt txt = txt
.replace(/<script([^>]*?)>/g, '&lt;script$1&gt;') .replace(/<script([^>]*?)>/g, '&lt;script$1&gt;')
.replace(/<\/script>/g, '&lt;/script&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); .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 {width:5px;height:5px;background:nth($cp, 2);}
::-webkit-scrollbar:hover {background:rgba(0,0,0,.05);} ::-webkit-scrollbar:hover {background:nth($cp, 1);}
::-webkit-scrollbar-button {display:none;} ::-webkit-scrollbar-button {display:none;}
::-webkit-scrollbar-thumb {background:nth($ct, 1);} ::-webkit-scrollbar-thumb {background:nth($cgr, 2);}
::-webkit-scrollbar-thumb:hover {background:nth($ct, 3);} ::-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);} &::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 { &:hover {
@ -126,30 +126,27 @@
section {width:100%;height: 40px;margin:10px 0;line-height:40px; 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;border-radius:5px;background:nth($cp, 2);color:nth($cgr, 1);font-size:14px;}
.label {float: left;width:50%;}
.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%;}
.submit {float:right;width:30%;} .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; section {display:block;width:100%;height:auto;margin:10px 0;line-height:35px;
.label {float: left;width:80px;} .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;} &::-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;} textarea {width:100%;height:300px;padding:5px 10px;border:0;border-radius:5px;background:nth($cp, 2);font-size:14px;resize:none;outline:none;
.submit {float:right;width:80px;height:35px;background:#ddd;color:#666;text-align:center;}
&:focus {box-shadow:0 0 5px nth($ct, 2)}
}
.submit {float:right;width:80px;}
} }
} }