marktext/src/renderer/components/editorWithTabs/editor.vue

1102 lines
34 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div
class="editor-wrapper"
:class="[{ 'typewriter': typewriter, 'focus': focus, 'source': sourceCode }]"
:style="{ 'lineHeight': lineHeight, 'fontSize': `${fontSize}px`,
'font-family': editorFontFamily ? `${editorFontFamily}, ${defaultFontFamily}` : `${defaultFontFamily}` }"
:dir="textDirection"
>
<div
ref="editor"
class="editor-component"
></div>
<div
class="image-viewer"
v-show="imageViewerVisible"
>
<span class="icon-close" @click="setImageViewerVisible(false)">
<svg :viewBox="CloseIcon.viewBox">
<use :xlink:href="CloseIcon.url"></use>
</svg>
</span>
<div
ref="imageViewer"
>
</div>
</div>
<el-dialog
:visible.sync="dialogTableVisible"
:show-close="isShowClose"
:modal="true"
custom-class="ag-dialog-table"
width="454px"
center
dir='ltr'
>
<div slot="title" class="dialog-title">
Insert Table
</div>
<el-form :model="tableChecker" :inline="true">
<el-form-item label="Rows">
<el-input-number
ref="rowInput"
size="mini"
v-model="tableChecker.rows"
controls-position="right"
:min="1"
:max="30"
></el-input-number>
</el-form-item>
<el-form-item label="Columns">
<el-input-number
size="mini"
v-model="tableChecker.columns"
controls-position="right"
:min="1"
:max="20"
></el-input-number>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogTableVisible = false">
Cancel
</el-button>
<el-button type="primary" @click="handleDialogTableConfirm">
OK
</el-button>
</div>
</el-dialog>
<search
v-if="!sourceCode"
></search>
</div>
</template>
<script>
import { shell } from 'electron'
import log from 'electron-log'
import { mapState } from 'vuex'
import ViewImage from 'view-image'
import Muya from 'muya/lib'
import TablePicker from 'muya/lib/ui/tablePicker'
import QuickInsert from 'muya/lib/ui/quickInsert'
import CodePicker from 'muya/lib/ui/codePicker'
import EmojiPicker from 'muya/lib/ui/emojiPicker'
import ImagePathPicker from 'muya/lib/ui/imagePicker'
import ImageSelector from 'muya/lib/ui/imageSelector'
import ImageToolbar from 'muya/lib/ui/imageToolbar'
import Transformer from 'muya/lib/ui/transformer'
import FormatPicker from 'muya/lib/ui/formatPicker'
import LinkTools from 'muya/lib/ui/linkTools'
import TableBarTools from 'muya/lib/ui/tableTools'
import FrontMenu from 'muya/lib/ui/frontMenu'
import Search from '../search'
import bus from '@/bus'
import { DEFAULT_EDITOR_FONT_FAMILY } from '@/config'
import { showContextMenu } from '@/contextMenu/editor'
import notice from '@/services/notification'
import Printer from '@/services/printService'
import { offsetToWordCursor, validateLineCursor, SpellChecker } from '@/spellchecker'
import { isOsx, animatedScrollTo } from '@/util'
import { moveImageToFolder, uploadImage } from '@/util/fileSystem'
import { guessClipboardFilePath } from '@/util/clipboard'
import { addCommonStyle, setEditorWidth } from '@/util/theme'
import 'muya/themes/default.css'
import '@/assets/themes/codemirror/one-dark.css'
import 'view-image/lib/imgViewer.css'
import CloseIcon from '@/assets/icons/close.svg'
const STANDAR_Y = 320
export default {
components: {
Search
},
props: {
filename: {
type: String
},
markdown: String,
cursor: Object,
textDirection: {
type: String,
required: true
},
platform: String
},
computed: {
...mapState({
preferences: state => state.preferences,
preferLooseListItem: state => state.preferences.preferLooseListItem,
autoPairBracket: state => state.preferences.autoPairBracket,
autoPairMarkdownSyntax: state => state.preferences.autoPairMarkdownSyntax,
autoPairQuote: state => state.preferences.autoPairQuote,
bulletListMarker: state => state.preferences.bulletListMarker,
orderListDelimiter: state => state.preferences.orderListDelimiter,
tabSize: state => state.preferences.tabSize,
listIndentation: state => state.preferences.listIndentation,
frontmatterType: state => state.preferences.frontmatterType,
lineHeight: state => state.preferences.lineHeight,
fontSize: state => state.preferences.fontSize,
codeFontSize: state => state.preferences.codeFontSize,
codeFontFamily: state => state.preferences.codeFontFamily,
trimUnnecessaryCodeBlockEmptyLines: state => state.preferences.trimUnnecessaryCodeBlockEmptyLines,
editorFontFamily: state => state.preferences.editorFontFamily,
hideQuickInsertHint: state => state.preferences.hideQuickInsertHint,
hideLinkPopup: state => state.preferences.hideLinkPopup,
editorLineWidth: state => state.preferences.editorLineWidth,
imageInsertAction: state => state.preferences.imageInsertAction,
imageFolderPath: state => state.preferences.imageFolderPath,
theme: state => state.preferences.theme,
hideScrollbar: state => state.preferences.hideScrollbar,
spellcheckerEnabled: state => state.preferences.spellcheckerEnabled,
spellcheckerIsHunspell: state => state.preferences.spellcheckerIsHunspell,
spellcheckerNoUnderline: state => state.preferences.spellcheckerNoUnderline,
spellcheckerAutoDetectLanguage: state => state.preferences.spellcheckerAutoDetectLanguage,
spellcheckerLanguage: state => state.preferences.spellcheckerLanguage,
currentFile: state => state.editor.currentFile,
// edit modes
typewriter: state => state.preferences.typewriter,
focus: state => state.preferences.focus,
sourceCode: state => state.preferences.sourceCode
})
},
data () {
this.defaultFontFamily = DEFAULT_EDITOR_FONT_FAMILY
this.CloseIcon = CloseIcon
// Helper to ignore changes when the spell check provider was changed in settings.
this.spellcheckerIgnorChanges = false
return {
selectionChange: null,
editor: null,
pathname: '',
isShowClose: false,
dialogTableVisible: false,
imageViewerVisible: false,
tableChecker: {
rows: 4,
columns: 3
}
}
},
watch: {
typewriter: function (value) {
if (value) {
this.scrollToCursor()
}
},
focus: function (value) {
this.editor.setFocusMode(value)
},
fontSize: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setFont({ fontSize: value })
}
},
lineHeight: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setFont({ lineHeight: value })
}
},
preferLooseListItem: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setOptions({
preferLooseListItem: value
})
}
},
tabSize: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setTabSize(value)
}
},
theme: function (value, oldValue) {
if (value !== oldValue && this.editor) {
// AgreementAny black series theme needs to contain dark `word`.
if (/dark/i.test(value)) {
this.editor.setOptions({
mermaidTheme: 'dark',
vegaTheme: 'dark'
}, true)
} else {
this.editor.setOptions({
mermaidTheme: 'default',
vegaTheme: 'latimes'
}, true)
}
}
},
listIndentation: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setListIndentation(value)
}
},
frontmatterType: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setOptions({ frontmatterType: value })
}
},
hideQuickInsertHint: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setOptions({ hideQuickInsertHint: value })
}
},
editorLineWidth: function (value, oldValue) {
if (value !== oldValue) {
setEditorWidth(value)
}
},
autoPairBracket: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setOptions({ autoPairBracket: value })
}
},
autoPairMarkdownSyntax: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setOptions({ autoPairMarkdownSyntax: value })
}
},
autoPairQuote: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setOptions({ autoPairQuote: value })
}
},
trimUnnecessaryCodeBlockEmptyLines: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setOptions({ trimUnnecessaryCodeBlockEmptyLines: value })
}
},
bulletListMarker: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setOptions({ bulletListMarker: value })
}
},
orderListDelimiter: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setOptions({ orderListDelimiter: value })
}
},
hideLinkPopup: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setOptions({ hideLinkPopup: value })
}
},
codeFontSize: function (value, oldValue) {
if (value !== oldValue) {
addCommonStyle({
codeFontSize: value,
codeFontFamily: this.codeFontFamily,
hideScrollbar: this.hideScrollbar
})
}
},
codeFontFamily: function (value, oldValue) {
if (value !== oldValue) {
addCommonStyle({
codeFontSize: this.codeFontSize,
codeFontFamily: value,
hideScrollbar: this.hideScrollbar
})
}
},
hideScrollbar: function (value, oldValue) {
if (value !== oldValue) {
addCommonStyle({
codeFontSize: this.codeFontSize,
codeFontFamily: this.codeFontFamily,
hideScrollbar: value
})
}
},
spellcheckerEnabled: function (value, oldValue) {
if (value !== oldValue) {
const { editor, spellchecker } = this
const { isInitialized } = spellchecker
// Set Muya's spellcheck container attribute.
editor.setOptions({ spellcheckEnabled: value })
// Spell check is available but not initialized.
if (value && !isInitialized) {
this.initSpellchecker()
return
}
// Enable or disable spell checker.
if (isInitialized) {
if (value) {
this.enableSpellchecker()
} else {
spellchecker.disableSpellchecker()
}
}
}
},
spellcheckerIsHunspell: function (value, oldValue) {
// Special case when the OS supports multiple spell checker because the
// language may be invalid (provider 1 may support language xyz
// but provider 2 not). Otherwise ignore this event.
const multiProviderSupported = isOsx
if (multiProviderSupported && value !== oldValue) {
const { spellchecker } = this
const { isHunspell } = spellchecker
if (value === isHunspell) {
this.spellcheckerIgnorChanges = false
// Apply language from settings that may have changed.
const { spellcheckerLanguage } = this
const { isEnabled, lang } = spellchecker
if (isEnabled && spellcheckerLanguage !== lang) {
this.switchSpellcheckLanguage(spellcheckerLanguage)
}
} else {
// Ignore all settings language changes that occur when another
// spell check provider is selected.
this.spellcheckerIgnorChanges = true
}
}
},
spellcheckerNoUnderline: function (value, oldValue) {
if (value !== oldValue) {
const { editor, spellchecker } = this
// Set Muya's spellcheck container attribute.
editor.setOptions({ spellcheckEnabled: !value })
const { isEnabled } = spellchecker
if (isEnabled) {
spellchecker.isPassiveMode = value
}
}
},
spellcheckerAutoDetectLanguage: function (value, oldValue) {
const { spellchecker } = this
const { isEnabled } = spellchecker
if (value !== oldValue && isEnabled) {
spellchecker.automaticallyIdentifyLanguages = value
}
},
spellcheckerLanguage: function (value, oldValue) {
const { spellchecker, spellcheckerIgnorChanges } = this
if (!spellcheckerIgnorChanges && value !== oldValue) {
const { isEnabled, isInvalidState } = spellchecker
if (isEnabled) {
this.switchSpellcheckLanguage(value)
} else if (isInvalidState) {
// Spell checker is in an invalid state due to a missing dictionary and
// therefore deactivated. We can safely enable the spell checker again
// with the new language.
this.enableSpellchecker()
}
}
},
currentFile: function (value, oldValue) {
if (value && value !== oldValue) {
this.scrollToCursor(0)
// Hide float tools if needed.
this.editor && this.editor.hideAllFloatTools()
}
},
sourceCode: function (value, oldValue) {
if (value && value !== oldValue) {
this.editor && this.editor.hideAllFloatTools()
}
}
},
created () {
this.$nextTick(() => {
this.printer = new Printer()
const ele = this.$refs.editor
const {
focus: focusMode,
markdown,
preferLooseListItem,
typewriter,
autoPairBracket,
autoPairMarkdownSyntax,
autoPairQuote,
trimUnnecessaryCodeBlockEmptyLines,
bulletListMarker,
orderListDelimiter,
tabSize,
listIndentation,
frontmatterType,
hideQuickInsertHint,
editorLineWidth,
theme,
spellcheckerEnabled,
hideLinkPopup
} = this
// use muya UI plugins
Muya.use(TablePicker)
Muya.use(QuickInsert)
Muya.use(CodePicker)
Muya.use(EmojiPicker)
Muya.use(ImagePathPicker)
Muya.use(ImageSelector, {
applicationId: process.env.UNSPLASH_ACCESS_KEY,
secret: process.env.UNSPLASH_SECRET_KEY,
photoCreatorClick: this.photoCreatorClick
})
Muya.use(Transformer)
Muya.use(ImageToolbar)
Muya.use(FormatPicker)
Muya.use(FrontMenu)
Muya.use(LinkTools, {
jumpClick: this.jumpClick
})
Muya.use(TableBarTools)
const options = {
focusMode,
markdown,
preferLooseListItem,
autoPairBracket,
autoPairMarkdownSyntax,
trimUnnecessaryCodeBlockEmptyLines,
autoPairQuote,
bulletListMarker,
orderListDelimiter,
tabSize,
listIndentation,
frontmatterType,
hideQuickInsertHint,
hideLinkPopup,
spellcheckEnabled: spellcheckerEnabled,
imageAction: this.imageAction.bind(this),
imagePathPicker: this.imagePathPicker.bind(this),
clipboardFilePath: guessClipboardFilePath,
imagePathAutoComplete: this.imagePathAutoComplete.bind(this)
}
if (/dark/i.test(theme)) {
Object.assign(options, {
mermaidTheme: 'dark',
vegaTheme: 'dark'
})
} else {
Object.assign(options, {
mermaidTheme: 'default',
vegaTheme: 'latimes'
})
}
const { container } = this.editor = new Muya(ele, options)
// Create spell check wrapper and enable spell checking if prefered.
this.spellchecker = new SpellChecker(spellcheckerEnabled)
if (spellcheckerEnabled) {
this.initSpellchecker()
}
if (typewriter) {
this.scrollToCursor()
}
// listen for bus events.
bus.$on('file-loaded', this.setMarkdownToEditor)
bus.$on('undo', this.handleUndo)
bus.$on('redo', this.handleRedo)
bus.$on('selectAll', this.handleSelectAll)
bus.$on('export', this.handleExport)
bus.$on('print-service-clearup', this.handlePrintServiceClearup)
bus.$on('paragraph', this.handleEditParagraph)
bus.$on('format', this.handleInlineFormat)
bus.$on('searchValue', this.handleSearch)
bus.$on('replaceValue', this.handReplace)
bus.$on('find-action', this.handleFindAction)
bus.$on('insert-image', this.insertImage)
bus.$on('image-uploaded', this.handleUploadedImage)
bus.$on('file-changed', this.handleFileChange)
bus.$on('editor-blur', this.blurEditor)
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('scroll-to-header', this.scrollToHeader)
bus.$on('print', this.handlePrint)
bus.$on('screenshot-captured', this.handleScreenShot)
bus.$on('switch-spellchecker-language', this.switchSpellcheckLanguage)
this.editor.on('change', changes => {
// WORKAROUND: "id: 'muya'"
this.$store.dispatch('LISTEN_FOR_CONTENT_CHANGE', Object.assign(changes, { id: 'muya' }))
})
this.editor.on('format-click', ({ event, formatType, data }) => {
const ctrlOrMeta = (isOsx && event.metaKey) || (!isOsx && event.ctrlKey)
if (formatType === 'link' && ctrlOrMeta) {
this.$store.dispatch('FORMAT_LINK_CLICK', { data, dirname: window.DIRNAME })
} else if (formatType === 'image' && ctrlOrMeta) {
if (this.imageViewer) {
this.imageViewer.destroy()
}
this.imageViewer = new ViewImage(this.$refs.imageViewer, {
url: data,
snapView: true
})
this.setImageViewerVisible(true)
}
})
this.editor.on('preview-image', ({ data }) => {
if (this.imageViewer) {
this.imageViewer.destroy()
}
this.imageViewer = new ViewImage(this.$refs.imageViewer, {
url: data,
snapView: true
})
this.setImageViewerVisible(true)
})
this.editor.on('selectionChange', changes => {
const { y } = changes.cursorCoords
if (this.typewriter) {
animatedScrollTo(container, container.scrollTop + y - STANDAR_Y, 100)
}
this.selectionChange = changes
this.$store.dispatch('SELECTION_CHANGE', changes)
})
this.editor.on('selectionFormats', formats => {
this.$store.dispatch('SELECTION_FORMATS', formats)
})
this.editor.on('contextmenu', (event, selection) => {
const { isEnabled } = this.spellchecker
// NOTE: Right clicking on a misspelled word select the whole word
// by Chromium.
if (isEnabled && validateLineCursor(selection)) {
const { start: startCursor } = selection
const { offset: lineOffset } = startCursor
const { text } = startCursor.block
const wordInfo = SpellChecker.extractWord(text, lineOffset)
if (wordInfo) {
const { left, right, word } = wordInfo
// Translate offsets into a cursor with the given line.
const wordRange = offsetToWordCursor(selection, left, right)
this.spellchecker.getWordSuggestion(word)
.then(wordSuggestions => {
const replaceCallback = replacement => {
// wordRange := replace this range with the replacement
this.editor.replaceWordInline(selection, wordRange, replacement, true)
}
showContextMenu(event, selection, this.spellchecker, word, wordSuggestions, replaceCallback)
})
return
}
}
// No word selected or fallback
showContextMenu(event, selection, isEnabled ? this.spellchecker : null, '', [], null)
})
document.addEventListener('keyup', this.keyup)
setEditorWidth(editorLineWidth)
})
},
methods: {
photoCreatorClick: (url) => {
shell.openExternal(url)
},
jumpClick: (linkInfo) => {
const { href } = linkInfo
if (href && href.startsWith('http')) {
shell.openExternal(href)
}
},
async imagePathAutoComplete (src) {
const files = await this.$store.dispatch('ASK_FOR_IMAGE_AUTO_PATH', src)
return files.map(f => {
const iconClass = f.type === 'directory' ? 'icon-folder' : 'icon-image'
return Object.assign(f, { iconClass, text: f.file + (f.type === 'directory' ? '/' : '') })
})
},
async imageAction (image, id, alt = '') {
const { imageInsertAction, imageFolderPath, preferences } = this
const { pathname } = this.currentFile
let result
switch (imageInsertAction) {
case 'upload': {
try {
result = await uploadImage(pathname, image, preferences)
} catch (err) {
notice.notify({
title: 'Upload Image',
type: 'info',
message: err
})
result = await moveImageToFolder(pathname, image, imageFolderPath)
}
break
}
case 'folder': {
result = await moveImageToFolder(pathname, image, imageFolderPath)
break
}
case 'path': {
if (typeof image === 'string') {
result = image
} else {
// Move image to image folder if it's Blob object.
result = await moveImageToFolder(pathname, image, imageFolderPath)
}
break
}
}
if (id && this.sourceCode) {
bus.$emit('image-action', {
id,
result,
alt
})
}
return result
},
imagePathPicker () {
return this.$store.dispatch('ASK_FOR_IMAGE_PATH')
},
keyup (event) {
if (event.key === 'Escape') {
this.setImageViewerVisible(false)
}
},
setImageViewerVisible (status) {
this.imageViewerVisible = status
},
// Helper methods for spell checker that are needed multiple times.
initSpellchecker () {
const {
editor,
spellchecker,
spellcheckerNoUnderline,
spellcheckerAutoDetectLanguage,
spellcheckerLanguage
} = this
const { container } = editor
spellchecker.init(
spellcheckerLanguage,
spellcheckerAutoDetectLanguage,
spellcheckerNoUnderline,
container
)
.catch(error => {
log.error(`Error while initializing spell checker for language "${spellcheckerLanguage}":`)
log.error(error)
notice.notify({
title: 'Spelling',
type: 'error',
message: `Error while initializing spell checker for language "${spellcheckerLanguage}": ${error.message}`
})
})
},
enableSpellchecker () {
const {
spellchecker,
spellcheckerNoUnderline,
spellcheckerAutoDetectLanguage,
spellcheckerLanguage
} = this
spellchecker.enableSpellchecker(
spellcheckerLanguage,
spellcheckerAutoDetectLanguage,
spellcheckerNoUnderline
)
.then(status => {
if (!status) {
// Unable to switch language due to missing dictionary. The spell checker is now in an invalid state.
notice.notify({
title: 'Spelling',
type: 'warning',
message: `Unable to switch to language "${spellcheckerLanguage}". Spell checker is now disabled.`
})
}
})
.catch(error => {
log.error(`Error while enabling spell checking for "${spellcheckerLanguage}":`)
log.error(error)
notice.notify({
title: 'Spelling',
type: 'error',
message: `Error while enabling spell checking for "${spellcheckerLanguage}": ${error.message}`
})
})
},
switchSpellcheckLanguage (languageCode) {
const { spellchecker } = this
const { isEnabled } = spellchecker
// This method is also called from bus, so validate state before continuing.
if (!isEnabled) {
throw new Error('Cannot switch switch because spell checker is disabled!')
}
spellchecker.switchLanguage(languageCode)
.then(langCode => {
if (!langCode) {
// Unable to switch language due to missing dictionary. The spell checker is now in an invalid state.
notice.notify({
title: 'Spelling',
type: 'warning',
message: `Unable to switch to language "${languageCode}". Spell checker is now disabled.`
})
} else if (langCode !== languageCode) {
// Unable to switch language but fallback was successful.
notice.notify({
title: 'Spelling',
type: 'warning',
message: `Current spelling language is "${langCode}" because "${languageCode}" dictionary is missing.`
})
}
})
.catch(error => {
log.error(`Error while switching to language "${languageCode}":`)
log.error(error)
notice.notify({
title: 'Spelling',
type: 'error',
message: `Error while switching to "${languageCode}": ${error.message}`
})
})
},
handleUndo () {
if (this.editor) {
this.editor.undo()
}
},
handleRedo () {
if (this.editor) {
this.editor.redo()
}
},
handleSelectAll () {
if (this.editor && !this.sourceCode) {
this.editor.selectAll()
}
},
// Custom copyAsMarkdown copyAsHtml pasteAsPlainText
handleCopyPaste (type) {
if (this.editor) {
this.editor[type]()
}
},
insertImage (src) {
if (!this.sourceCode) {
this.editor && this.editor.insertImage({ src })
}
},
handleSearch (value, opt) {
const searchMatches = this.editor.search(value, opt)
this.$store.dispatch('SEARCH', searchMatches)
this.scrollToHighlight()
},
handReplace (value, opt) {
const searchMatches = this.editor.replace(value, opt)
this.$store.dispatch('SEARCH', searchMatches)
},
handleUploadedImage (url, deletionUrl) {
this.insertImage(url)
this.$store.dispatch('SHOW_IMAGE_DELETION_URL', deletionUrl)
},
scrollToCursor (duration = 300) {
this.$nextTick(() => {
const { container } = this.editor
const { y } = this.editor.getSelection().cursorCoords
animatedScrollTo(container, container.scrollTop + y - STANDAR_Y, duration)
})
},
scrollToHighlight () {
return this.scrollToElement('.ag-highlight')
},
scrollToHeader (slug) {
return this.scrollToElement(`#${slug}`)
},
scrollToElement (selector) {
// Scroll to search highlight word
const { container } = this.editor
const anchor = document.querySelector(selector)
if (anchor) {
const { y } = anchor.getBoundingClientRect()
const DURATION = 300
animatedScrollTo(container, container.scrollTop + y - STANDAR_Y, DURATION)
}
},
handleFindAction (action) {
const searchMatches = this.editor.find(action)
this.$store.dispatch('SEARCH', searchMatches)
this.scrollToHighlight()
},
async handlePrint () {
// generate styled HTML with empty title and optimized for printing
const html = await this.editor.exportStyledHTML('', true)
this.printer.renderMarkdown(html, true)
this.$store.dispatch('PRINT_RESPONSE')
},
async handleExport (type) {
const markdown = this.editor.getMarkdown()
switch (type) {
case 'styledHtml': {
const content = await this.editor.exportStyledHTML(this.filename)
this.$store.dispatch('EXPORT', { type, content, markdown })
break
}
case 'pdf': {
// generate styled HTML with empty title and optimized for printing
const html = await this.editor.exportStyledHTML('', true)
this.printer.renderMarkdown(html, true)
this.$store.dispatch('EXPORT', { type, markdown })
break
}
}
},
handlePrintServiceClearup () {
this.printer.clearup()
},
handleEditParagraph (type) {
if (type === 'table') {
this.tableChecker = { rows: 4, columns: 3 }
this.dialogTableVisible = true
this.$nextTick(() => {
this.$refs.rowInput.focus()
})
} else {
this.editor && this.editor.updateParagraph(type)
}
},
// 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}`)
}
}
},
handleInlineFormat (type) {
this.editor && this.editor.format(type)
},
handleDialogTableConfirm () {
this.dialogTableVisible = false
this.editor && this.editor.createTable(this.tableChecker)
},
// listen for `open-single-file` event, it will call this method only when open a new file.
setMarkdownToEditor ({ id, markdown, cursor }) {
const { editor } = this
if (editor) {
editor.clearHistory()
if (cursor) {
editor.setMarkdown(markdown, cursor, true)
} else {
editor.setMarkdown(markdown)
}
}
},
// listen for markdown change form source mode or change tabs etc
handleFileChange ({ id, markdown, cursor, renderCursor, history }) {
const { editor } = this
this.$nextTick(() => {
if (editor) {
if (history) {
editor.setHistory(history)
}
if (typeof markdown === 'string') {
editor.setMarkdown(markdown, cursor, renderCursor)
} else if (cursor) {
editor.setCursor(cursor)
}
if (renderCursor) {
this.scrollToCursor(0)
}
}
})
},
handleInsertParagraph (location) {
const { editor } = this
editor && editor.insertParagraph(location)
},
blurEditor () {
this.editor.blur()
},
handleScreenShot () {
if (this.editor) {
document.execCommand('paste')
}
}
},
beforeDestroy () {
bus.$off('file-loaded', this.setMarkdownToEditor)
bus.$off('undo', this.handleUndo)
bus.$off('redo', this.handleRedo)
bus.$off('selectAll', this.handleSelectAll)
bus.$off('export', this.handleExport)
bus.$off('print-service-clearup', this.handlePrintServiceClearup)
bus.$off('paragraph', this.handleEditParagraph)
bus.$off('format', this.handleInlineFormat)
bus.$off('searchValue', this.handleSearch)
bus.$off('replaceValue', this.handReplace)
bus.$off('find-action', this.handleFindAction)
bus.$off('insert-image', this.insertImage)
bus.$off('image-uploaded', this.handleUploadedImage)
bus.$off('file-changed', this.handleFileChange)
bus.$off('editor-blur', this.blurEditor)
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('scroll-to-header', this.scrollToHeader)
bus.$off('print', this.handlePrint)
bus.$off('screenshot-captured', this.handleScreenShot)
bus.$off('switch-spellchecker-language', this.switchSpellcheckLanguage)
document.removeEventListener('keyup', this.keyup)
this.editor.destroy()
this.editor = null
}
}
</script>
<style>
.editor-wrapper {
height: 100%;
position: relative;
flex: 1;
color: var(--editorColor);
& .ag-dialog-table {
& .el-button {
font-size: 13px;
width: 70px;
}
}
}
.editor-wrapper.source {
position: absolute;
z-index: -1;
top: 0;
left: 0;
overflow: hidden;
}
.editor-component {
height: 100%;
overflow: auto;
box-sizing: border-box;
}
.typewriter .editor-component {
padding-top: calc(50vh - 136px);
padding-bottom: calc(50vh - 54px);
}
.image-viewer {
position: fixed;
backdrop-filter: blur(5px);
top: 0;
right: 0;
left: 0;
bottom: 0;
background: rgba(0, 0, 0, .8);
z-index: 11;
& .icon-close {
z-index: 1000;
width: 30px;
height: 30px;
position: absolute;
top: 50px;
left: 50px;
display: block;
& svg {
fill: #efefef;
width: 100%;
height: 100%;
}
}
}
.iv-container {
width: 100%;
height: 100%;
}
.iv-snap-view {
opacity: 1;
bottom: 20px;
right: 20px;
top: auto;
left: auto;
}
</style>