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
wcui/src/editor/index.wc

651 lines
14 KiB
Plaintext
Raw Normal View History

2019-09-16 16:53:51 +08:00
<template>
<div class="neditor">
<section class="toolbar"></section>
<wc-scroll>
<div contenteditable="true" class="editor" spellcheck="false"></div>
</wc-scroll>
<div class="font-layer">
2019-10-31 20:45:24 +08:00
<span data-size="6">6号字体</span>
<span data-size="5">5号字体</span>
<span data-size="4">4号字体</span>
<span data-size="3">3号字体</span>
<span data-size="2">2号字体</span>
2019-09-16 16:53:51 +08:00
</div>
<div class="color-layer">
<span data-color="#f3f5fb"></span>
<span data-color="#dae1e9"></span>
<span data-color="#62778d"></span>
<span data-color="#58d68d"></span>
<span data-color="#3fc2a7"></span>
<span data-color="#52a3de"></span>
<span data-color="#ac61ce"></span>
<span data-color="#ffb618"></span>
<span data-color="#e67e22"></span>
<span data-color="#ff5061"></span>
<span data-color="#ff0000"></span>
<span data-color="#000000"></span>
</div>
<div class="link-layer">
<wc-input label="请输入链接地址"></wc-input>
<wc-button color="teal" size="mini">插入</wc-button>
</div>
</div>
</template>
<style lang="scss">
:host {
display: flex;
min-width: 200px;
min-height: 100px;
max-height: 640px;
2021-05-26 11:49:11 +08:00
border-radius: 3px;
2021-05-26 19:45:09 +08:00
transition: box-shadow 0.15s linear;
2019-09-16 16:53:51 +08:00
}
2019-09-24 11:47:08 +08:00
table {
2021-05-26 11:49:11 +08:00
width: 100%;
2019-09-24 11:47:08 +08:00
border-spacing: 0;
border-collapse: collapse;
tr {
background: #fff;
}
thead tr {
2020-12-14 14:44:05 +08:00
background: var(--color-plain-1);
2019-09-24 11:47:08 +08:00
}
th,
td {
2021-05-26 11:49:11 +08:00
padding: 6px 13px;
border: 1px solid var(--color-plain-2);
vertical-align: middle;
2019-09-24 11:47:08 +08:00
}
th {
font-weight: bold;
}
2021-05-26 11:49:11 +08:00
tr:nth-child(2n) {
background: #fcfdff;
}
}
ul,
ol {
margin-left: 1em;
}
a {
color: var(--color-teal-1);
2019-09-24 11:47:08 +08:00
}
2019-09-16 16:53:51 +08:00
.neditor {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #e7e8eb;
border-radius: inherit;
font-size: 14px;
}
.toolbar {
display: flex;
height: 34px;
padding: 5px;
line-height: 24px;
border-bottom: 1px solid #e7e8eb;
span {
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
margin: 0 3px;
border-radius: 3px;
input {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
}
.icon {
overflow: hidden;
width: 70%;
height: 70%;
fill: currentColor;
color: #62778d;
}
&:hover {
background: #f7f8fb;
}
}
}
wc-scroll {
overflow: hidden;
2019-09-16 16:53:51 +08:00
flex: 1;
}
.editor {
height: 100%;
padding: 5px 8px;
outline: none;
img {
max-width: 100%;
}
}
2020-07-29 17:46:14 +08:00
:host([disabled]) {
.neditor {
cursor: not-allowed;
opacity: 0.6;
}
}
2021-05-26 19:45:09 +08:00
:host([readonly]) {
.neditor {
cursor: default;
opacity: 0.8;
}
}
:host(:focus-within) {
box-shadow: 0 0 0 2px var(--color-plain-a);
}
2019-09-16 16:53:51 +08:00
.font-layer,
.color-layer,
.link-layer {
visibility: hidden;
position: absolute;
left: 0;
top: 0;
z-index: 99;
width: 80px;
padding: 5px 0;
line-height: 25px;
background: #fff;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
font-size: 13px;
user-select: none;
opacity: 0;
transition: all ease-in-out 0.2s;
&.fadein {
visibility: visible;
top: 34px;
opacity: 1;
}
}
.font-layer {
span {
display: block;
padding: 0 8px;
&:hover {
background: #f7f8fb;
}
}
}
.color-layer {
display: flex;
flex-flow: row wrap;
left: 30px;
width: 96px;
span {
width: 20px;
height: 20px;
margin: 2px;
&:nth-child(1) {
background: #f3f5fb;
}
&:nth-child(2) {
background: #dae1e9;
}
&:nth-child(3) {
background: #62778d;
}
&:nth-child(4) {
background: #58d68d;
}
&:nth-child(5) {
background: #3fc2a7;
}
&:nth-child(6) {
background: #52a3de;
}
&:nth-child(7) {
background: #ac61ce;
}
&:nth-child(8) {
background: #ffb618;
}
&:nth-child(9) {
background: #e67e22;
}
&:nth-child(10) {
background: #ff5061;
}
&:nth-child(11) {
background: #ff0000;
}
&:nth-child(12) {
background: #000000;
}
}
}
.link-layer {
display: flex;
flex-direction: column;
left: 330px;
width: 230px;
padding: 8px;
wc-button {
width: 40px;
margin-top: 8px;
}
}
</style>
<script>
import ICONS from './svg'
import '../form/input'
import '../form/button'
2019-12-19 01:04:15 +08:00
import $ from '../utils'
2019-09-16 16:53:51 +08:00
const ACTTION = {
bold: 'bold',
italic: 'italic',
under: 'underline',
delete: 'strikeThrough',
left: 'justifyLeft',
center: 'justifyCenter',
right: 'justifyRight',
image: 'insertImage',
font: 'fontSize',
color: 'foreColor',
link: 'createLink',
ordered: 'insertOrderedList',
unordered: 'insertUnorderedList'
}
const DEFAULT_TOOLS = [
'font',
'color',
'bold',
'italic',
'under',
'delete',
'ordered',
'unordered',
'left',
'center',
'right',
'link',
2019-11-05 21:15:51 +08:00
'image'
2019-09-16 16:53:51 +08:00
]
function renderToolbar(list) {
return (list || DEFAULT_TOOLS)
.map(
it =>
`<span data-act="${it}"><svg class="icon" viewBox="0 0 1024 1024"><path d="${
ICONS[it]
}"/></svg>${it === 'image' ? '<input type="file">' : ''}</span>`
)
.join('')
}
export default class Editor {
2019-09-16 16:53:51 +08:00
props = {
toolbar: '',
2020-07-29 17:46:14 +08:00
value: '',
readonly: false,
disabled: false
2019-09-16 16:53:51 +08:00
}
state = {
toolbar: null
}
2019-09-16 16:53:51 +08:00
__init__() {
/* render */
var ct = this.root.children[1]
this.__TOOLBAR__ = ct.children[0]
2019-09-16 17:27:16 +08:00
this.__EDITOR__ = ct.children[1].firstElementChild
2019-09-16 16:53:51 +08:00
this.__FONT__ = ct.children[2]
this.__COLOR__ = ct.children[3]
this.__LINK__ = ct.children[4]
this.__LINK_BTN__ = this.__LINK__.querySelector('wc-button')
}
__stat__(name, val) {
2020-07-29 17:46:14 +08:00
var type = typeof val
if (val === this.props[name]) {
2020-07-29 17:46:14 +08:00
return
}
if ((type === name && val) || type !== 'boolean') {
this.props[name] = true
this.setAttribute(name, '')
2020-07-29 17:46:14 +08:00
this.__EDITOR__.removeAttribute('contenteditable')
} else {
this.props[name] = false
this.removeAttribute(name)
2020-07-29 17:46:14 +08:00
this.__EDITOR__.setAttribute('contenteditable', true)
}
}
get readOnly() {
return this.props.readonly
}
set readOnly(val) {
this.__stat__('readonly', val)
}
2020-07-29 17:46:14 +08:00
get disabled() {
return this.props.disabled
}
set disabled(val) {
this.__stat__('disabled', val)
2020-07-29 17:46:14 +08:00
}
2019-09-16 16:53:51 +08:00
get value() {
2019-09-24 11:47:08 +08:00
var html = this.__EDITOR__.innerHTML
if (~html.indexOf('<table>')) {
html =
'<style>table{border-spacing:0;border-collapse:collapse;}table tr{background:#fff;}table thead tr{background:#f3f5fb;}table th,table td{padding:6px 12px;border:1px solid #dae1e9;}table th{font-weight: bold;}</style>' +
html
}
return html
2019-09-16 16:53:51 +08:00
}
set value(val) {
2019-09-16 17:27:16 +08:00
this.__EDITOR__.innerHTML = val
2019-09-16 16:53:51 +08:00
}
_updateToolbar() {
var { toolbar } = this.state
this.__TOOLBAR__.innerHTML = renderToolbar(toolbar)
2019-09-16 16:53:51 +08:00
}
// 保存选中
saveSelection() {
var gs = this.root.getSelection()
if (gs.getRangeAt && gs.rangeCount) {
this.__SELECT__ = gs.getRangeAt(0)
}
}
// 清除选中并重置选中
restoreSelection() {
var gs = this.root.getSelection()
if (this.__SELECT__) {
try {
gs.removeAllRanges()
} catch (err) {}
gs.addRange(this.__SELECT__)
}
}
// 执行命令
exec(cmd, val = '') {
document.execCommand(cmd, false, val)
}
// 处理图片
_handleImage(file) {
this.dispatchEvent(
new CustomEvent('upload', {
detail: {
file,
send: link => {
this.__EDITOR__.focus()
this.restoreSelection()
this.exec(ACTTION.image, link)
this.saveSelection()
// 修正插入的图片,宽度不得超出容器
this.__EDITOR__.querySelectorAll('img').forEach(_ => {
_.style.maxWidth = '100%'
})
}
}
})
)
}
mounted() {
this._updateToolbar()
this.exec('styleWithCSS', true)
const LINK_INPUT = this.__LINK__.querySelector('wc-input')
const FILE_INPUT = this.__TOOLBAR__.querySelector('input')
if (FILE_INPUT) {
2019-12-19 01:04:15 +08:00
$.bind(FILE_INPUT, 'change', ev => {
2019-09-16 16:53:51 +08:00
this._handleImage(FILE_INPUT.files[0])
})
}
/* ------------------------------ */
// 工具栏点击事件
2020-07-29 17:46:14 +08:00
this._toolFn = $.catch(this.__TOOLBAR__, 'click', ev => {
var target = ev.target
var act, val
2019-09-16 17:27:16 +08:00
this.restoreSelection()
2020-07-29 17:46:14 +08:00
2019-09-16 16:53:51 +08:00
if (ev.target === ev.currentTarget) {
return
}
2020-07-29 17:46:14 +08:00
if (this.props.readonly || this.props.disabled) {
2021-05-26 11:49:11 +08:00
return
2020-07-29 17:46:14 +08:00
}
2019-09-16 16:53:51 +08:00
while (target.tagName !== 'SPAN') {
target = target.parentNode
}
2020-07-29 17:46:14 +08:00
act = target.dataset.act
val = ''
2019-09-16 16:53:51 +08:00
switch (act) {
case 'font':
this.__COLOR__.classList.remove('fadein')
this.__LINK__.classList.remove('fadein')
if (this.__FONT__.classList.contains('fadein')) {
this.__FONT__.classList.remove('fadein')
} else {
this.__FONT__.classList.add('fadein')
}
break
case 'color':
this.__LINK__.classList.remove('fadein')
this.__FONT__.classList.remove('fadein')
if (this.__COLOR__.classList.contains('fadein')) {
this.__COLOR__.classList.remove('fadein')
} else {
this.__COLOR__.classList.add('fadein')
}
break
case 'link':
this.__COLOR__.classList.remove('fadein')
this.__FONT__.classList.remove('fadein')
if (this.__LINK__.classList.contains('fadein')) {
this.__LINK__.classList.remove('fadein')
} else {
this.__LINK__.classList.add('fadein')
}
break
case 'image':
// 这里不作任何处理
break
default:
this.__EDITOR__.focus()
this.restoreSelection()
this.exec(ACTTION[act])
this.saveSelection()
}
})
// 字体大小设置
2019-12-19 01:04:15 +08:00
this._fontFn = $.bind(this.__FONT__, 'click', ev => {
2019-09-16 16:53:51 +08:00
if (ev.target === ev.currentTarget) {
return
}
this.__FONT__.classList.remove('fadein')
this.__EDITOR__.focus()
this.restoreSelection()
this.exec(ACTTION.font, ev.target.dataset.size)
this.saveSelection()
})
// 颜色
2019-12-19 01:04:15 +08:00
this._colorFn = $.bind(this.__COLOR__, 'click', ev => {
2019-09-16 16:53:51 +08:00
if (ev.target === ev.currentTarget) {
return
}
this.__COLOR__.classList.remove('fadein')
this.__EDITOR__.focus()
this.restoreSelection()
this.exec(ACTTION.color, ev.target.dataset.color)
this.saveSelection()
})
// 超链接
2021-05-26 11:49:11 +08:00
this.__linkFn = $.bind(this.__LINK_BTN__, 'click', ev => {
2019-09-16 16:53:51 +08:00
if (LINK_INPUT.value) {
this.__LINK__.classList.remove('fadein')
this.__EDITOR__.focus()
this.restoreSelection()
this.exec(ACTTION.link, LINK_INPUT.value)
this.saveSelection()
LINK_INPUT.value = ''
}
})
//监听鼠标事件的,以缓存选中状态
2019-12-19 01:04:15 +08:00
this.__mouseFn = $.bind(this.__EDITOR__, 'mouseleave', ev => {
2019-09-16 16:53:51 +08:00
this.saveSelection()
})
2019-12-19 01:04:15 +08:00
$.outside(this, ev => {
2019-09-16 17:27:16 +08:00
this.__FONT__.classList.remove('fadein')
this.__COLOR__.classList.remove('fadein')
this.__LINK__.classList.remove('fadein')
})
2019-09-16 16:53:51 +08:00
// 粘贴板事件
2019-12-19 01:04:15 +08:00
this.__pasteFn = $.bind(this.__EDITOR__, 'paste', ev => {
2019-09-16 16:53:51 +08:00
ev.preventDefault()
2019-09-24 11:47:08 +08:00
var html = ev.clipboardData.getData('text/html')
2019-09-16 16:53:51 +08:00
var txt = ev.clipboardData.getData('text/plain')
var items = ev.clipboardData.items
2019-09-24 11:47:08 +08:00
if (html) {
html = html
.replace(/\t/g, ' ')
2019-11-01 20:01:41 +08:00
.replace(/<\/?(meta|link|script)[^>]*?>/g, '')
.replace(/<!--[\w\W]*?-->/g, '')
2019-09-24 11:47:08 +08:00
.replace(
/<a[^>]*? href\s?=\s?["']?([^"']*)["']?[^>]*?>/g,
'<a href="$1">'
)
.replace(
2019-12-12 12:10:15 +08:00
/<img[^>]*? src\s?=\s?["']?([^"']*)["']?[^>]*?>/g,
2019-09-24 11:47:08 +08:00
'<img src="$1">'
)
.replace(/<(?!a|img)([\w\-]+)[^>]*>/g, '<$1>')
2020-07-29 17:46:14 +08:00
.replace(/<xml[^>]*?>[\w\W]*?<\/xml>/g, '')
.replace(/<style>[\w\W]*?<\/style>/g, '')
2019-09-24 11:47:08 +08:00
return this.exec('insertHtml', html)
}
2019-09-16 16:53:51 +08:00
if (txt) {
return this.exec('insertText', txt)
}
if (items && items.length) {
let blob = null
for (let it of items) {
if (it.type.indexOf('image') > -1) {
blob = it.getAsFile()
}
}
this._handleImage(blob)
}
})
this.__observer = new MutationObserver(_ => {
this.dispatchEvent(new CustomEvent('input'))
2019-09-16 16:53:51 +08:00
})
this.__observer.observe(this.__EDITOR__, {
childList: true,
subtree: true,
characterData: true
})
}
2021-05-07 16:06:33 +08:00
unmounted() {
2019-12-19 01:04:15 +08:00
$.unbind(this.__TOOLBAR__, 'click', this.__toolFn)
$.unbind(this.__FONT__, 'click', this.__fontFn)
$.unbind(this.__COLOR__, 'click', this.__colorFn)
$.unbind(this.__LINK_BTN__, 'click', this.__linkFn)
$.unbind(this.__EDITOR__, 'mouseleave', this.__mouseFn)
$.unbind(this.__EDITOR__, 'paste', this.__pasteFn)
2019-09-16 16:53:51 +08:00
this.__observer.disconnect()
}
watch() {
switch (name) {
case 'toolbar':
if (val === null) {
this.state.toolbar = [...DEFAULT_TOOLS]
} else if (val) {
2019-09-16 16:53:51 +08:00
val = val.split(',').map(it => it.trim())
this.state.toolbar = val
2019-09-16 16:53:51 +08:00
}
break
case 'value':
this.value = val
break
case 'readonly':
case 'disabled':
2020-07-29 17:46:14 +08:00
var k = name
if (k === 'readonly') {
k = 'readOnly'
}
this[k] = val !== null
2019-09-16 16:53:51 +08:00
break
default:
break
}
}
}
</script>