|
|
<template lang='pug'> .editor-markdown v-toolbar.editor-markdown-toolbar(dense, color='primary', dark, flat, style='overflow-x: hidden;') template(v-if='isModalShown') v-spacer v-btn.animated.fadeInRight(text, @click='closeAllModal') v-icon(left) mdi-arrow-left-circle span {{$t('editor:backToEditor')}} template(v-else) v-tooltip(bottom, color='primary') template(v-slot:activator='{ on }') v-btn.animated.fadeIn(icon, tile, v-on='on', @click='toggleMarkup({ start: `**` })').mx-0 v-icon mdi-format-bold span {{$t('editor:markup.bold')}} v-tooltip(bottom, color='primary') template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p1s(icon, tile, v-on='on', @click='toggleMarkup({ start: `*` })').mx-0 v-icon mdi-format-italic span {{$t('editor:markup.italic')}} v-tooltip(bottom, color='primary') template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p2s(icon, tile, v-on='on', @click='toggleMarkup({ start: `~~` })').mx-0 v-icon mdi-format-strikethrough span {{$t('editor:markup.strikethrough')}} v-menu(offset-y, open-on-hover) template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p3s(icon, tile, v-on='on').mx-0 v-icon mdi-format-header-pound v-list.py-0 template(v-for='(n, idx) in 6') v-list-item(@click='setHeaderLine(n)', :key='idx') v-list-item-action v-icon(:size='24 - (idx - 1) * 2') mdi-format-header-{{n}} v-list-item-title {{$t('editor:markup.heading', { level: n })}} v-divider(v-if='idx < 5') v-tooltip(bottom, color='primary') template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p4s(icon, tile, v-on='on', @click='toggleMarkup({ start: `~` })').mx-0 v-icon mdi-format-subscript span {{$t('editor:markup.subscript')}} v-tooltip(bottom, color='primary') template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p5s(icon, tile, v-on='on', @click='toggleMarkup({ start: `^` })').mx-0 v-icon mdi-format-superscript span {{$t('editor:markup.superscript')}} v-menu(offset-y, open-on-hover) template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p6s(icon, tile, v-on='on').mx-0 v-icon mdi-alpha-t-box-outline v-list.py-0 v-list-item(@click='insertBeforeEachLine({ content: `> `})') v-list-item-action v-icon mdi-alpha-t-box-outline v-list-item-title {{$t('editor:markup.blockquote')}} v-divider v-list-item(@click='insertBeforeEachLine({ content: `> `, after: `{.is-info}`})') v-list-item-action v-icon(color='blue') mdi-alpha-i-box-outline v-list-item-title {{$t('editor:markup.blockquoteInfo')}} v-divider v-list-item(@click='insertBeforeEachLine({ content: `> `, after: `{.is-success}`})') v-list-item-action v-icon(color='success') mdi-alpha-s-box-outline v-list-item-title {{$t('editor:markup.blockquoteSuccess')}} v-divider v-list-item(@click='insertBeforeEachLine({ content: `> `, after: `{.is-warning}`})') v-list-item-action v-icon(color='warning') mdi-alpha-w-box-outline v-list-item-title {{$t('editor:markup.blockquoteWarning')}} v-divider v-list-item(@click='insertBeforeEachLine({ content: `> `, after: `{.is-danger}`})') v-list-item-action v-icon(color='error') mdi-alpha-e-box-outline v-list-item-title {{$t('editor:markup.blockquoteError')}} v-divider v-tooltip(bottom, color='primary') template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p7s(icon, tile, v-on='on', @click='insertBeforeEachLine({ content: `- `})').mx-0 v-icon mdi-format-list-bulleted span {{$t('editor:markup.unorderedList')}} v-tooltip(bottom, color='primary') template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p8s(icon, tile, v-on='on', @click='insertBeforeEachLine({ content: `1. `})').mx-0 v-icon mdi-format-list-numbered span {{$t('editor:markup.orderedList')}} v-tooltip(bottom, color='primary') template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p9s(icon, tile, v-on='on', @click='toggleMarkup({ start: "`" })').mx-0 v-icon mdi-code-tags span {{$t('editor:markup.inlineCode')}} v-tooltip(bottom, color='primary') template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p10s(icon, tile, v-on='on', @click='toggleMarkup({ start: `<kbd>`, end: `</kbd>` })').mx-0 v-icon mdi-keyboard-variant span {{$t('editor:markup.keyboardKey')}} v-tooltip(bottom, color='primary') template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p11s(icon, tile, v-on='on', @click='insertAfter({ content: `---`, newLine: true })').mx-0 v-icon mdi-minus span {{$t('editor:markup.horizontalBar')}} template(v-if='$vuetify.breakpoint.mdAndUp') v-spacer v-tooltip(bottom, color='primary', v-if='previewShown') template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p1s(icon, tile, v-on='on', @click='spellModeActive = !spellModeActive').mx-0 v-icon(:color='spellModeActive ? `amber` : `white`') mdi-spellcheck span {{$t('editor:markup.toggleSpellcheck')}} v-tooltip(bottom, color='primary') template(v-slot:activator='{ on }') v-btn.animated.fadeIn.wait-p2s(icon, tile, v-on='on', @click='previewShown = !previewShown').mx-0 v-icon mdi-book-open-outline span {{$t('editor:markup.togglePreviewPane')}} .editor-markdown-main .editor-markdown-sidebar v-tooltip(right, color='teal') template(v-slot:activator='{ on }') v-btn.animated.fadeInLeft(icon, tile, v-on='on', dark, @click='insertLink').mx-0 v-icon mdi-link-plus span {{$t('editor:markup.insertLink')}} v-tooltip(right, color='teal') template(v-slot:activator='{ on }') v-btn.mt-3.animated.fadeInLeft.wait-p1s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalMedia`)').mx-0 v-icon(:color='activeModal === `editorModalMedia` ? `teal` : ``') mdi-folder-multiple-image span {{$t('editor:markup.insertAssets')}} v-tooltip(right, color='teal') template(v-slot:activator='{ on }') v-btn.mt-3.animated.fadeInLeft.wait-p2s(icon, tile, v-on='on', dark, disabled, @click='toggleModal(`editorModalBlocks`)').mx-0 v-icon(:color='activeModal === `editorModalBlocks` ? `teal` : ``') mdi-view-dashboard-outline span {{$t('editor:markup.insertBlock')}} v-tooltip(right, color='teal') template(v-slot:activator='{ on }') v-btn.mt-3.animated.fadeInLeft.wait-p3s(icon, tile, v-on='on', dark, disabled).mx-0 v-icon mdi-code-braces span {{$t('editor:markup.insertCodeBlock')}} v-tooltip(right, color='teal') template(v-slot:activator='{ on }') v-btn.mt-3.animated.fadeInLeft.wait-p4s(icon, tile, v-on='on', dark, disabled).mx-0 v-icon mdi-movie span {{$t('editor:markup.insertVideoAudio')}} v-tooltip(right, color='teal') template(v-slot:activator='{ on }') v-btn.mt-3.animated.fadeInLeft.wait-p5s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalDrawio`)').mx-0 v-icon mdi-chart-multiline span {{$t('editor:markup.insertDiagram')}} v-tooltip(right, color='teal') template(v-slot:activator='{ on }') v-btn.mt-3.animated.fadeInLeft.wait-p6s(icon, tile, v-on='on', dark, disabled).mx-0 v-icon mdi-function-variant span {{$t('editor:markup.insertMathExpression')}} v-tooltip(right, color='teal') template(v-slot:activator='{ on }') v-btn.mt-3.animated.fadeInLeft.wait-p7s(icon, tile, v-on='on', dark, disabled).mx-0 v-icon mdi-table-plus span {{$t('editor:markup.tableHelper')}} template(v-if='$vuetify.breakpoint.mdAndUp') v-spacer v-tooltip(right, color='teal') template(v-slot:activator='{ on }') v-btn.mt-3.animated.fadeInLeft.wait-p8s(icon, tile, v-on='on', dark, @click='toggleFullscreen').mx-0 v-icon mdi-arrow-expand-all span {{$t('editor:markup.distractionFreeMode')}} v-tooltip(right, color='teal') template(v-slot:activator='{ on }') v-btn.mt-3.animated.fadeInLeft.wait-p9s(icon, tile, v-on='on', dark, @click='toggleHelp').mx-0 v-icon(:color='helpShown ? `teal` : ``') mdi-help-circle span {{$t('editor:markup.markdownFormattingHelp')}} .editor-markdown-editor textarea(ref='cm') transition(name='editor-markdown-preview') .editor-markdown-preview(v-if='previewShown') .editor-markdown-preview-content.contents(ref='editorPreviewContainer') div( ref='editorPreview' v-html='previewHTML' :spellcheck='spellModeActive' :contenteditable='spellModeActive' @blur='spellModeActive = false' )
v-system-bar.editor-markdown-sysbar(dark, status, color='grey darken-3') .caption.editor-markdown-sysbar-locale {{locale.toUpperCase()}} .caption.px-3 /{{path}} template(v-if='$vuetify.breakpoint.mdAndUp') v-spacer .caption Markdown v-spacer .caption Ln {{cursorPos.line + 1}}, Col {{cursorPos.ch + 1}}
markdown-help(v-if='helpShown') page-selector(mode='select', v-model='insertLinkDialog', :open-handler='insertLinkHandler', :path='path', :locale='locale') </template>
<script> import _ from 'lodash' import { get, sync } from 'vuex-pathify' import markdownHelp from './markdown/help.vue' import gql from 'graphql-tag' import DOMPurify from 'dompurify'
/* global siteConfig, siteLangs */
// ========================================
// IMPORTS
// ========================================
// Code Mirror
import CodeMirror from 'codemirror' import 'codemirror/lib/codemirror.css'
// Language
import 'codemirror/mode/markdown/markdown.js'
// Addons
import 'codemirror/addon/selection/active-line.js' import 'codemirror/addon/display/fullscreen.js' import 'codemirror/addon/display/fullscreen.css' import 'codemirror/addon/selection/mark-selection.js' import 'codemirror/addon/search/searchcursor.js' import 'codemirror/addon/hint/show-hint.js' import 'codemirror/addon/fold/foldcode.js' import 'codemirror/addon/fold/foldgutter.js' import 'codemirror/addon/fold/foldgutter.css' import './markdown/fold'
// Markdown-it
import MarkdownIt from 'markdown-it' import mdAttrs from 'markdown-it-attrs' import mdEmoji from 'markdown-it-emoji' import mdTaskLists from 'markdown-it-task-lists' import mdExpandTabs from 'markdown-it-expand-tabs' import mdAbbr from 'markdown-it-abbr' import mdSup from 'markdown-it-sup' import mdSub from 'markdown-it-sub' import mdMark from 'markdown-it-mark' import mdMultiTable from 'markdown-it-multimd-table' import mdFootnote from 'markdown-it-footnote' import mdImsize from 'markdown-it-imsize' import katex from 'katex' import underline from '../../libs/markdown-it-underline' import 'katex/dist/contrib/mhchem' import twemoji from 'twemoji' import plantuml from './markdown/plantuml'
// Prism (Syntax Highlighting)
import Prism from 'prismjs'
// Mermaid
import mermaid from 'mermaid'
// Helpers
import katexHelper from './common/katex' import tabsetHelper from './markdown/tabset'
// ========================================
// INIT
// ========================================
// Platform detection
const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'
// Prism Config
Prism.plugins.autoloader.languages_path = '/_assets/js/prism/' Prism.plugins.NormalizeWhitespace.setDefaults({ 'remove-trailing': true, 'remove-indent': true, 'left-trim': true, 'right-trim': true, 'remove-initial-line-feed': true, 'tabs-to-spaces': 2 })
// Markdown Instance
const md = new MarkdownIt({ html: true, breaks: true, linkify: true, typography: true, highlight(str, lang) { if (lang === 'diagram') { return `<pre class="diagram">` + Buffer.from(str, 'base64').toString() + `</pre>` } else if (['mermaid', 'plantuml'].includes(lang)) { return `<pre class="codeblock-${lang}"><code>${_.escape(str)}</code></pre>` } else { return `<pre class="line-numbers"><code class="language-${lang}">${_.escape(str)}</code></pre>` } } }) .use(mdAttrs, { allowedAttributes: ['id', 'class', 'target'] }) .use(underline) .use(mdEmoji) .use(mdTaskLists, {label: true, labelAfter: true}) .use(mdExpandTabs) .use(mdAbbr) .use(mdSup) .use(mdSub) .use(mdMultiTable, {multiline: true, rowspan: true, headerless: true}) .use(mdMark) .use(mdFootnote) .use(mdImsize)
// DOMPurify fix for draw.io
DOMPurify.addHook('uponSanitizeElement', (elm) => { if (elm.querySelectorAll) { const breaks = elm.querySelectorAll('foreignObject br, foreignObject p') if (breaks && breaks.length) { for (let i = 0; i < breaks.length; i++) { breaks[i].parentNode.replaceChild( document.createElement('div'), breaks[i] ) } } } })
// ========================================
// HELPER FUNCTIONS
// ========================================
// Inject line numbers for preview scroll sync
let linesMap = [] function injectLineNumbers (tokens, idx, options, env, slf) { let line if (tokens[idx].map && tokens[idx].level === 0) { line = tokens[idx].map[0] tokens[idx].attrJoin('class', 'line') tokens[idx].attrSet('data-line', String(line)) linesMap.push(line) } return slf.renderToken(tokens, idx, options, env, slf) } md.renderer.rules.paragraph_open = injectLineNumbers md.renderer.rules.heading_open = injectLineNumbers md.renderer.rules.blockquote_open = injectLineNumbers
// ========================================
// PLANTUML
// ========================================
// TODO: Use same options as defined in backend
plantuml.init(md, {})
// ========================================
// KATEX
// ========================================
md.inline.ruler.after('escape', 'katex_inline', katexHelper.katexInline) md.renderer.rules.katex_inline = (tokens, idx) => { try { return katex.renderToString(tokens[idx].content, { displayMode: false }) } catch (err) { console.warn(err) return tokens[idx].content } } md.block.ruler.after('blockquote', 'katex_block', katexHelper.katexBlock, { alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] }) md.renderer.rules.katex_block = (tokens, idx) => { try { return `<p>` + katex.renderToString(tokens[idx].content, { displayMode: true }) + `</p>` } catch (err) { console.warn(err) return tokens[idx].content } }
// ========================================
// TWEMOJI
// ========================================
md.renderer.rules.emoji = (token, idx) => { return twemoji.parse(token[idx].content, { callback (icon, opts) { return `/_assets/svg/twemoji/${icon}.svg` } }) }
// ========================================
// Vue Component
// ========================================
let mermaidId = 0
export default { components: { markdownHelp }, props: { save: { type: Function, default: () => {} } }, data() { return { fabInsertMenu: false, cm: null, cursorPos: { ch: 0, line: 1 }, previewShown: true, previewHTML: '', helpShown: false, spellModeActive: false, insertLinkDialog: false } }, computed: { isMobile() { return this.$vuetify.breakpoint.smAndDown }, isModalShown() { return this.helpShown || this.activeModal !== '' }, locale: get('page/locale'), path: get('page/path'), mode: get('editor/mode'), activeModal: sync('editor/activeModal') }, watch: { previewShown (newValue, oldValue) { if (newValue && !oldValue) { this.$nextTick(() => { this.renderMermaidDiagrams() Prism.highlightAllUnder(this.$refs.editorPreview) Array.from(this.$refs.editorPreview.querySelectorAll('pre.line-numbers')).forEach(pre => pre.classList.add('prismjs')) }) } }, spellModeActive (newValue, oldValue) { if (newValue) { this.$nextTick(() => { this.$refs.editorPreview.focus() }) } } }, methods: { toggleModal(key) { this.activeModal = (this.activeModal === key) ? '' : key this.helpShown = false }, closeAllModal() { this.activeModal = '' this.helpShown = false }, onCmInput: _.debounce(function (newContent) { this.processContent(newContent) }, 600), onCmPaste (cm, ev) { // const clipItems = (ev.clipboardData || ev.originalEvent.clipboardData).items
// for (let clipItem of clipItems) {
// if (_.startsWith(clipItem.type, 'image/')) {
// const file = clipItem.getAsFile()
// const reader = new FileReader()
// reader.onload = evt => {
// this.$store.commit(`loadingStart`, 'editor-paste-image')
// this.insertAfter({
// content: `![${file.name}](${evt.target.result})`,
// newLine: true
// })
// }
// reader.readAsDataURL(file)
// }
// }
}, processContent (newContent) { linesMap = [] // this.$store.set('editor/content', newContent)
this.processMarkers(this.cm.firstLine(), this.cm.lastLine()) this.previewHTML = DOMPurify.sanitize(md.render(newContent), { ADD_TAGS: ['foreignObject'] }) this.$nextTick(() => { tabsetHelper.format() this.renderMermaidDiagrams() Prism.highlightAllUnder(this.$refs.editorPreview) Array.from(this.$refs.editorPreview.querySelectorAll('pre.line-numbers')).forEach(pre => pre.classList.add('prismjs')) this.scrollSync(this.cm) }) }, /** * Update cursor state */ positionSync(cm) { this.cursorPos = cm.getCursor('head') }, /** * Wrap selection with start / end tags */ toggleMarkup({ start, end }) { if (!end) { end = start } if (!this.cm.doc.somethingSelected()) { return this.$store.commit('showNotification', { message: this.$t('editor:markup.noSelectionError'), style: 'warning', icon: 'warning' }) } this.cm.doc.replaceSelections(this.cm.doc.getSelections().map(s => start + s + end)) }, /** * Set current line as header */ setHeaderLine(lvl) { const curLine = this.cm.doc.getCursor('head').line let lineContent = this.cm.doc.getLine(curLine) const lineLength = lineContent.length if (_.startsWith(lineContent, '#')) { lineContent = lineContent.replace(/^(#+ )/, '') } lineContent = _.times(lvl, n => '#').join('') + ` ` + lineContent this.cm.doc.replaceRange(lineContent, { line: curLine, ch: 0 }, { line: curLine, ch: lineLength }) }, /** * Get the header lever of the current line */ getHeaderLevel(cm) { const curLine = this.cm.doc.getCursor('head').line let lineContent = this.cm.doc.getLine(curLine) let lvl = 0
const result = lineContent.match(/^(#+) /) if (result) { lvl = _.get(result, '[1]', '').length } return lvl }, /** * Insert content at cursor */ insertAtCursor({ content }) { const cursor = this.cm.doc.getCursor('head') this.cm.doc.replaceRange(content, cursor) }, /** * Insert content after current line */ insertAfter({ content, newLine }) { const curLine = this.cm.doc.getCursor('to').line const lineLength = this.cm.doc.getLine(curLine).length this.cm.doc.replaceRange(newLine ? `\n${content}\n` : content, { line: curLine, ch: lineLength + 1 }) }, /** * Insert content before current line */ insertBeforeEachLine({ content, after }) { let lines = [] if (!this.cm.doc.somethingSelected()) { lines.push(this.cm.doc.getCursor('head').line) } else { lines = _.flatten(this.cm.doc.listSelections().map(sl => { const range = Math.abs(sl.anchor.line - sl.head.line) + 1 const lowestLine = (sl.anchor.line > sl.head.line) ? sl.head.line : sl.anchor.line return _.times(range, l => l + lowestLine) })) } lines.forEach(ln => { let lineContent = this.cm.doc.getLine(ln) const lineLength = lineContent.length if (_.startsWith(lineContent, content)) { lineContent = lineContent.substring(content.length) }
this.cm.doc.replaceRange(content + lineContent, { line: ln, ch: 0 }, { line: ln, ch: lineLength }) }) if (after) { const lastLine = _.last(lines) this.cm.doc.replaceRange(`\n${after}\n`, { line: lastLine, ch: this.cm.doc.getLine(lastLine).length + 1 }) } }, /** * Update scroll sync */ scrollSync: _.debounce(function (cm) { if (!this.previewShown || cm.somethingSelected()) { return } let currentLine = cm.getCursor().line if (currentLine < 3) { this.Velocity(this.$refs.editorPreview, 'stop', true) this.Velocity(this.$refs.editorPreview.firstChild, 'scroll', { offset: '-50', duration: 1000, container: this.$refs.editorPreviewContainer }) } else { let closestLine = _.findLast(linesMap, n => n <= currentLine) let destElm = this.$refs.editorPreview.querySelector(`[data-line='${closestLine}']`) if (destElm) { this.Velocity(this.$refs.editorPreview, 'stop', true) this.Velocity(destElm, 'scroll', { offset: '-100', duration: 1000, container: this.$refs.editorPreviewContainer }) } } }, 500), toggleHelp () { this.helpShown = !this.helpShown this.activeModal = '' }, toggleFullscreen () { this.cm.setOption('fullScreen', true) }, refresh() { this.$nextTick(() => { this.cm.refresh() }) }, renderMermaidDiagrams () { document.querySelectorAll('.editor-markdown-preview pre.codeblock-mermaid > code').forEach(elm => { mermaidId++ const mermaidDef = elm.innerText const mmElm = document.createElement('div') mmElm.innerHTML = `<div id="mermaid-id-${mermaidId}">${mermaid.render(`mermaid-id-${mermaidId}`, mermaidDef)}</div>` elm.parentElement.replaceWith(mmElm) }) }, autocomplete (cm, change) { if (cm.getModeAt(cm.getCursor()).name !== 'markdown') { return }
// Links
if (change.text[0] === '(') { const curLine = cm.getLine(change.from.line).substring(0, change.from.ch) if (curLine[curLine.length - 1] === ']') { cm.showHint({ hint: async (cm, options) => { const cur = cm.getCursor() const curLine = cm.getLine(cur.line).substring(0, cur.ch) const queryString = curLine.substring(curLine.lastIndexOf('[')+1,curLine.length-2) const token = cm.getTokenAt(cur) try { const respRaw = await this.$apollo.query({ query: gql`
query ($query: String!, $locale: String) { pages { search(query:$query, locale:$locale) { results { title path locale } totalHits } } } `,
variables: { query: queryString, locale: this.locale }, fetchPolicy: 'cache-first' }) const resp = _.get(respRaw, 'data.pages.search', {}) if (resp && resp.totalHits > 0) { return { list: resp.results.map(r => ({ text: '(' + (siteLangs.length > 0 ? `/${r.locale}/${r.path}` : `/${r.path}`) + ')', displayText: siteLangs.length > 0 ? `/${r.locale}/${r.path} - ${r.title}` : `/${r.path} - ${r.title}` })), from: CodeMirror.Pos(cur.line, token.start), to: CodeMirror.Pos(cur.line, token.end) } } } catch (err) {} return { list: [], from: CodeMirror.Pos(cur.line, token.start), to: CodeMirror.Pos(cur.line, token.end) } } }) } } }, insertLink () { this.insertLinkDialog = true }, insertLinkHandler ({ locale, path }) { const lastPart = _.last(path.split('/')) this.insertAtCursor({ content: siteLangs.length > 0 ? `[${lastPart}](/${locale}/${path})` : `[${lastPart}](/${path})` }) }, processMarkers (from, to) { let found = null let foundStart = 0 this.cm.doc.getAllMarks().forEach(mk => { if (mk.__kind) { mk.clear() } }) this.cm.eachLine(from, to, ln => { const line = ln.lineNo() if (ln.text.startsWith('```diagram')) { found = 'diagram' foundStart = line } else if (ln.text === '```' && found) { switch (found) { // ------------------------------
// -> DIAGRAM
// ------------------------------
case 'diagram': { if (line - foundStart !== 2) { return } this.addMarker({ kind: 'diagram', from: { line: foundStart, ch: 3 }, to: { line: foundStart, ch: 10 }, text: 'Edit Diagram', action: ((start, end) => { return (ev) => { this.cm.doc.setSelection({ line: start, ch: 0 }, { line: end, ch: 3 }) try { const raw = this.cm.doc.getLine(end - 1) this.$store.set('editor/activeModalData', Buffer.from(raw, 'base64').toString()) this.toggleModal(`editorModalDrawio`) } catch (err) { return this.$store.commit('showNotification', { message: 'Failed to process diagram data.', style: 'warning', icon: 'warning' }) } } })(foundStart, line) }) if (ln.height > 0) { this.cm.foldCode(foundStart) } break } } found = null } }) }, addMarker ({ kind, from, to, text, action }) { const markerElm = document.createElement('span') markerElm.appendChild(document.createTextNode(text)) markerElm.className = 'CodeMirror-buttonmarker' markerElm.addEventListener('click', action) this.cm.markText(from, to, { replacedWith: markerElm, __kind: kind }) } }, mounted() { this.$store.set('editor/editorKey', 'markdown')
if (this.mode === 'create' && !this.$store.get('editor/content')) { this.$store.set('editor/content', '# Header\nYour content here') }
// Initialize Mermaid API
mermaid.initialize({ startOnLoad: false, theme: this.$vuetify.theme.dark ? `dark` : `default` })
// Initialize CodeMirror
this.cm = CodeMirror.fromTextArea(this.$refs.cm, { tabSize: 2, mode: 'text/markdown', theme: 'wikijs-dark', lineNumbers: true, lineWrapping: true, line: true, styleActiveLine: true, highlightSelectionMatches: { annotateScrollbar: true }, viewportMargin: 50, inputStyle: 'contenteditable', allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif'], direction: siteConfig.rtl ? 'rtl' : 'ltr', foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] }) this.cm.setValue(this.$store.get('editor/content')) this.cm.on('change', c => { this.$store.set('editor/content', c.getValue()) this.onCmInput(this.$store.get('editor/content')) }) if (this.$vuetify.breakpoint.mdAndUp) { this.cm.setSize(null, 'calc(100vh - 112px - 24px)') } else { this.cm.setSize(null, 'calc(100vh - 112px - 16px)') }
// Set Keybindings
const keyBindings = { 'F11' (c) { c.setOption('fullScreen', !c.getOption('fullScreen')) }, 'Esc' (c) { if (c.getOption('fullScreen')) c.setOption('fullScreen', false) } } _.set(keyBindings, `${CtrlKey}-S`, c => { this.save() return false }) _.set(keyBindings, `${CtrlKey}-B`, c => { this.toggleMarkup({ start: `**` }) return false }) _.set(keyBindings, `${CtrlKey}-I`, c => { this.toggleMarkup({ start: `*` }) return false }) _.set(keyBindings, `${CtrlKey}-Alt-Right`, c => { let lvl = this.getHeaderLevel(c) if (lvl >= 6) { lvl = 5 } this.setHeaderLine(lvl + 1) return false }) _.set(keyBindings, `${CtrlKey}-Alt-Left`, c => { let lvl = this.getHeaderLevel(c) if (lvl <= 1) { lvl = 2 } this.setHeaderLine(lvl - 1) return false }) this.cm.setOption('extraKeys', keyBindings)
this.cm.on('inputRead', this.autocomplete)
// Handle cursor movement
this.cm.on('cursorActivity', c => { this.positionSync(c) this.scrollSync(c) })
// Handle special paste
this.cm.on('paste', this.onCmPaste)
// Render initial preview
this.processContent(this.$store.get('editor/content')) this.refresh()
this.$root.$on('editorInsert', opts => { switch (opts.kind) { case 'IMAGE': let img = `![${opts.text}](${opts.path})` if (opts.align && opts.align !== '') { img += `{.align-${opts.align}}` } this.insertAtCursor({ content: img }) break case 'BINARY': this.insertAtCursor({ content: `[${opts.text}](${opts.path})` }) break case 'DIAGRAM': const selStartLine = this.cm.getCursor('from').line const selEndLine = this.cm.getCursor('to').line + 1 this.cm.doc.replaceSelection('```diagram\n' + opts.text + '\n```\n', 'start') this.processMarkers(selStartLine, selEndLine) break } })
// Handle save conflict
this.$root.$on('saveConflict', () => { this.toggleModal(`editorModalConflict`) }) this.$root.$on('overwriteEditorContent', () => { this.cm.setValue(this.$store.get('editor/content')) }) }, beforeDestroy() { this.$root.$off('editorInsert') } } </script>
<style lang='scss'>
$editor-height: calc(100vh - 112px - 24px); $editor-height-mobile: calc(100vh - 112px - 16px);
.editor-markdown { &-main { display: flex; width: 100%; }
&-editor { background-color: darken(mc('grey', '900'), 4.5%); flex: 1 1 50%; display: block; height: $editor-height; position: relative;
@include until($tablet) { height: $editor-height-mobile; } }
&-preview { flex: 1 1 50%; background-color: mc('grey', '100'); position: relative; height: $editor-height; overflow: hidden; padding: 1rem;
@at-root .theme--dark & { background-color: mc('grey', '900'); }
@include until($tablet) { display: none; }
&-enter-active, &-leave-active { transition: max-width .5s ease; max-width: 50vw;
.editor-code-preview-content { width: 50vw; overflow:hidden; } } &-enter, &-leave-to { max-width: 0; }
&-content { height: $editor-height; overflow-y: scroll; padding: 0; width: calc(100% + 17px); // -ms-overflow-style: none;
// &::-webkit-scrollbar {
// width: 0px;
// background: transparent;
// }
@include until($tablet) { height: $editor-height-mobile; }
> div { outline: none; }
p.line { overflow-wrap: break-word; }
.tabset { background-color: mc('teal', '700'); color: mc('teal', '100') !important; padding: 5px 12px; font-size: 14px; font-weight: 500; border-radius: 5px 0 0 0; font-style: italic;
&::after { display: none; }
&-header { background-color: mc('teal', '500'); color: #FFF !important; padding: 5px 12px; font-size: 14px; font-weight: 500; margin-top: 0 !important;
&::after { display: none; } }
&-content { border-left: 5px solid mc('teal', '500'); background-color: mc('teal', '50'); padding: 0 15px 15px; overflow: hidden;
@at-root .theme--dark & { background-color: rgba(mc('teal', '500'), .1); } } } } }
&-toolbar { background-color: mc('blue', '700'); background-image: linear-gradient(to bottom, mc('blue', '700') 0%, mc('blue','800') 100%); color: #FFF;
.v-toolbar__content { padding-left: 64px;
@include until($tablet) { padding-left: 8px; } } }
&-insert:not(.v-speed-dial--right) { @include from($tablet) { left: 50%; margin-left: -28px; } }
&-sidebar { background-color: mc('grey', '900'); width: 64px; display: flex; flex-direction: column; justify-content: flex-start; align-items: center; padding: 24px 0;
@include until($tablet) { padding: 12px 0; width: 40px; } }
&-sysbar { padding-left: 0;
&-locale { background-color: rgba(255,255,255,.25); display:inline-flex; padding: 0 12px; height: 24px; width: 63px; justify-content: center; align-items: center; } }
// ==========================================
// Fix FAB revealing under codemirror
// ==========================================
.speed-dial--fixed { z-index: 8; }
// ==========================================
// CODE MIRROR
// ==========================================
.CodeMirror { height: auto; font-family: 'Roboto Mono', monospace; font-size: .9rem;
.cm-header-1 { font-size: 1.5rem; } .cm-header-2 { font-size: 1.25rem; } .cm-header-3 { font-size: 1.15rem; } .cm-header-4 { font-size: 1.1rem; } .cm-header-5 { font-size: 1.05rem; } .cm-header-6 { font-size: 1.025rem; } }
.CodeMirror-wrap pre.CodeMirror-line, .CodeMirror-wrap pre.CodeMirror-line-like { word-break: break-word; }
.CodeMirror-focused .cm-matchhighlight { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==); background-position: bottom; background-repeat: repeat-x; } .cm-matchhighlight { background-color: mc('grey', '800'); } .CodeMirror-selection-highlight-scrollbar { background-color: mc('green', '600'); } }
// HINT DROPDOWN
.CodeMirror-hints { position: absolute; z-index: 10; overflow: hidden; list-style: none;
margin: 0; padding: 1px;
box-shadow: 2px 3px 5px rgba(0,0,0,.2); border: 1px solid mc('grey', '700');
background: mc('grey', '900'); font-family: 'Roboto Mono', monospace; font-size: .9rem;
max-height: 150px; overflow-y: auto;
min-width: 250px; max-width: 80vw; }
.CodeMirror-hint { margin: 0; padding: 0 4px; white-space: pre; color: #FFF; cursor: pointer; }
li.CodeMirror-hint-active { background: mc('blue', '500'); color: #FFF; } </style>
|