marktext/src/muya/lib/contentState/arrowCtrl.js
2019-07-24 10:57:21 +08:00

219 lines
6.4 KiB
JavaScript

import { EVENT_KEYS, CLASS_OR_ID } from '../config'
import { findNearestParagraph } from '../selection/dom'
import selection from '../selection'
// If the next block is header, put cursor after the `#{1,6} *`
const adjustOffset = (offset, block, event) => {
if (/^span$/.test(block.type) && block.functionType === 'atxLine' && event.key === EVENT_KEYS.ArrowDown) {
const match = /^\s{0,3}(?:#{1,6})(?:\s{1,}|$)/.exec(block.text)
if (match) {
return match[0].length
}
}
return offset
}
const arrowCtrl = ContentState => {
ContentState.prototype.findNextRowCell = function (cell) {
if (!/th|td/.test(cell.type)) {
throw new Error(`block with type ${cell && cell.type} is not a table cell`)
}
const row = this.getParent(cell)
const rowContainer = this.getParent(row) // thead or tbody
const column = row.children.indexOf(cell)
if (rowContainer.type === 'thead') {
const tbody = this.getNextSibling(rowContainer)
if (tbody && tbody.children.length) {
return tbody.children[0].children[column]
}
} else if (rowContainer.type === 'tbody') {
const nextRow = this.getNextSibling(row)
if (nextRow) {
return nextRow.children[column]
}
}
return null
}
ContentState.prototype.findPrevRowCell = function (cell) {
if (!/th|td/.test(cell.type)) throw new Error(`block with type ${cell && cell.type} is not a table cell`)
const row = this.getParent(cell)
const rowContainer = this.getParent(row) // thead or tbody
const rowIndex = rowContainer.children.indexOf(row)
const column = row.children.indexOf(cell)
if (rowContainer.type === 'tbody') {
if (rowIndex === 0 && rowContainer.preSibling) {
const thead = this.getPreSibling(rowContainer)
return thead.children[0].children[column]
} else if (rowIndex > 0) {
return this.getPreSibling(row).children[column]
}
return null
}
return null
}
ContentState.prototype.docArrowHandler = function (event) {
const { selectedImage } = this
if (selectedImage) {
const { key, token } = selectedImage
const { start, end } = token.range
event.preventDefault()
event.stopPropagation()
const block = this.getBlock(key)
switch (event.key) {
case EVENT_KEYS.ArrowUp:
case EVENT_KEYS.ArrowLeft: {
this.cursor = {
start: { key, offset: start },
end: { key, offset: start }
}
break
}
case EVENT_KEYS.ArrowDown:
case EVENT_KEYS.ArrowRight: {
this.cursor = {
start: { key, offset: end },
end: { key, offset: end }
}
break
}
}
return this.singleRender(block)
}
}
ContentState.prototype.arrowHandler = function (event) {
const node = selection.getSelectionStart()
const paragraph = findNearestParagraph(node)
const id = paragraph.id
const block = this.getBlock(id)
const preBlock = this.findPreBlockInLocation(block)
const nextBlock = this.findNextBlockInLocation(block)
const { start, end } = selection.getCursorRange()
const { topOffset, bottomOffset } = selection.getCursorYOffset(paragraph)
if (!start || !end) {
return
}
// fix #101
if (event.key === EVENT_KEYS.ArrowRight && node && node.classList && node.classList.contains(CLASS_OR_ID['AG_MATH_TEXT'])) {
const { right } = selection.getCaretOffsets(node)
if (right === 0 && start.key === end.key && start.offset === end.offset) {
// It's not recommended to use such lower API, but it's work well.
return selection.select(node.parentNode.nextElementSibling, 0)
}
}
// Just do nothing if the cursor is not collapsed or `shiftKey` pressed
if (
(start.key === end.key && start.offset !== end.offset) ||
start.key !== end.key || event.shiftKey
) {
return
}
if (
(event.key === EVENT_KEYS.ArrowUp && topOffset > 0) ||
(event.key === EVENT_KEYS.ArrowDown && bottomOffset > 0)
) {
if (!/pre|th|td/.test(block.type)) {
return
}
}
if (/th|td/.test(block.type)) {
let activeBlock
const cellInNextRow = this.findNextRowCell(block)
const cellInPrevRow = this.findPrevRowCell(block)
if (event.key === EVENT_KEYS.ArrowUp) {
if (cellInPrevRow) {
activeBlock = cellInPrevRow
} else {
activeBlock = this.findPreBlockInLocation(this.getTableBlock())
}
}
if (event.key === EVENT_KEYS.ArrowDown) {
if (cellInNextRow) {
activeBlock = cellInNextRow
} else {
activeBlock = this.findNextBlockInLocation(this.getTableBlock())
}
}
if (activeBlock) {
event.preventDefault()
event.stopPropagation()
let offset = activeBlock.type === 'p'
? 0
: (event.key === EVENT_KEYS.ArrowUp
? activeBlock.text.length
: 0)
offset = adjustOffset(offset, activeBlock, event)
const key = activeBlock.type === 'p'
? activeBlock.children[0].key
: activeBlock.key
this.cursor = {
start: {
key,
offset
},
end: {
key,
offset
}
}
return this.partialRender()
}
}
if (
(event.key === EVENT_KEYS.ArrowUp) ||
(event.key === EVENT_KEYS.ArrowLeft && start.offset === 0)
) {
event.preventDefault()
event.stopPropagation()
if (!preBlock) return
const key = preBlock.key
const offset = preBlock.text.length
this.cursor = {
start: { key, offset },
end: { key, offset }
}
return this.partialRender()
} else if (
(event.key === EVENT_KEYS.ArrowDown) ||
(event.key === EVENT_KEYS.ArrowRight && start.offset === block.text.length)
) {
event.preventDefault()
event.stopPropagation()
let key
let newBlock
if (nextBlock) {
key = nextBlock.key
} else {
newBlock = this.createBlockP()
const lastBlock = this.blocks[this.blocks.length - 1]
this.insertAfter(newBlock, lastBlock)
key = newBlock.children[0].key
}
const offset = adjustOffset(0, nextBlock || newBlock, event)
this.cursor = {
start: { key, offset },
end: { key, offset }
}
return this.partialRender()
}
}
}
export default arrowCtrl