feat: add front menu (#875)

* feat: add front menu

* update changelog

* feat: add short cut of paragraph edit

* fix location bug of submenu

* update checkbox style in editor

* update selected background color

* update KEYBINDINGS.md

* Bullet to ordered list issue:

* emit change event after control paragraph

* fix: marked parse error

* disable table, front-matter and horizontal line in paragraph turn into, and fixed paragraph turn into html and math

* fix unwanted space before text paragraph when heading turn into text paragraph

* fix error when turn heading to paragraph

* put front menu on the left on front icon

* update readme

* if the selection span in two lnes, disable paragraph turn into heading
This commit is contained in:
Ran Luo 2019-04-07 22:16:49 +08:00 committed by Felix Häusler
parent 962fdf356f
commit 4e918503f4
27 changed files with 847 additions and 122 deletions

View File

@ -42,6 +42,7 @@ foo<section>bar</section>zar
- Watch file changed in tabs and show a notice(autoSave is `false`) or update the file(autoSave is `true`)
- Support input inline Ruby charactors as raw html (#257)
- Added unsaved tab indicator
- Add front Menu by click the front menu icon (#875)
**:butterfly:Optimization**

View File

@ -112,7 +112,7 @@
Mark Text is an MIT licensed open source project, you will always be able to download the latest version for free from the GitHub release page. Mark Text is still in development, and its development is inseparable from all sponsors. I hope you join them:
- [Become a backer or sponsor on Patreon](https://www.patreon.com/ranluo) or [one time donation](https://github.com/Jocs/sponsor.me)
- [Become a backer or sponsor on Patreon](https://www.patreon.com/ranluo) or [One time donation](https://github.com/Jocs/sponsor.me)
- [Become a backer or sponsor on Open Collective](https://opencollective.com/marktext)
##### What's the difference between Patreon and OpenCollective?

View File

@ -49,6 +49,9 @@ Here is an example:
| `editCopyAsMarkdown` | Copy selected text as markdown |
| `editCopyAsPlaintext` | Copy selected text as plaintext |
| `editSelectAll` | Select all text of the document |
| `editDuplicate` | Duplicate the current paragraph |
| `editCreateParagraph` | Create a new paragraph after the current one |
| `editDeleteParagraph` | Delete current paragraph |
| `editFind` | Find information in the document |
| `editFindNext` | Continue the search and find the next match |
| `editFindPrevious` | Continue the search and find the previous match |

View File

@ -59,6 +59,26 @@ export default {
role: 'selectall'
}, {
type: 'separator'
}, {
label: 'Duplicate',
accelerator: keybindings.getAccelerator('editDuplicate'),
click (menuItem, browserWindow) {
actions.edit(browserWindow, 'duplicate')
}
}, {
label: 'Create Paragraph',
accelerator: keybindings.getAccelerator('editCreateParagraph'),
click (menuItem, browserWindow) {
actions.edit(browserWindow, 'createParagraph')
}
}, {
label: 'Delete Paragraph',
accelerator: keybindings.getAccelerator('editDeleteParagraph'),
click (menuItem, browserWindow) {
actions.edit(browserWindow, 'deleteParagraph')
}
}, {
type: 'separator'
}, {
label: 'Find',
accelerator: keybindings.getAccelerator('editFind'),

View File

@ -41,6 +41,9 @@ class Keybindings {
['editCopyAsMarkdown', 'CmdOrCtrl+Shift+C'],
['editCopyAsPlaintext', 'CmdOrCtrl+Shift+V'],
['editSelectAll', 'CmdOrCtrl+A'],
['editDuplicate', 'Shift+CmdOrCtrl+P'],
['editCreateParagraph', 'Shift+CmdOrCtrl+N'],
['editDeleteParagraph', 'Shift+CmdOrCtrl+D'],
['editFind', 'CmdOrCtrl+F'],
['editFindNext', 'CmdOrCtrl+Alt+U'],
['editFindPrevious', 'CmdOrCtrl+Shift+U'],

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1554467898300" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2600" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M204.8 358.4a51.2 51.2 0 0 0-51.2 51.2v409.6a51.2 51.2 0 0 0 51.2 51.2h409.6a51.2 51.2 0 0 0 51.2-51.2V409.6a51.2 51.2 0 0 0-51.2-51.2H204.8z m0-51.2h409.6a102.4 102.4 0 0 1 102.4 102.4v409.6a102.4 102.4 0 0 1-102.4 102.4H204.8a102.4 102.4 0 0 1-102.4-102.4V409.6a102.4 102.4 0 0 1 102.4-102.4z" fill="#000000" p-id="2601"></path><path d="M766.1568 716.8H819.2a102.4 102.4 0 0 0 102.4-102.4V204.8a102.4 102.4 0 0 0-102.4-102.4H409.6a102.4 102.4 0 0 0-102.4 102.4v49.5104h51.2V204.8a51.2 51.2 0 0 1 51.2-51.2h409.6a51.2 51.2 0 0 1 51.2 51.2v409.6a51.2 51.2 0 0 1-51.2 51.2h-53.0432v51.2z" fill="#000000" p-id="2602"></path></svg>

After

Width:  |  Height:  |  Size: 1014 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1554472187522" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2600" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M275.0464 22.1184h58.368L506.0608 460.8H451.3792l-46.6944-122.88H203.1616L156.4672 460.8H102.4L275.0464 22.1184zM219.136 295.5264h169.5744l-82.944-219.3408h-2.4576L219.136 295.5264zM607.4368 534.1184h195.9936c40.5504 0 73.1136 9.8304 97.0752 29.4912 23.3472 19.6608 35.0208 46.6944 35.0208 81.1008 0 24.576-6.7584 46.08-19.6608 63.8976-13.5168 15.9744-31.3344 27.648-53.4528 35.0208 28.8768 4.9152 50.9952 17.2032 66.9696 35.6352 15.9744 18.432 24.576 41.7792 24.576 71.2704 0 43.008-15.36 74.9568-45.4656 95.8464-25.8048 17.2032-61.44 26.4192-106.2912 26.4192h-194.7648v-438.6816z m50.3808 42.3936v149.2992h133.3248c30.72 0 54.0672-6.7584 70.0416-19.0464 15.9744-12.9024 24.576-31.9488 24.576-57.1392 0-24.576-7.9872-43.008-23.9616-55.296-15.9744-12.288-39.3216-17.8176-70.0416-17.8176h-133.9392z m0 191.0784v162.816h140.0832c30.72 0 54.6816-6.144 73.1136-17.2032 21.504-13.5168 32.5632-35.0208 32.5632-63.2832 0-28.2624-9.8304-49.152-28.2624-62.6688-17.8176-13.5168-43.6224-19.6608-77.4144-19.6608h-140.0832z" fill="#000000" opacity=".8" p-id="2601"></path><path d="M896 380.672a25.6 25.6 0 1 1-51.2 0V332.8a204.8512 204.8512 0 0 0-204.8-204.8 25.6 25.6 0 1 1 0-51.2 256.0512 256.0512 0 0 1 256 256v47.872z" fill="#000000" opacity=".8" p-id="2602"></path><path d="M870.4 460.8l102.4-102.4h-204.8z" fill="#000000" opacity=".8" p-id="2603"></path><path d="M179.2 644.352a25.6 25.6 0 1 1 51.2 0v46.848a204.8512 204.8512 0 0 0 204.8 204.8 25.6 25.6 0 0 1 0 51.2 256.0512 256.0512 0 0 1-256-256v-46.848z" fill="#000000" opacity=".8" p-id="2604"></path><path d="M204.8 563.2l-102.4 102.4h204.8z" fill="#000000" opacity=".8" p-id="2605"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -39,6 +39,15 @@ div.ag-show-quick-insert-hint p.ag-paragraph.ag-active > span.ag-line:first-of-t
word-break: break-word;
}
.ag-selected {
background: linear-gradient(17deg, var(--editorBgColor) 36.65%, var(--editorColor04));
border-radius: 3px;
}
.ag-gray {
font-family: monospace;
}
.ag-reference-marker {
font-size: .9em;
color: var(--editorColor50);
@ -290,10 +299,10 @@ li.ag-task-list-item {
li.ag-task-list-item > input[type=checkbox] {
position: absolute;
cursor: pointer;
width: 10px;
height: 10px;
width: 12px;
height: 12px;
top: 5px;
left: -22px;
left: -23px;
transform-origin: center;
transition: all .2s ease;
}
@ -309,37 +318,44 @@ li.ag-task-list-item > input.ag-checkbox-checked ~ p {
li.ag-task-list-item > input[type=checkbox]::before {
content: '';
width: 14px;
height: 14px;
width: 18px;
height: 18px;
box-sizing: border-box;
display: inline-block;
border: 1px solid var(--iconColor);
border: 2px solid var(--editorColor50);
border-radius: 50%;
background-color: var(--editorBgColor);
position: absolute;
top: -2px;
left: -2px;
box-sizing: border-box;
transition: all .2s ease;
}
li.ag-task-list-item > input::after {
content: '';
transform: rotate(-28deg) skew(0, -25deg) scale(0);
transform: rotate(-45deg) scale(0);
width: 8px;
height: 4px;
border: 1px solid var(--iconColor);
border: 2px solid var(--editorBgColor);
border-top: none;
border-right: none;
position: absolute;
display: inline-block;
top: 0px;
left: 2px;
top: 1px;
left: 4px;
transform-origin: bottom;
transition: all .2s ease;
}
li.ag-task-list-item > input.ag-checkbox-checked::after {
transform: rotate(-28deg) skew(0, -25deg) scale(1);
transform: rotate(-45deg) scale(1);
}
li.ag-task-list-item > input.ag-checkbox-checked::before {
background: var(--themeColor);
border-color: var(--themeColor);
box-shadow: 0 3px 12px 0 var(--highlightColor);
}
/* li p .ag-hide:first-child {
@ -705,7 +721,7 @@ span.ag-reference-link {
top: 2px;
left: -30px;
display: none;
/* cursor: pointer; */
cursor: pointer;
}
.ag-front-icon::before {

View File

@ -104,6 +104,12 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_LIST_ITEM',
'AG_LOOSE_LIST_ITEM',
'AG_MATH',
'AG_MATH_TEXT',
'AG_MATH_RENDER',
'AG_RUBY',
'AG_RUBY_TEXT',
'AG_RUBY_RENDER',
'AG_SELECTED',
'AG_MATH_ERROR',
'AG_MATH_MARKER',
'AG_MATH_RENDER',
@ -228,6 +234,7 @@ export const MUYA_DEFAULT_OPTION = {
autoPairMarkdownSyntax: true,
autoPairQuote: true,
bulletListMarker: '-',
orderListMarker: '.',
tabSize: 4,
sequenceTheme: 'hand', // hand or simple
mermaidTheme: 'forest', // dark or forest

View File

@ -4,6 +4,36 @@ import { HAS_TEXT_BLOCK_REG } from '../config'
const clickCtrl = ContentState => {
ContentState.prototype.clickHandler = function (event) {
const { eventCenter } = this.muya
const { target } = event
// 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) {

View File

@ -1,4 +1,5 @@
import { VOID_HTML_TAGS, HTML_TAGS, HTML_TOOLS } from '../config'
import { inlineRules } from '../parser/rules'
const HTML_BLOCK_REG = /^<([a-zA-Z\d-]+)(?=\s|>)[^<>]*?>$/
const LINE_BREAKS = /\n/
@ -95,10 +96,36 @@ const htmlBlock = ContentState => {
return block
}
ContentState.prototype.initHtmlBlock = function (block, tagName) {
const isVoidTag = VOID_HTML_TAGS.indexOf(tagName) > -1
const { text } = block.children[0]
const htmlContent = isVoidTag ? text : `${text}\n\n</${tagName}>`
ContentState.prototype.initHtmlBlock = function (block) {
let htmlContent = ''
const text = block.type === 'p'
? block.children.map((child => {
return child.text
})).join('\n').trim()
: block.text
const matches = inlineRules.html_tag.exec(text)
if (matches) {
const tag = matches[3]
const content = matches[4] || ''
const openTag = matches[2]
const closeTag = matches[5]
const isVoidTag = VOID_HTML_TAGS.indexOf(tag) > -1
if (closeTag) {
htmlContent = text
} else if (isVoidTag) {
htmlContent = text
if (content) {
// TODO: @jocs notice user that the html is not valid.
console.warn('Invalid html content.')
}
} else {
htmlContent = `${openTag}\n${content}\n</${tag}>`
}
} else {
htmlContent = `<div>\n${text}\n</div>`
}
block.type = 'figure'
block.functionType = 'html'
block.text = htmlContent
@ -116,7 +143,7 @@ const htmlBlock = ContentState => {
const { text } = block.children[0]
const match = HTML_BLOCK_REG.exec(text)
const tagName = match && match[1] && HTML_TAGS.find(t => t === match[1])
return VOID_HTML_TAGS.indexOf(tagName) === -1 && tagName ? this.initHtmlBlock(block, tagName) : false
return VOID_HTML_TAGS.indexOf(tagName) === -1 && tagName ? this.initHtmlBlock(block) : false
}
}

View File

@ -1,5 +1,5 @@
import { HAS_TEXT_BLOCK_REG, DEFAULT_TURNDOWN_CONFIG } from '../config'
import { getUniqueId } from '../utils'
import { getUniqueId, deepCopy } from '../utils'
import selection from '../selection'
import StateRender from '../parser/render'
import enterCtrl from './enterCtrl'
@ -65,6 +65,8 @@ class ContentState {
this.codeBlocks = new Map()
this.renderRange = [ null, null ]
this.currentCursor = null
// you'll select the outmost block of current cursor when you click the front icon.
this.selectedBlock = null
this.prevCursor = null
this.historyTimer = null
this.history = new History(this)
@ -147,19 +149,19 @@ class ContentState {
}
render (isRenderCursor = true) {
const { blocks, cursor, searchMatches: { matches, index } } = this
const { blocks, cursor, searchMatches: { matches, index }, selectedBlock } = this
const activeBlocks = this.getActiveBlocks()
matches.forEach((m, i) => {
m.active = i === index
})
this.setNextRenderRange()
this.stateRender.collectLabels(blocks)
this.stateRender.render(blocks, cursor, activeBlocks, matches)
this.stateRender.render(blocks, cursor, activeBlocks, matches, selectedBlock)
if (isRenderCursor) this.setCursor()
}
partialRender () {
const { blocks, cursor, searchMatches: { matches, index } } = this
const { blocks, cursor, searchMatches: { matches, index }, selectedBlock } = this
const activeBlocks = this.getActiveBlocks()
const [ startKey, endKey ] = this.renderRange
matches.forEach((m, i) => {
@ -171,7 +173,7 @@ class ContentState {
this.setNextRenderRange()
this.stateRender.collectLabels(blocks)
this.stateRender.partialRender(needRenderBlocks, cursor, activeBlocks, matches, startKey, endKey)
this.stateRender.partialRender(needRenderBlocks, cursor, activeBlocks, matches, startKey, endKey, selectedBlock)
this.setCursor()
}
@ -233,6 +235,31 @@ class ContentState {
return result
}
copyBlock (origin) {
const copiedBlock = deepCopy(origin)
const travel = (block, parent, preBlock, nextBlock) => {
const key = getUniqueId()
block.key = key
block.parent = parent ? parent.key : null
block.preSibling = preBlock ? preBlock.key : null
block.nextSibling = nextBlock ? nextBlock.key : null
const { children } = block
const len = children.length
if (children && len) {
let i
for (i = 0; i < len; i++) {
const b = children[i]
const preB = i >= 1 ? children[i - 1] : null
const nextB = i < len - 1 ? children[i + 1] : null
travel(b, block, preB, nextB)
}
}
}
travel(copiedBlock, null, null, null)
return copiedBlock
}
getParent (block) {
if (block && block.parent) {
return this.getBlock(block.parent)

View File

@ -86,7 +86,8 @@ const paragraphCtrl = ContentState => {
}
ContentState.prototype.handleListMenu = function (paraType, insertMode) {
const { start, end, affiliation } = this.selectionChange()
const { start, end, affiliation } = this.selectionChange(this.cursor)
const { orderListMarker, bulletListMarker } = this
const [blockType, listType] = paraType.split('-')
const isListed = affiliation.slice(0, 3).filter(b => /ul|ol/.test(b.type))
const { preferLooseListItem } = this
@ -113,13 +114,23 @@ const paragraphCtrl = ContentState => {
inputBlock && this.removeBlock(inputBlock)
})
}
const oldListType = listBlock.listType
listBlock.type = blockType
listBlock.listType = listType
listBlock.children.forEach(b => (b.listItemType = listType))
if (listType === 'order') {
listBlock.start = listBlock.start || 1
listBlock.children.forEach(b => (b.bulletMarkerOrDelimiter = orderListMarker))
}
if (
(listType === 'bullet' && oldListType === 'order') ||
(listType === 'task' && oldListType === 'order')
) {
delete listBlock.start
listBlock.children.forEach(b => (b.bulletMarkerOrDelimiter = bulletListMarker))
}
// if the new block is task list, add checkbox
if (listType === 'task') {
const listItems = listBlock.children
@ -175,7 +186,7 @@ const paragraphCtrl = ContentState => {
}
ContentState.prototype.handleLooseListItem = function () {
const { affiliation } = this.selectionChange()
const { affiliation } = this.selectionChange(this.cursor)
let listContainer = []
if (affiliation.length >= 1 && /ul|ol/.test(affiliation[0].type)) {
listContainer = affiliation[0].children
@ -191,7 +202,7 @@ const paragraphCtrl = ContentState => {
}
ContentState.prototype.handleCodeBlockMenu = function () {
const { start, end, affiliation } = this.selectionChange()
const { start, end, affiliation } = this.selectionChange(this.cursor)
let startBlock = this.getBlock(start.key)
const endBlock = this.getBlock(end.key)
const startParents = this.getParents(startBlock)
@ -290,7 +301,7 @@ const paragraphCtrl = ContentState => {
}
ContentState.prototype.handleQuoteMenu = function (insertMode) {
const { start, end, affiliation } = this.selectionChange()
const { start, end, affiliation } = this.selectionChange(this.cursor)
let startBlock = this.getBlock(start.key)
const isBlockQuote = affiliation.slice(0, 2).filter(b => /blockquote/.test(b.type))
// change blockquote to paragraph
@ -330,22 +341,19 @@ const paragraphCtrl = ContentState => {
}
}
ContentState.prototype.insertContainerBlock = function (functionType, value = '') {
const { start, end } = selection.getCursorRange()
if (!start || !end) {
return
}
if (start.key !== end.key) return
let block = this.getBlock(start.key)
ContentState.prototype.insertContainerBlock = function (functionType, block) {
if (block.type === 'span') {
block = this.getParent(block)
}
const mathBlock = this.createContainerBlock(functionType, value)
this.insertAfter(mathBlock, block)
if (block.type === 'p' && block.children.length === 1 && !block.children[0].text) {
const value = block.type === 'p'
? block.children.map(child => child.text).join('\n').trim()
: block.text
const containerBlock = this.createContainerBlock(functionType, value)
this.insertAfter(containerBlock, block)
this.removeBlock(block)
}
const cursorBlock = mathBlock.children[0].children[0].children[0]
const cursorBlock = containerBlock.children[0].children[0].children[0]
const { key } = cursorBlock
const offset = 0
this.cursor = {
@ -365,10 +373,14 @@ const paragraphCtrl = ContentState => {
}
ContentState.prototype.insertHtmlBlock = function (block) {
const parentBlock = this.getParent(block)
block.text = '<div>'
const preBlock = this.initHtmlBlock(parentBlock, 'div')
const key = preBlock.children[0].children[1].key
if (block.type === 'span') {
block = this.getParent(block)
}
const preBlock = this.initHtmlBlock(block)
const key = preBlock.children[0].children[1]
? preBlock.children[0].children[1].key
: preBlock.children[0].children[0].key
const offset = 0
this.cursor = {
start: { key, offset },
@ -379,7 +391,7 @@ const paragraphCtrl = ContentState => {
ContentState.prototype.updateParagraph = function (paraType, insertMode = false) {
const { start, end } = this.cursor
const block = this.getBlock(start.key)
const { type, text } = block
const { type, text, functionType } = block
switch (paraType) {
case 'front-matter': {
@ -405,7 +417,7 @@ const paragraphCtrl = ContentState => {
break
}
case 'mathblock': {
this.insertContainerBlock('multiplemath')
this.insertContainerBlock('multiplemath', block)
break
}
case 'table': {
@ -420,7 +432,7 @@ const paragraphCtrl = ContentState => {
case 'sequence':
case 'mermaid':
case 'vega-lite':
this.insertContainerBlock(paraType)
this.insertContainerBlock(paraType, block)
break
case 'heading 1':
case 'heading 2':
@ -432,7 +444,7 @@ const paragraphCtrl = ContentState => {
case 'degrade heading':
case 'paragraph': {
if (start.key !== end.key) return
const [, hash, partText] = /(^#*)(.*)/.exec(text)
const [, hash, partText] = /(^#*\s*)(.*)/.exec(text)
let newLevel = 0 // 1, 2, 3, 4, 5, 6
let newType = 'p'
let key
@ -452,9 +464,15 @@ const paragraphCtrl = ContentState => {
newType = newLevel === 0 ? 'p' : `h${newLevel}`
}
const startOffset = start.offset + newLevel - hash.length + 1
const endOffset = end.offset + newLevel - hash.length + 1
const newText = '#'.repeat(newLevel) + `${String.fromCharCode(160)}${partText}` // &nbsp; code: 160
const startOffset = newLevel > 0
? start.offset + newLevel - hash.length + 1
: start.offset - hash.length // no need to add `1`, because we didn't add `String.fromCharCode(160)` to text paragraph
const endOffset = newLevel > 0
? end.offset + newLevel - hash.length + 1
: end.offset - hash.length
const newText = newLevel > 0
? '#'.repeat(newLevel) + `${String.fromCharCode(160)}${partText}` // &nbsp; code: 160
: partText
if (block.type === 'span' && newType !== 'p') {
const header = this.createBlock(newType, newText)
@ -488,6 +506,9 @@ const paragraphCtrl = ContentState => {
key = pBlock.children[0].key
this.insertAfter(pBlock, block)
this.removeBlock(block)
} else if (type === 'span' && !functionType && newType === 'p') {
// The original is a paragraph, the new type is also paragraph, no need to update.
return
} else {
const newHeader = this.createBlock(newType, newText)
newHeader.headingStyle = DEFAULT_TURNDOWN_CONFIG.headingStyle
@ -530,16 +551,20 @@ const paragraphCtrl = ContentState => {
this.partialRender()
}
// update menu status
const selectionChanges = this.selectionChange()
const selectionChanges = this.selectionChange(this.cursor)
this.muya.eventCenter.dispatch('selectionChange', selectionChanges)
// emit change event
this.muya.eventCenter.dispatch('stateChange')
}
ContentState.prototype.insertParagraph = function (location, text = '') {
ContentState.prototype.insertParagraph = function (location, text = '', outMost = false) {
const { start, end } = this.cursor
// if cursor is not in one line or paragraph, can not insert paragraph
if (start.key !== end.key) return
let block = this.getBlock(start.key)
if (block.type === 'span' && !block.functionType) {
if (outMost) {
block = this.findOutMostBlock(block)
} else if (block.type === 'span' && !block.functionType) {
block = this.getParent(block)
} else if (block.type === 'span' && block.functionType === 'codeLine') {
const preBlock = this.getParent(this.getParent(block))
@ -580,6 +605,61 @@ const paragraphCtrl = ContentState => {
end: { key, offset }
}
this.partialRender()
this.muya.eventCenter.dispatch('stateChange')
}
// make a dulication of the current block
ContentState.prototype.duplicate = function () {
const { start, end } = this.cursor
const startOutmostBlock = this.findOutMostBlock(this.getBlock(start.key))
const endOutmostBlock = this.findOutMostBlock(this.getBlock(end.key))
if (startOutmostBlock !== endOutmostBlock) {
// if the cursor is not in one paragraph, just return
return
}
const copiedBlock = this.copyBlock(startOutmostBlock)
this.insertAfter(copiedBlock, startOutmostBlock)
const cursorBlock = this.firstInDescendant(copiedBlock)
// set cursor at the end of the first descendant of the duplicated block.
const { key, text } = cursorBlock
const offset = text.length
this.cursor = {
start: { key, offset },
end: { key, offset }
}
this.partialRender()
return this.muya.eventCenter.dispatch('stateChange')
}
// delete current paragraph
ContentState.prototype.deleteParagraph = function () {
const { start, end } = this.cursor
const startOutmostBlock = this.findOutMostBlock(this.getBlock(start.key))
const endOutmostBlock = this.findOutMostBlock(this.getBlock(end.key))
if (startOutmostBlock !== endOutmostBlock) {
// if the cursor is not in one paragraph, just return
return
}
const preBlock = this.getBlock(startOutmostBlock.preSibling)
const nextBlock = this.getBlock(startOutmostBlock.nextSibling)
let cursorBlock = null
if (nextBlock) {
cursorBlock = this.firstInDescendant(nextBlock)
} else if (preBlock) {
cursorBlock = this.lastInDescendant(preBlock)
} else {
const newBlock = this.createBlockP()
this.insertAfter(newBlock, startOutmostBlock)
cursorBlock = this.firstInDescendant(newBlock)
}
this.removeBlock(startOutmostBlock)
const { key, text } = cursorBlock
const offset = text.length
this.cursor = {
start: { key, offset },
end: { key, offset }
}
this.partialRender()
return this.muya.eventCenter.dispatch('stateChange')
}
}

View File

@ -21,6 +21,14 @@ class Keyboard {
// cache shown float box
this.muya.eventCenter.subscribe('muya-float', (name, status) => {
status ? this.shownFloat.add(name) : this.shownFloat.delete(name)
if (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')
}
}
})
}

View File

@ -150,8 +150,16 @@ class Muya {
this.contentState.updateParagraph(type)
}
insertParagraph (location/* before or after */) {
this.contentState.insertParagraph(location)
duplicate () {
this.contentState.duplicate()
}
deleteParagraph () {
this.contentState.deleteParagraph()
}
insertParagraph (location/* before or after */, text = '', outMost = false) {
this.contentState.insertParagraph(location, text, outMost)
}
editTable (data) {

View File

@ -206,12 +206,11 @@ Lexer.prototype.token = function (src, top) {
if (cap) {
src = src.substring(cap[0].length)
bull = cap[2]
let isOrdered = bull.length > 1 && /\d{1,9}/.test(bull)
let isOrdered = bull.length > 1
this.tokens.push({
type: 'list_start',
ordered: isOrdered,
listType: bull.length > 1 ? (/\d{1,9}/.test(bull) ? 'order' : 'task') : 'bullet',
listType: bull.length > 1 ? 'order' : (/^( {0,3})([-*+]) \[[xX ]\]/.test(cap[0]) ? 'task' : 'bullet'),
start: isOrdered ? +(bull.slice(0, -1)) : ''
})
@ -226,6 +225,7 @@ Lexer.prototype.token = function (src, top) {
for (; i < l; i++) {
const itemWithBullet = cap[i]
let isTaskListItem = false
item = itemWithBullet
// Remove the list item's bullet
@ -257,7 +257,7 @@ Lexer.prototype.token = function (src, top) {
this.tokens.push({
type: 'list_start',
ordered: isOrdered,
listType: bull.length > 1 ? (/\d{1,9}/.test(bull) ? 'order' : 'task') : 'bullet',
listType: bull.length > 1 ? 'order' : (/^( {0,3})([-*+]) \[[xX ]\]/.test(itemWithBullet) ? 'task' : 'bullet'),
start: isOrdered ? +(bull.slice(0, -1)) : ''
})
}
@ -267,6 +267,7 @@ Lexer.prototype.token = function (src, top) {
if (checked) {
checked = checked[1] === 'x' || checked[1] === 'X'
item = item.replace(this.rules.checkbox, '')
isTaskListItem = true
} else {
checked = undefined
}
@ -323,7 +324,7 @@ Lexer.prototype.token = function (src, top) {
const isOrderedListItem = /\d/.test(bull)
this.tokens.push({
checked: checked,
listItemType: bull.length > 1 ? (isOrderedListItem ? 'order' : 'task') : 'bullet',
listItemType: bull.length > 1 ? 'order' : (isTaskListItem ? 'task' : 'bullet'),
bulletMarkerOrDelimiter: isOrderedListItem ? bull.slice(-1) : bull.charAt(0),
type: loose ? 'loose_item_start' : 'list_item_start'
})

View File

@ -76,7 +76,7 @@ class StateRender {
return active ? CLASS_OR_ID['AG_HIGHLIGHT'] : CLASS_OR_ID['AG_SELECTION']
}
getSelector (block, cursor, activeBlocks) {
getSelector (block, cursor, activeBlocks, selectedBlock) {
const type = block.type === 'hr' ? 'p' : block.type
const isActive = activeBlocks.some(b => b.key === block.key) || block.key === cursor.start.key
@ -87,6 +87,9 @@ class StateRender {
if (type === 'span') {
selector += `.${CLASS_OR_ID['AG_LINE']}`
}
if (!block.parent && selectedBlock && block.key === selectedBlock.key) {
selector += `.${CLASS_OR_ID['AG_SELECTED']}`
}
return selector
}
@ -132,11 +135,11 @@ class StateRender {
}
}
render (blocks, cursor, activeBlocks, matches) {
render (blocks, cursor, activeBlocks, matches, selectedBlock) {
const selector = `div#${CLASS_OR_ID['AG_EDITOR_ID']}`
const children = blocks.map(block => {
return this.renderBlock(block, cursor, activeBlocks, matches, true)
return this.renderBlock(block, cursor, activeBlocks, selectedBlock, matches, true)
})
const newVdom = h(selector, children)
@ -149,11 +152,11 @@ class StateRender {
}
// Only render the blocks which you updated
partialRender (blocks, cursor, activeBlocks, matches, startKey, endKey) {
partialRender (blocks, cursor, activeBlocks, matches, startKey, endKey, selectedBlock) {
const cursorOutMostBlock = activeBlocks[activeBlocks.length - 1]
// If cursor is not in render blocks, need to render cursor block independently
const needRenderCursorBlock = blocks.indexOf(cursorOutMostBlock) === -1
const newVnode = h('section', blocks.map(block => this.renderBlock(block, cursor, activeBlocks, matches)))
const newVnode = h('section', blocks.map(block => this.renderBlock(block, cursor, activeBlocks, selectedBlock, matches)))
const html = toHTML(newVnode).replace(/^<section>([\s\S]+?)<\/section>$/, '$1')
const needToRemoved = []
@ -182,7 +185,7 @@ class StateRender {
const cursorDom = document.querySelector(`#${key}`)
if (cursorDom) {
const oldCursorVnode = toVNode(cursorDom)
const newCursorVnode = this.renderBlock(cursorOutMostBlock, cursor, activeBlocks, matches)
const newCursorVnode = this.renderBlock(cursorOutMostBlock, cursor, activeBlocks, selectedBlock, matches)
patch(oldCursorVnode, newCursorVnode)
}
}

View File

@ -1,10 +1,10 @@
/**
* [renderBlock render one block, no matter it is a container block or text block]
*/
export default function renderBlock (block, cursor, activeBlocks, matches, useCache = false) {
export default function renderBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
const method = block.children.length > 0
? 'renderContainerBlock'
: 'renderLeafBlock'
return this[method](block, cursor, activeBlocks, matches, useCache)
return this[method](block, cursor, activeBlocks, selectedBlock, matches, useCache)
}

View File

@ -13,8 +13,8 @@ const PRE_BLOCK_HASH = {
'vega-lite': `.${CLASS_OR_ID['AG_VEGA_LITE']}`
}
export default function renderContainerBlock (block, cursor, activeBlocks, matches, useCache = false) {
let selector = this.getSelector(block, cursor, activeBlocks)
export default function renderContainerBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
let selector = this.getSelector(block, cursor, activeBlocks, selectedBlock)
const data = {
attrs: {},
dataset: {}
@ -104,8 +104,8 @@ export default function renderContainerBlock (block, cursor, activeBlocks, match
selector += PRE_BLOCK_HASH[block.functionType]
}
if (!block.parent) {
return h(selector, data, [this.renderIcon(block), ...block.children.map(child => this.renderBlock(child, cursor, activeBlocks, matches, useCache))])
return h(selector, data, [this.renderIcon(block), ...block.children.map(child => this.renderBlock(child, cursor, activeBlocks, selectedBlock, matches, useCache))])
} else {
return h(selector, data, block.children.map(child => this.renderBlock(child, cursor, activeBlocks, matches, useCache)))
return h(selector, data, block.children.map(child => this.renderBlock(child, cursor, activeBlocks, selectedBlock, matches, useCache)))
}
}

View File

@ -62,9 +62,9 @@ const hasReferenceToken = tokens => {
return result
}
export default function renderLeafBlock (block, cursor, activeBlocks, matches, useCache = false) {
export default function renderLeafBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
const { loadMathMap } = this
let selector = this.getSelector(block, cursor, activeBlocks)
let selector = this.getSelector(block, cursor, activeBlocks, selectedBlock)
// highlight search key in block
const highlights = matches.filter(m => m.key === block.key)
const {

View File

@ -7,13 +7,13 @@
top: -1000px;
right: -1000px;
border-radius: 8px;
box-shadow: 0 4px 8px 0 var(--floatShadow);
box-shadow: 0 3px 11px 0 var(--floatShadow);
border: 1px solid var(--floatBorderColor);
background-color: var(--floatBgColor);
transition: opacity .25s ease-in-out;
transform-origin: top;
box-sizing: border-box;
z-index: 1000;
z-index: 10000;
overflow: hidden;
}

View File

@ -48,8 +48,8 @@ class FormatPicker extends BaseFloat {
render () {
const { icons, oldVnode, formatContainer, formats } = this
const children = icons.map(i => {
let icon;
let iconWrapperSelector;
let icon
let iconWrapperSelector
if (i.icon) {
// SVG icon Asset
iconWrapperSelector = 'div.icon-wrapper'

View File

@ -0,0 +1,144 @@
import copyIcon from '../../assets/icons/copy.svg'
import newIcon from '../../assets/icons/paragraph.svg'
import deleteIcon from '../../assets/icons/delete.svg'
import turnIcon from '../../assets/icons/turn.svg'
import { isOsx } from '../../config'
import { quicInsertObj } from '../quickInsert/config'
const wholeSubMenu = Object.keys(quicInsertObj).reduce((acc, key) => {
const items = quicInsertObj[key]
return [...acc, ...items]
}, [])
const COMMAND_KEY = isOsx ? '⌘' : '⌃'
export const menu = [{
icon: copyIcon,
label: 'duplicate',
text: 'Duplicate',
shortCut: `${COMMAND_KEY}P`
}, {
icon: turnIcon,
label: 'turnInto',
text: 'Turn Into'
}, {
icon: newIcon,
label: 'new',
text: 'Create Paragraph',
shortCut: `${COMMAND_KEY}N`
}, {
icon: deleteIcon,
label: 'delete',
text: 'Delete',
shortCut: `${COMMAND_KEY}D`
}]
export const getLabel = block => {
const { type, functionType, listType } = block
let label = ''
switch (type) {
case 'p': {
label = 'paragraph'
break
}
case 'figure': {
if (functionType === 'table') {
label = 'table'
} else if (functionType === 'html') {
label = 'html'
} else if (functionType === 'multiplemath') {
label = 'mathblock'
}
break
}
case 'pre': {
if (functionType === 'fencecode' || functionType === 'indentcode') {
label = 'pre'
} else if (functionType === 'frontmatter') {
label = 'front-matter'
}
break
}
case 'ul': {
if (listType === 'task') {
label = 'ul-task'
} else {
label = 'ul-bullet'
}
break
}
case 'ol': {
label = 'ol-order'
break
}
case 'blockquote': {
label = 'blockquote'
break
}
case 'h1': {
label = 'heading 1'
break
}
case 'h2': {
label = 'heading 2'
break
}
case 'h3': {
label = 'heading 3'
break
}
case 'h4': {
label = 'heading 4'
break
}
case 'h5': {
label = 'heading 5'
break
}
case 'h6': {
label = 'heading 6'
break
}
case 'hr': {
label = 'hr'
break
}
default:
label = 'paragraph'
break
}
return label
}
export const getSubMenu = (block, startBlock, endBlock) => {
const { type } = block
switch (type) {
case 'p': {
return wholeSubMenu.filter(menuItem => {
const REG_EXP = startBlock.key === endBlock.key
? /front-matter|hr|table/
: /front-matter|hr|table|heading/
return !REG_EXP.test(menuItem.label)
})
}
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6': {
return wholeSubMenu.filter(menuItem => {
return /heading|paragraph/.test(menuItem.label)
})
}
case 'ul':
case 'ol': {
return wholeSubMenu.filter(menuItem => {
return /ul|ol/.test(menuItem.label)
})
}
default:
return []
}
}

View File

@ -0,0 +1,140 @@
.ag-front-menu {
width: 220px;
font-size: 14px;
}
.ag-front-menu ul,
.ag-front-menu li {
padding: 0;
margin: 0;
list-style: none;
}
.ag-front-menu > ul {
padding: 10px 0;
display: flex;
flex-direction: column;
}
.ag-front-menu > ul li {
display: flex;
flex-direction: row;
height: 36px;
align-items: center;
cursor: pointer;
position: relative;
user-select: none;
}
.ag-front-menu > ul li:hover {
background: var(--floatHoverColor);
}
.ag-front-menu li.item .icon-wrapper {
margin-left: 12px;
margin-right: 9px;
width: 20px;
height: 20px;
color: var(--iconColor);
opacity: .5;
}
.ag-front-menu li.item .icon-wrapper svg {
fill: var(--iconColor);
}
.ag-front-menu > ul li > span {
display: inline-block;
flex: 1;
color: var(--editorColor50);
}
.ag-front-menu ul > li.delete > span {
color: var(--deleteColor);
}
.ag-front-menu > ul li > .short-cut {
color: var(--editorColor30);
margin-right: 12px;
}
.ag-front-menu .submenu {
width: 220px;
max-height: 400px;
position: absolute;
left: calc(100% + 5px);
top: -46px;
border-radius: 8px;
box-shadow: 0 4px 8px 0 var(--floatShadow);
border: 1px solid var(--floatBorderColor);
background-color: var(--floatBgColor);
transition: all .25s ease-in-out;
transform-origin: left;
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
opacity: 0;
transform: scale(0);
}
.ag-front-menu .submenu.align-bottom {
top: unset;
bottom: -82px;
}
.ag-front-menu li.turnInto > .short-cut {
width: 40px;
height: 20px;
position: relative;
}
.ag-front-menu li.turnInto > .short-cut::before {
content: '';
display: block;
width: 0;
height: 0;
border: 5px solid var(--editorColor30);
border-top-width: 4px;
border-bottom-width: 4px;
border-top-color: transparent;
border-right-color: transparent;
border-bottom-color: transparent;
position: absolute;
top: 5px;
right: 0px;
}
.ag-front-menu ul li.item.disabled {
cursor: not-allowed;
color: var(--editorColor30);
}
.ag-front-menu ul li.item.active > span,
.ag-front-menu ul li.item.active > .short-cut > span {
color: var(--themeColor);
}
.ag-front-menu ul li.item.active > .icon-wrapper svg {
fill: var(--themeColor);
}
.ag-front-menu li.turnInto > .short-cut > span {
display: inline-block;
width: 20px;
height: 36px;
position: absolute;
top: 0;
left: 50px;
}
.ag-front-menu li.turnInto:hover .submenu {
opacity: 1;
transform: scale(1);
}
.ag-front-menu .submenu ul {
padding: 10px 0;
}
.ag-front-menu .submenu::-webkit-scrollbar:vertical {
width: 0px;
}

View File

@ -0,0 +1,196 @@
import BaseFloat from '../baseFloat'
import { patch, h } from '../../parser/render/snabbdom'
import { menu, getSubMenu, getLabel } from './config'
import './index.css'
const MAX_SUBMENU_HEIGHT = 400
const ITEM_HEIGHT = 36
const PADDING = 10
const defaultOptions = {
placement: 'left',
modifiers: {
offset: {
offset: '20, 20'
}
},
showArrow: false
}
class FrontMenu extends BaseFloat {
static pluginName = 'frontMenu'
constructor (muya, options = {}) {
const name = 'ag-front-menu'
const opts = Object.assign({}, defaultOptions, options)
super(muya, name, opts)
this.oldVnode = null
this.outmostBlock = null
this.startBlock = null
this.endBlock = null
this.options = opts
this.reference = null
const frontMenuContainer = this.frontMenuContainer = document.createElement('div')
Object.assign(this.container.parentNode.style, {
overflow: 'visible'
})
this.container.appendChild(frontMenuContainer)
this.listen()
}
listen () {
const { eventCenter } = this.muya
super.listen()
eventCenter.subscribe('muya-front-menu', ({ reference, outmostBlock, startBlock, endBlock }) => {
if (reference) {
this.outmostBlock = outmostBlock
this.startBlock = startBlock
this.endBlock = endBlock
this.reference = reference
setTimeout(() => {
this.show(reference)
this.render()
}, 0)
} else {
this.hide()
this.reference = null
}
})
}
renderSubMenu (subMenu) {
const { reference } = this
const rect = reference.getBoundingClientRect()
const windowHeight = document.documentElement.clientHeight
const children = subMenu.map(menuItem => {
const { icon, title, label, shortCut } = menuItem
const iconWrapperSelector = 'div.icon-wrapper'
const iconWrapper = h(iconWrapperSelector, h('svg', {
attrs: {
'viewBox': icon.viewBox,
'aria-hidden': 'true'
},
hook: {
prepatch (oldvnode, vnode) {
// cheat snabbdom that the pre block is changed!!!
oldvnode.children = []
oldvnode.elm.innerHTML = ''
}
}
}, [h('use', {
attrs: {
'xlink:href': `${icon.url}`
}
})]
))
const textWrapper = h('span', title)
const shortCutWrapper = h('div.short-cut', [
h('span', shortCut)
])
let itemSelector = `li.item.${label}`
if (label === getLabel(this.outmostBlock)) {
itemSelector += `.active`
}
return h(itemSelector, {
on: {
click: event => {
this.selectItem(event, { label })
}
}
}, [iconWrapper, textWrapper, shortCutWrapper])
})
let subMenuSelector = 'div.submenu'
if (windowHeight - rect.bottom < MAX_SUBMENU_HEIGHT - (ITEM_HEIGHT + PADDING)) {
subMenuSelector += '.align-bottom'
}
return h(subMenuSelector, h('ul', children))
}
render () {
const { oldVnode, frontMenuContainer, outmostBlock, startBlock, endBlock } = this
const { type, functionType } = outmostBlock
const children = menu.map(({ icon, label, text, shortCut }) => {
const subMenu = getSubMenu(outmostBlock, startBlock, endBlock)
const iconWrapperSelector = 'div.icon-wrapper'
const iconWrapper = h(iconWrapperSelector, h('svg', {
attrs: {
'viewBox': icon.viewBox,
'aria-hidden': 'true'
}
}, [h('use', {
attrs: {
'xlink:href': `${icon.url}`
}
})]
))
const textWrapper = h('span', text)
const shortCutWrapper = h('div.short-cut', [
h('span', shortCut)
])
let itemSelector = `li.item.${label}`
const itemChildren = [iconWrapper, textWrapper, shortCutWrapper]
if (label === 'turnInto' && subMenu.length !== 0) {
itemChildren.push(this.renderSubMenu(subMenu))
}
if (label === 'turnInto' && subMenu.length === 0) {
itemSelector += `.disabled`
}
// front matter can not be duplicated.
if (label === 'duplicate' && type === 'pre' && functionType === 'frontmatter') {
itemSelector += `.disabled`
}
return h(itemSelector, {
on: {
click: event => {
this.selectItem(event, { label })
}
}
}, itemChildren)
})
const vnode = h('ul', children)
if (oldVnode) {
patch(oldVnode, vnode)
} else {
patch(frontMenuContainer, vnode)
}
this.oldVnode = vnode
}
selectItem (event, { label }) {
event.preventDefault()
event.stopPropagation()
const { type, functionType } = this.outmostBlock
// front matter can not be duplicated.
if (label === 'duplicate' && type === 'pre' && functionType === 'frontmatter') {
return
}
const { contentState } = this.muya
contentState.selectedBlock = null
switch (label) {
case 'duplicate': {
contentState.duplicate()
break
}
case 'delete': {
contentState.deleteParagraph()
break
}
case 'new': {
contentState.insertParagraph('after', '', true)
break
}
case 'turnInto':
// do nothing, do not hide float box.
return
default:
contentState.updateParagraph(label)
break
}
// delay hide to avoid dispatch enter hander
setTimeout(this.hide.bind(this))
}
}
export default FrontMenu

View File

@ -44,148 +44,127 @@ export const quicInsertObj = {
// title: 'Vega Chart',
// subTitle: 'Render flow chart by vega-lite.js.',
// label: 'vega-lite',
// icon: vegaIcon,
// color: 'rgb(224, 54, 54)'
// icon: vegaIcon
// }, {
// title: 'Flow Chart',
// subTitle: 'Render flow chart by flowchart.js.',
// label: 'flowchart',
// icon: flowchartIcon,
// color: 'rgb(224, 54, 54)'
// icon: flowchartIcon
// }, {
// title: 'Sequence Diagram',
// subTitle: 'Render sequence diagram by js-sequence.',
// label: 'sequence',
// icon: sequenceIcon,
// color: 'rgb(224, 54, 54)'
// icon: sequenceIcon
// }, {
// title: 'Mermaid',
// subTitle: 'Render Diagram by mermaid.',
// label: 'mermaid',
// icon: mermaidIcon,
// color: 'rgb(224, 54, 54)'
// icon: mermaidIcon
// }],
'basic block': [{
title: 'Paragraph',
subTitle: 'Lorem Ipsum is simply dummy text',
label: 'paragraph',
shortCut: `${COMMAND_KEY}0`,
icon: paragraphIcon,
color: 'rgb(224, 54, 54)'
icon: paragraphIcon
}, {
title: 'Horizontal Line',
subTitle: '---',
label: 'hr',
shortCut: `${COMMAND_KEY}-`,
icon: hrIcon,
color: 'rgb(255, 83, 77)'
icon: hrIcon
}, {
title: 'Front Matter',
subTitle: '--- Lorem Ipsum ---',
label: 'front-matter',
shortCut: `${COMMAND_KEY}Y`,
icon: frontMatterIcon,
color: 'rgb(37, 198, 252)'
icon: frontMatterIcon
}],
'header': [{
title: 'Header 1',
subTitle: '# Lorem Ipsum is simply ...',
label: 'heading 1',
shortCut: `${COMMAND_KEY}1`,
icon: header1Icon,
color: 'rgb(86, 163, 108)'
icon: header1Icon
}, {
title: 'Header 2',
subTitle: '## Lorem Ipsum is simply ...',
label: 'heading 2',
shortCut: `${COMMAND_KEY}2`,
icon: header2Icon,
color: 'rgb(94, 133, 121)'
icon: header2Icon
}, {
title: 'Header 3',
subTitle: '### Lorem Ipsum is simply ...',
label: 'heading 3',
shortCut: `${COMMAND_KEY}3`,
icon: header3Icon,
color: 'rgb(119, 195, 79)'
icon: header3Icon
}, {
title: 'Header 4',
subTitle: '#### Lorem Ipsum is simply ...',
label: 'heading 4',
shortCut: `${COMMAND_KEY}4`,
icon: header4Icon,
color: 'rgb(46, 104, 170)'
icon: header4Icon
}, {
title: 'Header 5',
subTitle: '##### Lorem Ipsum is simply ...',
label: 'heading 5',
shortCut: `${COMMAND_KEY}5`,
icon: header5Icon,
color: 'rgb(126, 136, 79)'
icon: header5Icon
}, {
title: 'Header 6',
subTitle: '###### Lorem Ipsum is simply ...',
label: 'heading 6',
shortCut: `${COMMAND_KEY}6`,
icon: header6Icon,
color: 'rgb(29, 176, 184)'
icon: header6Icon
}],
'advanced block': [{
title: 'Table Block',
subTitle: '|Lorem | Ipsum is simply |',
label: 'table',
shortCut: `${COMMAND_KEY}T`,
icon: newTableIcon,
color: 'rgb(13, 23, 64)'
icon: newTableIcon
}, {
title: 'Mathematical Formula',
title: 'Math Formula',
subTitle: '$$ Lorem Ipsum is simply $$',
label: 'mathblock',
shortCut: `${COMMAND_KEY}M`,
icon: mathblockIcon,
color: 'rgb(252, 214, 146)'
icon: mathblockIcon
}, {
title: 'HTML Block',
subTitle: '<div> Lorem Ipsum is simply </div>',
label: 'html',
shortCut: `${COMMAND_KEY}J`,
icon: htmlIcon,
color: 'rgb(13, 23, 64)'
icon: htmlIcon
}, {
title: 'Code Block',
subTitle: '```java Lorem Ipsum is simply ```',
label: 'pre',
shortCut: `${COMMAND_KEY}C`,
icon: codeIcon,
color: 'rgb(164, 159, 147)'
icon: codeIcon
}, {
title: 'Quote Block',
subTitle: '>Lorem Ipsum is simply ...',
label: 'blockquote',
shortCut: `${COMMAND_KEY}Q`,
icon: quoteIcon,
color: 'rgb(31, 111, 181)'
icon: quoteIcon
}],
'list block': [{
title: 'Order List',
subTitle: '1. Lorem Ipsum is simply ...',
label: 'ol-order',
shortCut: `${COMMAND_KEY}O`,
icon: orderListIcon,
color: 'rgb(242, 159, 63)'
icon: orderListIcon
}, {
title: 'Bullet List',
subTitle: '- Lorem Ipsum is simply ...',
label: 'ul-bullet',
shortCut: `${COMMAND_KEY}U`,
icon: bulletListIcon,
color: 'rgb(242, 117, 63)'
icon: bulletListIcon
}, {
title: 'To-do List',
subTitle: '- [x] Lorem Ipsum is simply ...',
label: 'ul-task',
shortCut: `${COMMAND_KEY}X`,
icon: todoListIcon,
color: 'rgb(222, 140, 124)'
icon: todoListIcon
}]
}

View File

@ -82,6 +82,7 @@
import EmojiPicker from 'muya/lib/ui/emojiPicker'
import ImagePathPicker from 'muya/lib/ui/imagePicker'
import FormatPicker from 'muya/lib/ui/formatPicker'
import FrontMenu from 'muya/lib/ui/frontMenu'
import bus from '../../bus'
import Search from '../search.vue'
import { animatedScrollTo } from '../../util'
@ -206,6 +207,8 @@
Muya.use(EmojiPicker)
Muya.use(ImagePathPicker)
Muya.use(FormatPicker)
Muya.use(FrontMenu)
const { container } = this.editor = new Muya(ele, {
focusMode,
markdown,
@ -241,6 +244,9 @@
bus.$on('copyAsMarkdown', this.handleCopyPaste)
bus.$on('copyAsHtml', this.handleCopyPaste)
bus.$on('pasteAsPlainText', this.handleCopyPaste)
bus.$on('duplicate', this.handleParagraph)
bus.$on('createParagraph', this.handleParagraph)
bus.$on('deleteParagraph', this.handleParagraph)
bus.$on('insertParagraph', this.handleInsertParagraph)
bus.$on('editTable', this.handleEditTable)
bus.$on('scroll-to-header', this.scrollToHeader)
@ -436,6 +442,27 @@
}
},
// handle `duplicate`, `delete`, `create paragraph bellow`
handleParagraph (type) {
const { editor } = this
if (editor) {
switch (type) {
case 'duplicate': {
return editor.duplicate()
}
case 'createParagraph': {
return editor.insertParagraph('after', '', true)
}
case 'deleteParagraph': {
return editor.deleteParagraph()
}
default:
console.error(`unknow paragraph edit type: ${type}`)
return
}
}
},
handleInlineFormat (type) {
this.editor && this.editor.format(type)
},
@ -503,6 +530,9 @@
bus.$off('copyAsMarkdown', this.handleCopyPaste)
bus.$off('copyAsHtml', this.handleCopyPaste)
bus.$off('pasteAsPlainText', this.handleCopyPaste)
bus.$off('duplicate', this.handleParagraph)
bus.$off('createParagraph', this.handleParagraph)
bus.$off('deleteParagraph', this.handleParagraph)
bus.$off('insertParagraph', this.handleInsertParagraph)
bus.$off('editTable', this.handleEditTable)
bus.$off('scroll-to-header', this.scrollToHeader)