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']
if (ot + 18 < layh) {
arrowOffset[0] = 'bottom'
$arrow.style.borderBottomColor = state.background
style.top = ot + ch + 8
} else {
$arrow.style.borderTopColor = state.background
style.top = ot - layh - 8
}
if (ol + cw * 0.7 + layw > window.innerWidth) {
style.left = ol + cw * 0.3 - layw
arrowOffset[1] = 'left'
} else {
style.left = ol + cw * 0.7
}
$arrow.classList.add('offset-' + arrowOffset.join('-'))
Anot(layerDom[$id][1]).css(style) Anot(layerDom[$id][1]).css(style)
$container.bind('mouseenter', ev => { $container.bind('mouseenter', ev => {
layerDom[$id][1].style.visibility = 'visible' 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
tmpStyle.top = ot + ch + 8
} else {
$arrow.style.borderTopColor = state.background
tmpStyle.top = ot - layh - 8
}
if (ol + cw * 0.7 + layw > window.innerWidth) {
tmpStyle.left = ol + cw * 0.3 - layw
arrowOffset[1] = 'left'
} else {
tmpStyle.left = ol + cw * 0.7
}
$arrow.classList.add('offset-' + arrowOffset.join('-'))
Anot(layerDom[$id][1]).css(tmpStyle)
}) })
$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,432 +7,419 @@
'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() const $doc = Anot(document)
class Uploader {
constructor(url) {
this.url = url
this.xhr = new XMLHttpRequest()
this.form = new FormData()
} }
Uploader.prototype = { field(key, val) {
init: function() { this.form.append(key, val)
this.xhr = new XMLHttpRequest() return this
this.form = new FormData() }
return this onProgress(fn) {
}, this.progress = fn
field: function(key, val) { return this
if (typeof key === 'object') { }
for (var i in key) { then(cb) {
this.form.append(i, key[i]) if (!this.url) {
} Anot.error('invalid upload url')
} else {
this.form.append(key, val)
}
return this
},
start: function() {
var _this = this
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)
}
},
false
)
this.xhr.onreadystatechange = function() {
if (_this.xhr.readyState === 4) {
if (_this.xhr.status >= 200 && _this.xhr.status < 205) {
var res = _this.xhr.responseText
try {
res = JSON.parse(res)
} catch (err) {}
_this.end && _this.end(res)
} else {
console.error(_this.xhr)
}
}
}
this.xhr.send(this.form)
},
onProgress: function(fn) {
this.progress = fn
return this
},
onEnd: function(fn) {
this.end = fn
return this
} }
} let defer = Promise.defer()
function uploadScreenshot(vm, blob) { this.xhr.open('POST', this.url, true)
var upload = new Uploader(vm.uploadUrl || ME.uploadUrl) this.xhr.upload.addEventListener(
if (ME.beforeUpload) { 'progress',
ME.beforeUpload(Date.now().toString(16) + '.jpg', function(qn) { evt => {
upload if (evt.lengthComputable && this.progress) {
.field('file', blob) let res = Math.round(evt.loaded * 100 / evt.total)
.field('token', qn.token) this.progress(res)
.field('key', qn.key) }
.onEnd(function(json) { },
ME.insert(vm.$editor, '![截图](' + qn.url + ')') false
}) )
.start()
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) {}
defer.resolve(cb(res))
} else {
defer.reject(this.xhr)
}
}
}
this.xhr.send(this.form)
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()
//文本类型直接默认处理
if (txt || html) {
return
}
if (ev.clipboardData.items) {
var items = ev.clipboardData.items,
len = items.length,
blob = null
for (var i = 0, it; (it = items[i++]); ) {
if (it.type.indexOf('image') > -1) {
blob = it.getAsFile()
}
}
if (blob !== null) {
layer.msg('截图处理中...')
// 压缩截图,避免文件过大
var reader = new FileReader()
reader.onload = function() {
var img = document.createElement('img'),
canvas = document.createElement('canvas')
img.onload = function() {
canvas.width = img.width
canvas.height = img.height
var ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(this, 0, 0, canvas.width, canvas.height)
if (canvas.toBlob) {
canvas.toBlob(
function(obj) {
uploadScreenshot(vm, obj)
},
'image/jpeg',
0.8
)
} else {
var base64 = canvas.toDataURL('image/jpeg', 0.8),
buf = atob(base64.split(',')[1]),
arrBuf = new ArrayBuffer(buf.length),
intArr = new Uint8Array(arrBuf),
obj = null
for (var i = 0; i < buf.length; i++) {
intArr[i] = buf.charCodeAt(i)
}
obj = new Blob([intArr], { type: 'image/jpeg' })
uploadScreenshot(vm, obj)
}
}
img.src = this.result
}
reader.readAsDataURL(blob)
}
}
ev.preventDefault()
})
}
let cache = {
//缓存附件列表
image: [],
file: []
}
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]} vm [vm对象]
* @param {[type]} tool [image/file]
*/
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>',
url: ''
}) })
continue
}
if (vm.props.uploadSizeLimit && it.size > vm.props.uploadSizeLimit) {
this.uploadQueue.push({
name: it.name,
progress: '<span class="red">0%(文件体积过大)</span>',
url: ''
})
continue
}
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)
this.uploadQueue.push(attach)
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 + '%'
})
.then(res => {
return res.data
})
})
.then(data => {
this.uploadQueue[idx].url = data.url
})
.catch(err => {
Anot.error(err)
})
} else { } else {
upload upload
.field('file', blob) .onProgress(val => {
.onEnd(function(json) { this.uploadQueue[idx].progress = val + '%'
ME.insert(vm.$editor, '![截图](' + json.data.url + ')') })
.then(res => {
return res.data
})
.then(data => {
this.uploadQueue[idx].url = data.url
})
.catch(err => {
Anot.error(err)
}) })
.start()
} }
} }
}
var $init = function(vm) { function uploadScreenshot(vm, blob) {
if (!vm.uploadUrl && !ME.uploadUrl) { let name = new Date().format('YmdHis') + '.jpg'
console.error( let attach = { name, url: '' }
'使用附件上传,必须先设置uploadUrl;\n可以给vm增加uploadUrl属性,也可以通过ME.uploadUrl设置' let upload = new Uploader(vm.props.uploadUrl).field('file', blob)
)
}
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) { if (vm.props.beforeUpload) {
var txt = ev.clipboardData.getData('text/plain').trim(), vm.props
html = ev.clipboardData.getData('text/html').trim() .beforeUpload(attach, upload)
.then(upload => {
//文本类型直接默认处理 return upload.then(res => {
if (txt || html) { return res.data
return })
} })
.then(data => {
if (ev.clipboardData.items) { vm.insert(`![截图](${data.url})`)
var items = ev.clipboardData.items, })
len = items.length, } else {
blob = null upload
for (var i = 0, it; (it = items[i++]); ) { .then(res => {
if (it.type.indexOf('image') > -1) { return res.data
blob = it.getAsFile() })
} .then(data => {
} vm.insert(`![截图](${data.url})`)
if (blob !== null) {
layer.msg('截图处理中...')
// 压缩截图,避免文件过大
var reader = new FileReader()
reader.onload = function() {
var img = document.createElement('img'),
canvas = document.createElement('canvas')
img.onload = function() {
canvas.width = img.width
canvas.height = img.height
var ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(this, 0, 0, canvas.width, canvas.height)
if (canvas.toBlob) {
canvas.toBlob(
function(obj) {
uploadScreenshot(vm, obj)
},
'image/jpeg',
0.8
)
} else {
var base64 = canvas.toDataURL('image/jpeg', 0.8),
buf = atob(base64.split(',')[1]),
arrBuf = new ArrayBuffer(buf.length),
intArr = new Uint8Array(arrBuf),
obj = null
for (var i = 0; i < buf.length; i++) {
intArr[i] = buf.charCodeAt(i)
}
obj = new Blob([intArr], { type: 'image/jpeg' })
uploadScreenshot(vm, obj)
}
}
img.src = this.result
}
reader.readAsDataURL(blob)
}
}
ev.preventDefault()
}) })
},
lang = {
image: ['远程图片', '图片管理', '图片描述', '图片地址'],
file: ['远程附件', '附件管理', '附件描述', '附件地址']
},
opened = false, //记录是否已经打开
openType = 'image', //打开类型, 图片/附件
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>'
)
}
/**
* [uploadFile 文件上传]
* @param {[type]} files [文件列表]
* @param {[type]} vm [vm对象]
* @param {[type]} type [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({
name: it.name,
progress: '<span class="red">0%(失败,不允许的文件类型)</span>',
url: ''
})
continue
}
if (ME.maxSize > 0 && it.size > ME.maxSize) {
vm.uploadFile.push({
name: it.name,
progress: '<span class="red">0%(文件体积过大)</span>',
url: ''
})
continue
}
var fixName =
Date.now().toString(16) + it.name.slice(it.name.lastIndexOf('.'))
var idx = vm.uploadFile.length,
upload = new Uploader(vm.uploadUrl || ME.uploadUrl)
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 + '%'
})
.onEnd(function(json) {
vm.uploadFile[idx].url = qn.url
})
.start()
})
} else {
upload
.onProgress(function(val) {
vm.uploadFile[idx].progress = val + '%'
})
.onEnd(function(json) {
vm.uploadFile[idx].url = json.data.url
})
.start()
}
}
} }
}
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
if (/\?/.test(url)) { if (/\?/.test(url)) {
url += '&type=' + openType url += '&type=' + openType
} else {
url += '?type=' + openType
}
url += '&t=' + Math.random()
xhr.open('GET', url, true)
xhr.onreadystatechange = function() {
if (
this.readyState === 4 &&
this.status === 200 &&
this.responseText !== ''
) {
var res = this.responseText
try {
res = JSON.parse(res)
} catch (err) {}
cb(res)
} else { } else {
url += '?type=' + openType if (this.status !== 200 && this.responseText)
console.error(this.responseText)
} }
url += '&t=' + Math.random()
xhr.open('GET', url, true)
xhr.onreadystatechange = function() {
if (
this.readyState === 4 &&
this.status === 200 &&
this.responseText !== ''
) {
var res = this.responseText
try {
res = JSON.parse(res)
} catch (err) {}
cb(res)
} else {
if (this.status !== 200 && this.responseText)
console.error(this.responseText)
}
}
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({
type: 7,
menubar: false,
shade: false,
fixed: true,
offset: [offset.top + 37 - ME.doc.scrollTop()],
tab: 2,
attach: '',
attachAlt: '',
uploadFile: [], //当前上传的列表
attachList: [], //附件管理列表
$switch: function(id) {
var lvm = yua.vmodels[layid]
lvm.tab = id layer.open({
if (id === 3) { type: 7,
lvm.attachList.clear() menubar: false,
if (cache[openType].length) { fixed: true,
lvm.attachList = cache[openType] offset: [offset.top + 37 - $doc.scrollTop()],
} else { tab: 2,
getAttach(vm, function(json) { attach: '',
if (json) { attachAlt: '',
cache[openType] = json.data.list.map(function(it) { uploadQueue: [], //当前上传的列表
it.thumb = attachList: [], //附件管理列表
openType === 'image' switchTab: function(id) {
? '<img src="' + it.url + '"/>' this.tab = id
: '<em class="attach-icon">&#xe73e;</em>' if (id === 3) {
return it this.attachList.clear()
}) if (cache[tool].length) {
lvm.attachList = json.data.list this.attachList = cache[tool]
} } else {
getAttach(vm, function(json) {
if (json) {
cache[tool] = json.data.list.map(function(it) {
it.thumb =
tool === 'image'
? '<img src="' + it.url + '"/>'
: '<em class="attach-icon">&#xe73e;</em>'
return it
}) })
this.attachList = json.data.list
} }
} })
}, }
$select: yua.noop, }
$change: yua.noop, },
$insert: function(it) { select() {
if (!it.url) { let ev = document.createEvent('MouseEvent')
return ev.initEvent('click', false, false)
} this.$refs.attach.dispatchEvent(ev)
var val = },
(openType === 'image' ? '!' : '') + change(ev) {
'[' + this.files = ev.target.files
it.name + uploadFile.call(this, vm, tool)
'](' + },
it.url + insert: function(it) {
')' if (!it.url) {
ME.insert(vm.$editor, val) return
}, }
$confirm: function() { let val = `\n${tool === 'image' ? '!' : ''}[${it.name}](${it.url})`
var lvm = yua.vmodels[layid] vm.insert(val)
if (!lvm.attach || !lvm.attachAlt) { },
return layer.alert('描述和地址不能为空') confirm: function() {
} if (!this.attach || !this.attachAlt) {
var val = '![' + lvm.attachAlt + '](' + lvm.attach + ')' return layer.toast('描述和地址不能为空', 'error')
}
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() { const addon = {
var ev = document.createEvent('MouseEvent') attach(elem) {
ev.initEvent('click', false, false) showDialog(elem, this, 'file')
$file.dispatchEvent(ev) },
} image(elem) {
_this.$change = function() { showDialog(elem, this, 'image')
uploadFile(this.files, _this)
}
},
content: fixCont()
})
} }
}
ME.addon.image = function(elem, vm) { export default addon
if (opened) {
return
}
openType = 'image'
showDialog(elem, vm)
}
ME.addon.file = function(elem, vm) {
if (opened) {
return
}
openType = 'file'
showDialog(elem, vm)
}
return $init
})

View File

@ -5,58 +5,76 @@
* @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;} .tab-box {float:left;width:130px;height:300px;padding:10px 5px;text-align:center;background:nth($cp, 2);border-radius:5px;
.item.active {background:#fff;}
.item.active::after {position:absolute;left:0;bottom:-1px;width:100%;height:1px;background:#fff;content:""} .item {display:block;width:100%;height:40px;line-height:40px;border-radius:3px;cursor:pointer;
a.action-close {top:5px;width:40px;height:40px;line-height:40px;font-size:20px;}
a.action-close:hover {line-height:40px;border:0;} &.active {background:#fff;}
}
}
.cont-box {position:relative;float:right;width:480px;height:auto;min-height:200px;
.remote,
.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;
.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;}
} }
.cont-box {position:relative;width:100%;height:auto;min-height:200px;background:#fff;
.remote, .select-file {width:100%;height:35px;line-height:35px;
.local {position:relative;width:60%;height:auto;margin:0 auto;padding:15px 0 30px;}
.local {width:96%;}
.hide {display:none;} .file {float:left;width:100px;height:35px;border-radius:3px;background:nth($cp, 1);text-align:center;cursor:pointer;
.section {display:block;width:100%;height:auto;margin:15px 0;line-height:35px; &:hover {background:nth($cp, 2);}
&:active {background:nth($cp, 3);}
&.input {line-height:33px;border:1px solid #e9e9e9;} }
.tips {display:inline-block;padding:0 10px;}
.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: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;}
.manager .item:hover {background:#f7f7f7;}
.manager .thumb {display:block;width:100%;height:100px;}
.manager .name {overflow:hidden;height:20px;line-height:30px; text-align:center;}
.manager img {width:100%;height:100%;}
.manager .attach-icon {display:inline-block;width:100%;height:100px;text-align:center;font:50px/100px "ui font" !important;-webkit-font-smoothing: antialiased;-webkit-text-stroke-width: 0.2px;-moz-osx-font-smoothing: grayscale;}
} }
.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;}
}
.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:hover {background:#f7f7f7;}
.manager .thumb {display:block;width:100%;height:100px;}
.manager .name {overflow:hidden;height:20px;line-height:30px; text-align:center;}
.manager img {width:100%;height:100%;}
.manager .attach-icon {display:inline-block;width:100%;height:100px;text-align:center;font:50px/100px "ui font" !important;-webkit-font-smoothing: antialiased;-webkit-text-stroke-width: 0.2px;-moz-osx-font-smoothing: grayscale;}
}
} }

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,158 +17,71 @@ marked.setOptions({
return Prism.highlight(code, Prism.languages[lang]) return Prism.highlight(code, Prism.languages[lang])
} }
}) })
let editorVM = [] if (!String.prototype.repeat) {
Anot.ui.meditor = '1.0.0' String.prototype.repeat = function(num) {
const log = console.log let result = ''
//存放编辑器公共静态资源 while (num > 0) {
window.ME = { result += this
version: Anot.ui.meditor, num--
// 工具栏title
toolbar: {
pipe: '',
h1: '标题',
quote: '引用文本',
bold: '粗体',
italic: '斜体',
through: '删除线',
unordered: '无序列表',
ordered: '有序列表',
link: '超链接',
hr: '横线',
time: '插入当前时间',
face: '表情',
table: '插入表格',
image: '插入图片',
file: '插入附件',
inlinecode: '行内代码',
blockcode: '代码块',
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()
} }
}, return result
/**
* [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) { Anot.ui.meditor = '1.0.0'
return inner ? '\n' + inner + '\n' : '' const log = console.log
}, // 工具栏title
br: '\n', const TOOLBAR = {
'h([1-6])': function(str, level, attr, inner) { pipe: '',
var h = ME.repeat('#', level) h1: '标题',
return '\n' + h + ' ' + inner + '\n' quote: '引用文本',
}, bold: '粗体',
hr: '\n\n___\n\n', italic: '斜体',
through: '删除线',
unordered: '无序列表',
ordered: '有序列表',
link: '超链接',
hr: '横线',
time: '插入当前时间',
face: '表情',
table: '插入表格',
image: '插入图片',
file: '插入附件',
inlinecode: '行内代码',
blockcode: '代码块',
preview: '预览',
fullscreen: '全屏',
about: '关于编辑器'
}
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) { 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'), '')
'<$1>' .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) { // 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;}
} }
} }