feat(plugin-md-power): improve normal demo sandbox (#448)

This commit is contained in:
pengzhanbo 2025-01-28 00:26:21 +08:00 committed by GitHub
parent 58cc9aba20
commit c703b89e1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 226 additions and 178 deletions

View File

@ -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)

View File

@ -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">

View File

@ -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)

View File

@ -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>

View File

@ -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">

View 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 {}
}
}

View File

@ -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;

View File

@ -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)
}

View File

@ -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
},
})
}

View File

@ -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'] },
]