marktext/src/muya/lib/contentState/clickCtrl.js

253 lines
8.1 KiB
JavaScript

import selection from '../selection'
import { isMuyaEditorElement } from '../selection/dom'
import { HAS_TEXT_BLOCK_REG, CLASS_OR_ID } from '../config'
import { getParentCheckBox } from '../utils/getParentCheckBox'
import { cumputeCheckboxStatus } from '../utils/cumputeCheckBoxStatus'
const clickCtrl = ContentState => {
ContentState.prototype.clickHandler = function (event) {
const { eventCenter } = this.muya
const { target } = event
if (isMuyaEditorElement(target)) {
const lastBlock = this.getLastBlock()
const archor = this.findOutMostBlock(lastBlock)
const archorParagraph = document.querySelector(`#${archor.key}`)
const rect = archorParagraph.getBoundingClientRect()
// If click below the last paragraph
// and the last paragraph is not empty, create a new empty paragraph
if (event.clientY > rect.top + rect.height) {
let needToInsertNewParagraph = false
if (lastBlock.type === 'span') {
if (/atxLine|paragraphContent/.test(lastBlock.functionType) && /\S/.test(lastBlock.text)) {
needToInsertNewParagraph = true
}
if (!/atxLine|paragraphContent/.test(lastBlock.functionType)) {
needToInsertNewParagraph = true
}
} else {
needToInsertNewParagraph = true
}
if (needToInsertNewParagraph) {
event.preventDefault()
const paragraphBlock = this.createBlockP()
this.insertAfter(paragraphBlock, archor)
const key = paragraphBlock.children[0].key
const offset = 0
this.cursor = {
start: { key, offset },
end: { key, offset }
}
return this.render()
}
}
}
// handle front menu click
const { start: oldStart, end: oldEnd } = this.cursor
if (oldStart && oldEnd) {
let hasSameParent = false
const startBlock = this.getBlock(oldStart.key)
const endBlock = this.getBlock(oldEnd.key)
if (startBlock && endBlock) {
const startOutBlock = this.findOutMostBlock(startBlock)
const endOutBlock = this.findOutMostBlock(endBlock)
hasSameParent = startOutBlock === endOutBlock
}
// show the muya-front-menu only when the cursor in the same paragraph
if (target.closest('.ag-front-icon') && hasSameParent) {
const currentBlock = this.findOutMostBlock(startBlock)
const frontIcon = target.closest('.ag-front-icon')
const rect = frontIcon.getBoundingClientRect()
const reference = {
getBoundingClientRect () {
return rect
},
clientWidth: rect.width,
clientHeight: rect.height,
id: currentBlock.key
}
this.selectedBlock = currentBlock
eventCenter.dispatch('muya-front-menu', { reference, outmostBlock: currentBlock, startBlock, endBlock })
return this.partialRender()
}
}
const { start, end } = selection.getCursorRange()
// fix #625, the selection maybe not in edit area.
if (!start || !end) {
return
}
// format-click
const node = selection.getSelectionStart()
const inlineNode = node ? node.closest('.ag-inline-rule') : null
// link-format-click
let parentNode = inlineNode
while (parentNode !== null && parentNode.classList.contains(CLASS_OR_ID.AG_INLINE_RULE)) {
if (parentNode.tagName === 'A') {
const formatType = 'link' // auto link or []() link
const data = {
text: inlineNode.textContent,
href: parentNode.getAttribute('href')
}
eventCenter.dispatch('format-click', {
event,
formatType,
data
})
break
} else {
parentNode = parentNode.parentNode
}
}
if (inlineNode) {
let formatType = null
let data = null
switch (inlineNode.tagName) {
case 'SPAN': {
if (inlineNode.hasAttribute('data-emoji')) {
formatType = 'emoji'
data = inlineNode.getAttribute('data-emoji')
} else if (inlineNode.classList.contains('ag-math-text')) {
formatType = 'inline_math'
data = inlineNode.textContent
}
break
}
case 'STRONG': {
formatType = 'strong'
data = inlineNode.textContent
break
}
case 'EM': {
formatType = 'em'
data = inlineNode.textContent
break
}
case 'DEL': {
formatType = 'del'
data = inlineNode.textContent
break
}
case 'CODE': {
formatType = 'inline_code'
data = inlineNode.textContent
break
}
}
if (formatType) {
eventCenter.dispatch('format-click', {
event,
formatType,
data
})
}
}
const block = this.getBlock(start.key)
let needRender = false
// is show format float box?
if (
start.key === end.key &&
start.offset !== end.offset &&
HAS_TEXT_BLOCK_REG.test(block.type) &&
block.functionType !== 'codeContent' &&
block.functionType !== 'languageInput'
) {
const reference = this.getPositionReference()
const { formats } = this.selectionFormats()
eventCenter.dispatch('muya-format-picker', { reference, formats })
}
// update '```xxx' to code block when you click other place or use press arrow key.
if (block && start.key !== this.cursor.start.key) {
const oldBlock = this.getBlock(this.cursor.start.key)
if (oldBlock) {
needRender = needRender || this.codeBlockUpdate(oldBlock)
}
}
// change active status when paragraph changed
if (
start.key !== this.cursor.start.key ||
end.key !== this.cursor.end.key
) {
needRender = true
}
const needMarkedUpdate = this.checkNeedRender(this.cursor) || this.checkNeedRender({ start, end })
if (needRender) {
this.cursor = { start, end }
return this.partialRender()
} else if (needMarkedUpdate) {
// Fix: whole select can not be canceled #613
requestAnimationFrame(() => {
const cursor = selection.getCursorRange()
if (!cursor.start || !cursor.end) {
return
}
this.cursor = cursor
return this.partialRender()
})
} else {
this.cursor = { start, end }
}
}
ContentState.prototype.setCheckBoxState = function (checkbox, checked) {
checkbox.checked = checked
const block = this.getBlock(checkbox.id)
block.checked = checked
checkbox.classList.toggle(CLASS_OR_ID.AG_CHECKBOX_CHECKED)
}
ContentState.prototype.updateParentsCheckBoxState = function (checkbox) {
let parent = getParentCheckBox(checkbox)
while (parent !== null) {
const checked = cumputeCheckboxStatus(parent)
if (parent.checked !== checked) {
this.setCheckBoxState(parent, checked)
parent = getParentCheckBox(parent)
} else {
break
}
}
}
ContentState.prototype.updateChildrenCheckBoxState = function (checkbox, checked) {
const checkboxes = checkbox.parentElement.querySelectorAll(`input ~ ul .${CLASS_OR_ID.AG_TASK_LIST_ITEM_CHECKBOX}`)
const len = checkboxes.length
for (let i = 0; i < len; i++) {
const checkbox = checkboxes[i]
if (checkbox.checked !== checked) {
this.setCheckBoxState(checkbox, checked)
}
}
}
// handle task list item checkbox click
ContentState.prototype.listItemCheckBoxClick = function (checkbox) {
const { checked } = checkbox
this.setCheckBoxState(checkbox, checked)
// A task checked, then related task should be update
const { autoCheck } = this.muya.options
if (autoCheck) {
this.updateChildrenCheckBoxState(checkbox, checked)
this.updateParentsCheckBoxState(checkbox)
}
const block = this.getBlock(checkbox.id)
const parentBlock = this.getParent(block)
const firstEditableBlock = this.firstInDescendant(parentBlock)
const { key } = firstEditableBlock
const offset = 0
this.cursor = { start: { key, offset }, end: { key, offset } }
return this.partialRender()
}
}
export default clickCtrl