update
parent
5b454bad12
commit
12630d66c0
|
@ -511,6 +511,10 @@ class Editor extends Component {
|
|||
let txt = ev.clipboardData.getData('text/plain')
|
||||
let items = ev.clipboardData.items
|
||||
|
||||
if (this.readOnly || this.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
// 先文件判断, 避免右键单击复制图片时, 当成html处理
|
||||
if (items && items.length) {
|
||||
let blob = null
|
||||
|
|
|
@ -594,7 +594,7 @@ class Tool {
|
|||
|
||||
// 引用结束
|
||||
if (isBlockquote) {
|
||||
if (emptyLineLength > 1) {
|
||||
if (emptyLineLength > 0) {
|
||||
isBlockquote = false
|
||||
emptyLineLength = 0
|
||||
while (blockquoteLevel > 0) {
|
||||
|
|
|
@ -10,6 +10,8 @@ import md2html from './core.js'
|
|||
import '../code/index.js'
|
||||
import '../form/checkbox.js'
|
||||
|
||||
export default md2html
|
||||
|
||||
class Markd extends Component {
|
||||
static props = {
|
||||
code: { type: String, default: '', attribute: false }
|
||||
|
|
|
@ -124,18 +124,15 @@ export default {
|
|||
},
|
||||
|
||||
fullscreen(elem) {
|
||||
//
|
||||
this.props.fullscreen = !this.props.fullscreen
|
||||
if (this.props.fullscreen) {
|
||||
this.setAttribute('fullscreen', '')
|
||||
} else {
|
||||
this.removeAttribute('fullscreen')
|
||||
}
|
||||
elem.classList.toggle('active', this.props.fullscreen)
|
||||
this.classList.toggle('fullscreen')
|
||||
elem.classList.toggle('active')
|
||||
},
|
||||
preview(elem) {
|
||||
this.state.preview = !this.state.preview
|
||||
this.__VIEW__.classList.toggle('active', this.state.preview)
|
||||
elem.classList.toggle('active', this.state.preview)
|
||||
this.previewEnabled = !this.previewEnabled
|
||||
this.$refs.view.classList.toggle('active')
|
||||
elem.classList.toggle('active')
|
||||
if (this.previewEnabled) {
|
||||
this.$refs.view.code = this.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,17 +16,17 @@ const ELEMS = {
|
|||
|
||||
href = (href && href[1]) || null
|
||||
title = (title && title[1]) || null
|
||||
tar = (tar && tar[1]) || '_self'
|
||||
tar = tar && tar[1]
|
||||
|
||||
if (!href) {
|
||||
return inner || href
|
||||
}
|
||||
|
||||
href = href.replace('viod(0)', '')
|
||||
attrs = `target=${tar}`
|
||||
href = href.replace('viod(0)', '').replaceAll('&', '&')
|
||||
attrs = tar ? `target=${tar}` : ''
|
||||
attrs += title ? `;title=${title}` : ''
|
||||
|
||||
return `[${inner || href}](${href} "${attrs}")`
|
||||
return `[${inner || href}](${href}${attrs ? ` "${attrs}"` : ''})`
|
||||
},
|
||||
em: function (str, attr, inner) {
|
||||
return (inner && '_' + inner + '_') || ''
|
||||
|
@ -134,10 +134,10 @@ export function renderToolbar(list = [], dict = {}, showText = false) {
|
|||
let title = showText ? '' : dict[it]
|
||||
let text = showText ? dict[it] || '' : ''
|
||||
|
||||
return html`<section data-act=${it} title=${title}>
|
||||
return html`<span data-act=${it} title=${title}>
|
||||
<svg class="icon" viewBox="0 0 1024 1024"><path d=${ICONS[it]} /></svg>
|
||||
${text}
|
||||
</section>`
|
||||
</span>`
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -151,11 +151,19 @@ export function html2md(str) {
|
|||
|
||||
str = str
|
||||
.replace(/\t/g, ' ')
|
||||
.replace(/<meta [^>]*>/, '')
|
||||
.replace(/<\/?(meta|link|script)[^>]*?>/g, '')
|
||||
.replace(/<!--[\w\W]*?-->/g, '')
|
||||
.replace(/<xml[^>]*?>[\w\W]*?<\/xml>/g, '')
|
||||
.replace(/<style>[\w\W]*?<\/style>/g, '')
|
||||
.replace(attrExp('class', 'g'), '')
|
||||
.replace(attrExp('style', '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(/<svg[^>]*>.*?<\/svg>/g, '{invalid image}')
|
||||
.replace(/<svg[^>]*>.*?<\/svg>/g, '{svg not support}')
|
||||
|
||||
// log(str)
|
||||
for (let i in ELEMS) {
|
||||
|
|
|
@ -18,10 +18,13 @@ import {
|
|||
clearOutsideClick
|
||||
} from 'wkit'
|
||||
|
||||
import { renderToolbar, DEFAULT_TOOLS } from './helper.js'
|
||||
import { renderToolbar, DEFAULT_TOOLS, html2md } from './helper.js'
|
||||
import Addon from './addon.js'
|
||||
import markd from '../markd/index.js'
|
||||
|
||||
import '../form/input.js'
|
||||
import '../form/button.js'
|
||||
import '../code/index.js'
|
||||
|
||||
const COLORS = [
|
||||
'#f3f5fb',
|
||||
|
@ -113,7 +116,7 @@ class MEditor extends Component {
|
|||
line-height: 24px;
|
||||
border-bottom: 1px solid var(--color-grey-1);
|
||||
|
||||
section {
|
||||
span {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
@ -415,7 +418,12 @@ class MEditor extends Component {
|
|||
|
||||
set value(val) {
|
||||
if (this.$refs.editor) {
|
||||
if (this.$refs.editor.value === val) {
|
||||
return
|
||||
}
|
||||
this.$refs.editor.value = val
|
||||
this.#updatePreview()
|
||||
this.$emit('input')
|
||||
} else {
|
||||
nextTick(_ => (this.value = val))
|
||||
}
|
||||
|
@ -429,6 +437,8 @@ class MEditor extends Component {
|
|||
|
||||
#select = null
|
||||
|
||||
previewEnabled = false
|
||||
|
||||
__init__() {
|
||||
//
|
||||
let { outer, inner, thumb } = this.$refs
|
||||
|
@ -476,7 +486,7 @@ class MEditor extends Component {
|
|||
}
|
||||
|
||||
// 处理图片
|
||||
#handleImage(ev, file) {
|
||||
#handleFile(ev, file, t = '!') {
|
||||
if (ev && ev.type === 'change') {
|
||||
file = ev.target.files[0]
|
||||
ev.target.value = ''
|
||||
|
@ -486,14 +496,7 @@ class MEditor extends Component {
|
|||
file,
|
||||
send: link => {
|
||||
this.$refs.editor.focus()
|
||||
this.restoreSelection()
|
||||
this.exec(ACTTION.image, link)
|
||||
this.saveSelection()
|
||||
|
||||
// 修正插入的图片,宽度不得超出容器
|
||||
this.$refs.editor.querySelectorAll('img').forEach(_ => {
|
||||
_.style.maxWidth = '100%'
|
||||
})
|
||||
this.insert(`${t}[${file.name}](${link})`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -504,52 +507,50 @@ class MEditor extends Component {
|
|||
let txt = ev.clipboardData.getData('text/plain')
|
||||
let items = ev.clipboardData.items
|
||||
|
||||
if (this.readOnly || this.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
// 先文件判断, 避免右键单击复制图片时, 当成html处理
|
||||
if (items && items.length) {
|
||||
let blob = null
|
||||
let file = null
|
||||
|
||||
for (let it of items) {
|
||||
if (it.type.indexOf('image') > -1) {
|
||||
blob = it.getAsFile()
|
||||
file = it.getAsFile()
|
||||
if (file) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (blob) {
|
||||
return this.#handleImage(null, blob)
|
||||
if (file) {
|
||||
return this.#handleFile(
|
||||
null,
|
||||
file,
|
||||
file.type.includes('image') ? '!' : ''
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
this.insert(html2md(html))
|
||||
} else if (txt) {
|
||||
this.insert(txt)
|
||||
}
|
||||
}
|
||||
|
||||
if (txt) {
|
||||
return this.exec('insertText', txt)
|
||||
#updatePreview() {
|
||||
if (this.previewEnabled) {
|
||||
this.$refs.view.code = this.value
|
||||
}
|
||||
}
|
||||
|
||||
#toolbarClick(ev) {
|
||||
var target = ev.target
|
||||
var elem = ev.target
|
||||
var act
|
||||
|
||||
this.restoreSelection()
|
||||
|
||||
if (ev.target === ev.currentTarget) {
|
||||
if (elem === ev.currentTarget) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -557,41 +558,15 @@ class MEditor extends Component {
|
|||
return
|
||||
}
|
||||
|
||||
while (target.tagName !== 'SPAN') {
|
||||
target = target.parentNode
|
||||
while (elem.tagName !== 'SPAN') {
|
||||
elem = elem.parentNode
|
||||
}
|
||||
|
||||
act = target.dataset.act
|
||||
act = elem.dataset.act
|
||||
|
||||
this.#hideLayers()
|
||||
// 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
|
||||
}
|
||||
Addon[act].call(this, elem)
|
||||
}
|
||||
|
||||
#chnageFontSize(ev) {
|
||||
|
@ -688,9 +663,248 @@ class MEditor extends Component {
|
|||
gs.addRange(this.#select)
|
||||
}
|
||||
}
|
||||
// 执行命令
|
||||
exec(cmd, val = '') {
|
||||
document.execCommand(cmd, false, val)
|
||||
|
||||
/**
|
||||
* 往文本框中插入内容
|
||||
* @param {String} val [要插入的文本]
|
||||
* @param {Boolean} isSelect [插入之后是否要选中文本]
|
||||
* @param {Boolean} offset [选中文本时的偏移量]
|
||||
*/
|
||||
insert(val = '', isSelect = false, offset = 0) {
|
||||
let $el = this.$refs.editor
|
||||
let start = $el.selectionStart
|
||||
let end = $el.selectionEnd
|
||||
let scrollTop = $el.scrollTop
|
||||
|
||||
if (start || start === 0) {
|
||||
$el.value = $el.value.slice(0, start) + val + $el.value.slice(end)
|
||||
|
||||
this.select(
|
||||
(isSelect ? start : start + val.length) + offset,
|
||||
start + val.length - offset
|
||||
)
|
||||
$el.scrollTop = scrollTop
|
||||
$el.focus()
|
||||
} else {
|
||||
$el.value += val
|
||||
$el.focus()
|
||||
}
|
||||
this.#updatePreview()
|
||||
this.$emit('input')
|
||||
}
|
||||
|
||||
/**
|
||||
* [selection 获取选中的文本]
|
||||
* @param {[type]} forceHoleLine [是否强制光标所在的整行文本]
|
||||
*/
|
||||
selection(forceHoleLine = false) {
|
||||
let $el = this.$refs.editor
|
||||
let start = $el.selectionStart
|
||||
let end = $el.selectionEnd
|
||||
|
||||
if (end) {
|
||||
//强制选择整行
|
||||
if (forceHoleLine) {
|
||||
start = $el.value.slice(0, start).lastIndexOf('\n')
|
||||
|
||||
let tmpEnd = $el.value.slice(end).indexOf('\n')
|
||||
tmpEnd = tmpEnd < 0 ? $el.value.slice(end).length : tmpEnd
|
||||
|
||||
start += 1 // 把\n加上
|
||||
end += tmpEnd
|
||||
|
||||
$el.selectionStart = start
|
||||
$el.selectionEnd = end
|
||||
}
|
||||
} else {
|
||||
//强制选择整行
|
||||
if (forceHoleLine) {
|
||||
end = $el.value.indexOf('\n')
|
||||
end = end < 0 ? $el.value.length : end
|
||||
$el.selectionEnd = end
|
||||
}
|
||||
}
|
||||
$el.focus()
|
||||
return $el.value.slice(start, end)
|
||||
}
|
||||
|
||||
select(start = 0, end = 0) {
|
||||
let $el = this.$refs.editor
|
||||
$el.selectionStart = start
|
||||
$el.selectionEnd = end
|
||||
}
|
||||
|
||||
// 设置光标
|
||||
cursor(pos) {
|
||||
this.select(pos, pos)
|
||||
}
|
||||
|
||||
#cursorMove(step) {
|
||||
let $el = this.$refs.editor
|
||||
let pos = (step < 0 ? $el.selectionStart : $el.selectionEnd) || 0
|
||||
pos += step
|
||||
|
||||
if (step === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pos < 0) {
|
||||
pos = 0
|
||||
}
|
||||
this.cursor(pos)
|
||||
}
|
||||
|
||||
// 获取光标处的字符
|
||||
#getCursorText(n) {
|
||||
let $el = this.$refs.editor
|
||||
let start = $el.selectionStart
|
||||
let pos = $el.selectionEnd
|
||||
|
||||
if (n < 0) {
|
||||
pos = start - 1
|
||||
}
|
||||
return this.value[pos] || ''
|
||||
}
|
||||
|
||||
#handleKeydown(ev) {
|
||||
let $el = this.$refs.editor
|
||||
let wrapTxt = this.selection() || ''
|
||||
let selected = wrapTxt.length > 0
|
||||
let newTxt = ''
|
||||
|
||||
if (this.readOnly || this.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (ev.keyCode) {
|
||||
//tab键改为插入2个空格,阻止默认事件,防止焦点失去
|
||||
case 9:
|
||||
ev.preventDefault()
|
||||
if (ev.shiftKey && !selected) {
|
||||
let pos = $el.selectionStart
|
||||
let line = this.selection(true)
|
||||
newTxt = line.replace(/^\s{2}/, '')
|
||||
// 防止无法往左取消缩进时, 选中整行
|
||||
if (line === newTxt) {
|
||||
this.cursor(pos)
|
||||
break
|
||||
}
|
||||
|
||||
pos -= 2
|
||||
this.insert(newTxt, selected)
|
||||
this.cursor(pos)
|
||||
} else {
|
||||
newTxt = wrapTxt
|
||||
.split('\n')
|
||||
.map(function (it) {
|
||||
return ev.shiftKey ? it.replace(/^\s{2}/, '') : ' ' + it
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
if (newTxt === wrapTxt) {
|
||||
break
|
||||
}
|
||||
this.insert(newTxt, selected)
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
// 退格键, 遇到成对的符号时,同时删除
|
||||
case 8:
|
||||
if (!selected) {
|
||||
let pos = $el.selectionStart
|
||||
let prev = this.#getCursorText(-1)
|
||||
let next = this.#getCursorText(1)
|
||||
|
||||
if (
|
||||
(prev === next && (prev === '"' || prev === "'")) ||
|
||||
(prev === next && prev === '`') ||
|
||||
(prev === '[' && next === ']') ||
|
||||
(prev === '{' && next === '}') ||
|
||||
(prev === '(' && next === ')')
|
||||
) {
|
||||
this.select(pos - 1, pos + 1)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
// (
|
||||
case 57:
|
||||
if (ev.shiftKey) {
|
||||
ev.preventDefault()
|
||||
|
||||
this.insert('(' + wrapTxt + ')', selected, (selected && 1) || 0)
|
||||
this.#cursorMove(selected ? 0 : -1)
|
||||
}
|
||||
break
|
||||
// )
|
||||
case 48:
|
||||
if (ev.shiftKey) {
|
||||
let prev = this.#getCursorText(-1)
|
||||
let next = this.#getCursorText(1)
|
||||
if (prev === '(' && next === ')') {
|
||||
ev.preventDefault()
|
||||
this.#cursorMove(1)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 219: // [ & {
|
||||
ev.preventDefault()
|
||||
this.insert(
|
||||
ev.shiftKey ? `{${wrapTxt}}` : `[${wrapTxt}]`,
|
||||
selected,
|
||||
selected ^ 0
|
||||
)
|
||||
this.#cursorMove(selected ? 0 : -1)
|
||||
break
|
||||
// } & ]
|
||||
case 221: {
|
||||
let prev = this.#getCursorText(-1)
|
||||
let next = this.#getCursorText(1)
|
||||
if (ev.shiftKey) {
|
||||
if (prev === '{' && next === '}') {
|
||||
ev.preventDefault()
|
||||
this.#cursorMove(1)
|
||||
}
|
||||
} else {
|
||||
if (prev === '[' && next === ']') {
|
||||
ev.preventDefault()
|
||||
this.#cursorMove(1)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// `
|
||||
case 192:
|
||||
// ' & "
|
||||
case 222:
|
||||
if (ev.shiftKey && ev.keyCode === 192) {
|
||||
break
|
||||
} else {
|
||||
let val = ev.keyCode === 192 ? '`' : ev.shiftKey ? '"' : "'"
|
||||
let prev = this.#getCursorText(-1)
|
||||
let next = this.#getCursorText(1)
|
||||
|
||||
if (prev === '' && next === val) {
|
||||
break
|
||||
}
|
||||
ev.preventDefault()
|
||||
|
||||
if (selected) {
|
||||
this.insert(val + wrapTxt + val, true, 1)
|
||||
} else {
|
||||
if (prev === next && prev === val) {
|
||||
this.#cursorMove(1)
|
||||
} else {
|
||||
this.insert(val + wrapTxt + val)
|
||||
this.#cursorMove(-1)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mounted() {}
|
||||
|
@ -704,6 +918,7 @@ class MEditor extends Component {
|
|||
<div class="meditor">
|
||||
<header
|
||||
class=${classMap({ toolbar: true, active: this.#toolbar.length })}
|
||||
@click=${this.#toolbarClick}
|
||||
>
|
||||
${renderToolbar(this.#toolbar)}
|
||||
</header>
|
||||
|
@ -714,11 +929,11 @@ class MEditor extends Component {
|
|||
spellcheck="false"
|
||||
:readOnly=${this.readOnly}
|
||||
:disabled=${this.disabled}
|
||||
@input=${function (ev) {
|
||||
// this.value = ev.target.value
|
||||
}}
|
||||
@keydown=${this.#handleKeydown}
|
||||
@paste.prevent=${this.#handlePaste}
|
||||
@input=${this.#updatePreview}
|
||||
></textarea>
|
||||
<wc-markd class="preview"></wc-markd>
|
||||
<wc-markd ref="view" class="preview"></wc-markd>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
|
Loading…
Reference in New Issue