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 txt = ev.clipboardData.getData('text/plain')
let items = ev.clipboardData.items let items = ev.clipboardData.items
if (this.readOnly || this.disabled) {
return
}
// 先文件判断, 避免右键单击复制图片时, 当成html处理 // 先文件判断, 避免右键单击复制图片时, 当成html处理
if (items && items.length) { if (items && items.length) {
let blob = null let blob = null

View File

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

View File

@ -10,6 +10,8 @@ import md2html from './core.js'
import '../code/index.js' import '../code/index.js'
import '../form/checkbox.js' import '../form/checkbox.js'
export default md2html
class Markd extends Component { class Markd extends Component {
static props = { static props = {
code: { type: String, default: '', attribute: false } code: { type: String, default: '', attribute: false }

View File

@ -124,18 +124,15 @@ export default {
}, },
fullscreen(elem) { fullscreen(elem) {
// this.classList.toggle('fullscreen')
this.props.fullscreen = !this.props.fullscreen elem.classList.toggle('active')
if (this.props.fullscreen) {
this.setAttribute('fullscreen', '')
} else {
this.removeAttribute('fullscreen')
}
elem.classList.toggle('active', this.props.fullscreen)
}, },
preview(elem) { preview(elem) {
this.state.preview = !this.state.preview this.previewEnabled = !this.previewEnabled
this.__VIEW__.classList.toggle('active', this.state.preview) this.$refs.view.classList.toggle('active')
elem.classList.toggle('active', this.state.preview) 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 href = (href && href[1]) || null
title = (title && title[1]) || null title = (title && title[1]) || null
tar = (tar && tar[1]) || '_self' tar = tar && tar[1]
if (!href) { if (!href) {
return inner || href return inner || href
} }
href = href.replace('viod(0)', '') href = href.replace('viod(0)', '').replaceAll('&', '&')
attrs = `target=${tar}` attrs = tar ? `target=${tar}` : ''
attrs += title ? `;title=${title}` : '' attrs += title ? `;title=${title}` : ''
return `[${inner || href}](${href} "${attrs}")` return `[${inner || href}](${href}${attrs ? ` "${attrs}"` : ''})`
}, },
em: function (str, attr, inner) { em: function (str, attr, inner) {
return (inner && '_' + inner + '_') || '' return (inner && '_' + inner + '_') || ''
@ -134,10 +134,10 @@ export function renderToolbar(list = [], dict = {}, showText = false) {
let title = showText ? '' : dict[it] let title = showText ? '' : dict[it]
let text = 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> <svg class="icon" viewBox="0 0 1024 1024"><path d=${ICONS[it]} /></svg>
${text} ${text}
</section>` </span>`
}) })
} }
@ -151,11 +151,19 @@ export function html2md(str) {
str = str str = str
.replace(/\t/g, ' ') .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('class', 'g'), '')
.replace(attrExp('style', '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(/<(?!a |img )(\w+) [^>]*>/g, '<$1>')
.replace(/<svg[^>]*>.*?<\/svg>/g, '{invalid image}') .replace(/<svg[^>]*>.*?<\/svg>/g, '{svg not support}')
// log(str) // log(str)
for (let i in ELEMS) { for (let i in ELEMS) {

View File

@ -18,10 +18,13 @@ import {
clearOutsideClick clearOutsideClick
} from 'wkit' } 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/input.js'
import '../form/button.js' import '../form/button.js'
import '../code/index.js'
const COLORS = [ const COLORS = [
'#f3f5fb', '#f3f5fb',
@ -113,7 +116,7 @@ class MEditor extends Component {
line-height: 24px; line-height: 24px;
border-bottom: 1px solid var(--color-grey-1); border-bottom: 1px solid var(--color-grey-1);
section { span {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
@ -415,7 +418,12 @@ class MEditor extends Component {
set value(val) { set value(val) {
if (this.$refs.editor) { if (this.$refs.editor) {
if (this.$refs.editor.value === val) {
return
}
this.$refs.editor.value = val this.$refs.editor.value = val
this.#updatePreview()
this.$emit('input')
} else { } else {
nextTick(_ => (this.value = val)) nextTick(_ => (this.value = val))
} }
@ -429,6 +437,8 @@ class MEditor extends Component {
#select = null #select = null
previewEnabled = false
__init__() { __init__() {
// //
let { outer, inner, thumb } = this.$refs 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') { if (ev && ev.type === 'change') {
file = ev.target.files[0] file = ev.target.files[0]
ev.target.value = '' ev.target.value = ''
@ -486,14 +496,7 @@ class MEditor extends Component {
file, file,
send: link => { send: link => {
this.$refs.editor.focus() this.$refs.editor.focus()
this.restoreSelection() this.insert(`${t}[${file.name}](${link})`)
this.exec(ACTTION.image, link)
this.saveSelection()
// 修正插入的图片,宽度不得超出容器
this.$refs.editor.querySelectorAll('img').forEach(_ => {
_.style.maxWidth = '100%'
})
} }
} }
}) })
@ -504,52 +507,50 @@ class MEditor extends Component {
let txt = ev.clipboardData.getData('text/plain') let txt = ev.clipboardData.getData('text/plain')
let items = ev.clipboardData.items let items = ev.clipboardData.items
if (this.readOnly || this.disabled) {
return
}
// 先文件判断, 避免右键单击复制图片时, 当成html处理 // 先文件判断, 避免右键单击复制图片时, 当成html处理
if (items && items.length) { if (items && items.length) {
let blob = null let file = null
for (let it of items) { for (let it of items) {
if (it.type.indexOf('image') > -1) { file = it.getAsFile()
blob = it.getAsFile() if (file) {
break
} }
} }
if (blob) { if (file) {
return this.#handleImage(null, blob) return this.#handleFile(
null,
file,
file.type.includes('image') ? '!' : ''
)
} }
} }
if (html) { if (html) {
html = html this.insert(html2md(html))
.replace(/\t/g, ' ') } else if (txt) {
.replace(/<\/?(meta|link|script)[^>]*?>/g, '') this.insert(txt)
.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) { #updatePreview() {
return this.exec('insertText', txt) if (this.previewEnabled) {
this.$refs.view.code = this.value
} }
} }
#toolbarClick(ev) { #toolbarClick(ev) {
var target = ev.target var elem = ev.target
var act var act
this.restoreSelection() this.restoreSelection()
if (ev.target === ev.currentTarget) { if (elem === ev.currentTarget) {
return return
} }
@ -557,41 +558,15 @@ class MEditor extends Component {
return return
} }
while (target.tagName !== 'SPAN') { while (elem.tagName !== 'SPAN') {
target = target.parentNode elem = elem.parentNode
} }
act = target.dataset.act act = elem.dataset.act
this.#hideLayers() // this.#hideLayers()
switch (act) { Addon[act].call(this, elem)
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) { #chnageFontSize(ev) {
@ -688,9 +663,248 @@ class MEditor extends Component {
gs.addRange(this.#select) 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() {} mounted() {}
@ -704,6 +918,7 @@ class MEditor extends Component {
<div class="meditor"> <div class="meditor">
<header <header
class=${classMap({ toolbar: true, active: this.#toolbar.length })} class=${classMap({ toolbar: true, active: this.#toolbar.length })}
@click=${this.#toolbarClick}
> >
${renderToolbar(this.#toolbar)} ${renderToolbar(this.#toolbar)}
</header> </header>
@ -714,11 +929,11 @@ class MEditor extends Component {
spellcheck="false" spellcheck="false"
:readOnly=${this.readOnly} :readOnly=${this.readOnly}
:disabled=${this.disabled} :disabled=${this.disabled}
@input=${function (ev) { @keydown=${this.#handleKeydown}
// this.value = ev.target.value @paste.prevent=${this.#handlePaste}
}} @input=${this.#updatePreview}
></textarea> ></textarea>
<wc-markd class="preview"></wc-markd> <wc-markd ref="view" class="preview"></wc-markd>
</div> </div>
</div> </div>
` `