mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 04:51:28 +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`)
|
||||
- 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**
|
||||
|
||||
|
@ -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?
|
||||
|
@ -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 |
|
||||
|
@ -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'),
|
||||
|
@ -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'],
|
||||
|
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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}` // 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}` // 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')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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'
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
|
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',
|
||||
// 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
|
||||
}]
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user