完成富文本编辑器的移植
parent
e057349494
commit
d5e8ddb613
|
@ -4,7 +4,19 @@
|
||||||
* @date 2023/09/14 16:49:15
|
* @date 2023/09/14 16:49:15
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { css, raw, html, Component, nextTick, styleMap } from 'wkit'
|
import {
|
||||||
|
css,
|
||||||
|
raw,
|
||||||
|
html,
|
||||||
|
Component,
|
||||||
|
bind,
|
||||||
|
unbind,
|
||||||
|
nextTick,
|
||||||
|
styleMap,
|
||||||
|
classMap,
|
||||||
|
outsideClick,
|
||||||
|
clearOutsideClick
|
||||||
|
} from 'wkit'
|
||||||
import ICONS from './svg.js'
|
import ICONS from './svg.js'
|
||||||
import '../form/input.js'
|
import '../form/input.js'
|
||||||
import '../form/button.js'
|
import '../form/button.js'
|
||||||
|
@ -34,11 +46,13 @@ const DEFAULT_TOOLS = [
|
||||||
'delete',
|
'delete',
|
||||||
'ordered',
|
'ordered',
|
||||||
'unordered',
|
'unordered',
|
||||||
|
'table',
|
||||||
'left',
|
'left',
|
||||||
'center',
|
'center',
|
||||||
'right',
|
'right',
|
||||||
'link',
|
'link',
|
||||||
'image'
|
'image',
|
||||||
|
'fullscreen'
|
||||||
]
|
]
|
||||||
|
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
|
@ -56,7 +70,18 @@ const COLORS = [
|
||||||
'#000000'
|
'#000000'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 获取一维数组转二维的行
|
||||||
|
function getY(i) {
|
||||||
|
return (i / 9) >> 0
|
||||||
|
}
|
||||||
|
//获取一维数组转二维的列
|
||||||
|
function getX(i) {
|
||||||
|
return i % 9
|
||||||
|
}
|
||||||
|
|
||||||
class Editor extends Component {
|
class Editor extends Component {
|
||||||
|
static watches = ['value']
|
||||||
|
|
||||||
static props = {
|
static props = {
|
||||||
toolbar: {
|
toolbar: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -70,9 +95,18 @@ class Editor extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
value: 'str!',
|
readonly: {
|
||||||
readonly: false,
|
type: Boolean,
|
||||||
disabled: false
|
observer(v) {
|
||||||
|
this.#updateStat()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
observer(v) {
|
||||||
|
this.#updateStat()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = [
|
static styles = [
|
||||||
|
@ -84,6 +118,7 @@ class Editor extends Component {
|
||||||
max-height: 720px;
|
max-height: 720px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
transition: box-shadow 0.15s linear;
|
transition: box-shadow 0.15s linear;
|
||||||
|
background: var(--wc-editor-background, #fff);
|
||||||
}
|
}
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -132,7 +167,7 @@ class Editor extends Component {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #e7e8eb;
|
border: 1px solid var(--wc-editor-border-color, var(--color-grey-2));
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
@ -141,7 +176,7 @@ class Editor extends Component {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
border-bottom: 1px solid #e7e8eb;
|
border-bottom: 1px solid var(--color-grey-1);
|
||||||
|
|
||||||
span {
|
span {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -189,10 +224,13 @@ class Editor extends Component {
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-vertical {
|
.is-vertical {
|
||||||
// visibility: hidden;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -200,17 +238,15 @@ class Editor extends Component {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
// opacity: 0;
|
|
||||||
transition: opacity 0.3s linear, visibility 0.3s linear;
|
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
display: block;
|
display: block;
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 90px;
|
height: 0;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: rgba(44, 47, 53, 0.25);
|
background: rgba(44, 47, 53, 0.25);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
transition: width 0.1s linear, height 0.1s linear;
|
transition: width 0.1s linear;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
|
@ -222,6 +258,7 @@ class Editor extends Component {
|
||||||
.typearea {
|
.typearea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 54px;
|
||||||
outline: none;
|
outline: none;
|
||||||
text-wrap: wrap;
|
text-wrap: wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
@ -233,10 +270,10 @@ class Editor extends Component {
|
||||||
`,
|
`,
|
||||||
|
|
||||||
css`
|
css`
|
||||||
:host(:hover) {
|
:host([readonly]) {
|
||||||
.is-vertical {
|
.editor {
|
||||||
visibility: visible;
|
cursor: default;
|
||||||
opacity: 1;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
:host([disabled]) {
|
:host([disabled]) {
|
||||||
|
@ -245,13 +282,6 @@ class Editor extends Component {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([readonly]) {
|
|
||||||
.editor {
|
|
||||||
cursor: default;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:host([readonly]),
|
:host([readonly]),
|
||||||
:host([disabled]) {
|
:host([disabled]) {
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
@ -264,6 +294,17 @@ class Editor extends Component {
|
||||||
:host(:focus-within) {
|
:host(:focus-within) {
|
||||||
box-shadow: 0 0 0 2px var(--color-plain-a);
|
box-shadow: 0 0 0 2px var(--color-plain-a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host(.fullscreen) {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 9;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 100vh;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
|
|
||||||
css`
|
css`
|
||||||
|
@ -271,7 +312,8 @@ class Editor extends Component {
|
||||||
|
|
||||||
.font-layer,
|
.font-layer,
|
||||||
.color-layer,
|
.color-layer,
|
||||||
.link-layer {
|
.link-layer,
|
||||||
|
.table-layer {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -326,6 +368,27 @@ class Editor extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-layer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
left: 240px;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
padding: 2px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
span {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--color-plain-1);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: rgba(77, 182, 172, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.link-layer {
|
.link-layer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -342,6 +405,384 @@ class Editor extends Component {
|
||||||
]
|
]
|
||||||
|
|
||||||
#toolbar = []
|
#toolbar = []
|
||||||
|
#value = ''
|
||||||
|
#cache = { bar: 0, y: 0 }
|
||||||
|
#gridx = 0
|
||||||
|
#gridy = 0
|
||||||
|
|
||||||
|
#select = null
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
let html = this.$refs.editor?.innerHTML || ''
|
||||||
|
|
||||||
|
html = html.replace(/<\!\-\-(.*?)\-\->/g, '')
|
||||||
|
if (~html.indexOf('<table>') && !html.startsWith('<style>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
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(val) {
|
||||||
|
if (this.$refs.editor) {
|
||||||
|
this.#value = val
|
||||||
|
this.$refs.editor.innerHTML = val
|
||||||
|
} else {
|
||||||
|
nextTick(_ => (this.value = val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__init__() {
|
||||||
|
//
|
||||||
|
let { outer, inner, thumb } = this.$refs
|
||||||
|
let height = outer.offsetHeight
|
||||||
|
let scrollHeight = inner.scrollHeight + 10
|
||||||
|
|
||||||
|
let bar = 50 // 滚动条的高度
|
||||||
|
bar = (height * (height / scrollHeight)) >> 0
|
||||||
|
|
||||||
|
if (bar < 50) {
|
||||||
|
bar = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
// 100%或主体高度比滚动条还短时不显示
|
||||||
|
if (bar >= height) {
|
||||||
|
bar = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#cache.bar = bar
|
||||||
|
thumb.style.height = bar + 'px'
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateStat() {
|
||||||
|
if (this.$refs.editor) {
|
||||||
|
if (this.readOnly || this.disabled) {
|
||||||
|
this.$refs.editor.removeAttribute('contenteditable')
|
||||||
|
} else {
|
||||||
|
this.$refs.editor.setAttribute('contenteditable', '')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nextTick(_ => this.#updateStat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#fetchScroll(moveY) {
|
||||||
|
let { bar } = this.#cache
|
||||||
|
let { outer, thumb, inner } = this.$refs
|
||||||
|
let height = outer.offsetHeight
|
||||||
|
let scrollHeight = inner.scrollHeight + 10
|
||||||
|
|
||||||
|
if (moveY < 0) {
|
||||||
|
moveY = 0
|
||||||
|
} else if (moveY > height - bar) {
|
||||||
|
moveY = height - bar
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.scrollTop = (scrollHeight - height) * (moveY / (height - bar))
|
||||||
|
thumb.style.transform = `translateY(${moveY}px)`
|
||||||
|
return moveY
|
||||||
|
}
|
||||||
|
|
||||||
|
#hideLayers() {
|
||||||
|
this.$refs.font.classList.remove('fadein')
|
||||||
|
this.$refs.color.classList.remove('fadein')
|
||||||
|
this.$refs.link.classList.remove('fadein')
|
||||||
|
this.$refs.table.classList.remove('fadein')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理图片
|
||||||
|
#handleImage(ev, file) {
|
||||||
|
if (ev && ev.type === 'change') {
|
||||||
|
file = ev.target.files[0]
|
||||||
|
ev.target.value = ''
|
||||||
|
}
|
||||||
|
this.$emit('upload', {
|
||||||
|
detail: {
|
||||||
|
file,
|
||||||
|
send: link => {
|
||||||
|
this.$refs.editor.focus()
|
||||||
|
this.restoreSelection()
|
||||||
|
this.exec(ACTTION.image, link)
|
||||||
|
this.saveSelection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#handlePaste(ev) {
|
||||||
|
let html = ev.clipboardData.getData('text/html')
|
||||||
|
let txt = ev.clipboardData.getData('text/plain')
|
||||||
|
let items = ev.clipboardData.items
|
||||||
|
|
||||||
|
// 先文件判断, 避免右键单击复制图片时, 当成html处理
|
||||||
|
if (items && items.length) {
|
||||||
|
let blob = null
|
||||||
|
for (let it of items) {
|
||||||
|
if (it.type.indexOf('image') > -1) {
|
||||||
|
blob = it.getAsFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blob) {
|
||||||
|
return this.#handleImage(null, blob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (html) {
|
||||||
|
html = html
|
||||||
|
.replace(/\t/g, ' ')
|
||||||
|
.replace(/<\/?(meta|link|script)[^>]*?>/g, '')
|
||||||
|
.replace(/<!--[\w\W]*?-->/g, '')
|
||||||
|
.replace(
|
||||||
|
/<a[^>]*? href\s?=\s?["']?([^"']*)["']?[^>]*?>/g,
|
||||||
|
'<a href="$1">'
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<img[^>]*? src\s?=\s?["']?([^"']*)["']?[^>]*?>/g,
|
||||||
|
'<img src="$1">'
|
||||||
|
)
|
||||||
|
.replace(/<(?!a|img)([\w\-]+)[^>]*>/g, '<$1>')
|
||||||
|
.replace(/<xml[^>]*?>[\w\W]*?<\/xml>/g, '')
|
||||||
|
.replace(/<style>[\w\W]*?<\/style>/g, '')
|
||||||
|
|
||||||
|
return this.exec('insertHtml', html)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txt) {
|
||||||
|
return this.exec('insertText', txt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbarClick(ev) {
|
||||||
|
var target = ev.target
|
||||||
|
var act
|
||||||
|
|
||||||
|
this.restoreSelection()
|
||||||
|
|
||||||
|
if (ev.target === ev.currentTarget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.readOnly || this.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
while (target.tagName !== 'SPAN') {
|
||||||
|
target = target.parentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
act = target.dataset.act
|
||||||
|
|
||||||
|
this.#hideLayers()
|
||||||
|
|
||||||
|
switch (act) {
|
||||||
|
case 'font':
|
||||||
|
case 'color':
|
||||||
|
case 'link':
|
||||||
|
case 'table':
|
||||||
|
this.$refs[act].classList.add('fadein')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'image':
|
||||||
|
// 这里不作任何处理
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'copy':
|
||||||
|
navigator.clipboard.writeText(this.value)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'fullscreen':
|
||||||
|
this.classList.toggle('fullscreen')
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.$refs.editor.focus()
|
||||||
|
this.restoreSelection()
|
||||||
|
this.exec(ACTTION[act])
|
||||||
|
this.saveSelection()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#chnageFontSize(ev) {
|
||||||
|
if (ev.target === ev.currentTarget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$refs.font.classList.remove('fadein')
|
||||||
|
this.$refs.editor.focus()
|
||||||
|
this.restoreSelection()
|
||||||
|
this.exec(ACTTION.font, ev.target.dataset.size)
|
||||||
|
this.saveSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
#chnageColor(ev) {
|
||||||
|
if (ev.target === ev.currentTarget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$refs.color.classList.remove('fadein')
|
||||||
|
this.$refs.editor.focus()
|
||||||
|
this.restoreSelection()
|
||||||
|
this.exec(ACTTION.color, ev.target.dataset.value)
|
||||||
|
this.saveSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
#insertLink(ev) {
|
||||||
|
let value = this.$refs.linkinput.value.trim()
|
||||||
|
if (value) {
|
||||||
|
this.$refs.link.classList.remove('fadein')
|
||||||
|
this.$refs.editor.focus()
|
||||||
|
this.restoreSelection()
|
||||||
|
this.exec(ACTTION.link, value)
|
||||||
|
this.saveSelection()
|
||||||
|
this.$refs.linkinput.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#insertTable(ev) {
|
||||||
|
let th = `<th> </th>`.repeat(this.#gridx + 1)
|
||||||
|
let td = `<td> </td>`.repeat(this.#gridx + 1)
|
||||||
|
this.exec(
|
||||||
|
'insertHtml',
|
||||||
|
`<br>
|
||||||
|
<table>
|
||||||
|
<thead><tr>${th}</tr></thead>
|
||||||
|
<tbody>${`<tr>${td}</tr>`.repeat(this.#gridy + 1)}</tbody>
|
||||||
|
</table><br>`
|
||||||
|
)
|
||||||
|
this.$refs.table.classList.remove('fadein')
|
||||||
|
}
|
||||||
|
|
||||||
|
#tableSelect(ev) {
|
||||||
|
let grids = Array.from(this.$refs.table.children)
|
||||||
|
if (ev.type === 'mousemove') {
|
||||||
|
if (ev.target === ev.currentTarget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let idx = +ev.target.dataset.idx
|
||||||
|
let x = getX(idx)
|
||||||
|
let y = getY(idx)
|
||||||
|
|
||||||
|
if (x === this.#gridx && y === this.#gridy) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.#gridx = x
|
||||||
|
this.#gridy = y
|
||||||
|
|
||||||
|
for (let i = 0; i < grids.length; i++) {
|
||||||
|
let _x = getX(i)
|
||||||
|
let _y = getY(i)
|
||||||
|
grids[i].classList.toggle('active', _x <= x && _y <= y)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
grids.forEach(it => it.classList.remove('active'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存选中
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
let startY,
|
||||||
|
moveY,
|
||||||
|
mousemoveFn = ev => {
|
||||||
|
let { y } = this.#cache
|
||||||
|
if (startY !== undefined) {
|
||||||
|
moveY = this.#fetchScroll(y + ev.pageY - startY)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mouseupFn = ev => {
|
||||||
|
startY = undefined
|
||||||
|
this.#cache.y = moveY || 0
|
||||||
|
delete this._active
|
||||||
|
unbind(document, 'mousemove', mousemoveFn)
|
||||||
|
unbind(document, 'mouseup', mouseupFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.exec('styleWithCSS', true)
|
||||||
|
|
||||||
|
bind(this.$refs.thumb, 'mousedown', ev => {
|
||||||
|
startY = ev.pageY
|
||||||
|
|
||||||
|
this._active = true
|
||||||
|
this.#hideLayers()
|
||||||
|
|
||||||
|
bind(document, 'mousemove', mousemoveFn)
|
||||||
|
bind(document, 'mouseup', mouseupFn)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 鼠标滚动事件
|
||||||
|
bind(this.$refs.inner, 'scroll', ev => {
|
||||||
|
// 拖拽时忽略滚动事件
|
||||||
|
if (this._active) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let { bar, y } = this.#cache
|
||||||
|
let { outer, thumb, inner } = this.$refs
|
||||||
|
let height = outer.offsetHeight
|
||||||
|
let scrollHeight = inner.scrollHeight + 10
|
||||||
|
let scrollTop = inner.scrollTop
|
||||||
|
|
||||||
|
this.#hideLayers()
|
||||||
|
|
||||||
|
// y轴 都为0时, 不作任何处理
|
||||||
|
if (bar === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修正滚动条的位置
|
||||||
|
// 滚动比例 y 滚动条的可移动距离
|
||||||
|
let fixedY = ~~((scrollTop / (scrollHeight - height)) * (height - bar))
|
||||||
|
|
||||||
|
if (fixedY !== y) {
|
||||||
|
this.#cache.y = fixedY
|
||||||
|
thumb.style.transform = `translateY(${fixedY}px)`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this._clickoutsideFn = outsideClick(this, _ => this.#hideLayers())
|
||||||
|
|
||||||
|
this._scrollFn = new ResizeObserver(this.__init__.bind(this))
|
||||||
|
this._scrollFn.observe(this.$refs.cont)
|
||||||
|
|
||||||
|
this._inputFn = new MutationObserver(_ => {
|
||||||
|
this.$emit('input')
|
||||||
|
})
|
||||||
|
|
||||||
|
this._inputFn.observe(this.$refs.editor, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
characterData: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
this._scrollFn?.disconnect()
|
||||||
|
this._inputFn?.disconnect()
|
||||||
|
|
||||||
|
clearOutsideClick(this._clickoutsideFn)
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
toolbar = [
|
toolbar = [
|
||||||
|
@ -351,7 +792,7 @@ class Editor extends Component {
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<section class="toolbar">
|
<section class="toolbar" @click=${this.#toolbarClick}>
|
||||||
${toolbar.map(
|
${toolbar.map(
|
||||||
it => html`
|
it => html`
|
||||||
<span data-act=${it}>
|
<span data-act=${it}>
|
||||||
|
@ -359,35 +800,65 @@ class Editor extends Component {
|
||||||
<path d=${ICONS[it]} />
|
<path d=${ICONS[it]} />
|
||||||
</svg>
|
</svg>
|
||||||
${it === 'image'
|
${it === 'image'
|
||||||
? html`<input type="file" accept="image/*" />`
|
? html`<input
|
||||||
|
type="file"
|
||||||
|
:disabled=${this.readOnly || this.disabled}
|
||||||
|
@change=${this.#handleImage}
|
||||||
|
accept="image/*"
|
||||||
|
/>`
|
||||||
: ''}
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
<div class="scroll-outerbox">
|
<div class="scroll-outerbox" ref="outer">
|
||||||
<div class="scroll-innerbox">
|
<div class="scroll-innerbox" ref="inner">
|
||||||
<div contenteditable="true" class="typearea" spellcheck="false">
|
<div class="wrapper" ref="cont" @click=${this.#hideLayers}>
|
||||||
${raw(this.value)}
|
<div
|
||||||
|
ref="editor"
|
||||||
|
contenteditable="true"
|
||||||
|
class="typearea"
|
||||||
|
@mouseleave=${this.saveSelection}
|
||||||
|
@paste.prevent=${this.#handlePaste}
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="is-vertical noselect">
|
<div class="is-vertical noselect">
|
||||||
<span ref="y" class="thumb"></span>
|
<span class="thumb" ref="thumb"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-layer noselect">
|
<div
|
||||||
|
class="font-layer noselect"
|
||||||
|
ref="font"
|
||||||
|
@click=${this.#chnageFontSize}
|
||||||
|
>
|
||||||
<span data-size="6">6号字体</span>
|
<span data-size="6">6号字体</span>
|
||||||
<span data-size="5">5号字体</span>
|
<span data-size="5">5号字体</span>
|
||||||
<span data-size="4">4号字体</span>
|
<span data-size="4">4号字体</span>
|
||||||
<span data-size="3">3号字体</span>
|
<span data-size="3">3号字体</span>
|
||||||
<span data-size="2">2号字体</span>
|
<span data-size="2">2号字体</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-layer noselect">
|
<div
|
||||||
|
class="color-layer noselect"
|
||||||
|
ref="color"
|
||||||
|
@click=${this.#chnageColor}
|
||||||
|
>
|
||||||
${COLORS.map(c => html`<span data-value=${c}></span>`)}
|
${COLORS.map(c => html`<span data-value=${c}></span>`)}
|
||||||
</div>
|
</div>
|
||||||
<div class="link-layer noselect">
|
<div
|
||||||
<wc-input label="请输入链接地址"></wc-input>
|
class="table-layer noselect"
|
||||||
<wc-button color="teal" size="mini">插入</wc-button>
|
ref="table"
|
||||||
|
@click=${this.#insertTable}
|
||||||
|
@mousemove=${this.#tableSelect}
|
||||||
|
@mouseleave=${this.#tableSelect}
|
||||||
|
>
|
||||||
|
${Array(81)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, n) => html`<span data-idx=${n}></span>`)}
|
||||||
|
</div>
|
||||||
|
<div class="link-layer noselect" ref="link">
|
||||||
|
<wc-input ref="linkinput" label="请输入链接地址"></wc-input>
|
||||||
|
<wc-button size="m" @click=${this.#insertLink}>插入</wc-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
|
@ -24,8 +24,12 @@ export default {
|
||||||
'M232 872h560v-80H232v80m280-160c132.4 0 240-107.6 240-240V152H652v320c0 77.2-62.8 140-140 140s-140-62.8-140-140V152H272v320c0 132.4 107.6 240 240 240z',
|
'M232 872h560v-80H232v80m280-160c132.4 0 240-107.6 240-240V152H652v320c0 77.2-62.8 140-140 140s-140-62.8-140-140V152H272v320c0 132.4 107.6 240 240 240z',
|
||||||
center:
|
center:
|
||||||
'M128 128h768v85.333H128V128m170.667 170.667h426.666V384H298.667v-85.333M128 469.333h768v85.334H128v-85.334M298.667 640h426.666v85.333H298.667V640M128 810.667h768V896H128v-85.333z',
|
'M128 128h768v85.333H128V128m170.667 170.667h426.666V384H298.667v-85.333M128 469.333h768v85.334H128v-85.334M298.667 640h426.666v85.333H298.667V640M128 810.667h768V896H128v-85.333z',
|
||||||
|
table:
|
||||||
|
'M860 159H164c-19.88 0-36 16.93-36 37.82v630.36c0 20.89 16.12 37.82 36 37.82h696c19.88 0 36-16.93 36-37.82V196.82c0-20.89-16.12-37.82-36-37.82z m-422 26.74c20.98 0 38 17.01 38 38 0 20.98-17.02 38-38 38-20.99 0-38-17.01-38-38s17.02-38 38-38z m-108 0c20.98 0 38 17.01 38 38 0 20.98-17.02 38-38 38-20.99 0-38-17.01-38-38s17.01-38 38-38z m-108 0c20.98 0 38 17.01 38 38 0 20.98-17.01 38-38 38s-38-17.01-38-38 17.01-38 38-38zM664 808V664h176v144H664z m-64-144v144H424V664h176z m-416 0h176v144H184V664z m480-190h176v144H664V474zM424 618V474h176v144H424z m-64 0H184V474h176v144z m480-338v144H664V280h176z m-240 0v144H424V280h176z m-385.54 0H360v144H184V280h30.46z',
|
||||||
unordered:
|
unordered:
|
||||||
'M298.667 213.333v85.334H896v-85.334M298.667 554.667H896v-85.334H298.667m0 341.334H896v-85.334H298.667m-128-14.08c-31.574 0-56.747 25.6-56.747 56.747s25.6 56.747 56.747 56.747c31.146 0 56.746-25.6 56.746-56.747s-25.173-56.747-56.746-56.747m0-519.253c-35.414 0-64 28.587-64 64s28.586 64 64 64c35.413 0 64-28.587 64-64s-28.587-64-64-64m0 256c-35.414 0-64 28.587-64 64s28.586 64 64 64c35.413 0 64-28.587 64-64s-28.587-64-64-64z',
|
'M298.667 213.333v85.334H896v-85.334M298.667 554.667H896v-85.334H298.667m0 341.334H896v-85.334H298.667m-128-14.08c-31.574 0-56.747 25.6-56.747 56.747s25.6 56.747 56.747 56.747c31.146 0 56.746-25.6 56.746-56.747s-25.173-56.747-56.746-56.747m0-519.253c-35.414 0-64 28.587-64 64s28.586 64 64 64c35.413 0 64-28.587 64-64s-28.587-64-64-64m0 256c-35.414 0-64 28.587-64 64s28.586 64 64 64c35.413 0 64-28.587 64-64s-28.587-64-64-64z',
|
||||||
right:
|
right:
|
||||||
'M128 128h768v85.333H128V128m256 170.667h512V384H384v-85.333M128 469.333h768v85.334H128v-85.334M384 640h512v85.333H384V640M128 810.667h768V896H128v-85.333z'
|
'M128 128h768v85.333H128V128m256 170.667h512V384H384v-85.333M128 469.333h768v85.334H128v-85.334M384 640h512v85.333H384V640M128 810.667h768V896H128v-85.333z',
|
||||||
|
fullscreen:
|
||||||
|
'M597.33 449.42l40.02 41.25 94.25-93.75 36.4 35V320H654.14l37.44 36.3-94.25 93.12z m-170.66 124.6l-40.41-40.68-93.53 93.93L256 592.76l0.95 111.24 113.96-0.9-37.78-35.78 93.54-93.3z m211.91-40.69l-41.25 40.02 93.75 94.24-35 36.4H768V590.14l-36.3 37.44-93.12-94.25z m-252.86-42.66l40.95-40.23-93.84-93.87L367.56 320l-111.56 0.5 0.48 113.91 36.02-37.62 93.22 93.88zM848 832H176c-26.47 0-48-19.57-48-43.64V235.64c0-24.06 21.53-43.64 48-43.64h672c26.47 0 48 19.58 48 43.64v552.73c0 24.06-21.53 43.63-48 43.63zM206.55 256c-8.02 0-14.55 5.74-14.55 12.8v486.4c0 7.06 6.53 12.8 14.55 12.8h610.91c8.02 0 14.55-5.74 14.55-12.8V268.8c0-7.06-6.53-12.8-14.55-12.8H206.55z'
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue