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>