完成代码沙盒组件

master
yutent 2023-04-20 12:12:59 +08:00
parent f3bbec5536
commit 1b2a31a833
3 changed files with 334 additions and 1 deletions

View File

@ -13,7 +13,7 @@
- @bd/core 针对`web components`的核心封装库, 以数据驱动, 可以更方便的开发 wc 组件 - @bd/core 针对`web components`的核心封装库, 以数据驱动, 可以更方便的开发 wc 组件
### 开发进度 && 计划 (32/54) ### 开发进度 && 计划 (33/54)
- [x] `wc-card` 卡片组件 - [x] `wc-card` 卡片组件
- [x] `wc-space` 间隔组件 - [x] `wc-space` 间隔组件
@ -69,6 +69,7 @@
- [ ] `wc-table` 表格组件 - [ ] `wc-table` 表格组件
- [ ] `wc-result` 结果反馈组件 - [ ] `wc-result` 结果反馈组件
- [ ] `wc-empty` 空状态组件 - [ ] `wc-empty` 空状态组件
- [x] `wc-sandbox` 代码沙盒组件
### 测试预览 ### 测试预览

319
src/sandbox/index.js Normal file
View File

@ -0,0 +1,319 @@
/**
* {选项卡组件}
* @author yutent<yutent.io@gmail.com>
* @date 2023/03/06 15:17:25
*/
import {
css,
html,
bind,
Component,
nextTick,
styleMap,
classMap
} from '@bd/core'
import '../icon/index.js'
import { gzip } from '//jscdn.ink/@bytedo/gzip/2.1.0/gzip.js'
class Sandbox extends Component {
static props = {
tab: { type: String, default: 'preview', attribute: false }
}
static styles = css`
:host {
display: flex;
flex-direction: column;
border: 1px solid var(--color-plain-3);
font-size: 14px;
background: #fff;
}
.navs {
position: relative;
display: flex;
width: 100%;
height: 38px;
color: var(--color-dark-1);
background: var(--color-plain-1);
box-shadow: inset 0 -1px 0 0px var(--color-plain-3);
user-select: none;
}
.label {
position: relative;
display: inline-flex;
align-items: center;
justify-content: space-between;
max-width: 120px;
padding: 0 16px;
border-right: 1px solid var(--color-plain-3);
border-bottom: 1px solid var(--color-plain-3);
background: var(--color-plain-1);
--size: 16px;
cursor: pointer;
&:hover:not([disabled]),
&.active {
color: var(--color-teal-1);
}
&.active {
background: #fff;
border-bottom-color: transparent;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
}
.open {
position: absolute;
right: 12px;
top: 8px;
}
.content {
flex: 1;
}
iframe {
width: 100%;
border: 0;
background: #fff;
}
`
#cache = {
preview: { disabled: false, code: '' },
javascript: { disabled: true, code: '' },
html: { disabled: true, code: '' },
css: { disabled: true, code: '' }
}
#created = false
selectTab(ev) {
let elem = ev.target
let key
if (elem === ev.currentTarget) {
return
}
if (elem.tagName === 'LABEL') {
key = elem.dataset.key
if (key === this.tab) {
return
}
if (this.#cache[key].disabled) {
return
}
this.#cache[this.tab].panel
.$animate(true)
.then(_ => this.#cache[key].panel.$animate())
this.tab = key
} else {
window.open('https://bd-js.github.io/playground.html#' + gzip(this.code))
}
}
get code() {
let { javascript, html, css } = this.#cache
let code = { js: javascript.code, html: html.code, css: css.code }
return JSON.stringify(code)
}
created() {
bind(this.root, 'slotchange', ev => {
let slot = ev.target.assignedNodes().pop()
// 移除不合法的子组件
if (slot.tagName !== 'WC-LANG') {
return
}
let lang = slot.getAttribute('slot')
if (lang) {
this.#cache[lang].disabled = false
this.#cache[lang].code = slot.code
this.#cache[lang].panel = slot
}
this.updatePreview(lang)
this.$requestUpdate()
})
}
updatePreview(lang) {
let doc = this.$refs.preview.contentWindow.document
if (this.#created) {
switch (lang) {
case 'css':
doc.head.querySelector('style').innerText = this.#cache.css.code
break
case 'javascript':
doc.head.querySelector('script[type="module"]').innerText =
this.#cache.javascript.code
break
case 'html':
doc.body.innerHTML = this.#cache.html.code
break
}
} else {
let html = `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/dist/css/reset-basic.css">
<style>${this.#cache.css.code}</style>
<style>body {padding:32px;}</style>
<script type="importmap">{"imports":{"es.shim":"//jscdn.ink/es.shim/2.1.1/index.js","vue":"//jscdn.ink/vue/3.2.47/vue.esm-browser.prod.js","vue-router":"//jscdn.ink/@bytedo/vue-router/4.1.6/vue-router.js","fetch":"//jscdn.ink/@bytedo/fetch/2.1.5/next.js","@bd/core":"//jscdn.ink/@bd/core/1.9.0/index.js"}}</script>
<script type="module">${this.#cache.javascript.code}</script>
</head>
<body>
${this.#cache.html.code}
</body>
</html>`
try {
doc.open()
doc.write(html)
doc.close()
this.#created = true
} catch (e) {}
}
}
mounted() {
//
this.#cache.preview.panel = this.$refs.preview
}
render() {
let labels = ['preview', 'javascript', 'html', 'css']
return html`
<header class="navs" ref="navs" @click=${this.selectTab}>
${labels.map(
it => html`
<label
class=${classMap({
label: true,
active: it === this.tab
})}
data-key=${it}
disabled=${this.#cache[it].disabled}
>
${it}
</label>
`
)}
<wc-icon
title="在playground中打开"
class="open"
name="fly"
size="s"
></wc-icon>
</header>
<div class="content">
<iframe ref="preview" #animation=${{}}></iframe>
<slot name="javascript"></slot>
<slot name="html"></slot>
<slot name="css"></slot>
</div>
`
}
}
class Lang extends Component {
static animation = {}
static props = {
code: { type: String, default: '', attribute: false }
}
static styles = [
css`
:host {
display: flex;
position: relative;
width: 100%;
margin: 10px 0;
border-radius: 3px;
background: #f7f8fb;
color: var(--color-dark-1);
}
`,
css`
.code-block {
display: flex;
flex-direction: column;
overflow: hidden;
overflow-y: auto;
padding: 6px 0;
line-height: 20px;
font-size: 14px;
color: var(--color-dark-1);
cursor: text;
counter-reset: code;
code {
display: block;
position: relative;
min-height: 20px;
padding: 0 8px 0 45px;
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
white-space: pre-wrap;
word-break: break-word;
&::before {
position: absolute;
left: 0;
width: 40px;
height: 100%;
padding-right: 5px;
text-align: right;
color: var(--color-grey-1);
content: counter(code);
counter-increment: code;
}
}
}
`
]
mounted() {
var txt = this.innerHTML || this.textContent
txt = txt.trim().replace(/^[\r\n]|\s{2,}$/g, '')
if (txt.startsWith('<xmp>') && txt.endsWith('</xmp>')) {
txt = txt.slice(5, -6).trim()
}
if (txt) {
this.textContent = ''
nextTick(_ => {
this.code = txt.replace(/&lt;/g, '<').replace(/&gt;/g, '>')
})
}
}
render() {
return html`
<div class="code-block">
${this.code.split('\n').map(s => html`<code>${s}</code>`)}
</div>
`
}
}
Sandbox.reg('sandbox')
Lang.reg('lang')

View File

@ -484,6 +484,19 @@ class Tabs extends Component {
bind(this.root, 'slotchange', ev => { bind(this.root, 'slotchange', ev => {
let children = ev.target.assignedNodes() let children = ev.target.assignedNodes()
// 移除不合法的子组件
for (let it of children) {
if (it.tagName === 'WC-TAB') {
continue
}
it.remove()
return
}
if (children.length === 0) {
return
}
this.labels = children.map((it, i) => { this.labels = children.map((it, i) => {
let tmp = { let tmp = {
label: it.label, label: it.label,