marktext/src/muya/lib/eventHandler/keyboard.js
Ran Luo 289b17c015 Optimization of table block (#1456)
* Prepare for drag and drop row and column

* remove regexp th|td

* render drag button

* Feat: support drag and drop row and column of table

* Feat: table bar tools

* remove unnecessary codes

* Feat: support select multiple cells

* Do not show table drag bar when selected cells

* Feat: support delete selected cells content or remove row/column/table

* Feat: select one cell or table when press ctrl + a

* Support select all content

* Remove table tools in context menu

* Feat: support copy paste selected cells as sub table

* Fix: PR issue 1 press tab will not show the table drag bars

* Select one cell and press backspace will cause bug

* Fix: The table drag bar location error when there are tow tables in the editor

* Fix unable copy and paste 1* n or n * 1 table

* Drag any row to the top to editor will cause error.

* Update table resize icon

* Fix: table resize is not work in table tool bar

* Fix: No need to show left drag bar if only one row, and no need to show bottom drag bar if only one column.

* Fix: Create an empty table in source code mode, turn to preview mode, there are more than two drag bars in one table.

* Fix: resize table

* Opti: table is not 100% width now

* Fix drag in one row or column

* Change table delete icon

* Fix: backspace is not work

* Little style opti

* Fix: cmd + enter bug

* Update the table drag bar context menu text

* Handle delete key when select table cells

* remove all unnecessary debug codes

* Feat: support cut selected cells and copy/cut by context menu

* Fix typo

* Rename some methods name

* Fix an issue when drag and drop table drag bar

* fix do not handle cell selection when the context menu shown

* Do not handle select cells when mouse up outside table
2019-10-13 13:23:00 +02:00

297 lines
9.0 KiB
JavaScript

import { EVENT_KEYS } from '../config'
import selection from '../selection'
import { findNearestParagraph } from '../selection/dom'
import { getParagraphReference, getImageInfo } from '../utils'
import { checkEditEmoji } from '../ui/emojis'
class Keyboard {
constructor (muya) {
this.muya = muya
this.isComposed = false
this.shownFloat = new Set()
this.recordIsComposed()
this.dispatchEditorState()
this.keydownBinding()
this.keyupBinding()
this.inputBinding()
this.listen()
}
listen () {
// cache shown float box
this.muya.eventCenter.subscribe('muya-float', (tool, status) => {
status ? this.shownFloat.add(tool) : this.shownFloat.delete(tool)
if (tool.name === 'ag-front-menu' && !status) {
const seletedParagraph = this.muya.container.querySelector('.ag-selected')
if (seletedParagraph) {
this.muya.contentState.selectedBlock = null
// prevent rerender, so change the class manually.
seletedParagraph.classList.toggle('ag-selected')
}
}
})
}
hideAllFloatTools () {
for (const tool of this.shownFloat) {
tool.hide()
}
}
recordIsComposed () {
const { container, eventCenter, contentState } = this.muya
const handler = event => {
if (event.type === 'compositionstart') {
this.isComposed = true
} else if (event.type === 'compositionend') {
this.isComposed = false
// Because the compose event will not cause `input` event, So need call `inputHandler` by ourself
contentState.inputHandler(event)
eventCenter.dispatch('stateChange')
}
}
eventCenter.attachDOMEvent(container, 'compositionend', handler)
// eventCenter.attachDOMEvent(container, 'compositionupdate', handler)
eventCenter.attachDOMEvent(container, 'compositionstart', handler)
}
dispatchEditorState () {
const { container, eventCenter } = this.muya
let timer = null
const changeHandler = event => {
if (
event.type === 'keyup' &&
(event.key === EVENT_KEYS.ArrowUp || event.key === EVENT_KEYS.ArrowDown) &&
this.shownFloat.size > 0
) {
return
}
// Cursor outside editor area or over not editable elements.
if (event.target.closest('[contenteditable=false]')) {
return
}
// We need check cursor is null, because we may copy the html preview content,
// and no need to dispatch change.
const { start, end } = selection.getCursorRange()
if (!start || !end) {
return
}
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
this.muya.dispatchSelectionChange()
this.muya.dispatchSelectionFormats()
if (!this.isComposed && event.type === 'click') {
this.muya.dispatchChange()
}
})
}
eventCenter.attachDOMEvent(container, 'click', changeHandler)
eventCenter.attachDOMEvent(container, 'keyup', changeHandler)
}
keydownBinding () {
const { container, eventCenter, contentState } = this.muya
const docHandler = event => {
switch (event.code) {
case EVENT_KEYS.Enter:
return contentState.docEnterHandler(event)
case EVENT_KEYS.Space: {
if (contentState.selectedImage) {
const { token } = contentState.selectedImage
const { src } = getImageInfo(token.src || token.attrs.src)
if (src) {
eventCenter.dispatch('preview-image', {
data: src
})
}
}
break
}
case EVENT_KEYS.Backspace: {
return contentState.docBackspaceHandler(event)
}
case EVENT_KEYS.Delete: {
return contentState.docDeleteHandler(event)
}
case EVENT_KEYS.ArrowUp: // fallthrough
case EVENT_KEYS.ArrowDown: // fallthrough
case EVENT_KEYS.ArrowLeft: // fallthrough
case EVENT_KEYS.ArrowRight: // fallthrough
return contentState.docArrowHandler(event)
}
}
const handler = event => {
if (event.metaKey || event.ctrlKey) {
container.classList.add('ag-meta-or-ctrl')
}
if (
this.shownFloat.size > 0 &&
(
event.key === EVENT_KEYS.Enter ||
event.key === EVENT_KEYS.Escape ||
event.key === EVENT_KEYS.Tab ||
event.key === EVENT_KEYS.ArrowUp ||
event.key === EVENT_KEYS.ArrowDown
)
) {
let needPreventDefault = false
for (const tool of this.shownFloat) {
if (
tool.name === 'ag-format-picker' ||
tool.name === 'ag-table-picker' ||
tool.name === 'ag-quick-insert' ||
tool.name === 'ag-emoji-picker' ||
tool.name === 'ag-front-menu' ||
tool.name === 'ag-list-picker' ||
tool.name === 'ag-image-selector'
) {
needPreventDefault = true
break
}
}
if (needPreventDefault) {
event.preventDefault()
}
// event.stopPropagation()
return
}
switch (event.key) {
case EVENT_KEYS.Backspace:
contentState.backspaceHandler(event)
break
case EVENT_KEYS.Delete:
contentState.deleteHandler(event)
break
case EVENT_KEYS.Enter:
if (!this.isComposed) {
contentState.enterHandler(event)
this.muya.dispatchChange()
}
break
case EVENT_KEYS.ArrowUp: // fallthrough
case EVENT_KEYS.ArrowDown: // fallthrough
case EVENT_KEYS.ArrowLeft: // fallthrough
case EVENT_KEYS.ArrowRight: // fallthrough
if (!this.isComposed) {
contentState.arrowHandler(event)
}
break
case EVENT_KEYS.Tab:
contentState.tabHandler(event)
break
default:
break
}
}
eventCenter.attachDOMEvent(container, 'keydown', handler)
eventCenter.attachDOMEvent(document, 'keydown', docHandler)
}
inputBinding () {
const { container, eventCenter, contentState } = this.muya
const inputHandler = event => {
if (!this.isComposed) {
contentState.inputHandler(event)
this.muya.dispatchChange()
}
const { lang, paragraph } = contentState.checkEditLanguage()
if (lang) {
eventCenter.dispatch('muya-code-picker', {
reference: getParagraphReference(paragraph, paragraph.id),
lang,
cb: item => {
contentState.selectLanguage(paragraph, item.name)
}
})
} else {
// hide code picker float box
eventCenter.dispatch('muya-code-picker', { reference: null })
}
}
eventCenter.attachDOMEvent(container, 'input', inputHandler)
}
keyupBinding () {
const { container, eventCenter, contentState } = this.muya
const handler = event => {
container.classList.remove('ag-meta-or-ctrl')
// check if edit emoji
const node = selection.getSelectionStart()
const paragraph = findNearestParagraph(node)
const emojiNode = checkEditEmoji(node)
contentState.selectedImage = null
if (
paragraph &&
emojiNode &&
event.key !== EVENT_KEYS.Enter &&
event.key !== EVENT_KEYS.ArrowDown &&
event.key !== EVENT_KEYS.ArrowUp &&
event.key !== EVENT_KEYS.Tab &&
event.key !== EVENT_KEYS.Escape
) {
const reference = getParagraphReference(emojiNode, paragraph.id)
eventCenter.dispatch('muya-emoji-picker', {
reference,
emojiNode
})
}
if (!emojiNode) {
eventCenter.dispatch('muya-emoji-picker', {
emojiNode
})
}
const { anchor, focus, start, end } = selection.getCursorRange()
if (!anchor || !focus) {
return
}
if (
!this.isComposed
) {
const { anchor: oldAnchor, focus: oldFocus } = contentState.cursor
if (
anchor.key !== oldAnchor.key ||
anchor.offset !== oldAnchor.offset ||
focus.key !== oldFocus.key ||
focus.offset !== oldFocus.offset
) {
const needRender = contentState.checkNeedRender(contentState.cursor) || contentState.checkNeedRender({ start, end })
contentState.cursor = { anchor, focus }
if (needRender) {
return contentState.partialRender()
}
}
}
const block = contentState.getBlock(anchor.key)
if (
anchor.key === focus.key &&
anchor.offset !== focus.offset &&
block.functionType !== 'codeContent' &&
block.functionType !== 'languageInput'
) {
const reference = contentState.getPositionReference()
const { formats } = contentState.selectionFormats()
eventCenter.dispatch('muya-format-picker', { reference, formats })
} else {
eventCenter.dispatch('muya-format-picker', { reference: null })
}
}
eventCenter.attachDOMEvent(container, 'keyup', handler) // temp use input event
}
}
export default Keyboard