diff --git a/Readme.md b/Readme.md index 3a4cf65..84fa455 100644 --- a/Readme.md +++ b/Readme.md @@ -13,7 +13,7 @@ - @bd/core 针对`web components`的核心封装库, 以数据驱动, 可以更方便的开发 wc 组件 -### 开发进度 && 计划 (32/54) +### 开发进度 && 计划 (33/54) - [x] `wc-card` 卡片组件 - [x] `wc-space` 间隔组件 @@ -69,6 +69,7 @@ - [ ] `wc-table` 表格组件 - [ ] `wc-result` 结果反馈组件 - [ ] `wc-empty` 空状态组件 +- [x] `wc-sandbox` 代码沙盒组件 ### 测试预览 diff --git a/src/sandbox/index.js b/src/sandbox/index.js new file mode 100644 index 0000000..d70d762 --- /dev/null +++ b/src/sandbox/index.js @@ -0,0 +1,319 @@ +/** + * {选项卡组件} + * @author yutent + * @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 = ` + + + + + + + + + + + ${this.#cache.html.code} + + ` + + 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` + +
+ + + + +
+ ` + } +} + +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('') && txt.endsWith('')) { + txt = txt.slice(5, -6).trim() + } + if (txt) { + this.textContent = '' + nextTick(_ => { + this.code = txt.replace(/</g, '<').replace(/>/g, '>') + }) + } + } + + render() { + return html` +
+ ${this.code.split('\n').map(s => html`${s}`)} +
+ ` + } +} + +Sandbox.reg('sandbox') +Lang.reg('lang') diff --git a/src/tabs/index.js b/src/tabs/index.js index cf9c1f5..be04c54 100644 --- a/src/tabs/index.js +++ b/src/tabs/index.js @@ -484,6 +484,19 @@ class Tabs extends Component { bind(this.root, 'slotchange', ev => { 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) => { let tmp = { label: it.label,