946 lines
20 KiB
JavaScript
946 lines
20 KiB
JavaScript
/**
|
|
* {}
|
|
* @author yutent<yutent.io@gmail.com>
|
|
* @date 2023/09/14 16:49:15
|
|
*/
|
|
|
|
import {
|
|
css,
|
|
raw,
|
|
html,
|
|
Component,
|
|
bind,
|
|
unbind,
|
|
nextTick,
|
|
styleMap,
|
|
classMap,
|
|
outsideClick,
|
|
clearOutsideClick
|
|
} from 'wkit'
|
|
|
|
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',
|
|
'#dae1e9',
|
|
'#62778d',
|
|
'#58d68d',
|
|
'#3fc2a7',
|
|
'#52a3de',
|
|
'#ac61ce',
|
|
'#ffb618',
|
|
'#e67e22',
|
|
'#ff5061',
|
|
'#ff0000',
|
|
'#000000'
|
|
]
|
|
|
|
// 获取一维数组转二维的行
|
|
function getY(i) {
|
|
return (i / 9) >> 0
|
|
}
|
|
//获取一维数组转二维的列
|
|
function getX(i) {
|
|
return i % 9
|
|
}
|
|
|
|
function getIndex(x, y) {
|
|
return x + y * 9
|
|
}
|
|
|
|
class MEditor extends Component {
|
|
static watches = ['value']
|
|
|
|
static props = {
|
|
toolbar: {
|
|
type: String,
|
|
default: null,
|
|
attribute: false,
|
|
observer(v) {
|
|
if (v === null) {
|
|
this.#toolbar = [...DEFAULT_TOOLS]
|
|
} else {
|
|
this.#toolbar = v
|
|
.split(',')
|
|
.filter(it => it)
|
|
.map(it => it.trim())
|
|
}
|
|
}
|
|
},
|
|
readonly: false,
|
|
disabled: false
|
|
}
|
|
|
|
static styles = [
|
|
css`
|
|
:host {
|
|
display: flex;
|
|
min-width: 200px;
|
|
max-height: 720px;
|
|
border-radius: 3px;
|
|
transition: box-shadow 0.15s linear;
|
|
background: var(--wc-meditor-background, #fff);
|
|
}
|
|
.noselect {
|
|
-webkit-touch-callout: none;
|
|
-webkit-user-select: none;
|
|
-moz-user-select: none;
|
|
user-select: none;
|
|
}
|
|
`,
|
|
|
|
css`
|
|
.meditor {
|
|
position: relative;
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
border: 1px solid var(--wc-editor-border-color, var(--color-grey-2));
|
|
border-radius: inherit;
|
|
font-size: 14px;
|
|
background: #fff;
|
|
}
|
|
|
|
.toolbar {
|
|
display: none;
|
|
width: 100%;
|
|
height: 34px;
|
|
padding: 5px;
|
|
line-height: 24px;
|
|
border-bottom: 1px solid var(--color-grey-1);
|
|
|
|
span {
|
|
position: relative;
|
|
overflow: hidden;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
margin: 0 3px;
|
|
border-radius: 3px;
|
|
color: var(--color-grey-3);
|
|
|
|
.icon {
|
|
overflow: hidden;
|
|
width: 70%;
|
|
height: 70%;
|
|
fill: currentColor;
|
|
color: #62778d;
|
|
}
|
|
|
|
&:hover,
|
|
&.active {
|
|
background: var(--color-plain-1);
|
|
}
|
|
&.active {
|
|
color: var(--color-teal-1);
|
|
}
|
|
}
|
|
|
|
&.active {
|
|
display: flex;
|
|
}
|
|
}
|
|
`,
|
|
css`
|
|
.editor-outbox {
|
|
flex: 1;
|
|
display: flex;
|
|
width: 100%;
|
|
min-height: 300px;
|
|
border-radius: 3px;
|
|
|
|
.editor,
|
|
.preview {
|
|
flex: 1;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.editor {
|
|
height: 100%;
|
|
padding: 5px 8px;
|
|
line-height: 1.5;
|
|
border: 0;
|
|
font-size: 14px;
|
|
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
|
|
color: var(--color-dark-1);
|
|
background: none;
|
|
outline: none;
|
|
resize: none;
|
|
cursor: inherit;
|
|
|
|
&::placeholder {
|
|
color: var(--color-grey-1);
|
|
}
|
|
}
|
|
|
|
.preview {
|
|
overflow: hidden;
|
|
overflow-y: auto;
|
|
display: none;
|
|
padding: 6px 12px;
|
|
border-left: 1px solid var(--color-plain-2);
|
|
|
|
&.active {
|
|
display: block;
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
css`
|
|
:host([readonly]) {
|
|
.editor {
|
|
cursor: default;
|
|
opacity: 0.8;
|
|
}
|
|
}
|
|
:host([disabled]) {
|
|
.editor {
|
|
cursor: not-allowed;
|
|
opacity: 0.6;
|
|
}
|
|
}
|
|
:host([readonly]),
|
|
:host([disabled]) {
|
|
.toolbar {
|
|
span:hover {
|
|
background: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
:host(:focus-within) {
|
|
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`
|
|
.addon-table {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
width: 222px;
|
|
height: 222px;
|
|
padding: 2px;
|
|
background: #fff;
|
|
|
|
li {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
height: 20px;
|
|
|
|
span {
|
|
width: 20px;
|
|
height: 20px;
|
|
background: var(--color-plain-1);
|
|
|
|
&.active {
|
|
background: rgba(77, 182, 172, 0.3);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.addon-header {
|
|
width: 108px;
|
|
height: 190px;
|
|
padding: 5px 0;
|
|
line-height: 30px;
|
|
user-select: none;
|
|
background: #fff;
|
|
|
|
li {
|
|
display: flex;
|
|
align-items: center;
|
|
width: 100%;
|
|
height: 30px;
|
|
padding: 0 12px;
|
|
transition: background 0.1s ease-in-out;
|
|
cursor: pointer;
|
|
|
|
.icon {
|
|
width: 14px;
|
|
height: 14px;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
&:hover {
|
|
background: var(--color-plain-1);
|
|
}
|
|
}
|
|
}
|
|
|
|
.addon-link {
|
|
width: 320px;
|
|
padding: 8px 5px;
|
|
background: #fff;
|
|
font-size: 13px;
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
.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;
|
|
}
|
|
}
|
|
}
|
|
`
|
|
]
|
|
|
|
get value() {
|
|
try {
|
|
return this.$refs.editor.value
|
|
} catch (err) {
|
|
console.log(err)
|
|
return ''
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
#toolbar = [...DEFAULT_TOOLS]
|
|
#value = ''
|
|
#cache = { bar: 0, y: 0 }
|
|
#gridx = 0
|
|
#gridy = 0
|
|
|
|
#select = null
|
|
|
|
previewEnabled = false
|
|
|
|
__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'
|
|
}
|
|
|
|
#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')
|
|
}
|
|
|
|
// 处理图片
|
|
#handleFile(ev, file, t = '!') {
|
|
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.insert(`${t}[${file.name}](${link})`)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
#handlePaste(ev) {
|
|
let html = ev.clipboardData.getData('text/html')
|
|
let txt = ev.clipboardData.getData('text/plain')
|
|
let items = ev.clipboardData.items
|
|
|
|
if (this.readOnly || this.disabled) {
|
|
return
|
|
}
|
|
|
|
// 先文件判断, 避免右键单击复制图片时, 当成html处理
|
|
if (items && items.length) {
|
|
let file = null
|
|
|
|
for (let it of items) {
|
|
file = it.getAsFile()
|
|
if (file) {
|
|
break
|
|
}
|
|
}
|
|
|
|
if (file) {
|
|
return this.#handleFile(
|
|
null,
|
|
file,
|
|
file.type.includes('image') ? '!' : ''
|
|
)
|
|
}
|
|
}
|
|
|
|
if (html) {
|
|
this.insert(html2md(html))
|
|
} else if (txt) {
|
|
this.insert(txt)
|
|
}
|
|
}
|
|
|
|
#updatePreview() {
|
|
if (this.previewEnabled) {
|
|
this.$refs.view.code = this.value
|
|
}
|
|
}
|
|
|
|
#toolbarClick(ev) {
|
|
var elem = ev.target
|
|
var act
|
|
|
|
this.restoreSelection()
|
|
|
|
if (elem === ev.currentTarget) {
|
|
return
|
|
}
|
|
|
|
if (this.readOnly || this.disabled) {
|
|
return
|
|
}
|
|
|
|
while (elem.tagName !== 'SPAN') {
|
|
elem = elem.parentNode
|
|
}
|
|
|
|
act = elem.dataset.act
|
|
|
|
// this.#hideLayers()
|
|
|
|
Addon[act].call(this, elem)
|
|
}
|
|
|
|
#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)
|
|
// 避免每次遍历完所有的节点
|
|
let max = Math.max(getIndex(this.#gridx, this.#gridy), idx) + 1
|
|
|
|
if (x === this.#gridx && y === this.#gridy) {
|
|
return
|
|
}
|
|
this.#gridx = x
|
|
this.#gridy = y
|
|
|
|
for (let i = 0; i < max; 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)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 往文本框中插入内容
|
|
* @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() {
|
|
Addon.preview.call(this, this)
|
|
}
|
|
|
|
unmounted() {}
|
|
|
|
render() {
|
|
// console.log(this.#toolbar, this.value)
|
|
|
|
return html`
|
|
<div class="meditor">
|
|
<header
|
|
class=${classMap({ toolbar: true, active: this.#toolbar.length })}
|
|
@click=${this.#toolbarClick}
|
|
>
|
|
${renderToolbar(this.#toolbar)}
|
|
</header>
|
|
<div class="editor-outbox">
|
|
<textarea
|
|
ref="editor"
|
|
class="editor"
|
|
spellcheck="false"
|
|
:readOnly=${this.readOnly}
|
|
:disabled=${this.disabled}
|
|
@keydown=${this.#handleKeydown}
|
|
@paste.prevent=${this.#handlePaste}
|
|
@input=${this.#updatePreview}
|
|
></textarea>
|
|
<wc-markd ref="view" class="preview"></wc-markd>
|
|
</div>
|
|
</div>
|
|
`
|
|
}
|
|
}
|
|
|
|
MEditor.reg('meditor')
|
JavaScript
98.9%
CSS
1.1%