383 lines
8.3 KiB
Vue
Executable File
383 lines
8.3 KiB
Vue
Executable File
<template>
|
|
<default-field
|
|
:field="field"
|
|
:errors="errors"
|
|
:full-width-content="true"
|
|
:show-help-text="showHelpText"
|
|
>
|
|
<template slot="field">
|
|
<div
|
|
class="bg-white rounded-lg overflow-hidden"
|
|
:class="{
|
|
'markdown-fullscreen fixed pin z-50': isFullScreen,
|
|
'form-input form-input-bordered px-0': !isFullScreen,
|
|
'form-control-focus': isFocused,
|
|
'border-danger': errors.has('body'),
|
|
}"
|
|
>
|
|
<header
|
|
class="flex items-center content-center justify-between border-b border-60"
|
|
:class="{ 'bg-30': isReadonly }"
|
|
>
|
|
<ul class="w-full flex items-center content-center list-reset">
|
|
<button
|
|
:class="{
|
|
'text-primary font-bold': this.mode == 'write',
|
|
}"
|
|
@click.prevent="write"
|
|
class="ml-1 text-90 px-3 py-2"
|
|
>
|
|
{{ __('Write') }}
|
|
</button>
|
|
<button
|
|
:class="{
|
|
'text-primary font-bold': this.mode == 'preview',
|
|
}"
|
|
@click.prevent="preview"
|
|
class="text-90 px-3 py-2"
|
|
>
|
|
{{ __('Preview') }}
|
|
</button>
|
|
</ul>
|
|
|
|
<ul v-if="!isReadonly" class="flex items-center list-reset">
|
|
<button
|
|
:key="tool.action"
|
|
@click.prevent="callAction(tool.action)"
|
|
v-for="tool in tools"
|
|
class="rounded-none ico-button inline-flex items-center justify-center px-2 text-sm text-80 border-l border-60"
|
|
>
|
|
<component
|
|
:is="tool.icon"
|
|
class="fill-80 w-editor-icon h-editor-icon"
|
|
/>
|
|
</button>
|
|
</ul>
|
|
</header>
|
|
|
|
<div
|
|
v-show="mode == 'write'"
|
|
class="flex markdown-content relative p-4"
|
|
:class="{ 'readonly bg-30': isReadonly }"
|
|
>
|
|
<textarea ref="theTextarea" :class="{ 'bg-30': isReadonly }" />
|
|
</div>
|
|
|
|
<div
|
|
v-if="mode == 'preview'"
|
|
class="markdown overflow-scroll p-4"
|
|
v-html="previewContent"
|
|
></div>
|
|
</div>
|
|
</template>
|
|
</default-field>
|
|
</template>
|
|
|
|
<script>
|
|
import _ from 'lodash'
|
|
const md = require('markdown-it')()
|
|
import CodeMirror from 'codemirror'
|
|
import 'codemirror/mode/markdown/markdown'
|
|
import { FormField, HandlesValidationErrors } from 'laravel-nova'
|
|
|
|
const actions = {
|
|
bold() {
|
|
if (!this.isEditable) return
|
|
|
|
this.insertAround('**', '**')
|
|
},
|
|
|
|
italicize() {
|
|
if (!this.isEditable) return
|
|
|
|
this.insertAround('*', '*')
|
|
},
|
|
|
|
image() {
|
|
if (!this.isEditable) return
|
|
|
|
this.insertBefore('', 2)
|
|
},
|
|
|
|
link() {
|
|
if (!this.isEditable) return
|
|
|
|
this.insertAround('[', '](url)')
|
|
},
|
|
|
|
toggleFullScreen() {
|
|
this.fullScreen = !this.fullScreen
|
|
this.$nextTick(() => this.codemirror.refresh())
|
|
},
|
|
|
|
fullScreen() {
|
|
this.fullScreen = true
|
|
},
|
|
|
|
exitFullScreen() {
|
|
this.fullScreen = false
|
|
},
|
|
}
|
|
|
|
const keyMaps = {
|
|
'Cmd-B': 'bold',
|
|
'Cmd-I': 'italicize',
|
|
'Cmd-Alt-I': 'image',
|
|
'Cmd-K': 'link',
|
|
F11: 'fullScreen',
|
|
Esc: 'exitFullScreen',
|
|
}
|
|
|
|
export default {
|
|
mixins: [HandlesValidationErrors, FormField],
|
|
|
|
data: () => ({
|
|
fullScreen: false,
|
|
isFocused: false,
|
|
codemirror: null,
|
|
mode: 'write',
|
|
tools: [
|
|
{
|
|
name: 'bold',
|
|
action: 'bold',
|
|
className: 'fa fa-bold',
|
|
icon: 'editor-bold',
|
|
},
|
|
{
|
|
name: 'italicize',
|
|
action: 'italicize',
|
|
className: 'fa fa-italic',
|
|
icon: 'editor-italic',
|
|
},
|
|
{
|
|
name: 'link',
|
|
action: 'link',
|
|
className: 'fa fa-link',
|
|
icon: 'editor-link',
|
|
},
|
|
{
|
|
name: 'image',
|
|
action: 'image',
|
|
className: 'fa fa-image',
|
|
icon: 'editor-image',
|
|
},
|
|
{
|
|
name: 'fullScreen',
|
|
action: 'toggleFullScreen',
|
|
className: 'fa fa-expand',
|
|
icon: 'editor-fullscreen',
|
|
},
|
|
],
|
|
}),
|
|
|
|
mounted() {
|
|
this.codemirror = CodeMirror.fromTextArea(this.$refs.theTextarea, {
|
|
tabSize: 4,
|
|
indentWithTabs: true,
|
|
lineWrapping: true,
|
|
mode: 'markdown',
|
|
viewportMargin: Infinity,
|
|
extraKeys: {
|
|
Enter: 'newlineAndIndentContinueMarkdownList',
|
|
..._.map(this.tools, tool => {
|
|
return tool.action
|
|
}),
|
|
},
|
|
...{ readOnly: this.isReadonly },
|
|
})
|
|
|
|
_.each(keyMaps, (action, map) => {
|
|
const realMap = map.replace(
|
|
'Cmd-',
|
|
CodeMirror.keyMap['default'] == CodeMirror.keyMap.macDefault
|
|
? 'Cmd-'
|
|
: 'Ctrl-'
|
|
)
|
|
this.codemirror.options.extraKeys[realMap] = actions[keyMaps[map]].bind(
|
|
this
|
|
)
|
|
})
|
|
|
|
this.doc.on('change', (cm, changeObj) => {
|
|
this.value = cm.getValue()
|
|
})
|
|
|
|
this.codemirror.on('focus', () => (this.isFocused = true))
|
|
this.codemirror.on('blur', () => (this.isFocused = false))
|
|
|
|
if (this.field.value) {
|
|
this.doc.setValue(this.field.value)
|
|
}
|
|
|
|
Nova.$on(this.field.attribute + '-value', value => {
|
|
this.doc.setValue(value)
|
|
this.$nextTick(() => this.codemirror.refresh())
|
|
})
|
|
|
|
this.$nextTick(() => this.codemirror.refresh())
|
|
},
|
|
|
|
methods: {
|
|
focus() {
|
|
this.codemirror.focus()
|
|
},
|
|
|
|
write() {
|
|
this.mode = 'write'
|
|
this.$nextTick(() => {
|
|
this.codemirror.refresh()
|
|
})
|
|
},
|
|
|
|
preview() {
|
|
this.mode = 'preview'
|
|
},
|
|
|
|
insert(insertion) {
|
|
this.doc.replaceRange(insertion, {
|
|
line: this.cursor.line,
|
|
ch: this.cursor.ch,
|
|
})
|
|
},
|
|
|
|
insertAround(start, end) {
|
|
if (this.doc.somethingSelected()) {
|
|
const selection = this.doc.getSelection()
|
|
this.doc.replaceSelection(start + selection + end)
|
|
} else {
|
|
this.doc.replaceRange(start + end, {
|
|
line: this.cursor.line,
|
|
ch: this.cursor.ch,
|
|
})
|
|
this.doc.setCursor({
|
|
line: this.cursor.line,
|
|
ch: this.cursor.ch - end.length,
|
|
})
|
|
}
|
|
},
|
|
|
|
insertBefore(insertion, cursorOffset) {
|
|
if (this.doc.somethingSelected()) {
|
|
const selects = this.doc.listSelections()
|
|
selects.forEach(selection => {
|
|
const pos = [selection.head.line, selection.anchor.line].sort()
|
|
|
|
for (let i = pos[0]; i <= pos[1]; i++) {
|
|
this.doc.replaceRange(insertion, { line: i, ch: 0 })
|
|
}
|
|
|
|
this.doc.setCursor({ line: pos[0], ch: cursorOffset || 0 })
|
|
})
|
|
} else {
|
|
this.doc.replaceRange(insertion, {
|
|
line: this.cursor.line,
|
|
ch: 0,
|
|
})
|
|
this.doc.setCursor({
|
|
line: this.cursor.line,
|
|
ch: cursorOffset || 0,
|
|
})
|
|
}
|
|
},
|
|
|
|
callAction(action) {
|
|
if (!this.isReadonly) {
|
|
this.focus()
|
|
actions[action].call(this)
|
|
}
|
|
},
|
|
},
|
|
|
|
computed: {
|
|
doc() {
|
|
return this.codemirror.getDoc()
|
|
},
|
|
|
|
isFullScreen() {
|
|
return this.fullScreen == true
|
|
},
|
|
|
|
cursor() {
|
|
return this.doc.getCursor()
|
|
},
|
|
|
|
rawContent() {
|
|
return this.codemirror.getValue()
|
|
},
|
|
|
|
previewContent() {
|
|
return md.render(this.rawContent || '')
|
|
},
|
|
|
|
isEditable() {
|
|
return !this.isReadonly && this.mode == 'write'
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style src="codemirror/lib/codemirror.css" />
|
|
|
|
<style>
|
|
.ico-button {
|
|
width: 35px;
|
|
height: 35px;
|
|
}
|
|
|
|
.ico-button:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.ico-button:active {
|
|
color: var(--brand-80);
|
|
}
|
|
|
|
.cm-fat-cursor .CodeMirror-cursor {
|
|
background: #000;
|
|
}
|
|
|
|
.cm-s-default .cm-header {
|
|
color: black;
|
|
}
|
|
.cm-s-default .cm-link {
|
|
color: var(--primary);
|
|
}
|
|
.CodeMirror-line {
|
|
color: var(--gray-60);
|
|
}
|
|
.cm-s-default .cm-variable-2 {
|
|
color: var(--gray-60);
|
|
}
|
|
.cm-s-default .cm-quote {
|
|
color: var(--gray-60);
|
|
}
|
|
.cm-s-default .cm-comment {
|
|
color: var(--gray-60);
|
|
}
|
|
.cm-s-default .cm-string {
|
|
color: var(--gray-40);
|
|
}
|
|
.cm-s-default .cm-url {
|
|
color: var(--gray-40);
|
|
}
|
|
|
|
.CodeMirror {
|
|
height: auto;
|
|
font: 14px/1.5 Menlo, Consolas, Monaco, 'Andale Mono', monospace;
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
}
|
|
|
|
.readonly > .CodeMirror {
|
|
background-color: var(--30) !important;
|
|
}
|
|
|
|
.markdown-fullscreen .markdown-content {
|
|
height: calc(100vh - 30px);
|
|
}
|
|
|
|
.markdown-fullscreen .CodeMirror {
|
|
height: 100%;
|
|
}
|
|
</style>
|