mirror of
https://github.com/marktext/marktext.git
synced 2025-05-05 06:39:10 +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)
|
- [ ] 在通过 Aganippe 打开文件时,通过右键选择软件,但是打开无内容。(严重 bug)
|
||||||
- [ ] export html: (3) keyframe 和 font-face 以及 bar-top 的样式都可以删除。(4) 打包后的应用 axios 获取样式有问题。
|
- [ ] export html: (3) keyframe 和 font-face 以及 bar-top 的样式都可以删除。(4) 打包后的应用 axios 获取样式有问题。
|
||||||
- [ ] table: 如果 table 在 selection 后面,那么删除cell 的时候,会把整个 row 删除了。(小 bug)
|
- [ ] table: 如果 table 在 selection 后面,那么删除cell 的时候,会把整个 row 删除了。(小 bug)
|
||||||
|
- [ ] task list 中 checkbox 无反应
|
||||||
|
|
||||||
**菜单**
|
**菜单**
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { generateKeyHash, genUpper2LowerKeyHash } from './utils'
|
|||||||
* configs
|
* configs
|
||||||
*/
|
*/
|
||||||
// export const INLINE_RULES = ['autolink', 'backticks', 'emphasis', 'escape', 'image', 'link', 'strikethrough']
|
// 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 = [
|
export const blockContainerElementNames = [
|
||||||
// elements our editor generates
|
// elements our editor generates
|
||||||
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol',
|
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol',
|
||||||
|
@ -29,9 +29,9 @@ const enterCtrl = ContentState => {
|
|||||||
return trBlock
|
return trBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentState.prototype.createBlockLi = function (text = '') {
|
ContentState.prototype.createBlockLi = function (text = '', type = 'p') {
|
||||||
const liBlock = this.createBlock('li')
|
const liBlock = this.createBlock('li')
|
||||||
const pBlock = this.createBlock('p', text)
|
const pBlock = this.createBlock(type, text)
|
||||||
this.appendChild(liBlock, pBlock)
|
this.appendChild(liBlock, pBlock)
|
||||||
return liBlock
|
return liBlock
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ class ContentState {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
// return block and its parents
|
||||||
getParents (block) {
|
getParents (block) {
|
||||||
const result = []
|
const result = []
|
||||||
result.push(block)
|
result.push(block)
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import selection from '../selection'
|
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 => {
|
const getCurrentLevel = type => {
|
||||||
if (/\d/.test(type)) {
|
if (/\d/.test(type)) {
|
||||||
return Number(/\d/.exec(type)[0])
|
return Number(/\d/.exec(type)[0])
|
||||||
@ -11,14 +16,16 @@ const getCurrentLevel = type => {
|
|||||||
const paragraphCtrl = ContentState => {
|
const paragraphCtrl = ContentState => {
|
||||||
ContentState.prototype.selectionChange = function () {
|
ContentState.prototype.selectionChange = function () {
|
||||||
const { start, end } = selection.getCursorRange()
|
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 startBlock = this.getBlock(start.key)
|
||||||
const endBlock = this.getBlock(end.key)
|
const endBlock = this.getBlock(end.key)
|
||||||
const startParents = this.getParents(startBlock)
|
const startParents = this.getParents(startBlock)
|
||||||
const endParents = this.getParents(endBlock)
|
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 {
|
return {
|
||||||
start,
|
start,
|
||||||
@ -27,16 +34,212 @@ const paragraphCtrl = ContentState => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
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) {
|
ContentState.prototype.updateParagraph = function (paraType) {
|
||||||
const {
|
const { start, end } = selection.getCursorRange()
|
||||||
start, end
|
|
||||||
} = selection.getCursorRange()
|
|
||||||
const block = this.getBlock(start.key)
|
const block = this.getBlock(start.key)
|
||||||
const {
|
const { type, text } = block
|
||||||
type, text
|
|
||||||
} = block
|
|
||||||
|
|
||||||
switch (paraType) {
|
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 1':
|
||||||
case 'heading 2':
|
case 'heading 2':
|
||||||
case 'heading 3':
|
case 'heading 3':
|
||||||
|
@ -78,7 +78,7 @@ const updateCtrl = ContentState => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentState.prototype.updateTaskListItem = function (block, type, marker) {
|
ContentState.prototype.updateTaskListItem = function (block, type, marker = '') {
|
||||||
const parent = this.getParent(block)
|
const parent = this.getParent(block)
|
||||||
const grandpa = this.getParent(parent)
|
const grandpa = this.getParent(parent)
|
||||||
const checked = /\[x\]\s/i.test(marker) // use `i` flag to ignore upper case or lower case
|
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
|
block.checked = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentState.prototype.updateList = function (block, type, marker) {
|
ContentState.prototype.updateList = function (block, type, marker = '') {
|
||||||
const parent = this.getParent(block)
|
const parent = this.getParent(block)
|
||||||
const preSibling = this.getPreSibling(block)
|
const preSibling = this.getPreSibling(block)
|
||||||
const nextSibling = this.getNextSibling(block)
|
const nextSibling = this.getNextSibling(block)
|
||||||
@ -151,7 +151,7 @@ const updateCtrl = ContentState => {
|
|||||||
const { start, end } = this.cursor
|
const { start, end } = this.cursor
|
||||||
const startOffset = start.offset
|
const startOffset = start.offset
|
||||||
const endOffset = end.offset
|
const endOffset = end.offset
|
||||||
const newBlock = this.createBlockLi(newText)
|
const newBlock = this.createBlockLi(newText, block.type)
|
||||||
newBlock.listItemType = type
|
newBlock.listItemType = type
|
||||||
|
|
||||||
if (preSibling && preSibling.listType === type && nextSibling && nextSibling.listType === type) {
|
if (preSibling && preSibling.listType === type && nextSibling && nextSibling.listType === type) {
|
||||||
@ -178,7 +178,7 @@ const updateCtrl = ContentState => {
|
|||||||
block.text = ''
|
block.text = ''
|
||||||
if (wrapperTag === 'ol') {
|
if (wrapperTag === 'ol') {
|
||||||
const start = marker.split('.')[0]
|
const start = marker.split('.')[0]
|
||||||
block.start = start
|
block.start = /^\d+$/.test(start) ? start : 1
|
||||||
}
|
}
|
||||||
this.appendChild(block, newBlock)
|
this.appendChild(block, newBlock)
|
||||||
}
|
}
|
||||||
@ -194,6 +194,7 @@ const updateCtrl = ContentState => {
|
|||||||
offset: Math.max(0, endOffset - marker.length)
|
offset: Math.max(0, endOffset - marker.length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return newBlock.children[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentState.prototype.updateBlockQuote = function (block) {
|
ContentState.prototype.updateBlockQuote = function (block) {
|
||||||
|
@ -16,7 +16,7 @@ const LABEL_MAP = {
|
|||||||
'Heading 6': 'h6',
|
'Heading 6': 'h6',
|
||||||
'Table': 'figure',
|
'Table': 'figure',
|
||||||
'Code Fences': 'pre',
|
'Code Fences': 'pre',
|
||||||
'Quote Block': 'quoteblock',
|
'Quote Block': 'blockquote',
|
||||||
'Order List': 'ol',
|
'Order List': 'ol',
|
||||||
'Bullet List': 'ul',
|
'Bullet List': 'ul',
|
||||||
'Task List': 'ul',
|
'Task List': 'ul',
|
||||||
@ -70,7 +70,7 @@ ipcMain.on('AGANI::selection-change', (e, { start, end, affiliation }) => {
|
|||||||
setCheckedMenuItem(affiliation)
|
setCheckedMenuItem(affiliation)
|
||||||
// handle disable
|
// handle disable
|
||||||
allCtrl(true)
|
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)
|
allCtrl(false)
|
||||||
} else if (start.key !== end.key) {
|
} else if (start.key !== end.key) {
|
||||||
formatMenuItem.submenu.items
|
formatMenuItem.submenu.items
|
||||||
|
@ -72,14 +72,14 @@ export default {
|
|||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
accelerator: 'Alt+CmdOrCtrl+C',
|
accelerator: 'Alt+CmdOrCtrl+C',
|
||||||
click (menuItem, browserWindow) {
|
click (menuItem, browserWindow) {
|
||||||
//
|
actions.paragraph(browserWindow, 'pre')
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
label: 'Quote Block',
|
label: 'Quote Block',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
accelerator: 'Alt+CmdOrCtrl+Q',
|
accelerator: 'Alt+CmdOrCtrl+Q',
|
||||||
click (menuItem, browserWindow) {
|
click (menuItem, browserWindow) {
|
||||||
//
|
actions.paragraph(browserWindow, 'blockquote')
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
@ -88,21 +88,21 @@ export default {
|
|||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
accelerator: 'Alt+CmdOrCtrl+O',
|
accelerator: 'Alt+CmdOrCtrl+O',
|
||||||
click (menuItem, browserWindow) {
|
click (menuItem, browserWindow) {
|
||||||
//
|
actions.paragraph(browserWindow, 'ol-order')
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
label: 'Bullet List',
|
label: 'Bullet List',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
accelerator: 'Alt+CmdOrCtrl+U',
|
accelerator: 'Alt+CmdOrCtrl+U',
|
||||||
click (menuItem, browserWindow) {
|
click (menuItem, browserWindow) {
|
||||||
//
|
actions.paragraph(browserWindow, 'ul-bullet')
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
label: 'Task List',
|
label: 'Task List',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
accelerator: 'Alt+CmdOrCtrl+X',
|
accelerator: 'Alt+CmdOrCtrl+X',
|
||||||
click (menuItem, browserWindow) {
|
click (menuItem, browserWindow) {
|
||||||
//
|
actions.paragraph(browserWindow, 'ul-task')
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
|
@ -92,12 +92,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleEditParagraph (type) {
|
handleEditParagraph (type) {
|
||||||
console.log(type)
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'table':
|
case 'table':
|
||||||
this.tableChecker = { rows: 2, columns: 2 }
|
this.tableChecker = { rows: 2, columns: 2 }
|
||||||
this.dialogTableVisible = true
|
this.dialogTableVisible = true
|
||||||
break
|
break
|
||||||
|
case 'ul-bullet':
|
||||||
|
case 'ul-task':
|
||||||
|
case 'ol-order':
|
||||||
|
case 'pre':
|
||||||
|
case 'blockquote':
|
||||||
case 'heading 1':
|
case 'heading 1':
|
||||||
case 'heading 2':
|
case 'heading 2':
|
||||||
case 'heading 3':
|
case 'heading 3':
|
||||||
|
Loading…
Reference in New Issue
Block a user