完成markdown编辑器的重构
parent
5dec1ebdb7
commit
b52c95f210
|
@ -6,14 +6,11 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
raw,
|
|
||||||
html,
|
html,
|
||||||
Component,
|
Component,
|
||||||
bind,
|
bind,
|
||||||
unbind,
|
range,
|
||||||
nextTick,
|
nextTick,
|
||||||
styleMap,
|
|
||||||
classMap,
|
|
||||||
outsideClick,
|
outsideClick,
|
||||||
clearOutsideClick
|
clearOutsideClick
|
||||||
} from 'wkit'
|
} from 'wkit'
|
||||||
|
@ -201,49 +198,24 @@ class Editor extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-outerbox {
|
.wrapper {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
padding: 5px 8px;
|
|
||||||
}
|
|
||||||
.scroll-innerbox {
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: 100%;
|
flex: 1;
|
||||||
height: 100%;
|
padding: 6px 12px;
|
||||||
scrollbar-width: 0;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-vertical {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 10px;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.thumb {
|
|
||||||
display: block;
|
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 0;
|
}
|
||||||
border-radius: 5px;
|
|
||||||
background: rgba(44, 47, 53, 0.25);
|
|
||||||
cursor: default;
|
|
||||||
transition: width 0.1s linear;
|
|
||||||
|
|
||||||
&:hover {
|
&::-webkit-scrollbar-thumb {
|
||||||
width: 10px;
|
visibility: hidden;
|
||||||
background: rgba(44, 47, 53, 0.5);
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover::-webkit-scrollbar-thumb {
|
||||||
|
visibility: visible;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,6 +223,7 @@ class Editor extends Component {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 54px;
|
min-height: 54px;
|
||||||
|
line-height: 1.5;
|
||||||
outline: none;
|
outline: none;
|
||||||
text-wrap: wrap;
|
text-wrap: wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
@ -397,7 +370,6 @@ class Editor extends Component {
|
||||||
]
|
]
|
||||||
|
|
||||||
#value = ''
|
#value = ''
|
||||||
#cache = { bar: 0, y: 0 }
|
|
||||||
#gridx = 0
|
#gridx = 0
|
||||||
#gridy = 0
|
#gridy = 0
|
||||||
|
|
||||||
|
@ -419,33 +391,12 @@ class Editor extends Component {
|
||||||
if (this.$refs.editor) {
|
if (this.$refs.editor) {
|
||||||
this.#value = val
|
this.#value = val
|
||||||
this.$refs.editor.innerHTML = val
|
this.$refs.editor.innerHTML = val
|
||||||
|
this.$emit('input')
|
||||||
} else {
|
} else {
|
||||||
nextTick(_ => (this.value = val))
|
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() {
|
#updateStat() {
|
||||||
if (this.$refs.editor) {
|
if (this.$refs.editor) {
|
||||||
if (this.readOnly || this.disabled) {
|
if (this.readOnly || this.disabled) {
|
||||||
|
@ -458,23 +409,6 @@ class Editor extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#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() {
|
#hideLayers() {
|
||||||
this.$refs.font.classList.remove('fadein')
|
this.$refs.font.classList.remove('fadein')
|
||||||
this.$refs.color.classList.remove('fadein')
|
this.$refs.color.classList.remove('fadein')
|
||||||
|
@ -707,83 +641,11 @@ class Editor extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
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)
|
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._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() {
|
unmounted() {
|
||||||
this._scrollFn?.disconnect()
|
|
||||||
this._inputFn?.disconnect()
|
|
||||||
clearOutsideClick(this._clickoutsideFn)
|
clearOutsideClick(this._clickoutsideFn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -809,22 +671,17 @@ class Editor extends Component {
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
<div class="scroll-outerbox" ref="outer">
|
|
||||||
<div class="scroll-innerbox" ref="inner">
|
<div class="wrapper" ref="cont" @click=${this.#hideLayers}>
|
||||||
<div class="wrapper" ref="cont" @click=${this.#hideLayers}>
|
<div
|
||||||
<div
|
ref="editor"
|
||||||
ref="editor"
|
contenteditable="true"
|
||||||
contenteditable="true"
|
class="typearea"
|
||||||
class="typearea"
|
@mouseleave=${this.saveSelection}
|
||||||
@mouseleave=${this.saveSelection}
|
@paste.prevent=${this.#handlePaste}
|
||||||
@paste.prevent=${this.#handlePaste}
|
></div>
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="is-vertical noselect">
|
|
||||||
<span class="thumb" ref="thumb"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="font-layer noselect"
|
class="font-layer noselect"
|
||||||
ref="font"
|
ref="font"
|
||||||
|
@ -836,6 +693,7 @@ class Editor extends Component {
|
||||||
<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
|
<div
|
||||||
class="color-layer noselect"
|
class="color-layer noselect"
|
||||||
ref="color"
|
ref="color"
|
||||||
|
@ -843,6 +701,7 @@ class Editor extends Component {
|
||||||
>
|
>
|
||||||
${COLORS.map(c => html`<span data-value=${c}></span>`)}
|
${COLORS.map(c => html`<span data-value=${c}></span>`)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="table-layer noselect"
|
class="table-layer noselect"
|
||||||
ref="table"
|
ref="table"
|
||||||
|
@ -850,12 +709,11 @@ class Editor extends Component {
|
||||||
@mousemove=${this.#tableSelect}
|
@mousemove=${this.#tableSelect}
|
||||||
@mouseleave=${this.#tableSelect}
|
@mouseleave=${this.#tableSelect}
|
||||||
>
|
>
|
||||||
${Array(81)
|
${range(81).map((_, n) => html`<span data-idx=${n}></span>`)}
|
||||||
.fill(0)
|
|
||||||
.map((_, n) => html`<span data-idx=${n}></span>`)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="link-layer noselect" ref="link">
|
<div class="link-layer noselect" ref="link">
|
||||||
<wc-input ref="linkinput" label="请输入链接地址"></wc-input>
|
<wc-input ref="linkinput" placeholder="请输入链接地址"></wc-input>
|
||||||
<wc-button size="m" @click=${this.#insertLink}>插入</wc-button>
|
<wc-button size="m" @click=${this.#insertLink}>插入</wc-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,134 +4,94 @@
|
||||||
* @date 2020/10/14 17:52:44
|
* @date 2020/10/14 17:52:44
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { offset } from 'wkit'
|
const PLACEHOLDER = '在此输入文本'
|
||||||
|
|
||||||
var placeholder = '在此输入文本'
|
|
||||||
|
|
||||||
function trim(str, sign) {
|
function trim(str, sign) {
|
||||||
return str.replace(new RegExp('^' + sign + '|' + sign + '$', 'g'), '')
|
return str.replace(new RegExp('^' + sign + '|' + sign + '$', 'g'), '')
|
||||||
}
|
}
|
||||||
|
|
||||||
function docScroll(k = 'X') {
|
|
||||||
return window[`page${k.toUpperCase()}Offset`]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通用的弹层触发
|
|
||||||
function showDialog(dialog, elem) {
|
|
||||||
var { left, top } = offset(elem)
|
|
||||||
left -= docScroll('X')
|
|
||||||
top += 29 - docScroll('Y')
|
|
||||||
left += 'px'
|
|
||||||
top += 'px'
|
|
||||||
dialog.moveTo({ top, left })
|
|
||||||
dialog.show()
|
|
||||||
return Promise.resolve(dialog)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
header(elem) {
|
header() {
|
||||||
this.$refs.header.classList.add('fadein')
|
this.$refs.header.classList.add('fadein')
|
||||||
},
|
},
|
||||||
|
|
||||||
h(level) {
|
h(level) {
|
||||||
var wrap = this.selection(true) || placeholder
|
var wrap = this.selection(true) || PLACEHOLDER
|
||||||
wrap = wrap.replace(/^(#+ )?/, '#'.repeat(level) + ' ')
|
wrap = wrap.replace(/^(#+ )?/, '#'.repeat(level) + ' ')
|
||||||
this.insert(wrap, true)
|
this.insert(wrap, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
quote(elem) {
|
quote() {
|
||||||
var wrap = this.selection(true) || placeholder
|
var wrap = this.selection(true) || PLACEHOLDER
|
||||||
wrap = wrap.replace(/^(>+ )?/, '> ')
|
wrap = wrap.replace(/^(>+ )?/, '> ')
|
||||||
|
|
||||||
this.insert(wrap, true)
|
this.insert(wrap, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
bold(elem) {
|
bold() {
|
||||||
var wrap = this.selection() || placeholder
|
var wrap = this.selection() || PLACEHOLDER
|
||||||
var unwrap = trim(wrap, '\\*\\*')
|
var unwrap = trim(wrap, '\\*\\*')
|
||||||
wrap = wrap === unwrap ? `**${wrap}**` : unwrap
|
wrap = wrap === unwrap ? `**${wrap}**` : unwrap
|
||||||
this.insert(wrap, true)
|
this.insert(wrap, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
italic(elem) {
|
italic() {
|
||||||
var wrap = this.selection() || placeholder
|
var wrap = this.selection() || PLACEHOLDER
|
||||||
var unwrap = trim(wrap, '_')
|
var unwrap = trim(wrap, '_')
|
||||||
wrap = wrap === unwrap ? `_${wrap}_` : unwrap
|
wrap = wrap === unwrap ? `_${wrap}_` : unwrap
|
||||||
this.insert(wrap, true)
|
this.insert(wrap, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
through(elem) {
|
through() {
|
||||||
var wrap = this.selection() || placeholder
|
var wrap = this.selection() || PLACEHOLDER
|
||||||
var unwrap = trim(wrap, '~~')
|
var unwrap = trim(wrap, '~~')
|
||||||
wrap = wrap === unwrap ? `~~${wrap}~~` : unwrap
|
wrap = wrap === unwrap ? `~~${wrap}~~` : unwrap
|
||||||
this.insert(wrap, true)
|
this.insert(wrap, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
list(elem) {
|
list() {
|
||||||
var wrap = this.selection(true) || placeholder
|
var wrap = this.selection(true) || PLACEHOLDER
|
||||||
|
|
||||||
wrap = wrap.replace(/^([+\-*] )?/, '+ ')
|
wrap = wrap.replace(/^([+\-*] )?/, '+ ')
|
||||||
this.insert(wrap, true)
|
this.insert(wrap, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
order(elem) {
|
order() {
|
||||||
var wrap = this.selection(true) || placeholder
|
var wrap = this.selection(true) || PLACEHOLDER
|
||||||
|
|
||||||
wrap = wrap.replace(/^(\d+\. )?/, '1. ')
|
wrap = wrap.replace(/^(\d+\. )?/, '1. ')
|
||||||
this.insert(wrap, true)
|
this.insert(wrap, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
line(elem) {
|
line() {
|
||||||
this.insert('\n\n---\n\n', false)
|
this.insert('\n\n---\n\n', false)
|
||||||
},
|
},
|
||||||
|
|
||||||
code(elem) {
|
code() {
|
||||||
var wrap = this.selection() || placeholder
|
var wrap = this.selection() || PLACEHOLDER
|
||||||
var unwrap = trim(wrap, '`')
|
var unwrap = trim(wrap, '`')
|
||||||
wrap = wrap === unwrap ? `\`${wrap}\`` : unwrap
|
wrap = wrap === unwrap ? `\`${wrap}\`` : unwrap
|
||||||
this.insert(wrap, true)
|
this.insert(wrap, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
codeblock(elem) {
|
codeblock() {
|
||||||
this.insert('\n```language\n\n```\n')
|
this.insert('\n```language\n\n```\n')
|
||||||
},
|
},
|
||||||
|
|
||||||
table(elem) {
|
table() {
|
||||||
// showDialog(this.__TABLE_ADDON__, elem)
|
|
||||||
this.$refs.table.classList.add('fadein')
|
this.$refs.table.classList.add('fadein')
|
||||||
},
|
},
|
||||||
|
|
||||||
link(elem) {
|
link() {
|
||||||
showDialog(this.__LINK_ADDON__, elem).then(dialog => {
|
this.$refs.link.classList.add('fadein')
|
||||||
var wrap = this.selection() || placeholder
|
|
||||||
dialog.__txt__.value = wrap
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
image(elem) {
|
fullscreen() {
|
||||||
var $file = this.__ATTACH_ADDON__.querySelector('input')
|
|
||||||
|
|
||||||
this._attach = 'image'
|
|
||||||
$file.setAttribute('accept', 'image/*')
|
|
||||||
|
|
||||||
showDialog(this.__ATTACH_ADDON__, elem)
|
|
||||||
},
|
|
||||||
|
|
||||||
attach(elem) {
|
|
||||||
var $file = this.__ATTACH_ADDON__.querySelector('input')
|
|
||||||
this._attach = 'file'
|
|
||||||
$file.removeAttribute('accept')
|
|
||||||
showDialog(this.__ATTACH_ADDON__, elem)
|
|
||||||
},
|
|
||||||
|
|
||||||
fullscreen(elem) {
|
|
||||||
this.classList.toggle('fullscreen')
|
this.classList.toggle('fullscreen')
|
||||||
elem.classList.toggle('active')
|
|
||||||
},
|
},
|
||||||
preview(elem) {
|
preview() {
|
||||||
this.previewEnabled = !this.previewEnabled
|
this.previewEnabled = !this.previewEnabled
|
||||||
this.$refs.view.classList.toggle('active')
|
this.$refs.view.classList.toggle('active')
|
||||||
elem.classList.toggle('active')
|
|
||||||
if (this.previewEnabled) {
|
if (this.previewEnabled) {
|
||||||
this.$refs.view.code = this.value
|
this.$refs.view.code = this.value
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,6 @@
|
||||||
* @date 2020/10/12 18:23:23
|
* @date 2020/10/12 18:23:23
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { html } from 'wkit'
|
|
||||||
import ICONS from './svg.js'
|
|
||||||
|
|
||||||
const ELEMS = {
|
const ELEMS = {
|
||||||
a: function (str, attr, inner) {
|
a: function (str, attr, inner) {
|
||||||
let href = attr.match(attrExp('href'))
|
let href = attr.match(attrExp('href'))
|
||||||
|
@ -83,31 +80,6 @@ export const DEFAULT_TOOLS = [
|
||||||
'preview'
|
'preview'
|
||||||
]
|
]
|
||||||
|
|
||||||
export const TOOL_TITLE = {
|
|
||||||
header: '插入标题',
|
|
||||||
h1: '一级标题',
|
|
||||||
h2: '二级标题',
|
|
||||||
h3: '三级标题',
|
|
||||||
h4: '四级标题',
|
|
||||||
h5: '五级标题',
|
|
||||||
h6: '六级标题',
|
|
||||||
quote: '引用文本',
|
|
||||||
bold: '粗体',
|
|
||||||
italic: '斜体',
|
|
||||||
through: '横线',
|
|
||||||
list: '无序列表',
|
|
||||||
order: '有序列表',
|
|
||||||
line: '分割线',
|
|
||||||
code: '行内代码',
|
|
||||||
codeblock: '插入代码块',
|
|
||||||
table: '插入表格',
|
|
||||||
link: '插入连接',
|
|
||||||
image: '上传图片',
|
|
||||||
attach: '上传附件',
|
|
||||||
fullscreen: '全屏编辑',
|
|
||||||
preview: '预览'
|
|
||||||
}
|
|
||||||
|
|
||||||
const LI_EXP = /<(ul|ol)>(?:(?!<ul|<ol)[\s\S])*?<\/\1>/gi
|
const LI_EXP = /<(ul|ol)>(?:(?!<ul|<ol)[\s\S])*?<\/\1>/gi
|
||||||
|
|
||||||
// html标签的属性正则
|
// html标签的属性正则
|
||||||
|
@ -126,21 +98,6 @@ function tagExp(tag, open) {
|
||||||
return new RegExp(exp, 'gi')
|
return new RegExp(exp, 'gi')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 渲染工具栏图标
|
|
||||||
*/
|
|
||||||
export function renderToolbar(list = [], dict = {}, showText = false) {
|
|
||||||
return list.map(it => {
|
|
||||||
let title = showText ? '' : dict[it]
|
|
||||||
let text = showText ? dict[it] || '' : ''
|
|
||||||
|
|
||||||
return html`<span data-act=${it} title=${title}>
|
|
||||||
<svg class="icon" viewBox="0 0 1024 1024"><path d=${ICONS[it]} /></svg>
|
|
||||||
${text}
|
|
||||||
</span>`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* html转成md
|
* html转成md
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
raw,
|
raw,
|
||||||
html,
|
html,
|
||||||
Component,
|
Component,
|
||||||
|
range,
|
||||||
bind,
|
bind,
|
||||||
unbind,
|
unbind,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
@ -18,7 +19,7 @@ import {
|
||||||
clearOutsideClick
|
clearOutsideClick
|
||||||
} from 'wkit'
|
} from 'wkit'
|
||||||
|
|
||||||
import { renderToolbar, DEFAULT_TOOLS, html2md } from './helper.js'
|
import { DEFAULT_TOOLS, html2md } from './helper.js'
|
||||||
import Addon from './addon.js'
|
import Addon from './addon.js'
|
||||||
import markd from '../markd/index.js'
|
import markd from '../markd/index.js'
|
||||||
|
|
||||||
|
@ -26,6 +27,8 @@ import '../form/input.js'
|
||||||
import '../form/button.js'
|
import '../form/button.js'
|
||||||
import '../code/index.js'
|
import '../code/index.js'
|
||||||
|
|
||||||
|
import ICONS from './svg.js'
|
||||||
|
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
'#f3f5fb',
|
'#f3f5fb',
|
||||||
'#dae1e9',
|
'#dae1e9',
|
||||||
|
@ -83,6 +86,7 @@ class MEditor extends Component {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-height: 720px;
|
max-height: 720px;
|
||||||
|
min-height: 64px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
transition: box-shadow 0.15s linear;
|
transition: box-shadow 0.15s linear;
|
||||||
background: var(--wc-meditor-background, #fff);
|
background: var(--wc-meditor-background, #fff);
|
||||||
|
@ -135,6 +139,12 @@ class MEditor extends Component {
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
color: #62778d;
|
color: #62778d;
|
||||||
}
|
}
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&.active {
|
&.active {
|
||||||
|
@ -155,7 +165,6 @@ class MEditor extends Component {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 200px;
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
.editor,
|
.editor,
|
||||||
|
@ -197,16 +206,12 @@ class MEditor extends Component {
|
||||||
`,
|
`,
|
||||||
css`
|
css`
|
||||||
:host([readonly]) {
|
:host([readonly]) {
|
||||||
.editor {
|
cursor: default;
|
||||||
cursor: default;
|
opacity: 0.8;
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
:host([disabled]) {
|
:host([disabled]) {
|
||||||
.editor {
|
cursor: not-allowed;
|
||||||
cursor: not-allowed;
|
opacity: 0.6;
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
:host([readonly]),
|
:host([readonly]),
|
||||||
:host([disabled]) {
|
:host([disabled]) {
|
||||||
|
@ -235,6 +240,7 @@ class MEditor extends Component {
|
||||||
|
|
||||||
css`
|
css`
|
||||||
.font-layer,
|
.font-layer,
|
||||||
|
.link-layer,
|
||||||
.table-layer {
|
.table-layer {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -310,119 +316,16 @@ class MEditor extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.addon-link {
|
.link-layer {
|
||||||
width: 320px;
|
display: flex;
|
||||||
padding: 8px 5px;
|
flex-direction: column;
|
||||||
background: #fff;
|
left: 330px;
|
||||||
font-size: 13px;
|
width: 230px;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
li {
|
wc-button {
|
||||||
display: flex;
|
width: 40px;
|
||||||
align-items: center;
|
margin-top: 8px;
|
||||||
padding: 0 12px;
|
|
||||||
margin-top: 6px;
|
|
||||||
|
|
||||||
label {
|
|
||||||
width: 60px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
wc-input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
wc-button {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.addon-attach {
|
|
||||||
width: 320px;
|
|
||||||
padding: 8px 5px;
|
|
||||||
background: #fff;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 1px solid var(--color-plain-2);
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
span {
|
|
||||||
height: 28px;
|
|
||||||
padding: 0 8px;
|
|
||||||
line-height: 28px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: var(--color-teal-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.remote,
|
|
||||||
.locale {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.locale {
|
|
||||||
height: 120px;
|
|
||||||
padding: 24px 32px;
|
|
||||||
|
|
||||||
.button {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 12px 16px;
|
|
||||||
line-height: 46px;
|
|
||||||
border: 1px dashed var(--color-plain-3);
|
|
||||||
border-radius: 4px;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.1s ease-in-out;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '点击选择文件,或拖拽文件到此处';
|
|
||||||
}
|
|
||||||
&:hover,
|
|
||||||
&.active {
|
|
||||||
background: rgba(255, 228, 196, 0.15);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
z-index: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 12px;
|
|
||||||
margin-top: 6px;
|
|
||||||
|
|
||||||
label {
|
|
||||||
width: 60px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
wc-input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
wc-button {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
@ -481,7 +384,7 @@ class MEditor extends Component {
|
||||||
#hideLayers() {
|
#hideLayers() {
|
||||||
this.$refs.header.classList.remove('fadein')
|
this.$refs.header.classList.remove('fadein')
|
||||||
// this.$refs.color.classList.remove('fadein')
|
// this.$refs.color.classList.remove('fadein')
|
||||||
// this.$refs.link.classList.remove('fadein')
|
this.$refs.link.classList.remove('fadein')
|
||||||
this.$refs.table.classList.remove('fadein')
|
this.$refs.table.classList.remove('fadein')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -490,6 +393,7 @@ class MEditor extends Component {
|
||||||
if (ev && ev.type === 'change') {
|
if (ev && ev.type === 'change') {
|
||||||
file = ev.target.files[0]
|
file = ev.target.files[0]
|
||||||
ev.target.value = ''
|
ev.target.value = ''
|
||||||
|
t = ev.target.dataset.type === 'image' ? '!' : ''
|
||||||
}
|
}
|
||||||
this.$emit('upload', {
|
this.$emit('upload', {
|
||||||
detail: {
|
detail: {
|
||||||
|
@ -564,6 +468,10 @@ class MEditor extends Component {
|
||||||
|
|
||||||
this.#hideLayers()
|
this.#hideLayers()
|
||||||
|
|
||||||
|
if (act === 'image' || act === 'attach') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Addon[act].call(this, elem)
|
Addon[act].call(this, elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -582,9 +490,7 @@ class MEditor extends Component {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.$refs.link.classList.remove('fadein')
|
this.$refs.link.classList.remove('fadein')
|
||||||
this.$refs.editor.focus()
|
this.$refs.editor.focus()
|
||||||
this.restoreSelection()
|
this.insert(`[${value}](${value}) `)
|
||||||
this.exec(ACTTION.link, value)
|
|
||||||
this.saveSelection()
|
|
||||||
this.$refs.linkinput.value = ''
|
this.$refs.linkinput.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -889,14 +795,25 @@ class MEditor extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
#fixedPadding() {
|
||||||
let pb = ~~(this.clientHeight * 0.6)
|
if (this.clientHeight > 64) {
|
||||||
pb = pb < 64 ? 64 : pb
|
let pb = ~~(this.clientHeight * 0.6)
|
||||||
Addon.preview.call(this, this)
|
pb = pb < 64 ? 64 : pb
|
||||||
this.$refs.editor.style.paddingBottom = pb + 'px'
|
this.$refs.editor.style.paddingBottom = pb + 'px'
|
||||||
|
} else {
|
||||||
|
this.$refs.editor.style.cssText = 'padding-bottom:;'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unmounted() {}
|
mounted() {
|
||||||
|
this.#fixedPadding()
|
||||||
|
this.__observer = new ResizeObserver(this.#fixedPadding.bind(this))
|
||||||
|
this.__observer.observe(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
this.__observer?.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
|
@ -905,7 +822,22 @@ class MEditor extends Component {
|
||||||
class=${classMap({ toolbar: true, active: this.#toolbar.length })}
|
class=${classMap({ toolbar: true, active: this.#toolbar.length })}
|
||||||
@click=${this.#toolbarClick}
|
@click=${this.#toolbarClick}
|
||||||
>
|
>
|
||||||
${renderToolbar(this.#toolbar)}
|
${this.#toolbar.map(it => {
|
||||||
|
return html`<span data-act=${it}>
|
||||||
|
<svg class="icon" viewBox="0 0 1024 1024">
|
||||||
|
<path d=${ICONS[it]} />
|
||||||
|
</svg>
|
||||||
|
${it === 'image' || it === 'attach'
|
||||||
|
? html`<input
|
||||||
|
type="file"
|
||||||
|
data-type=${it}
|
||||||
|
accept=${it === 'image' ? 'image/*' : '*/*'}
|
||||||
|
:disabled=${this.readOnly || this.disabled}
|
||||||
|
@change=${this.#handleFile}
|
||||||
|
/>`
|
||||||
|
: ''}
|
||||||
|
</span>`
|
||||||
|
})}
|
||||||
</header>
|
</header>
|
||||||
<div class="editor-outbox">
|
<div class="editor-outbox">
|
||||||
<textarea
|
<textarea
|
||||||
|
@ -919,6 +851,7 @@ class MEditor extends Component {
|
||||||
@input=${this.#updatePreview}
|
@input=${this.#updatePreview}
|
||||||
@scroll=${this.#syncScrollToPreview}
|
@scroll=${this.#syncScrollToPreview}
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<wc-markd ref="view" class="preview"></wc-markd>
|
<wc-markd ref="view" class="preview"></wc-markd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -942,9 +875,12 @@ class MEditor extends Component {
|
||||||
@mousemove=${this.#tableSelect}
|
@mousemove=${this.#tableSelect}
|
||||||
@mouseleave=${this.#tableSelect}
|
@mouseleave=${this.#tableSelect}
|
||||||
>
|
>
|
||||||
${Array(81)
|
${range(81).map((_, n) => html`<span data-idx=${n}></span>`)}
|
||||||
.fill(0)
|
</div>
|
||||||
.map((_, n) => html`<span data-idx=${n}></span>`)}
|
|
||||||
|
<div class="link-layer noselect" ref="link">
|
||||||
|
<wc-input ref="linkinput" placeholder="请输入链接地址"></wc-input>
|
||||||
|
<wc-button size="m" @click=${this.#insertLink}>插入</wc-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
Loading…
Reference in New Issue