mirror of
https://github.com/marktext/marktext.git
synced 2025-05-05 02:00:31 +08:00
feat: paragraph menu
This commit is contained in:
parent
52c9f6dde7
commit
fea3afb056
1
TODO.md
1
TODO.md
@ -8,6 +8,7 @@
|
||||
- [ ] 在通过 Aganippe 打开文件时,通过右键选择软件,但是打开无内容。(严重 bug)
|
||||
- [ ] export html: (3) keyframe 和 font-face 以及 bar-top 的样式都可以删除。(4) 打包后的应用 axios 获取样式有问题。
|
||||
- [ ] table: 如果 table 在 selection 后面,那么删除cell 的时候,会把整个 row 删除了。(小 bug)
|
||||
- [ ] task list 中 checkbox 无反应
|
||||
|
||||
**菜单**
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { generateKeyHash, genUpper2LowerKeyHash } from './utils'
|
||||
* configs
|
||||
*/
|
||||
// export const INLINE_RULES = ['autolink', 'backticks', 'emphasis', 'escape', 'image', 'link', 'strikethrough']
|
||||
|
||||
export const PARAGRAPH_TYPES = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'ol', 'li', 'figure']
|
||||
export const blockContainerElementNames = [
|
||||
// elements our editor generates
|
||||
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol',
|
||||
|
@ -29,9 +29,9 @@ const enterCtrl = ContentState => {
|
||||
return trBlock
|
||||
}
|
||||
|
||||
ContentState.prototype.createBlockLi = function (text = '') {
|
||||
ContentState.prototype.createBlockLi = function (text = '', type = 'p') {
|
||||
const liBlock = this.createBlock('li')
|
||||
const pBlock = this.createBlock('p', text)
|
||||
const pBlock = this.createBlock(type, text)
|
||||
this.appendChild(liBlock, pBlock)
|
||||
return liBlock
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ class ContentState {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// return block and its parents
|
||||
getParents (block) {
|
||||
const result = []
|
||||
result.push(block)
|
||||
|
@ -1,5 +1,10 @@
|
||||
import selection from '../selection'
|
||||
import { PARAGRAPH_TYPES } from '../config'
|
||||
import ExportMarkdown from '../utils/exportMarkdown'
|
||||
|
||||
// get header level
|
||||
// eg: h1 => 1
|
||||
// h2 => 2
|
||||
const getCurrentLevel = type => {
|
||||
if (/\d/.test(type)) {
|
||||
return Number(/\d/.exec(type)[0])
|
||||
@ -11,14 +16,16 @@ const getCurrentLevel = type => {
|
||||
const paragraphCtrl = ContentState => {
|
||||
ContentState.prototype.selectionChange = function () {
|
||||
const { start, end } = selection.getCursorRange()
|
||||
start.type = this.getBlock(start.key).type
|
||||
end.type = this.getBlock(end.key).type
|
||||
const startBlock = this.getBlock(start.key)
|
||||
const endBlock = this.getBlock(end.key)
|
||||
const startParents = this.getParents(startBlock)
|
||||
const endParents = this.getParents(endBlock)
|
||||
const affiliation = startParents
|
||||
.filter(p => endParents.includes(p))
|
||||
.filter(p => PARAGRAPH_TYPES.includes(p.type))
|
||||
|
||||
const affiliation = startParents.filter(p => endParents.includes(p))
|
||||
start.type = startBlock.type
|
||||
end.type = endBlock.type
|
||||
|
||||
return {
|
||||
start,
|
||||
@ -27,16 +34,212 @@ const paragraphCtrl = ContentState => {
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.updateParagraph = function (paraType) {
|
||||
const {
|
||||
start, end
|
||||
} = selection.getCursorRange()
|
||||
ContentState.prototype.getCommonParent = function () {
|
||||
const { start, end, affiliation } = this.selectionChange()
|
||||
const parent = affiliation.length ? affiliation[0] : null
|
||||
const startBlock = this.getBlock(start.key)
|
||||
const endBlock = this.getBlock(end.key)
|
||||
const startParentKeys = this.getParents(startBlock).map(b => b.key)
|
||||
const endParentKeys = this.getParents(endBlock).map(b => b.key)
|
||||
const children = parent ? parent.children : this.blocks
|
||||
let startIndex
|
||||
let endIndex
|
||||
for (const child of children) {
|
||||
if (startParentKeys.includes(child.key)) {
|
||||
startIndex = children.indexOf(child)
|
||||
}
|
||||
if (endParentKeys.includes(child.key)) {
|
||||
endIndex = children.indexOf(child)
|
||||
}
|
||||
}
|
||||
return { parent, startIndex, endIndex }
|
||||
}
|
||||
|
||||
ContentState.prototype.handleListMenu = function (paraType) {
|
||||
const { start, end, affiliation } = this.selectionChange()
|
||||
const [blockType, listType] = paraType.split('-')
|
||||
const isListed = affiliation.slice(0, 3).filter(b => /ul|ol/.test(b.type))
|
||||
|
||||
if (isListed.length && listType !== isListed[0].listType) {
|
||||
const listBlock = isListed[0]
|
||||
// if the old list block is task list, remove checkbox
|
||||
if (listBlock.listType === 'task') {
|
||||
const listItems = listBlock.children
|
||||
listItems.forEach(item => {
|
||||
const inputBlock = item.children[0]
|
||||
inputBlock && this.removeBlock(inputBlock)
|
||||
})
|
||||
}
|
||||
listBlock.type = blockType
|
||||
listBlock.listType = listType
|
||||
listBlock.children.forEach(b => (b.listItemType = listType))
|
||||
|
||||
if (listType === 'order') {
|
||||
listBlock.start = listBlock.start || 1
|
||||
}
|
||||
// if the new block is task list, add checkbox
|
||||
if (listType === 'task') {
|
||||
const listItems = listBlock.children
|
||||
listItems.forEach(item => {
|
||||
const checkbox = this.createBlock('input')
|
||||
checkbox.checked = false
|
||||
this.insertBefore(checkbox, item.children[0])
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (start.key === end.key) {
|
||||
const block = this.getBlock(start.key)
|
||||
const {
|
||||
type, text
|
||||
} = block
|
||||
if (listType === 'task') {
|
||||
// 1. first update the block to bullet list
|
||||
const listItemParagraph = this.updateList(block, 'bullet')
|
||||
// 2. second update bullet list to task list
|
||||
setTimeout(() => {
|
||||
this.updateTaskListItem(listItemParagraph, listType)
|
||||
this.render()
|
||||
})
|
||||
} else {
|
||||
this.updateList(block, listType)
|
||||
}
|
||||
} else {
|
||||
const { parent, startIndex, endIndex } = this.getCommonParent()
|
||||
const children = parent ? parent.children : this.blocks
|
||||
const referBlock = children[endIndex]
|
||||
const listWrapper = this.createBlock(listType === 'order' ? 'ol' : 'ul')
|
||||
listWrapper.listType = listType
|
||||
this.insertAfter(listWrapper, referBlock)
|
||||
if (listType === 'order') listWrapper.start = 1
|
||||
let i
|
||||
let child
|
||||
const removeCache = []
|
||||
for (i = startIndex; i <= endIndex; i++) {
|
||||
child = children[i]
|
||||
removeCache.push(child)
|
||||
const listItem = this.createBlock('li')
|
||||
listItem.listItemType = listType
|
||||
this.appendChild(listWrapper, listItem)
|
||||
if (listType === 'task') {
|
||||
const checkbox = this.createBlock('input')
|
||||
checkbox.checked = false
|
||||
this.appendChild(listItem, checkbox)
|
||||
}
|
||||
this.appendChild(listItem, child)
|
||||
}
|
||||
removeCache.forEach(b => this.removeBlock(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.handleCodeBlockMenu = function () {
|
||||
const { start, end, affiliation } = this.selectionChange()
|
||||
const startBlock = this.getBlock(start.key)
|
||||
const endBlock = this.getBlock(end.key)
|
||||
const startParents = this.getParents(startBlock)
|
||||
const endParents = this.getParents(endBlock)
|
||||
const hasPreParent = () => {
|
||||
return startParents.some(b => b.type === 'pre') || endParents.some(b => b.type === 'pre')
|
||||
}
|
||||
if (affiliation.length && affiliation[0].type === 'pre') {
|
||||
const codeBlock = affiliation[0]
|
||||
delete codeBlock.pos
|
||||
delete codeBlock.history
|
||||
delete codeBlock.lang
|
||||
this.codeBlocks.delete(codeBlock.key)
|
||||
codeBlock.type = 'p'
|
||||
const key = codeBlock.key
|
||||
const offset = codeBlock.text.length
|
||||
this.cursor = {
|
||||
start: { key, offset },
|
||||
end: { key, offset }
|
||||
}
|
||||
} else {
|
||||
if (start.key === end.key) {
|
||||
startBlock.type = 'pre'
|
||||
startBlock.history = null
|
||||
startBlock.lang = ''
|
||||
} else if (!hasPreParent()) {
|
||||
const { parent, startIndex, endIndex } = this.getCommonParent()
|
||||
const children = parent ? parent.children : this.blocks
|
||||
const referBlock = children[endIndex]
|
||||
const codeBlock = this.createBlock('pre')
|
||||
codeBlock.history = null
|
||||
codeBlock.lang = ''
|
||||
const markdown = new ExportMarkdown(children.slice(startIndex, endIndex + 1)).generate()
|
||||
|
||||
codeBlock.text = markdown
|
||||
this.insertAfter(codeBlock, referBlock)
|
||||
let i
|
||||
const removeCache = []
|
||||
for (i = startIndex; i <= endIndex; i++) {
|
||||
const child = children[i]
|
||||
removeCache.push(child)
|
||||
}
|
||||
removeCache.forEach(b => this.removeBlock(b))
|
||||
const key = codeBlock.key
|
||||
this.cursor = {
|
||||
start: { key, offset: 0 },
|
||||
end: { key, offset: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.handleQuoteMenu = function () {
|
||||
const { start, end, affiliation } = this.selectionChange()
|
||||
const startBlock = this.getBlock(start.key)
|
||||
const isBlockQuote = affiliation.slice(0, 2).filter(b => /blockquote/.test(b.type))
|
||||
if (isBlockQuote.length) {
|
||||
const quoteBlock = isBlockQuote[0]
|
||||
console.log(quoteBlock)
|
||||
const children = quoteBlock.children
|
||||
for (const child of children) {
|
||||
this.insertBefore(child, quoteBlock)
|
||||
}
|
||||
this.removeBlock(quoteBlock)
|
||||
} else {
|
||||
if (start.key === end.key) {
|
||||
const quoteBlock = this.createBlock('blockquote')
|
||||
this.insertAfter(quoteBlock, startBlock)
|
||||
this.removeBlock(startBlock)
|
||||
this.appendChild(quoteBlock, startBlock)
|
||||
} else {
|
||||
const { parent, startIndex, endIndex } = this.getCommonParent()
|
||||
const children = parent ? parent.children : this.blocks
|
||||
const referBlock = children[endIndex]
|
||||
const quoteBlock = this.createBlock('blockquote')
|
||||
this.insertAfter(quoteBlock, referBlock)
|
||||
let i
|
||||
const removeCache = []
|
||||
for (i = startIndex; i <= endIndex; i++) {
|
||||
const child = children[i]
|
||||
removeCache.push(child)
|
||||
this.appendChild(quoteBlock, child)
|
||||
}
|
||||
|
||||
removeCache.forEach(b => this.removeBlock(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.updateParagraph = function (paraType) {
|
||||
const { start, end } = selection.getCursorRange()
|
||||
const block = this.getBlock(start.key)
|
||||
const { type, text } = block
|
||||
|
||||
switch (paraType) {
|
||||
case 'ul-bullet':
|
||||
case 'ul-task':
|
||||
case 'ol-order': {
|
||||
this.handleListMenu(paraType)
|
||||
break
|
||||
}
|
||||
case 'pre': {
|
||||
this.handleCodeBlockMenu()
|
||||
break
|
||||
}
|
||||
case 'blockquote': {
|
||||
this.handleQuoteMenu()
|
||||
break
|
||||
}
|
||||
case 'heading 1':
|
||||
case 'heading 2':
|
||||
case 'heading 3':
|
||||
|
@ -78,7 +78,7 @@ const updateCtrl = ContentState => {
|
||||
return false
|
||||
}
|
||||
|
||||
ContentState.prototype.updateTaskListItem = function (block, type, marker) {
|
||||
ContentState.prototype.updateTaskListItem = function (block, type, marker = '') {
|
||||
const parent = this.getParent(block)
|
||||
const grandpa = this.getParent(parent)
|
||||
const checked = /\[x\]\s/i.test(marker) // use `i` flag to ignore upper case or lower case
|
||||
@ -142,7 +142,7 @@ const updateCtrl = ContentState => {
|
||||
block.checked = checked
|
||||
}
|
||||
|
||||
ContentState.prototype.updateList = function (block, type, marker) {
|
||||
ContentState.prototype.updateList = function (block, type, marker = '') {
|
||||
const parent = this.getParent(block)
|
||||
const preSibling = this.getPreSibling(block)
|
||||
const nextSibling = this.getNextSibling(block)
|
||||
@ -151,7 +151,7 @@ const updateCtrl = ContentState => {
|
||||
const { start, end } = this.cursor
|
||||
const startOffset = start.offset
|
||||
const endOffset = end.offset
|
||||
const newBlock = this.createBlockLi(newText)
|
||||
const newBlock = this.createBlockLi(newText, block.type)
|
||||
newBlock.listItemType = type
|
||||
|
||||
if (preSibling && preSibling.listType === type && nextSibling && nextSibling.listType === type) {
|
||||
@ -178,7 +178,7 @@ const updateCtrl = ContentState => {
|
||||
block.text = ''
|
||||
if (wrapperTag === 'ol') {
|
||||
const start = marker.split('.')[0]
|
||||
block.start = start
|
||||
block.start = /^\d+$/.test(start) ? start : 1
|
||||
}
|
||||
this.appendChild(block, newBlock)
|
||||
}
|
||||
@ -194,6 +194,7 @@ const updateCtrl = ContentState => {
|
||||
offset: Math.max(0, endOffset - marker.length)
|
||||
}
|
||||
}
|
||||
return newBlock.children[0]
|
||||
}
|
||||
|
||||
ContentState.prototype.updateBlockQuote = function (block) {
|
||||
|
@ -16,7 +16,7 @@ const LABEL_MAP = {
|
||||
'Heading 6': 'h6',
|
||||
'Table': 'figure',
|
||||
'Code Fences': 'pre',
|
||||
'Quote Block': 'quoteblock',
|
||||
'Quote Block': 'blockquote',
|
||||
'Order List': 'ol',
|
||||
'Bullet List': 'ul',
|
||||
'Task List': 'ul',
|
||||
@ -70,7 +70,7 @@ ipcMain.on('AGANI::selection-change', (e, { start, end, affiliation }) => {
|
||||
setCheckedMenuItem(affiliation)
|
||||
// handle disable
|
||||
allCtrl(true)
|
||||
if (/th|td/.test(start.type) || /th|td/.test(end.type)) {
|
||||
if (/th|td/.test(start.type) && /th|td/.test(end.type)) {
|
||||
allCtrl(false)
|
||||
} else if (start.key !== end.key) {
|
||||
formatMenuItem.submenu.items
|
||||
|
@ -72,14 +72,14 @@ export default {
|
||||
type: 'checkbox',
|
||||
accelerator: 'Alt+CmdOrCtrl+C',
|
||||
click (menuItem, browserWindow) {
|
||||
//
|
||||
actions.paragraph(browserWindow, 'pre')
|
||||
}
|
||||
}, {
|
||||
label: 'Quote Block',
|
||||
type: 'checkbox',
|
||||
accelerator: 'Alt+CmdOrCtrl+Q',
|
||||
click (menuItem, browserWindow) {
|
||||
//
|
||||
actions.paragraph(browserWindow, 'blockquote')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
@ -88,21 +88,21 @@ export default {
|
||||
type: 'checkbox',
|
||||
accelerator: 'Alt+CmdOrCtrl+O',
|
||||
click (menuItem, browserWindow) {
|
||||
//
|
||||
actions.paragraph(browserWindow, 'ol-order')
|
||||
}
|
||||
}, {
|
||||
label: 'Bullet List',
|
||||
type: 'checkbox',
|
||||
accelerator: 'Alt+CmdOrCtrl+U',
|
||||
click (menuItem, browserWindow) {
|
||||
//
|
||||
actions.paragraph(browserWindow, 'ul-bullet')
|
||||
}
|
||||
}, {
|
||||
label: 'Task List',
|
||||
type: 'checkbox',
|
||||
accelerator: 'Alt+CmdOrCtrl+X',
|
||||
click (menuItem, browserWindow) {
|
||||
//
|
||||
actions.paragraph(browserWindow, 'ul-task')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
|
@ -92,12 +92,16 @@
|
||||
}
|
||||
},
|
||||
handleEditParagraph (type) {
|
||||
console.log(type)
|
||||
switch (type) {
|
||||
case 'table':
|
||||
this.tableChecker = { rows: 2, columns: 2 }
|
||||
this.dialogTableVisible = true
|
||||
break
|
||||
case 'ul-bullet':
|
||||
case 'ul-task':
|
||||
case 'ol-order':
|
||||
case 'pre':
|
||||
case 'blockquote':
|
||||
case 'heading 1':
|
||||
case 'heading 2':
|
||||
case 'heading 3':
|
||||
|
Loading…
Reference in New Issue
Block a user