components/markdown/Live.vue (243 lines of code) (raw):
<template>
<div
ref="container"
:class="`md-live layout-${layout}`"
v-observe-visibility="visibilityChanged"
v-if="innerCode"
>
<div class="md-live-editor">
<div class="md-live-editor-container">
<prism-editor
v-model="innerCode"
:highlight="highlighter"
:readonly="readOnly"
>
</prism-editor>
</div>
<div class="md-live-tag">live</div>
<code-block-copy-clipboard
:source="innerCode"
></code-block-copy-clipboard>
</div>
<div ref="previewContainer" class="md-live-preview"></div>
</div>
</template>
<script lang="ts">
import { PrismEditor } from 'vue-prism-editor'
import 'vue-prism-editor/dist/prismeditor.min.css'
import { highlight, languages } from 'prismjs/components/prism-core'
import 'prismjs/components/prism-clike'
import 'prismjs/components/prism-javascript'
import 'prism-themes/themes/prism-material-oceanic.css'
import { loadScriptsAsync } from '../helper/loadScripts'
import { addListener, removeListener } from 'resize-detector'
import debounce from 'lodash/debounce'
import {
defineComponent,
watch,
ref,
unref,
onMounted,
onUnmounted,
} from '@vue/composition-api'
import * as base64 from 'js-base64'
import { createSandbox } from '../helper/sandbox'
import CodeBlockCopyClipboard from './CodeBlockCopyClipboard.vue'
declare const echarts: any
function ensureECharts(locale) {
if (typeof echarts === 'undefined') {
// const isASFDeploy = process.env.NUXT_ENV_DEPLOY === 'asf'
// const lib = isASFDeploy ? 'echarts' : 'echarts-nightly'
return loadScriptsAsync([
// @ts-ignore
(window.ECHARTS_WWW_VENDORS_CDN_ROOT ||
'https://fastly.jsdelivr.net/npm/') + 'echarts/dist/echarts.min.js',
// isCN
// ? `https://registry.npmmirror.com/${lib}/latest/files/dist/echarts.min.js`
// : `https://fastly.jsdelivr.net/npm/${lib}@latest/dist/echarts.min.js`,
]).then(() => {})
}
return Promise.resolve()
}
export default defineComponent({
components: {
PrismEditor,
CodeBlockCopyClipboard,
},
props: {
lang: {
type: String,
default: 'js',
},
code: {
type: String,
},
layout: {
type: String,
default: 'tb',
validator(value: string) {
return ['lr', 'tb', 'rl', 'bt'].includes(value)
},
},
height: {
type: Number,
},
readOnly: {
type: Boolean,
default: false,
},
},
setup(props, context) {
const innerCode = ref(base64.decode(props.code))
const previewContainer = ref<HTMLElement | null>(null)
const container = ref<HTMLElement | null>(null)
let sandbox: ReturnType<typeof createSandbox>
function update() {
if (props.height) {
container.value!.style.height = props.height + 'px'
}
ensureECharts((context.root as any).$i18n.locale).then(() => {
if (!sandbox) {
addListener(unref(previewContainer)!, resize)
sandbox = createSandbox()
}
// TODO refresh.
try {
unref(previewContainer) &&
sandbox.run(unref(previewContainer)!, unref(innerCode))
} catch (e) {
console.error(e)
}
})
}
function resize() {
if (sandbox) {
sandbox.resize()
}
}
const debouncedUpdate = debounce(update, 500, {
trailing: true,
})
watch(innerCode, () => {
debouncedUpdate()
})
// onMounted(() => {
// debouncedUpdate()
// })
onUnmounted(() => {
removeListener(unref(previewContainer)!, resize)
})
return {
innerCode,
previewContainer,
container,
highlighter(code) {
return highlight(code, languages[props.lang] || languages.js)
},
visibilityChanged(isVisible) {
if (isVisible) {
if (!sandbox) {
debouncedUpdate()
} else {
sandbox.resume()
}
} else {
if (sandbox) {
sandbox.pause()
}
}
},
}
},
})
</script>
<style lang="postcss">
.md-live {
@apply overflow-hidden;
@apply shadow-lg rounded-lg mt-10 mb-20;
@apply flex flex-col-reverse;
@media (max-width: 768px) {
min-height: 500px;
}
@media (min-width: 768px) {
&.layout-lr {
@apply flex-row;
}
&.layout-rl {
@apply flex-row-reverse;
}
&.layout-tb {
@apply flex-col;
}
&.layout-bt {
@apply flex-col-reverse;
}
&.layout-lr,
&.layout-rl {
@apply items-stretch;
.md-live-editor-container {
height: 100%;
}
.md-live-editor {
@apply flex-1;
}
.md-live-preview {
@apply flex-1;
height: auto;
}
}
}
}
/* required class */
.md-live-editor {
position: relative;
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3) !important;
}
.md-live-editor-container {
/* we dont use `language-` classes anymore so thats why we need to add background and text color manually */
background: #263238;
max-height: 500px;
overflow-y: auto;
font-size: 13px;
padding: 10px;
@media (max-width: 768px) {
max-height: 300px;
}
}
pre {
color: #c3cee3;
}
.md-live-tag {
position: absolute;
right: 0;
top: 0;
text-transform: uppercase;
@apply mr-7 mt-3;
color: #f7fafc;
z-index: 10;
}
.clipboard {
display: none;
}
&:hover {
.clipboard {
display: block;
}
}
}
.md-live-preview {
height: 300px;
overflow: hidden;
}
.prism-editor-wrapper .prism-editor__editor,
.prism-editor-wrapper .prism-editor__textarea {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
line-height: 1.5;
}
/* optional class for removing the outline */
.prism-editor__textarea:focus {
outline: none;
}
</style>