mirror of
https://github.com/marktext/marktext.git
synced 2025-05-04 13:01:10 +08:00
253 lines
8.1 KiB
JavaScript
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
|