mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 04:39:47 +08:00
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:
parent
962fdf356f
commit
4e918503f4
1
.github/CHANGELOG.md
vendored
1
.github/CHANGELOG.md
vendored
@ -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`)
|
- 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)
|
- Support input inline Ruby charactors as raw html (#257)
|
||||||
- Added unsaved tab indicator
|
- Added unsaved tab indicator
|
||||||
|
- Add front Menu by click the front menu icon (#875)
|
||||||
|
|
||||||
**:butterfly:Optimization**
|
**:butterfly:Optimization**
|
||||||
|
|
||||||
|
@ -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:
|
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)
|
- [Become a backer or sponsor on Open Collective](https://opencollective.com/marktext)
|
||||||
|
|
||||||
##### What's the difference between Patreon and OpenCollective?
|
##### What's the difference between Patreon and OpenCollective?
|
||||||
|
@ -49,6 +49,9 @@ Here is an example:
|
|||||||
| `editCopyAsMarkdown` | Copy selected text as markdown |
|
| `editCopyAsMarkdown` | Copy selected text as markdown |
|
||||||
| `editCopyAsPlaintext` | Copy selected text as plaintext |
|
| `editCopyAsPlaintext` | Copy selected text as plaintext |
|
||||||
| `editSelectAll` | Select all text of the document |
|
| `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 |
|
| `editFind` | Find information in the document |
|
||||||
| `editFindNext` | Continue the search and find the next match |
|
| `editFindNext` | Continue the search and find the next match |
|
||||||
| `editFindPrevious` | Continue the search and find the previous match |
|
| `editFindPrevious` | Continue the search and find the previous match |
|
||||||
|
@ -59,6 +59,26 @@ export default {
|
|||||||
role: 'selectall'
|
role: 'selectall'
|
||||||
}, {
|
}, {
|
||||||
type: 'separator'
|
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',
|
label: 'Find',
|
||||||
accelerator: keybindings.getAccelerator('editFind'),
|
accelerator: keybindings.getAccelerator('editFind'),
|
||||||
|
@ -41,6 +41,9 @@ class Keybindings {
|
|||||||
['editCopyAsMarkdown', 'CmdOrCtrl+Shift+C'],
|
['editCopyAsMarkdown', 'CmdOrCtrl+Shift+C'],
|
||||||
['editCopyAsPlaintext', 'CmdOrCtrl+Shift+V'],
|
['editCopyAsPlaintext', 'CmdOrCtrl+Shift+V'],
|
||||||
['editSelectAll', 'CmdOrCtrl+A'],
|
['editSelectAll', 'CmdOrCtrl+A'],
|
||||||
|
['editDuplicate', 'Shift+CmdOrCtrl+P'],
|
||||||
|
['editCreateParagraph', 'Shift+CmdOrCtrl+N'],
|
||||||
|
['editDeleteParagraph', 'Shift+CmdOrCtrl+D'],
|
||||||
['editFind', 'CmdOrCtrl+F'],
|
['editFind', 'CmdOrCtrl+F'],
|
||||||
['editFindNext', 'CmdOrCtrl+Alt+U'],
|
['editFindNext', 'CmdOrCtrl+Alt+U'],
|
||||||
['editFindPrevious', 'CmdOrCtrl+Shift+U'],
|
['editFindPrevious', 'CmdOrCtrl+Shift+U'],
|
||||||
|
1
src/muya/lib/assets/icons/copy.svg
Normal file
1
src/muya/lib/assets/icons/copy.svg
Normal 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 |
1
src/muya/lib/assets/icons/turn.svg
Normal file
1
src/muya/lib/assets/icons/turn.svg
Normal 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 |
@ -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;
|
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 {
|
.ag-reference-marker {
|
||||||
font-size: .9em;
|
font-size: .9em;
|
||||||
color: var(--editorColor50);
|
color: var(--editorColor50);
|
||||||
@ -290,10 +299,10 @@ li.ag-task-list-item {
|
|||||||
li.ag-task-list-item > input[type=checkbox] {
|
li.ag-task-list-item > input[type=checkbox] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 10px;
|
width: 12px;
|
||||||
height: 10px;
|
height: 12px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
left: -22px;
|
left: -23px;
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
transition: all .2s ease;
|
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 {
|
li.ag-task-list-item > input[type=checkbox]::before {
|
||||||
content: '';
|
content: '';
|
||||||
width: 14px;
|
width: 18px;
|
||||||
height: 14px;
|
height: 18px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: 1px solid var(--iconColor);
|
border: 2px solid var(--editorColor50);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--editorBgColor);
|
background-color: var(--editorBgColor);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -2px;
|
top: -2px;
|
||||||
left: -2px;
|
left: -2px;
|
||||||
|
box-sizing: border-box;
|
||||||
transition: all .2s ease;
|
transition: all .2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.ag-task-list-item > input::after {
|
li.ag-task-list-item > input::after {
|
||||||
content: '';
|
content: '';
|
||||||
transform: rotate(-28deg) skew(0, -25deg) scale(0);
|
transform: rotate(-45deg) scale(0);
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
border: 1px solid var(--iconColor);
|
border: 2px solid var(--editorBgColor);
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
top: 0px;
|
top: 1px;
|
||||||
left: 2px;
|
left: 4px;
|
||||||
transform-origin: bottom;
|
transform-origin: bottom;
|
||||||
transition: all .2s ease;
|
transition: all .2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.ag-task-list-item > input.ag-checkbox-checked::after {
|
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 {
|
/* li p .ag-hide:first-child {
|
||||||
@ -705,7 +721,7 @@ span.ag-reference-link {
|
|||||||
top: 2px;
|
top: 2px;
|
||||||
left: -30px;
|
left: -30px;
|
||||||
display: none;
|
display: none;
|
||||||
/* cursor: pointer; */
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-front-icon::before {
|
.ag-front-icon::before {
|
||||||
|
@ -104,6 +104,12 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
|
|||||||
'AG_LIST_ITEM',
|
'AG_LIST_ITEM',
|
||||||
'AG_LOOSE_LIST_ITEM',
|
'AG_LOOSE_LIST_ITEM',
|
||||||
'AG_MATH',
|
'AG_MATH',
|
||||||
|
'AG_MATH_TEXT',
|
||||||
|
'AG_MATH_RENDER',
|
||||||
|
'AG_RUBY',
|
||||||
|
'AG_RUBY_TEXT',
|
||||||
|
'AG_RUBY_RENDER',
|
||||||
|
'AG_SELECTED',
|
||||||
'AG_MATH_ERROR',
|
'AG_MATH_ERROR',
|
||||||
'AG_MATH_MARKER',
|
'AG_MATH_MARKER',
|
||||||
'AG_MATH_RENDER',
|
'AG_MATH_RENDER',
|
||||||
@ -228,6 +234,7 @@ export const MUYA_DEFAULT_OPTION = {
|
|||||||
autoPairMarkdownSyntax: true,
|
autoPairMarkdownSyntax: true,
|
||||||
autoPairQuote: true,
|
autoPairQuote: true,
|
||||||
bulletListMarker: '-',
|
bulletListMarker: '-',
|
||||||
|
orderListMarker: '.',
|
||||||
tabSize: 4,
|
tabSize: 4,
|
||||||
sequenceTheme: 'hand', // hand or simple
|
sequenceTheme: 'hand', // hand or simple
|
||||||
mermaidTheme: 'forest', // dark or forest
|
mermaidTheme: 'forest', // dark or forest
|
||||||
|
@ -4,6 +4,36 @@ import { HAS_TEXT_BLOCK_REG } from '../config'
|
|||||||
const clickCtrl = ContentState => {
|
const clickCtrl = ContentState => {
|
||||||
ContentState.prototype.clickHandler = function (event) {
|
ContentState.prototype.clickHandler = function (event) {
|
||||||
const { eventCenter } = this.muya
|
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()
|
const { start, end } = selection.getCursorRange()
|
||||||
// fix #625, the selection maybe not in edit area.
|
// fix #625, the selection maybe not in edit area.
|
||||||
if (!start || !end) {
|
if (!start || !end) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { VOID_HTML_TAGS, HTML_TAGS, HTML_TOOLS } from '../config'
|
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 HTML_BLOCK_REG = /^<([a-zA-Z\d-]+)(?=\s|>)[^<>]*?>$/
|
||||||
const LINE_BREAKS = /\n/
|
const LINE_BREAKS = /\n/
|
||||||
@ -95,10 +96,36 @@ const htmlBlock = ContentState => {
|
|||||||
return block
|
return block
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentState.prototype.initHtmlBlock = function (block, tagName) {
|
ContentState.prototype.initHtmlBlock = function (block) {
|
||||||
const isVoidTag = VOID_HTML_TAGS.indexOf(tagName) > -1
|
let htmlContent = ''
|
||||||
const { text } = block.children[0]
|
const text = block.type === 'p'
|
||||||
const htmlContent = isVoidTag ? text : `${text}\n\n</${tagName}>`
|
? 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.type = 'figure'
|
||||||
block.functionType = 'html'
|
block.functionType = 'html'
|
||||||
block.text = htmlContent
|
block.text = htmlContent
|
||||||
@ -116,7 +143,7 @@ const htmlBlock = ContentState => {
|
|||||||
const { text } = block.children[0]
|
const { text } = block.children[0]
|
||||||
const match = HTML_BLOCK_REG.exec(text)
|
const match = HTML_BLOCK_REG.exec(text)
|
||||||
const tagName = match && match[1] && HTML_TAGS.find(t => t === match[1])
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { HAS_TEXT_BLOCK_REG, DEFAULT_TURNDOWN_CONFIG } from '../config'
|
import { HAS_TEXT_BLOCK_REG, DEFAULT_TURNDOWN_CONFIG } from '../config'
|
||||||
import { getUniqueId } from '../utils'
|
import { getUniqueId, deepCopy } from '../utils'
|
||||||
import selection from '../selection'
|
import selection from '../selection'
|
||||||
import StateRender from '../parser/render'
|
import StateRender from '../parser/render'
|
||||||
import enterCtrl from './enterCtrl'
|
import enterCtrl from './enterCtrl'
|
||||||
@ -65,6 +65,8 @@ class ContentState {
|
|||||||
this.codeBlocks = new Map()
|
this.codeBlocks = new Map()
|
||||||
this.renderRange = [ null, null ]
|
this.renderRange = [ null, null ]
|
||||||
this.currentCursor = 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.prevCursor = null
|
||||||
this.historyTimer = null
|
this.historyTimer = null
|
||||||
this.history = new History(this)
|
this.history = new History(this)
|
||||||
@ -147,19 +149,19 @@ class ContentState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render (isRenderCursor = true) {
|
render (isRenderCursor = true) {
|
||||||
const { blocks, cursor, searchMatches: { matches, index } } = this
|
const { blocks, cursor, searchMatches: { matches, index }, selectedBlock } = this
|
||||||
const activeBlocks = this.getActiveBlocks()
|
const activeBlocks = this.getActiveBlocks()
|
||||||
matches.forEach((m, i) => {
|
matches.forEach((m, i) => {
|
||||||
m.active = i === index
|
m.active = i === index
|
||||||
})
|
})
|
||||||
this.setNextRenderRange()
|
this.setNextRenderRange()
|
||||||
this.stateRender.collectLabels(blocks)
|
this.stateRender.collectLabels(blocks)
|
||||||
this.stateRender.render(blocks, cursor, activeBlocks, matches)
|
this.stateRender.render(blocks, cursor, activeBlocks, matches, selectedBlock)
|
||||||
if (isRenderCursor) this.setCursor()
|
if (isRenderCursor) this.setCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
partialRender () {
|
partialRender () {
|
||||||
const { blocks, cursor, searchMatches: { matches, index } } = this
|
const { blocks, cursor, searchMatches: { matches, index }, selectedBlock } = this
|
||||||
const activeBlocks = this.getActiveBlocks()
|
const activeBlocks = this.getActiveBlocks()
|
||||||
const [ startKey, endKey ] = this.renderRange
|
const [ startKey, endKey ] = this.renderRange
|
||||||
matches.forEach((m, i) => {
|
matches.forEach((m, i) => {
|
||||||
@ -171,7 +173,7 @@ class ContentState {
|
|||||||
|
|
||||||
this.setNextRenderRange()
|
this.setNextRenderRange()
|
||||||
this.stateRender.collectLabels(blocks)
|
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()
|
this.setCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,6 +235,31 @@ class ContentState {
|
|||||||
return result
|
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) {
|
getParent (block) {
|
||||||
if (block && block.parent) {
|
if (block && block.parent) {
|
||||||
return this.getBlock(block.parent)
|
return this.getBlock(block.parent)
|
||||||
|
@ -86,7 +86,8 @@ const paragraphCtrl = ContentState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ContentState.prototype.handleListMenu = function (paraType, insertMode) {
|
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 [blockType, listType] = paraType.split('-')
|
||||||
const isListed = affiliation.slice(0, 3).filter(b => /ul|ol/.test(b.type))
|
const isListed = affiliation.slice(0, 3).filter(b => /ul|ol/.test(b.type))
|
||||||
const { preferLooseListItem } = this
|
const { preferLooseListItem } = this
|
||||||
@ -113,13 +114,23 @@ const paragraphCtrl = ContentState => {
|
|||||||
inputBlock && this.removeBlock(inputBlock)
|
inputBlock && this.removeBlock(inputBlock)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const oldListType = listBlock.listType
|
||||||
listBlock.type = blockType
|
listBlock.type = blockType
|
||||||
listBlock.listType = listType
|
listBlock.listType = listType
|
||||||
listBlock.children.forEach(b => (b.listItemType = listType))
|
listBlock.children.forEach(b => (b.listItemType = listType))
|
||||||
|
|
||||||
if (listType === 'order') {
|
if (listType === 'order') {
|
||||||
listBlock.start = listBlock.start || 1
|
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 the new block is task list, add checkbox
|
||||||
if (listType === 'task') {
|
if (listType === 'task') {
|
||||||
const listItems = listBlock.children
|
const listItems = listBlock.children
|
||||||
@ -175,7 +186,7 @@ const paragraphCtrl = ContentState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ContentState.prototype.handleLooseListItem = function () {
|
ContentState.prototype.handleLooseListItem = function () {
|
||||||
const { affiliation } = this.selectionChange()
|
const { affiliation } = this.selectionChange(this.cursor)
|
||||||
let listContainer = []
|
let listContainer = []
|
||||||
if (affiliation.length >= 1 && /ul|ol/.test(affiliation[0].type)) {
|
if (affiliation.length >= 1 && /ul|ol/.test(affiliation[0].type)) {
|
||||||
listContainer = affiliation[0].children
|
listContainer = affiliation[0].children
|
||||||
@ -191,7 +202,7 @@ const paragraphCtrl = ContentState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ContentState.prototype.handleCodeBlockMenu = function () {
|
ContentState.prototype.handleCodeBlockMenu = function () {
|
||||||
const { start, end, affiliation } = this.selectionChange()
|
const { start, end, affiliation } = this.selectionChange(this.cursor)
|
||||||
let startBlock = this.getBlock(start.key)
|
let 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)
|
||||||
@ -290,7 +301,7 @@ const paragraphCtrl = ContentState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ContentState.prototype.handleQuoteMenu = function (insertMode) {
|
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)
|
let startBlock = this.getBlock(start.key)
|
||||||
const isBlockQuote = affiliation.slice(0, 2).filter(b => /blockquote/.test(b.type))
|
const isBlockQuote = affiliation.slice(0, 2).filter(b => /blockquote/.test(b.type))
|
||||||
// change blockquote to paragraph
|
// change blockquote to paragraph
|
||||||
@ -330,22 +341,19 @@ const paragraphCtrl = ContentState => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentState.prototype.insertContainerBlock = function (functionType, value = '') {
|
ContentState.prototype.insertContainerBlock = function (functionType, block) {
|
||||||
const { start, end } = selection.getCursorRange()
|
|
||||||
if (!start || !end) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (start.key !== end.key) return
|
|
||||||
let block = this.getBlock(start.key)
|
|
||||||
if (block.type === 'span') {
|
if (block.type === 'span') {
|
||||||
block = this.getParent(block)
|
block = this.getParent(block)
|
||||||
}
|
}
|
||||||
const mathBlock = this.createContainerBlock(functionType, value)
|
const value = block.type === 'p'
|
||||||
this.insertAfter(mathBlock, block)
|
? block.children.map(child => child.text).join('\n').trim()
|
||||||
if (block.type === 'p' && block.children.length === 1 && !block.children[0].text) {
|
: block.text
|
||||||
this.removeBlock(block)
|
|
||||||
}
|
const containerBlock = this.createContainerBlock(functionType, value)
|
||||||
const cursorBlock = mathBlock.children[0].children[0].children[0]
|
this.insertAfter(containerBlock, block)
|
||||||
|
this.removeBlock(block)
|
||||||
|
|
||||||
|
const cursorBlock = containerBlock.children[0].children[0].children[0]
|
||||||
const { key } = cursorBlock
|
const { key } = cursorBlock
|
||||||
const offset = 0
|
const offset = 0
|
||||||
this.cursor = {
|
this.cursor = {
|
||||||
@ -365,10 +373,14 @@ const paragraphCtrl = ContentState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ContentState.prototype.insertHtmlBlock = function (block) {
|
ContentState.prototype.insertHtmlBlock = function (block) {
|
||||||
const parentBlock = this.getParent(block)
|
if (block.type === 'span') {
|
||||||
block.text = '<div>'
|
block = this.getParent(block)
|
||||||
const preBlock = this.initHtmlBlock(parentBlock, 'div')
|
}
|
||||||
const key = preBlock.children[0].children[1].key
|
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
|
const offset = 0
|
||||||
this.cursor = {
|
this.cursor = {
|
||||||
start: { key, offset },
|
start: { key, offset },
|
||||||
@ -379,7 +391,7 @@ const paragraphCtrl = ContentState => {
|
|||||||
ContentState.prototype.updateParagraph = function (paraType, insertMode = false) {
|
ContentState.prototype.updateParagraph = function (paraType, insertMode = false) {
|
||||||
const { start, end } = this.cursor
|
const { start, end } = this.cursor
|
||||||
const block = this.getBlock(start.key)
|
const block = this.getBlock(start.key)
|
||||||
const { type, text } = block
|
const { type, text, functionType } = block
|
||||||
|
|
||||||
switch (paraType) {
|
switch (paraType) {
|
||||||
case 'front-matter': {
|
case 'front-matter': {
|
||||||
@ -405,7 +417,7 @@ const paragraphCtrl = ContentState => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'mathblock': {
|
case 'mathblock': {
|
||||||
this.insertContainerBlock('multiplemath')
|
this.insertContainerBlock('multiplemath', block)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'table': {
|
case 'table': {
|
||||||
@ -420,7 +432,7 @@ const paragraphCtrl = ContentState => {
|
|||||||
case 'sequence':
|
case 'sequence':
|
||||||
case 'mermaid':
|
case 'mermaid':
|
||||||
case 'vega-lite':
|
case 'vega-lite':
|
||||||
this.insertContainerBlock(paraType)
|
this.insertContainerBlock(paraType, block)
|
||||||
break
|
break
|
||||||
case 'heading 1':
|
case 'heading 1':
|
||||||
case 'heading 2':
|
case 'heading 2':
|
||||||
@ -432,7 +444,7 @@ const paragraphCtrl = ContentState => {
|
|||||||
case 'degrade heading':
|
case 'degrade heading':
|
||||||
case 'paragraph': {
|
case 'paragraph': {
|
||||||
if (start.key !== end.key) return
|
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 newLevel = 0 // 1, 2, 3, 4, 5, 6
|
||||||
let newType = 'p'
|
let newType = 'p'
|
||||||
let key
|
let key
|
||||||
@ -452,9 +464,15 @@ const paragraphCtrl = ContentState => {
|
|||||||
newType = newLevel === 0 ? 'p' : `h${newLevel}`
|
newType = newLevel === 0 ? 'p' : `h${newLevel}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const startOffset = start.offset + newLevel - hash.length + 1
|
const startOffset = newLevel > 0
|
||||||
const endOffset = end.offset + newLevel - hash.length + 1
|
? start.offset + newLevel - hash.length + 1
|
||||||
const newText = '#'.repeat(newLevel) + `${String.fromCharCode(160)}${partText}` // code: 160
|
: 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}` // code: 160
|
||||||
|
: partText
|
||||||
|
|
||||||
if (block.type === 'span' && newType !== 'p') {
|
if (block.type === 'span' && newType !== 'p') {
|
||||||
const header = this.createBlock(newType, newText)
|
const header = this.createBlock(newType, newText)
|
||||||
@ -488,6 +506,9 @@ const paragraphCtrl = ContentState => {
|
|||||||
key = pBlock.children[0].key
|
key = pBlock.children[0].key
|
||||||
this.insertAfter(pBlock, block)
|
this.insertAfter(pBlock, block)
|
||||||
this.removeBlock(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 {
|
} else {
|
||||||
const newHeader = this.createBlock(newType, newText)
|
const newHeader = this.createBlock(newType, newText)
|
||||||
newHeader.headingStyle = DEFAULT_TURNDOWN_CONFIG.headingStyle
|
newHeader.headingStyle = DEFAULT_TURNDOWN_CONFIG.headingStyle
|
||||||
@ -530,16 +551,20 @@ const paragraphCtrl = ContentState => {
|
|||||||
this.partialRender()
|
this.partialRender()
|
||||||
}
|
}
|
||||||
// update menu status
|
// update menu status
|
||||||
const selectionChanges = this.selectionChange()
|
const selectionChanges = this.selectionChange(this.cursor)
|
||||||
this.muya.eventCenter.dispatch('selectionChange', selectionChanges)
|
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
|
const { start, end } = this.cursor
|
||||||
// if cursor is not in one line or paragraph, can not insert paragraph
|
// if cursor is not in one line or paragraph, can not insert paragraph
|
||||||
if (start.key !== end.key) return
|
if (start.key !== end.key) return
|
||||||
let block = this.getBlock(start.key)
|
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)
|
block = this.getParent(block)
|
||||||
} else if (block.type === 'span' && block.functionType === 'codeLine') {
|
} else if (block.type === 'span' && block.functionType === 'codeLine') {
|
||||||
const preBlock = this.getParent(this.getParent(block))
|
const preBlock = this.getParent(this.getParent(block))
|
||||||
@ -580,6 +605,61 @@ const paragraphCtrl = ContentState => {
|
|||||||
end: { key, offset }
|
end: { key, offset }
|
||||||
}
|
}
|
||||||
this.partialRender()
|
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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,14 @@ class Keyboard {
|
|||||||
// cache shown float box
|
// cache shown float box
|
||||||
this.muya.eventCenter.subscribe('muya-float', (name, status) => {
|
this.muya.eventCenter.subscribe('muya-float', (name, status) => {
|
||||||
status ? this.shownFloat.add(name) : this.shownFloat.delete(name)
|
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')
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,8 +150,16 @@ class Muya {
|
|||||||
this.contentState.updateParagraph(type)
|
this.contentState.updateParagraph(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
insertParagraph (location/* before or after */) {
|
duplicate () {
|
||||||
this.contentState.insertParagraph(location)
|
this.contentState.duplicate()
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteParagraph () {
|
||||||
|
this.contentState.deleteParagraph()
|
||||||
|
}
|
||||||
|
|
||||||
|
insertParagraph (location/* before or after */, text = '', outMost = false) {
|
||||||
|
this.contentState.insertParagraph(location, text, outMost)
|
||||||
}
|
}
|
||||||
|
|
||||||
editTable (data) {
|
editTable (data) {
|
||||||
|
@ -206,12 +206,11 @@ Lexer.prototype.token = function (src, top) {
|
|||||||
if (cap) {
|
if (cap) {
|
||||||
src = src.substring(cap[0].length)
|
src = src.substring(cap[0].length)
|
||||||
bull = cap[2]
|
bull = cap[2]
|
||||||
let isOrdered = bull.length > 1 && /\d{1,9}/.test(bull)
|
let isOrdered = bull.length > 1
|
||||||
|
|
||||||
this.tokens.push({
|
this.tokens.push({
|
||||||
type: 'list_start',
|
type: 'list_start',
|
||||||
ordered: isOrdered,
|
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)) : ''
|
start: isOrdered ? +(bull.slice(0, -1)) : ''
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -226,6 +225,7 @@ Lexer.prototype.token = function (src, top) {
|
|||||||
|
|
||||||
for (; i < l; i++) {
|
for (; i < l; i++) {
|
||||||
const itemWithBullet = cap[i]
|
const itemWithBullet = cap[i]
|
||||||
|
let isTaskListItem = false
|
||||||
item = itemWithBullet
|
item = itemWithBullet
|
||||||
|
|
||||||
// Remove the list item's bullet
|
// Remove the list item's bullet
|
||||||
@ -257,7 +257,7 @@ Lexer.prototype.token = function (src, top) {
|
|||||||
this.tokens.push({
|
this.tokens.push({
|
||||||
type: 'list_start',
|
type: 'list_start',
|
||||||
ordered: isOrdered,
|
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)) : ''
|
start: isOrdered ? +(bull.slice(0, -1)) : ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -267,6 +267,7 @@ Lexer.prototype.token = function (src, top) {
|
|||||||
if (checked) {
|
if (checked) {
|
||||||
checked = checked[1] === 'x' || checked[1] === 'X'
|
checked = checked[1] === 'x' || checked[1] === 'X'
|
||||||
item = item.replace(this.rules.checkbox, '')
|
item = item.replace(this.rules.checkbox, '')
|
||||||
|
isTaskListItem = true
|
||||||
} else {
|
} else {
|
||||||
checked = undefined
|
checked = undefined
|
||||||
}
|
}
|
||||||
@ -323,7 +324,7 @@ Lexer.prototype.token = function (src, top) {
|
|||||||
const isOrderedListItem = /\d/.test(bull)
|
const isOrderedListItem = /\d/.test(bull)
|
||||||
this.tokens.push({
|
this.tokens.push({
|
||||||
checked: checked,
|
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),
|
bulletMarkerOrDelimiter: isOrderedListItem ? bull.slice(-1) : bull.charAt(0),
|
||||||
type: loose ? 'loose_item_start' : 'list_item_start'
|
type: loose ? 'loose_item_start' : 'list_item_start'
|
||||||
})
|
})
|
||||||
|
@ -76,7 +76,7 @@ class StateRender {
|
|||||||
return active ? CLASS_OR_ID['AG_HIGHLIGHT'] : CLASS_OR_ID['AG_SELECTION']
|
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 type = block.type === 'hr' ? 'p' : block.type
|
||||||
const isActive = activeBlocks.some(b => b.key === block.key) || block.key === cursor.start.key
|
const isActive = activeBlocks.some(b => b.key === block.key) || block.key === cursor.start.key
|
||||||
|
|
||||||
@ -87,6 +87,9 @@ class StateRender {
|
|||||||
if (type === 'span') {
|
if (type === 'span') {
|
||||||
selector += `.${CLASS_OR_ID['AG_LINE']}`
|
selector += `.${CLASS_OR_ID['AG_LINE']}`
|
||||||
}
|
}
|
||||||
|
if (!block.parent && selectedBlock && block.key === selectedBlock.key) {
|
||||||
|
selector += `.${CLASS_OR_ID['AG_SELECTED']}`
|
||||||
|
}
|
||||||
return selector
|
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 selector = `div#${CLASS_OR_ID['AG_EDITOR_ID']}`
|
||||||
|
|
||||||
const children = blocks.map(block => {
|
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)
|
const newVdom = h(selector, children)
|
||||||
@ -149,11 +152,11 @@ class StateRender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only render the blocks which you updated
|
// 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]
|
const cursorOutMostBlock = activeBlocks[activeBlocks.length - 1]
|
||||||
// If cursor is not in render blocks, need to render cursor block independently
|
// If cursor is not in render blocks, need to render cursor block independently
|
||||||
const needRenderCursorBlock = blocks.indexOf(cursorOutMostBlock) === -1
|
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 html = toHTML(newVnode).replace(/^<section>([\s\S]+?)<\/section>$/, '$1')
|
||||||
|
|
||||||
const needToRemoved = []
|
const needToRemoved = []
|
||||||
@ -182,7 +185,7 @@ class StateRender {
|
|||||||
const cursorDom = document.querySelector(`#${key}`)
|
const cursorDom = document.querySelector(`#${key}`)
|
||||||
if (cursorDom) {
|
if (cursorDom) {
|
||||||
const oldCursorVnode = toVNode(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)
|
patch(oldCursorVnode, newCursorVnode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* [renderBlock render one block, no matter it is a container block or text block]
|
* [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
|
const method = block.children.length > 0
|
||||||
? 'renderContainerBlock'
|
? 'renderContainerBlock'
|
||||||
: 'renderLeafBlock'
|
: 'renderLeafBlock'
|
||||||
|
|
||||||
return this[method](block, cursor, activeBlocks, matches, useCache)
|
return this[method](block, cursor, activeBlocks, selectedBlock, matches, useCache)
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ const PRE_BLOCK_HASH = {
|
|||||||
'vega-lite': `.${CLASS_OR_ID['AG_VEGA_LITE']}`
|
'vega-lite': `.${CLASS_OR_ID['AG_VEGA_LITE']}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function renderContainerBlock (block, cursor, activeBlocks, matches, useCache = false) {
|
export default function renderContainerBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
|
||||||
let selector = this.getSelector(block, cursor, activeBlocks)
|
let selector = this.getSelector(block, cursor, activeBlocks, selectedBlock)
|
||||||
const data = {
|
const data = {
|
||||||
attrs: {},
|
attrs: {},
|
||||||
dataset: {}
|
dataset: {}
|
||||||
@ -104,8 +104,8 @@ export default function renderContainerBlock (block, cursor, activeBlocks, match
|
|||||||
selector += PRE_BLOCK_HASH[block.functionType]
|
selector += PRE_BLOCK_HASH[block.functionType]
|
||||||
}
|
}
|
||||||
if (!block.parent) {
|
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 {
|
} 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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,9 +62,9 @@ const hasReferenceToken = tokens => {
|
|||||||
return result
|
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
|
const { loadMathMap } = this
|
||||||
let selector = this.getSelector(block, cursor, activeBlocks)
|
let selector = this.getSelector(block, cursor, activeBlocks, selectedBlock)
|
||||||
// highlight search key in block
|
// highlight search key in block
|
||||||
const highlights = matches.filter(m => m.key === block.key)
|
const highlights = matches.filter(m => m.key === block.key)
|
||||||
const {
|
const {
|
||||||
|
@ -7,13 +7,13 @@
|
|||||||
top: -1000px;
|
top: -1000px;
|
||||||
right: -1000px;
|
right: -1000px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 8px 0 var(--floatShadow);
|
box-shadow: 0 3px 11px 0 var(--floatShadow);
|
||||||
border: 1px solid var(--floatBorderColor);
|
border: 1px solid var(--floatBorderColor);
|
||||||
background-color: var(--floatBgColor);
|
background-color: var(--floatBgColor);
|
||||||
transition: opacity .25s ease-in-out;
|
transition: opacity .25s ease-in-out;
|
||||||
transform-origin: top;
|
transform-origin: top;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
z-index: 1000;
|
z-index: 10000;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,8 +48,8 @@ class FormatPicker extends BaseFloat {
|
|||||||
render () {
|
render () {
|
||||||
const { icons, oldVnode, formatContainer, formats } = this
|
const { icons, oldVnode, formatContainer, formats } = this
|
||||||
const children = icons.map(i => {
|
const children = icons.map(i => {
|
||||||
let icon;
|
let icon
|
||||||
let iconWrapperSelector;
|
let iconWrapperSelector
|
||||||
if (i.icon) {
|
if (i.icon) {
|
||||||
// SVG icon Asset
|
// SVG icon Asset
|
||||||
iconWrapperSelector = 'div.icon-wrapper'
|
iconWrapperSelector = 'div.icon-wrapper'
|
||||||
|
144
src/muya/lib/ui/frontMenu/config.js
Normal file
144
src/muya/lib/ui/frontMenu/config.js
Normal 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 []
|
||||||
|
}
|
||||||
|
}
|
140
src/muya/lib/ui/frontMenu/index.css
Normal file
140
src/muya/lib/ui/frontMenu/index.css
Normal 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;
|
||||||
|
}
|
196
src/muya/lib/ui/frontMenu/index.js
Normal file
196
src/muya/lib/ui/frontMenu/index.js
Normal 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
|
@ -44,148 +44,127 @@ export const quicInsertObj = {
|
|||||||
// title: 'Vega Chart',
|
// title: 'Vega Chart',
|
||||||
// subTitle: 'Render flow chart by vega-lite.js.',
|
// subTitle: 'Render flow chart by vega-lite.js.',
|
||||||
// label: 'vega-lite',
|
// label: 'vega-lite',
|
||||||
// icon: vegaIcon,
|
// icon: vegaIcon
|
||||||
// color: 'rgb(224, 54, 54)'
|
|
||||||
// }, {
|
// }, {
|
||||||
// title: 'Flow Chart',
|
// title: 'Flow Chart',
|
||||||
// subTitle: 'Render flow chart by flowchart.js.',
|
// subTitle: 'Render flow chart by flowchart.js.',
|
||||||
// label: 'flowchart',
|
// label: 'flowchart',
|
||||||
// icon: flowchartIcon,
|
// icon: flowchartIcon
|
||||||
// color: 'rgb(224, 54, 54)'
|
|
||||||
// }, {
|
// }, {
|
||||||
// title: 'Sequence Diagram',
|
// title: 'Sequence Diagram',
|
||||||
// subTitle: 'Render sequence diagram by js-sequence.',
|
// subTitle: 'Render sequence diagram by js-sequence.',
|
||||||
// label: 'sequence',
|
// label: 'sequence',
|
||||||
// icon: sequenceIcon,
|
// icon: sequenceIcon
|
||||||
// color: 'rgb(224, 54, 54)'
|
|
||||||
// }, {
|
// }, {
|
||||||
// title: 'Mermaid',
|
// title: 'Mermaid',
|
||||||
// subTitle: 'Render Diagram by mermaid.',
|
// subTitle: 'Render Diagram by mermaid.',
|
||||||
// label: 'mermaid',
|
// label: 'mermaid',
|
||||||
// icon: mermaidIcon,
|
// icon: mermaidIcon
|
||||||
// color: 'rgb(224, 54, 54)'
|
|
||||||
// }],
|
// }],
|
||||||
'basic block': [{
|
'basic block': [{
|
||||||
title: 'Paragraph',
|
title: 'Paragraph',
|
||||||
subTitle: 'Lorem Ipsum is simply dummy text',
|
subTitle: 'Lorem Ipsum is simply dummy text',
|
||||||
label: 'paragraph',
|
label: 'paragraph',
|
||||||
shortCut: `${COMMAND_KEY}0`,
|
shortCut: `${COMMAND_KEY}0`,
|
||||||
icon: paragraphIcon,
|
icon: paragraphIcon
|
||||||
color: 'rgb(224, 54, 54)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'Horizontal Line',
|
title: 'Horizontal Line',
|
||||||
subTitle: '---',
|
subTitle: '---',
|
||||||
label: 'hr',
|
label: 'hr',
|
||||||
shortCut: `⌥${COMMAND_KEY}-`,
|
shortCut: `⌥${COMMAND_KEY}-`,
|
||||||
icon: hrIcon,
|
icon: hrIcon
|
||||||
color: 'rgb(255, 83, 77)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'Front Matter',
|
title: 'Front Matter',
|
||||||
subTitle: '--- Lorem Ipsum ---',
|
subTitle: '--- Lorem Ipsum ---',
|
||||||
label: 'front-matter',
|
label: 'front-matter',
|
||||||
shortCut: `⌥${COMMAND_KEY}Y`,
|
shortCut: `⌥${COMMAND_KEY}Y`,
|
||||||
icon: frontMatterIcon,
|
icon: frontMatterIcon
|
||||||
color: 'rgb(37, 198, 252)'
|
|
||||||
}],
|
}],
|
||||||
'header': [{
|
'header': [{
|
||||||
title: 'Header 1',
|
title: 'Header 1',
|
||||||
subTitle: '# Lorem Ipsum is simply ...',
|
subTitle: '# Lorem Ipsum is simply ...',
|
||||||
label: 'heading 1',
|
label: 'heading 1',
|
||||||
shortCut: `${COMMAND_KEY}1`,
|
shortCut: `${COMMAND_KEY}1`,
|
||||||
icon: header1Icon,
|
icon: header1Icon
|
||||||
color: 'rgb(86, 163, 108)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'Header 2',
|
title: 'Header 2',
|
||||||
subTitle: '## Lorem Ipsum is simply ...',
|
subTitle: '## Lorem Ipsum is simply ...',
|
||||||
label: 'heading 2',
|
label: 'heading 2',
|
||||||
shortCut: `${COMMAND_KEY}2`,
|
shortCut: `${COMMAND_KEY}2`,
|
||||||
icon: header2Icon,
|
icon: header2Icon
|
||||||
color: 'rgb(94, 133, 121)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'Header 3',
|
title: 'Header 3',
|
||||||
subTitle: '### Lorem Ipsum is simply ...',
|
subTitle: '### Lorem Ipsum is simply ...',
|
||||||
label: 'heading 3',
|
label: 'heading 3',
|
||||||
shortCut: `${COMMAND_KEY}3`,
|
shortCut: `${COMMAND_KEY}3`,
|
||||||
icon: header3Icon,
|
icon: header3Icon
|
||||||
color: 'rgb(119, 195, 79)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'Header 4',
|
title: 'Header 4',
|
||||||
subTitle: '#### Lorem Ipsum is simply ...',
|
subTitle: '#### Lorem Ipsum is simply ...',
|
||||||
label: 'heading 4',
|
label: 'heading 4',
|
||||||
shortCut: `${COMMAND_KEY}4`,
|
shortCut: `${COMMAND_KEY}4`,
|
||||||
icon: header4Icon,
|
icon: header4Icon
|
||||||
color: 'rgb(46, 104, 170)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'Header 5',
|
title: 'Header 5',
|
||||||
subTitle: '##### Lorem Ipsum is simply ...',
|
subTitle: '##### Lorem Ipsum is simply ...',
|
||||||
label: 'heading 5',
|
label: 'heading 5',
|
||||||
shortCut: `${COMMAND_KEY}5`,
|
shortCut: `${COMMAND_KEY}5`,
|
||||||
icon: header5Icon,
|
icon: header5Icon
|
||||||
color: 'rgb(126, 136, 79)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'Header 6',
|
title: 'Header 6',
|
||||||
subTitle: '###### Lorem Ipsum is simply ...',
|
subTitle: '###### Lorem Ipsum is simply ...',
|
||||||
label: 'heading 6',
|
label: 'heading 6',
|
||||||
shortCut: `${COMMAND_KEY}6`,
|
shortCut: `${COMMAND_KEY}6`,
|
||||||
icon: header6Icon,
|
icon: header6Icon
|
||||||
color: 'rgb(29, 176, 184)'
|
|
||||||
}],
|
}],
|
||||||
'advanced block': [{
|
'advanced block': [{
|
||||||
title: 'Table Block',
|
title: 'Table Block',
|
||||||
subTitle: '|Lorem | Ipsum is simply |',
|
subTitle: '|Lorem | Ipsum is simply |',
|
||||||
label: 'table',
|
label: 'table',
|
||||||
shortCut: `${COMMAND_KEY}T`,
|
shortCut: `${COMMAND_KEY}T`,
|
||||||
icon: newTableIcon,
|
icon: newTableIcon
|
||||||
color: 'rgb(13, 23, 64)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'Mathematical Formula',
|
title: 'Math Formula',
|
||||||
subTitle: '$$ Lorem Ipsum is simply $$',
|
subTitle: '$$ Lorem Ipsum is simply $$',
|
||||||
label: 'mathblock',
|
label: 'mathblock',
|
||||||
shortCut: `⌥${COMMAND_KEY}M`,
|
shortCut: `⌥${COMMAND_KEY}M`,
|
||||||
icon: mathblockIcon,
|
icon: mathblockIcon
|
||||||
color: 'rgb(252, 214, 146)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'HTML Block',
|
title: 'HTML Block',
|
||||||
subTitle: '<div> Lorem Ipsum is simply </div>',
|
subTitle: '<div> Lorem Ipsum is simply </div>',
|
||||||
label: 'html',
|
label: 'html',
|
||||||
shortCut: `⌥${COMMAND_KEY}J`,
|
shortCut: `⌥${COMMAND_KEY}J`,
|
||||||
icon: htmlIcon,
|
icon: htmlIcon
|
||||||
color: 'rgb(13, 23, 64)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'Code Block',
|
title: 'Code Block',
|
||||||
subTitle: '```java Lorem Ipsum is simply ```',
|
subTitle: '```java Lorem Ipsum is simply ```',
|
||||||
label: 'pre',
|
label: 'pre',
|
||||||
shortCut: `⌥${COMMAND_KEY}C`,
|
shortCut: `⌥${COMMAND_KEY}C`,
|
||||||
icon: codeIcon,
|
icon: codeIcon
|
||||||
color: 'rgb(164, 159, 147)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'Quote Block',
|
title: 'Quote Block',
|
||||||
subTitle: '>Lorem Ipsum is simply ...',
|
subTitle: '>Lorem Ipsum is simply ...',
|
||||||
label: 'blockquote',
|
label: 'blockquote',
|
||||||
shortCut: `⌥${COMMAND_KEY}Q`,
|
shortCut: `⌥${COMMAND_KEY}Q`,
|
||||||
icon: quoteIcon,
|
icon: quoteIcon
|
||||||
color: 'rgb(31, 111, 181)'
|
|
||||||
}],
|
}],
|
||||||
'list block': [{
|
'list block': [{
|
||||||
title: 'Order List',
|
title: 'Order List',
|
||||||
subTitle: '1. Lorem Ipsum is simply ...',
|
subTitle: '1. Lorem Ipsum is simply ...',
|
||||||
label: 'ol-order',
|
label: 'ol-order',
|
||||||
shortCut: `⌥${COMMAND_KEY}O`,
|
shortCut: `⌥${COMMAND_KEY}O`,
|
||||||
icon: orderListIcon,
|
icon: orderListIcon
|
||||||
color: 'rgb(242, 159, 63)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'Bullet List',
|
title: 'Bullet List',
|
||||||
subTitle: '- Lorem Ipsum is simply ...',
|
subTitle: '- Lorem Ipsum is simply ...',
|
||||||
label: 'ul-bullet',
|
label: 'ul-bullet',
|
||||||
shortCut: `⌥${COMMAND_KEY}U`,
|
shortCut: `⌥${COMMAND_KEY}U`,
|
||||||
icon: bulletListIcon,
|
icon: bulletListIcon
|
||||||
color: 'rgb(242, 117, 63)'
|
|
||||||
}, {
|
}, {
|
||||||
title: 'To-do List',
|
title: 'To-do List',
|
||||||
subTitle: '- [x] Lorem Ipsum is simply ...',
|
subTitle: '- [x] Lorem Ipsum is simply ...',
|
||||||
label: 'ul-task',
|
label: 'ul-task',
|
||||||
shortCut: `⌥${COMMAND_KEY}X`,
|
shortCut: `⌥${COMMAND_KEY}X`,
|
||||||
icon: todoListIcon,
|
icon: todoListIcon
|
||||||
color: 'rgb(222, 140, 124)'
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,7 @@
|
|||||||
import EmojiPicker from 'muya/lib/ui/emojiPicker'
|
import EmojiPicker from 'muya/lib/ui/emojiPicker'
|
||||||
import ImagePathPicker from 'muya/lib/ui/imagePicker'
|
import ImagePathPicker from 'muya/lib/ui/imagePicker'
|
||||||
import FormatPicker from 'muya/lib/ui/formatPicker'
|
import FormatPicker from 'muya/lib/ui/formatPicker'
|
||||||
|
import FrontMenu from 'muya/lib/ui/frontMenu'
|
||||||
import bus from '../../bus'
|
import bus from '../../bus'
|
||||||
import Search from '../search.vue'
|
import Search from '../search.vue'
|
||||||
import { animatedScrollTo } from '../../util'
|
import { animatedScrollTo } from '../../util'
|
||||||
@ -206,6 +207,8 @@
|
|||||||
Muya.use(EmojiPicker)
|
Muya.use(EmojiPicker)
|
||||||
Muya.use(ImagePathPicker)
|
Muya.use(ImagePathPicker)
|
||||||
Muya.use(FormatPicker)
|
Muya.use(FormatPicker)
|
||||||
|
Muya.use(FrontMenu)
|
||||||
|
|
||||||
const { container } = this.editor = new Muya(ele, {
|
const { container } = this.editor = new Muya(ele, {
|
||||||
focusMode,
|
focusMode,
|
||||||
markdown,
|
markdown,
|
||||||
@ -241,6 +244,9 @@
|
|||||||
bus.$on('copyAsMarkdown', this.handleCopyPaste)
|
bus.$on('copyAsMarkdown', this.handleCopyPaste)
|
||||||
bus.$on('copyAsHtml', this.handleCopyPaste)
|
bus.$on('copyAsHtml', this.handleCopyPaste)
|
||||||
bus.$on('pasteAsPlainText', 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('insertParagraph', this.handleInsertParagraph)
|
||||||
bus.$on('editTable', this.handleEditTable)
|
bus.$on('editTable', this.handleEditTable)
|
||||||
bus.$on('scroll-to-header', this.scrollToHeader)
|
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) {
|
handleInlineFormat (type) {
|
||||||
this.editor && this.editor.format(type)
|
this.editor && this.editor.format(type)
|
||||||
},
|
},
|
||||||
@ -503,6 +530,9 @@
|
|||||||
bus.$off('copyAsMarkdown', this.handleCopyPaste)
|
bus.$off('copyAsMarkdown', this.handleCopyPaste)
|
||||||
bus.$off('copyAsHtml', this.handleCopyPaste)
|
bus.$off('copyAsHtml', this.handleCopyPaste)
|
||||||
bus.$off('pasteAsPlainText', 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('insertParagraph', this.handleInsertParagraph)
|
||||||
bus.$off('editTable', this.handleEditTable)
|
bus.$off('editTable', this.handleEditTable)
|
||||||
bus.$off('scroll-to-header', this.scrollToHeader)
|
bus.$off('scroll-to-header', this.scrollToHeader)
|
||||||
|
Loading…
Reference in New Issue
Block a user