mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 21:02:27 +08:00

* 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
297 lines
9.0 KiB
JavaScript
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
|