feat(plugin-md-power): improve normal demo sandbox (#448)
This commit is contained in:
parent
58cc9aba20
commit
c703b89e1c
@ -5,8 +5,8 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$('#message', document).text('So Awesome!')
|
||||
const datetime = $('#datetime', document)
|
||||
$('#message').text('So Awesome!')
|
||||
const datetime = $('#datetime')
|
||||
setInterval(() => {
|
||||
datetime.text(dayjs().format('YYYY-MM-DD HH:mm:ss'))
|
||||
}, 1000)
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<script lang="ts">
|
||||
const a = 'So Awesome!'
|
||||
const app = document.querySelector('#app')
|
||||
app.appendChild(window.document.createElement('small')).textContent = a
|
||||
app.appendChild(document.createElement('small')).textContent = a
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
|
||||
@ -154,7 +154,7 @@ export default defineUserConfig({
|
||||
|
||||
`code-setting=":lines-number"`,则会在代码块后面添加 `:lines-number`,使代码块支持显示行号。
|
||||
|
||||
`code-setting=":collapsed-lines=10"`,则会在代码块后面添加 `:collapsed-lines=10`,使代码块从第 210行开始折叠。
|
||||
`code-setting=":collapsed-lines=10"`,则会在代码块后面添加 `:collapsed-lines=10`,使代码块从第 10 行开始折叠。
|
||||
|
||||
```md
|
||||
@[demo vue expanded title="标题" desc="描述" code-setting=":collapsed-lines=10"](./demo/Counter.vue)
|
||||
@ -420,16 +420,6 @@ export default defineComponent({
|
||||
|
||||
同时,也支持通过 外部链接 的方式引入 第三方的库,比如 `jQuery` , `dayjs` 等。
|
||||
|
||||
::: important `document` 的差异
|
||||
普通代码演示 的代码 运行在 `ShadowDOM` 中,从而实现与 站点其他内容的隔离。避免对环境的污染。
|
||||
|
||||
因此,在普通演示的脚本代码中,**全局对象 `document` 指向的是 `ShadowDOM`** ,请着重注意此差异。
|
||||
如果您需要使用 浏览器的全局对象,请使用 `window.document` 代替 `document`。
|
||||
|
||||
如果引入了如 `JQuery` 库,由于此差异,`$(selector)` 的行为会发生变化,要查询 `ShadowDOM` 中的元素,
|
||||
需要使用 `$(selector, document)`,即在第二个参数中传入 `document` 作为查询的上下文。
|
||||
:::
|
||||
|
||||
::: warning 不建议过于复杂的演示。
|
||||
:::
|
||||
|
||||
@ -600,7 +590,7 @@ export default defineComponent({
|
||||
```js
|
||||
const a = 'So Awesome!'
|
||||
const app = document.querySelector('#app')
|
||||
app.appendChild(window.document.createElement('small')).textContent = a
|
||||
app.appendChild(document.createElement('small')).textContent = a
|
||||
```
|
||||
@tab CSS
|
||||
```css
|
||||
@ -633,7 +623,7 @@ app.appendChild(window.document.createElement('small')).textContent = a
|
||||
```js
|
||||
const a = 'So Awesome!'
|
||||
const app = document.querySelector('#app')
|
||||
app.appendChild(window.document.createElement('small')).textContent = a
|
||||
app.appendChild(document.createElement('small')).textContent = a
|
||||
```
|
||||
|
||||
@tab CSS
|
||||
@ -678,8 +668,8 @@ app.appendChild(window.document.createElement('small')).textContent = a
|
||||
```
|
||||
@tab Javascript
|
||||
```js
|
||||
$('#message', document).text('So Awesome!')
|
||||
const datetime = $('#datetime', document)
|
||||
$('#message').text('So Awesome!')
|
||||
const datetime = $('#datetime')
|
||||
setInterval(() => {
|
||||
datetime.text(dayjs().format('YYYY-MM-DD HH:mm:ss'))
|
||||
}, 1000)
|
||||
@ -725,8 +715,8 @@ setInterval(() => {
|
||||
@tab Javascript
|
||||
|
||||
```js
|
||||
$('#message', document).text('So Awesome!')
|
||||
const datetime = $('#datetime', document)
|
||||
$('#message').text('So Awesome!')
|
||||
const datetime = $('#datetime')
|
||||
setInterval(() => {
|
||||
datetime.text(dayjs().format('YYYY-MM-DD HH:mm:ss'))
|
||||
}, 1000)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useExpand } from '../composables/demo.js'
|
||||
|
||||
import '../styles/demo.css'
|
||||
|
||||
@ -10,10 +10,7 @@ const props = defineProps<{
|
||||
expanded?: boolean
|
||||
}>()
|
||||
|
||||
const showCode = ref(props.expanded ?? true)
|
||||
function toggleCode() {
|
||||
showCode.value = !showCode.value
|
||||
}
|
||||
const [showCode, toggleCode] = useExpand(props.expanded)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { computed, onMounted, ref, useId, useTemplateRef, watch } from 'vue'
|
||||
import { loadScript, loadStyle } from '../utils/shared.js'
|
||||
import Loading from './icons/Loading.vue'
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { type DemoConfig, useExpand, useFence, useNormalDemo, useResources } from '../composables/demo.js'
|
||||
|
||||
import '../styles/demo.css'
|
||||
|
||||
@ -10,121 +8,41 @@ const props = defineProps<{
|
||||
title?: string
|
||||
desc?: string
|
||||
expanded?: boolean
|
||||
config?: {
|
||||
html: string
|
||||
css: string
|
||||
script: string
|
||||
jsLib: string[]
|
||||
cssLib: string[]
|
||||
}
|
||||
config?: DemoConfig
|
||||
}>()
|
||||
|
||||
const draw = useTemplateRef<HTMLDivElement>('draw')
|
||||
const id = useId()
|
||||
const loaded = ref(true)
|
||||
const [showCode, toggleCode] = useExpand(props.expanded)
|
||||
|
||||
const resourcesEl = useTemplateRef<HTMLDivElement>('resourcesEl')
|
||||
const resources = computed<{
|
||||
name: string
|
||||
items: { name: string, url: string }[]
|
||||
}[]>(() => {
|
||||
if (!props.config)
|
||||
return []
|
||||
return [
|
||||
{ name: 'JavaScript', items: props.config.jsLib.map(url => ({ name: normalizeName(url), url })) },
|
||||
{ name: 'CSS', items: props.config.cssLib.map(url => ({ name: normalizeName(url), url })) },
|
||||
].filter(i => i.items.length)
|
||||
})
|
||||
const { resources, showResources, toggleResources } = useResources(
|
||||
useTemplateRef<HTMLDivElement>('resourcesEl'),
|
||||
() => props.config,
|
||||
)
|
||||
|
||||
function normalizeName(url: string) {
|
||||
return url.slice(url.lastIndexOf('/') + 1)
|
||||
}
|
||||
const { id, height } = useNormalDemo(
|
||||
useTemplateRef<HTMLIFrameElement>('draw'),
|
||||
() => props.title,
|
||||
() => props.config,
|
||||
)
|
||||
|
||||
const showResources = ref(false)
|
||||
|
||||
function toggleResources() {
|
||||
showResources.value = !showResources.value
|
||||
}
|
||||
|
||||
onClickOutside(resourcesEl, () => {
|
||||
showResources.value = false
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!draw.value)
|
||||
return
|
||||
const root = draw.value.attachShadow({ mode: 'open' })
|
||||
|
||||
watch(() => props.config, async () => {
|
||||
root.innerHTML = props.config?.html ?? ''
|
||||
|
||||
props.config?.cssLib?.forEach(url => loadStyle(url, root))
|
||||
if (props.config?.css) {
|
||||
const style = document.createElement('style')
|
||||
style.innerHTML = props.config?.css ?? ''
|
||||
root.appendChild(style)
|
||||
}
|
||||
|
||||
if (props.config?.jsLib?.length) {
|
||||
loaded.value = false
|
||||
await Promise.all(props.config.jsLib.map(url => loadScript(url)))
|
||||
.catch(e => console.warn(e))
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
if (props.config?.script) {
|
||||
const script = document.createElement('script')
|
||||
script.type = 'text/javascript'
|
||||
script.innerHTML = `;(function(document){\n${props.config.script}\n})(document.querySelector('#VPDemoNormalDraw${id}').shadowRoot);`
|
||||
root.appendChild(script)
|
||||
}
|
||||
}, { immediate: true })
|
||||
})
|
||||
|
||||
const fence = useTemplateRef<HTMLDivElement>('fence')
|
||||
const data = ref<{
|
||||
js: string
|
||||
css: string
|
||||
html: string
|
||||
jsType: string
|
||||
cssType: string
|
||||
}>({ js: '', css: '', html: '', jsType: '', cssType: '' })
|
||||
|
||||
onMounted(() => {
|
||||
if (!fence.value)
|
||||
return
|
||||
|
||||
data.value.html = props.config?.html ?? ''
|
||||
const els = Array.from(fence.value.querySelectorAll('div[class*="language-"]'))
|
||||
for (const el of els) {
|
||||
const lang = el.className.match(/language-(\w+)/)?.[1] ?? ''
|
||||
const content = el.querySelector('pre')?.textContent ?? ''
|
||||
if (lang === 'js' || lang === 'javascript') {
|
||||
data.value.js = content
|
||||
data.value.jsType = 'js'
|
||||
}
|
||||
if (lang === 'ts' || lang === 'typescript') {
|
||||
data.value.js = content
|
||||
data.value.jsType = 'ts'
|
||||
}
|
||||
if (lang === 'css' || lang === 'scss' || lang === 'less' || lang === 'stylus' || lang === 'styl') {
|
||||
data.value.css = content
|
||||
data.value.cssType = lang === 'styl' ? 'stylus' : lang
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const showCode = ref(props.expanded ?? false)
|
||||
function toggleCode() {
|
||||
showCode.value = !showCode.value
|
||||
}
|
||||
const data = useFence(
|
||||
useTemplateRef<HTMLDivElement>('fence'),
|
||||
() => props.config,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-demo-wrapper normal">
|
||||
<div class="demo-draw">
|
||||
<Loading v-if="!loaded" />
|
||||
<div :id="`VPDemoNormalDraw${id}`" ref="draw" />
|
||||
<iframe
|
||||
:id="`VPDemoNormalDraw${id}`"
|
||||
ref="draw"
|
||||
class="draw-iframe"
|
||||
allow="accelerometer *; bluetooth *; camera *; encrypted-media *; display-capture *; geolocation *; gyroscope *; microphone *; midi *; clipboard-read *; clipboard-write *; web-share *; serial *; xr-spatial-tracking *"
|
||||
allowfullscreen="true"
|
||||
allowpaymentrequest="true"
|
||||
allowtransparency="true"
|
||||
sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups-to-escape-sandbox allow-popups allow-presentation allow-same-origin allow-scripts allow-top-navigation-by-user-activation" :style="{ height }"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="title || desc" class="demo-info">
|
||||
<p v-if="title" class="title">
|
||||
|
||||
173
plugins/plugin-md-power/src/client/composables/demo.ts
Normal file
173
plugins/plugin-md-power/src/client/composables/demo.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import type { MaybeRefOrGetter, ShallowRef } from 'vue'
|
||||
import { onClickOutside, useEventListener } from '@vueuse/core'
|
||||
import { computed, getCurrentInstance, onMounted, ref, toValue, useId, watch } from 'vue'
|
||||
import { isPlainObject } from 'vuepress/shared'
|
||||
|
||||
export interface DemoConfig {
|
||||
html: string
|
||||
css: string
|
||||
script: string
|
||||
jsLib: string[]
|
||||
cssLib: string[]
|
||||
}
|
||||
|
||||
export function useExpand(defaultExpand = true) {
|
||||
const expanded = ref(defaultExpand)
|
||||
function toggle() {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
return [expanded, toggle] as const
|
||||
}
|
||||
|
||||
export function useResources(el: ShallowRef<HTMLDivElement | null>, config: MaybeRefOrGetter<DemoConfig | undefined>) {
|
||||
const resources = computed<{
|
||||
name: string
|
||||
items: { name: string, url: string }[]
|
||||
}[]>(() => {
|
||||
const conf = toValue(config)
|
||||
if (!conf)
|
||||
return []
|
||||
return [
|
||||
{ name: 'JavaScript', items: conf.jsLib.map(url => ({ name: normalizeName(url), url })) },
|
||||
{ name: 'CSS', items: conf.cssLib.map(url => ({ name: normalizeName(url), url })) },
|
||||
].filter(i => i.items.length)
|
||||
})
|
||||
|
||||
function normalizeName(url: string) {
|
||||
return url.slice(url.lastIndexOf('/') + 1)
|
||||
}
|
||||
|
||||
const showResources = ref(false)
|
||||
|
||||
function toggleResources() {
|
||||
showResources.value = !showResources.value
|
||||
}
|
||||
|
||||
onClickOutside(el, () => {
|
||||
showResources.value = false
|
||||
})
|
||||
|
||||
return {
|
||||
resources,
|
||||
showResources,
|
||||
toggleResources,
|
||||
}
|
||||
}
|
||||
|
||||
export function useFence(fence: ShallowRef<HTMLDivElement | null>, config: MaybeRefOrGetter<DemoConfig | undefined>) {
|
||||
const data = ref<{
|
||||
js: string
|
||||
css: string
|
||||
html: string
|
||||
jsType: string
|
||||
cssType: string
|
||||
}>({ js: '', css: '', html: '', jsType: '', cssType: '' })
|
||||
|
||||
onMounted(() => {
|
||||
if (!fence.value)
|
||||
return
|
||||
const conf = toValue(config)
|
||||
data.value.html = conf?.html ?? ''
|
||||
const els = Array.from(fence.value.querySelectorAll('div[class*="language-"]'))
|
||||
for (const el of els) {
|
||||
const lang = el.className.match(/language-(\w+)/)?.[1] ?? ''
|
||||
const content = el.querySelector('pre')?.textContent ?? ''
|
||||
if (lang === 'js' || lang === 'javascript') {
|
||||
data.value.js = content
|
||||
data.value.jsType = 'js'
|
||||
}
|
||||
if (lang === 'ts' || lang === 'typescript') {
|
||||
data.value.js = content
|
||||
data.value.jsType = 'ts'
|
||||
}
|
||||
if (lang === 'css' || lang === 'scss' || lang === 'less' || lang === 'stylus' || lang === 'styl') {
|
||||
data.value.css = content
|
||||
data.value.cssType = lang === 'styl' ? 'stylus' : lang
|
||||
}
|
||||
}
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export function useNormalDemo(
|
||||
draw: ShallowRef<HTMLIFrameElement | null>,
|
||||
title: MaybeRefOrGetter<string | undefined>,
|
||||
config: MaybeRefOrGetter<DemoConfig | undefined>,
|
||||
) {
|
||||
const current = getCurrentInstance()
|
||||
const id = useId()
|
||||
const isDark = computed(() => current?.appContext.config.globalProperties.$isDark.value)
|
||||
const height = ref('100px')
|
||||
|
||||
onMounted(() => {
|
||||
if (!draw.value)
|
||||
return
|
||||
const iframeDoc = draw.value.contentDocument || draw.value.contentWindow?.document
|
||||
|
||||
if (!iframeDoc)
|
||||
return
|
||||
|
||||
const templateId = `VPDemoNormalDraw${id}`
|
||||
useEventListener('message', (event) => {
|
||||
const data = parseData(event.data)
|
||||
if (data.type === templateId) {
|
||||
height.value = `${data.height + 5}px`
|
||||
}
|
||||
})
|
||||
watch([config, title], () => {
|
||||
iframeDoc.write(createHTMLTemplate(toValue(title) || 'Demo', templateId, toValue(config)))
|
||||
}, { immediate: true })
|
||||
|
||||
watch(isDark, () => {
|
||||
iframeDoc.documentElement.dataset.theme = isDark.value ? 'dark' : 'light'
|
||||
}, { immediate: true })
|
||||
})
|
||||
|
||||
return { id, height }
|
||||
}
|
||||
|
||||
function createHTMLTemplate(title: string, id: string, config?: DemoConfig): string {
|
||||
const { cssLib = [], jsLib = [], html, css, script } = config || {}
|
||||
const stylesheet = cssLib.map(url => `<link rel="stylesheet" href="${url}">`).join('')
|
||||
const scripts = jsLib.map(url => `<script src="${url}"></script>`).join('')
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>${title}</title>
|
||||
${stylesheet}${scripts}
|
||||
<style>${css}</style>
|
||||
</head>
|
||||
<body>
|
||||
${html}
|
||||
<script>;(function(){${script}})();</script>
|
||||
<script>;(function(){
|
||||
const height = Math.ceil(document.documentElement.getBoundingClientRect().height)
|
||||
window.parent?.postMessage({ type: '${id}', height }, '*')
|
||||
if (typeof window.ResizeObserver === 'undefined')
|
||||
return
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
const height = Math.ceil(document.documentElement.getBoundingClientRect().height)
|
||||
window.parent?.postMessage({ type: '${id}', height }, '*')
|
||||
})
|
||||
resizeObserver.observe(document.documentElement)
|
||||
})();</script>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
export function parseData(data: any) {
|
||||
try {
|
||||
if (typeof data === 'string') {
|
||||
return JSON.parse(data)
|
||||
}
|
||||
else if (isPlainObject(data)) {
|
||||
return data
|
||||
}
|
||||
return {}
|
||||
}
|
||||
catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,13 @@
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.vp-demo-wrapper .demo-draw .draw-iframe {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.vp-demo-wrapper .demo-info .title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
const cache: {
|
||||
[src: string]: Promise<void> | undefined
|
||||
} = {}
|
||||
export function loadScript(src: string) {
|
||||
if (__VUEPRESS_SSR__)
|
||||
return Promise.resolve()
|
||||
|
||||
if (document.querySelector(`script[src="${src}"]`)) {
|
||||
if (cache[src])
|
||||
return cache[src]
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = src
|
||||
document.body.appendChild(script)
|
||||
|
||||
cache[src] = new Promise((resolve, reject) => {
|
||||
script.onload = () => {
|
||||
resolve()
|
||||
delete cache[src]
|
||||
}
|
||||
script.onerror = reject
|
||||
})
|
||||
return cache[src]
|
||||
}
|
||||
|
||||
export function loadStyle(href: string, target: ShadowRoot) {
|
||||
if (__VUEPRESS_SSR__)
|
||||
return
|
||||
|
||||
if (target.querySelector(`link[href="${href}"]`))
|
||||
return
|
||||
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.href = href
|
||||
target.appendChild(link)
|
||||
}
|
||||
@ -80,9 +80,11 @@ export function demoWatcher(app: App, watchers: any[]) {
|
||||
watcher!.unwatch(path)
|
||||
})
|
||||
|
||||
watchers.push(() => {
|
||||
watcher!.close()
|
||||
watcher = null
|
||||
watchers.push({
|
||||
close: () => {
|
||||
watcher!.close()
|
||||
watcher = null
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -2,8 +2,8 @@ import { defineConfig, type Options } from 'tsup'
|
||||
import { argv } from '../../scripts/tsup-args.js'
|
||||
|
||||
const config = [
|
||||
{ dir: 'composables', files: ['codeRepl.ts', 'pdf.ts', 'rustRepl.ts', 'size.ts', 'audio.ts'] },
|
||||
{ dir: 'utils', files: ['http.ts', 'is.ts', 'link.ts', 'sleep.ts', 'shared.ts'] },
|
||||
{ dir: 'composables', files: ['codeRepl.ts', 'pdf.ts', 'rustRepl.ts', 'size.ts', 'audio.ts', 'demo.ts'] },
|
||||
{ dir: 'utils', files: ['http.ts', 'is.ts', 'link.ts', 'sleep.ts'] },
|
||||
{ dir: '', files: ['index.ts', 'options.ts'] },
|
||||
]
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user