master
yutent 2023-09-27 19:07:56 +08:00
parent 5b454bad12
commit 12630d66c0
6 changed files with 321 additions and 95 deletions

View File

@ -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

View File

@ -594,7 +594,7 @@ class Tool {
// 引用结束
if (isBlockquote) {
if (emptyLineLength > 1) {
if (emptyLineLength > 0) {
isBlockquote = false
emptyLineLength = 0
while (blockquoteLevel > 0) {

View File

@ -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 }

View File

@ -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
}
}
}

View File

@ -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) {

View File

@ -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>
`