marktext/src/muya/lib/contentState/inputCtrl.js
Ran Luo 5b8da2cdf4
Optimization of code block (#1445)
* duplicate css rule

* remove all codeLine

* Fix: #1446

* Fix #942 #1310

* Fix copy paste will add one more empty line in code block

* remove debug codes

* Fix update thematic break error

* fix: #1447

* Update octokit/rest and url-loader

* Fix: CI test error

* Fix comment issue1

* Fix: escape charachters in code block
2019-10-08 14:12:51 +08:00

295 lines
9.5 KiB
JavaScript

import selection from '../selection'
import { getTextContent } from '../selection/dom'
import { beginRules } from '../parser/rules'
import { tokenizer } from '../parser/'
import { CLASS_OR_ID } from '../config'
const BRACKET_HASH = {
'{': '}',
'[': ']',
'(': ')',
'*': '*',
_: '_',
'"': '"',
'\'': '\'',
$: '$',
'~': '~'
}
const BACK_HASH = {
'}': '{',
']': '[',
')': '(',
'*': '*',
_: '_',
'"': '"',
'\'': '\'',
$: '$',
'~': '~'
}
const inputCtrl = ContentState => {
// Input @ to quick insert paragraph
ContentState.prototype.checkQuickInsert = function (block) {
const { type, text, functionType } = block
if (type !== 'span' || functionType !== 'paragraphContent') return false
return /^@\S*$/.test(text)
}
ContentState.prototype.checkCursorInTokenType = function (text, offset, type) {
const tokens = tokenizer(text, [], false)
return tokens.filter(t => t.type === type).some(t => offset >= t.range.start && offset <= t.range.end)
}
ContentState.prototype.checkNotSameToken = function (oldText, text) {
const oldTokens = tokenizer(oldText)
const tokens = tokenizer(text)
const oldCache = {}
const cache = {}
for (const { type } of oldTokens) {
if (oldCache[type]) {
oldCache[type]++
} else {
oldCache[type] = 1
}
}
for (const { type } of tokens) {
if (cache[type]) {
cache[type]++
} else {
cache[type] = 1
}
}
if (Object.keys(oldCache).length !== Object.keys(cache).length) {
return true
}
for (const key of Object.keys(oldCache)) {
if (!cache[key] || oldCache[key] !== cache[key]) {
return true
}
}
return false
}
ContentState.prototype.inputHandler = function (event, notEqual = false) {
const { start, end } = selection.getCursorRange()
if (!start || !end) {
return
}
const { start: oldStart, end: oldEnd } = this.cursor
const key = start.key
const block = this.getBlock(key)
const paragraph = document.querySelector(`#${key}`)
// Fix issue 1447
// Fixme: any better solution?
if (
oldStart.key === oldEnd.key &&
oldStart.offset === oldEnd.offset &&
block.text.endsWith('\n') &&
oldStart.offset === block.text.length &&
event.inputType === 'insertText'
) {
event.preventDefault()
block.text += event.data
const offset = block.text.length
this.cursor = {
start: { key, offset },
end: { key, offset }
}
this.singleRender(block)
return this.inputHandler(event, true)
}
let text = getTextContent(paragraph, [CLASS_OR_ID.AG_MATH_RENDER, CLASS_OR_ID.AG_RUBY_RENDER])
let needRender = false
let needRenderAll = false
if (oldStart.key !== oldEnd.key) {
const startBlock = this.getBlock(oldStart.key)
const startOutmostBlock = this.findOutMostBlock(startBlock)
const endBlock = this.getBlock(oldEnd.key)
const endOutmostBlock = this.findOutMostBlock(endBlock)
if (
// fix #918.
startBlock.functionType === 'languageInput'
) {
if (startOutmostBlock === endOutmostBlock && !endBlock.nextSibling) {
this.removeBlocks(startBlock, endBlock, false)
endBlock.text = ''
} else if (startOutmostBlock !== endOutmostBlock) {
const preBlock = this.getParent(startBlock)
const pBlock = this.createBlock('p')
this.removeBlocks(startBlock, endBlock)
startBlock.functionType = 'paragraphContent'
this.appendChild(pBlock, startBlock)
this.insertBefore(pBlock, preBlock)
this.removeBlock(preBlock)
} else {
this.removeBlocks(startBlock, endBlock)
}
} else {
this.removeBlocks(startBlock, endBlock)
}
if (this.blocks.length === 1) {
needRenderAll = true
}
needRender = true
}
// auto pair (not need to auto pair in math block)
if (block && (block.text !== text || notEqual)) {
if (
start.key === end.key &&
start.offset === end.offset &&
event.type === 'input'
) {
const { offset } = start
const { autoPairBracket, autoPairMarkdownSyntax, autoPairQuote } = this.muya.options
const inputChar = text.charAt(+offset - 1)
const preInputChar = text.charAt(+offset - 2)
const prePreInputChar = text.charAt(+offset - 3)
const postInputChar = text.charAt(+offset)
if (/^delete/.test(event.inputType)) {
// handle `deleteContentBackward` or `deleteContentForward`
const deletedChar = block.text[offset]
if (event.inputType === 'deleteContentBackward' && postInputChar === BRACKET_HASH[deletedChar]) {
needRender = true
text = text.substring(0, offset) + text.substring(offset + 1)
}
if (event.inputType === 'deleteContentForward' && inputChar === BACK_HASH[deletedChar]) {
needRender = true
start.offset -= 1
end.offset -= 1
text = text.substring(0, offset - 1) + text.substring(offset)
}
/* eslint-disable no-useless-escape */
} else if (
(event.inputType.indexOf('delete') === -1) &&
(inputChar === postInputChar) &&
(
(autoPairQuote && /[']{1}/.test(inputChar)) ||
(autoPairQuote && /["]{1}/.test(inputChar)) ||
(autoPairBracket && /[\}\]\)]{1}/.test(inputChar)) ||
(autoPairMarkdownSyntax && /[$]{1}/.test(inputChar)) ||
(autoPairMarkdownSyntax && /[*$`~_]{1}/.test(inputChar)) && /[_*~]{1}/.test(prePreInputChar)
)
) {
needRender = true
text = text.substring(0, offset) + text.substring(offset + 1)
} else {
/* eslint-disable no-useless-escape */
// Not Unicode aware, since things like \p{Alphabetic} or \p{L} are not supported yet
const isInInlineMath = this.checkCursorInTokenType(text, offset, 'inline_math')
const isInInlineCode = this.checkCursorInTokenType(text, offset, 'inline_code')
if (
!/\\/.test(preInputChar) &&
((autoPairQuote && /[']{1}/.test(inputChar) && !(/[a-zA-Z\d]{1}/.test(preInputChar))) ||
(autoPairQuote && /["]{1}/.test(inputChar)) ||
(autoPairBracket && /[\{\[\(]{1}/.test(inputChar)) ||
(block.functionType !== 'codeContent' && !isInInlineMath && !isInInlineCode && autoPairMarkdownSyntax && /[*$`~_]{1}/.test(inputChar)))
) {
needRender = true
text = BRACKET_HASH[event.data]
? text.substring(0, offset) + BRACKET_HASH[inputChar] + text.substring(offset)
: text
}
/* eslint-enable no-useless-escape */
// Delete the last `*` of `**` when you insert one space between `**` to create a bullet list.
if (
/\s/.test(event.data) &&
/^\* /.test(text) &&
preInputChar === '*' &&
postInputChar === '*'
) {
text = text.substring(0, offset) + text.substring(offset + 1)
needRender = true
}
}
}
if (this.checkNotSameToken(block.text, text)) {
needRender = true
}
// Just work for `Shift + Enter` to create a soft and hard line break.
if (
block.text.endsWith('\n') &&
start.offset === text.length &&
(event.inputType === 'insertText' || event.type === 'compositionend')
) {
block.text += event.data
start.offset++
end.offset++
} else if (
block.text.length === oldStart.offset &&
block.text[oldStart.offset - 2] === '\n' &&
event.inputType === 'deleteContentBackward'
) {
block.text = block.text.substring(0, oldStart.offset - 1)
start.offset = block.text.length
end.offset = block.text.length
} else {
block.text = text
}
// Update code block language when modify code block identifer
if (block.functionType === 'languageInput') {
const parent = this.getParent(block)
parent.lang = block.text
}
if (beginRules.reference_definition.test(text)) {
needRenderAll = true
}
}
// show quick insert
const rect = paragraph.getBoundingClientRect()
const checkQuickInsert = this.checkQuickInsert(block)
const reference = this.getPositionReference()
reference.getBoundingClientRect = function () {
const { x, y, left, top, height, bottom } = rect
return Object.assign({}, {
left,
x,
top,
y,
bottom,
height,
width: 0,
right: left
})
}
this.muya.eventCenter.dispatch('muya-quick-insert', reference, block, !!checkQuickInsert)
if (block && block.type === 'span' && block.functionType === 'codeContent') {
needRender = true
}
this.cursor = { start, end }
const checkMarkedUpdate = this.checkNeedRender()
const inlineUpdatedBlock = this.isCollapse() && this.checkInlineUpdate(block)
// just for fix #707,need render All if in combines pre list and next list into one list.
if (inlineUpdatedBlock) {
const liBlock = this.getParent(inlineUpdatedBlock)
if (liBlock && liBlock.type === 'li' && liBlock.preSibling && liBlock.nextSibling) {
needRenderAll = true
}
}
if (checkMarkedUpdate || inlineUpdatedBlock || needRender) {
return needRenderAll ? this.render() : this.partialRender()
}
}
}
export default inputCtrl