feat: use live editor on desktop

This commit is contained in:
Justineo 2021-11-20 13:22:48 +08:00 committed by GU Yiling
parent 54393e41bc
commit 6fd9a5a4f4
20 changed files with 1257 additions and 1037 deletions

26
common/transform.js Normal file
View File

@ -0,0 +1,26 @@
import less from 'less/dist/less'
const lessRE = /<style([^>]* lang="less")?[^>]*>([\s\S]*?)<\/style>/gi
export function transformLessCode (sfcCode) {
return sfcCode.replace(lessRE, (_, p1, p2) => {
const lessCode = p2.trim()
const cssCode = render(lessCode)
return `<style${p1}>${cssCode}</style>`
})
}
function render (code) {
let css = null
less.render(code, {
syncImport: true
}, (err, output) => {
if (err) {
throw err
}
css = output.css
})
return css
}

View File

@ -1,7 +1,7 @@
<template>
<article
class="one-demo"
:class="{ expanded: localExpanded }"
:class="{ expanded }"
>
<section class="demo">
<browser-window
@ -26,27 +26,35 @@
ui="icon"
@click="play('CodeSandbox')"
>
<veui-icon
name="one-demo-codesandbox"
/>
<veui-icon name="one-demo-codesandbox"/>
</veui-button>
<veui-button
v-tooltip="t('playInStackBlitz')"
ui="icon"
@click="play('StackBlitz')"
>
<veui-icon
name="one-demo-stackblitz"
/>
<veui-icon name="one-demo-stackblitz"/>
</veui-button>
<veui-button
v-tooltip="t(localExpanded ? 'hideCode' : 'showCode')"
v-tooltip="t('expandEditor')"
class="toggle-editor"
ui="icon"
@click="localExpanded = !localExpanded"
@click="editing = true"
>
<veui-icon
scale="1.2"
:name="localExpanded ? 'one-demo-code-off' : 'one-demo-code'"
:name="expanded ? 'one-demo-code-off' : 'one-demo-code'"
/>
</veui-button>
<veui-button
v-tooltip="t(expanded ? 'hideCode' : 'showCode')"
class="toggle-source"
ui="icon"
@click="expanded = !expanded"
>
<veui-icon
scale="1.2"
:name="expanded ? 'one-demo-code-off' : 'one-demo-code'"
/>
</veui-button>
<one-edit-link
@ -59,16 +67,25 @@
v-if="$slots.source"
ref="source"
class="source"
:style="{ height: localExpanded ? `${sourceHeight || 0}px` : '0' }"
:style="{ height: expanded ? `${sourceHeight || 0}px` : '0' }"
>
<slot name="source"/>
</section>
<transition name="editor">
<one-repl
v-if="editing"
class="one-demo-editor"
:code="code"
@close="editing = false"
/>
</transition>
</article>
</template>
<script>
import { Button, Icon } from 'veui'
import tooltip from 'veui/directives/tooltip'
import modal from 'veui/managers/modal'
import i18n from 'veui/mixins/i18n'
import { BrowserWindow } from 'vue-windows'
import { getLocale } from '../common/i18n'
@ -84,26 +101,29 @@ export default {
'veui-button': Button,
'veui-icon': Icon,
BrowserWindow,
OneEditLink
OneEditLink,
OneRepl: () => import('./OneRepl')
},
mixins: [i18n],
props: {
expanded: Boolean,
browser: String,
path: String
},
data () {
return {
code: '',
sourceHeight: 0,
localExpanded: this.expanded
expanded: false,
editing: false
}
},
watch: {
expanded (val) {
this.localExpanded = val
},
localExpanded (val) {
this.$emit('update:expanded', val)
editing (value) {
if (value) {
modal.open()
} else {
modal.close()
}
}
},
mounted () {
@ -113,6 +133,8 @@ export default {
style.height = source.offsetHeight
this.sourceHeight = source.offsetHeight
style.height = '0'
this.code = this.$refs.source?.textContent
},
methods: {
play (vendor) {
@ -126,22 +148,26 @@ Icon.register({
'one-demo-code': {
width: 24,
height: 24,
d: 'M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6l6 6l1.4-1.4zm5.2 0l4.6-4.6l-4.6-4.6L16 6l6 6l-6 6l-1.4-1.4z'
d:
'M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6l6 6l1.4-1.4zm5.2 0l4.6-4.6l-4.6-4.6L16 6l6 6l-6 6l-1.4-1.4z'
},
'one-demo-code-off': {
width: 24,
height: 24,
d: 'M19.17 12l-4.58-4.59L16 6l6 6l-3.59 3.59L17 14.17L19.17 12zM1.39 4.22l4.19 4.19L2 12l6 6l1.41-1.41L4.83 12L7 9.83l12.78 12.78l1.41-1.41L2.81 2.81L1.39 4.22z'
d:
'M19.17 12l-4.58-4.59L16 6l6 6l-3.59 3.59L17 14.17L19.17 12zM1.39 4.22l4.19 4.19L2 12l6 6l1.41-1.41L4.83 12L7 9.83l12.78 12.78l1.41-1.41L2.81 2.81L1.39 4.22z'
},
'one-demo-codesandbox': {
width: 32,
height: 32,
d: 'M2.667 8l13.938-8l13.943 8l.12 15.932L16.605 32L2.667 24zm2.786 3.307v6.344l4.458 2.479v4.688l5.297 3.063V16.85zm22.318 0l-9.755 5.542V27.88l5.292-3.063v-4.682l4.464-2.484zM6.844 8.802l9.74 5.526l9.76-5.573l-5.161-2.932l-4.547 2.594l-4.573-2.625z'
d:
'M2.667 8l13.938-8l13.943 8l.12 15.932L16.605 32L2.667 24zm2.786 3.307v6.344l4.458 2.479v4.688l5.297 3.063V16.85zm22.318 0l-9.755 5.542V27.88l5.292-3.063v-4.682l4.464-2.484zM6.844 8.802l9.74 5.526l9.76-5.573l-5.161-2.932l-4.547 2.594l-4.573-2.625z'
},
'one-demo-stackblitz': {
width: 28,
height: 28,
d: 'M12.747 16.273h-7.46L18.925 1.5l-3.671 10.227h7.46L9.075 26.5l3.671-10.227z'
d:
'M12.747 16.273h-7.46L18.925 1.5l-3.671 10.227h7.46L9.075 26.5l3.671-10.227z'
}
})
</script>
@ -208,4 +234,33 @@ Icon.register({
top 50%
transform translateY(-50%)
font-size 12px
.one-demo-editor
position fixed
top 0
left 0
right 0
bottom 0
z-index 10
background-color #fff
.editor-enter-active
.editor-leave-active
transform-origin 50% 50%
transition all 0.3s
.editor-enter
.editor-leave-to
opacity 0
transform scale(0.99) translateY(3px)
.toggle-source
display none
@media (max-width 480px)
.toggle-source
display inline-block
.toggle-editor
display none
</style>

262
components/OneLive.vue Normal file
View File

@ -0,0 +1,262 @@
<template>
<v-splitpanes class="one-live">
<v-pane
min-size="30"
class="live-editor"
>
<v-live-editor
:code="localCode"
line-numbers
@change="handleChange"
/>
<div class="editor-toolbar">
<veui-button
v-tooltip="t('@onedemo.playInCodeSandbox')"
ui="s translucent square"
@click="play('CodeSandbox')"
>
<veui-icon name="one-demo-codesandbox"/>
</veui-button>
<veui-button
v-tooltip="t('@onedemo.playInStackBlitz')"
ui="s translucent square"
@click="play('StackBlitz')"
>
<veui-icon name="one-demo-stackblitz"/>
</veui-button>
<veui-button
v-tooltip="t('reset')"
ui="s translucent square"
@click="reset"
>
<veui-icon name="anticlockwise"/>
</veui-button>
<veui-button
v-tooltip="t('copyCode')"
ui="s translucent square"
@click="copy"
>
<veui-icon name="copy"/>
</veui-button>
<div class="editor-live-badge">
<span>Live</span>
</div>
</div>
</v-pane>
<v-pane
min-size="40"
class="live-preview"
>
<v-live-preview
:code="transformedCode"
:requires="imports"
:check-variable-availability="false"
@success="dismissError"
@error="handleError"
/>
<transition name="editor-error">
<veui-alert
v-if="error"
v-tooltip="t('dismiss')"
ui="s"
type="error"
class="editor-error"
@click.native="dismissError"
>
<code>{{ errorMessage }}</code>
</veui-alert>
</transition>
</v-pane>
</v-splitpanes>
</template>
<script>
import Vue from 'vue'
import { VueLiveEditor, VueLivePreview } from 'vue-live'
import 'vue-live/lib/vue-live.esm.css'
import 'prism-theme-night-owl/build/no-italics.css'
import { Button, Icon, Alert } from 'veui'
import * as veui from 'veui'
import lodash from 'lodash'
import 'veui-theme-dls-icons'
import tooltip from 'veui/directives/tooltip'
import i18n from 'veui/mixins/i18n'
import toast from 'veui/plugins/toast'
import 'veui-theme-dls-icons/copy'
import 'veui-theme-dls-icons/anticlockwise'
import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
import { getLocale } from '../common/i18n'
import { play } from '../common/play'
import { transformLessCode } from '../common/transform'
Vue.use(toast)
export default {
name: 'one-live',
components: {
'veui-button': Button,
'veui-icon': Icon,
'veui-alert': Alert,
'v-splitpanes': Splitpanes,
'v-pane': Pane,
'v-live-editor': VueLiveEditor,
'v-live-preview': VueLivePreview
},
directives: {
tooltip
},
mixins: [i18n],
inheritAttrs: false,
props: {
code: {
type: String,
default: ''
}
},
data () {
return {
localCode: this.code,
transformedCode: '',
error: null,
imports: {
veui,
lodash,
'veui-theme-dls-icons': {}
}
}
},
computed: {
errorMessage () {
const { error } = this
if (!error) {
return null
}
return error.name ? `${error.name}: ${error.message}` : error.message
}
},
watch: {
localCode: {
immediate: true,
handler (code) {
this.$nextTick(() => {
try {
this.transformedCode = transformLessCode(code)
} catch (e) {
this.error = e
return
}
this.error = null
})
}
}
},
methods: {
play (vendor) {
let locale = getLocale(this.$route.path)
play(this.code, { locale, vendor })
},
async copy () {
try {
await navigator.clipboard.writeText(this.code)
this.$toast.success(this.t('copySuccess'))
} catch (e) {
this.$toast.error(this.t('copyFailed'))
}
},
reset () {
this.localCode = this.code
},
handleChange (code) {
this.localCode = code
},
handleError (error) {
this.error = error
},
dismissError () {
this.error = null
}
}
}
</script>
<style lang="stylus" scoped>
.one-live
& >>> .splitpanes__pane
position relative
& >>> .splitpanes__splitter
width 6px
background #eee
transition all 0.3s
&:hover
background #ccc
transform scaleX(2)
.editor-toolbar
position absolute
top 12px
right 20px
display flex
align-items center
.editor-live-badge
display flex
align-items center
position relative
margin-left 8px
padding 0 4px 0 20px
border-radius 2px
font-size 12px
background-color #00bf5c
color #fff
height 18px
span
position relative
top -1px
&::before
content ""
position absolute
left 7px
top 6px
width 6px
height 6px
border-radius 50%
background-color #fff
box-shadow 0 0 0 0 rgba(255, 255, 255, 1)
animation pulse 2s infinite
.editor-error
position absolute
bottom 16px
right 16px
left 16px
cursor pointer
transition all 0.3s
&:hover
opacity 0.8
.editor-error-enter
.editor-error-leave-to
opacity 0
transform translateY(10px)
@keyframes pulse
0%
transform scale(0.95)
box-shadow 0 0 0 0 rgba(255, 255, 255, 0.9)
70%
transform scale(1)
box-shadow 0 0 0 12px rgba(255, 255, 255, 0)
100%
transform scale(0.95)
box-shadow 0 0 0 0 rgba(255, 255, 255, 0)
</style>

104
components/OneRepl.vue Normal file
View File

@ -0,0 +1,104 @@
<template>
<article class="repl">
<header class="header">
<h1>{{ t('liveEdit') }}</h1>
<section class="actions">
<veui-button
ui="strong text"
@click="handleClose"
>
{{ t('exit') }}
</veui-button>
</section>
</header>
<one-live
class="editor"
:code="code"
/>
</article>
</template>
<script>
import { Button } from 'veui'
import i18n from 'veui/mixins/i18n'
import OneLive from './OneLive.vue'
export default {
name: 'one-repl',
components: {
'veui-button': Button,
'one-live': OneLive
},
mixins: [i18n],
props: {
code: {
type: String,
default: ''
}
},
methods: {
handleClose () {
this.$emit('close')
}
}
}
</script>
<style lang="stylus" scoped>
.repl
display flex
flex-direction column
.header
display flex
align-items center
flex none
height 48px
padding 0 24px
box-shadow 0 0 4px #0006
position relative
h1
flex none
margin 0
font-size 16px
.actions
display flex
justify-content flex-end
flex 1 1 auto
.editor
flex 1 1 auto
height calc(100vh - 48px)
& >>> .prism-editor-wrapper
padding 8px 12px
font-size 12px
color #eee
background-color #0a0b0d
line-height 1.5
font-family Menlo, consolas, monospace
-webkit-font-smoothing auto
&::-webkit-scrollbar
width 8px
background transparent
transition all 0.3s
&-thumb
border-radius 4px
background-color #282c33
&:hover::-webkit-scrollbar-thumb
background-color #545b66
textarea
outline none
& >>> .live-preview
padding 24px 36px
& >>> .VueLive-error
display none
</style>

View File

@ -58,7 +58,7 @@ module.exports = {
transpile: ['veui', 'vue-awesome', 'resize-detector', 'less-plugin-dls', 'dls-graphics'],
babel: {
plugins: ['veui', 'lodash']
plugins: ['veui']
},
loaders: {
@ -104,6 +104,34 @@ module.exports = {
expr: "process.env.VUE_ENV === 'server'"
}
})
config.resolve.alias.vue$ = 'vue/dist/vue.esm.js'
config.resolve.alias['vue-inbrowser-compiler-utils'] = '@justfork/vue-inbrowser-compiler-utils'
},
optimization: {
splitChunks: {
cacheGroups: {
veui: {
test: /node_modules[\\/]veui/,
chunks: 'all',
priority: 20,
name: true
},
'veui-theme-dls-icons': {
test: /node_modules[\\/]veui-theme-dls-icons/,
chunks: 'all',
priority: 20,
name: true
},
'vue-live': {
test: /node_modules[\\/]vue-live/,
chunks: 'all',
priority: 20,
name: true
}
}
}
}
}
}

View File

@ -70,7 +70,7 @@
| ``readonly`` | `boolean=` | `false` | 是否为只读状态。 |
| ``overlay-class`` | `string | Array | Object=` | - | 参考 [`Overlay`](./overlay) 组件的 [`overlay-class`](./overlay#props-overlay-class) 属性。 |
| ``overlay-style`` | `string | Array | Object=` | - | 参考 [`Overlay`](./overlay) 组件的 [`overlay-style`](./overlay#props-overlay-style) 属性。 |
| ``match`` | `(item, keyword, { ancestors }) => boolean | [number, number] | Array<[number, number]>` | - | 支持自定义高亮逻辑, 默认大小写不敏感,参考 [`Autocomplete`](./Autocomplete#自定义搜索逻辑)。 |
| ``match`` | `(item, keyword, { ancestors }) => boolean | Array<[number, number]>` | - | 支持自定义高亮逻辑, 默认大小写不敏感,参考 [`Autocomplete`](./Autocomplete#自定义搜索逻辑)。 |
| ``filter`` | `(item, keyword, { ancestors, offsets }) => boolean` | - | 支持自定义搜索命中逻辑,参考 [`Autocomplete`](./Autocomplete#自定义搜索逻辑)。 |
^^^ui

View File

@ -57,7 +57,7 @@
| ``disabled`` | `boolean=` | `false` | 是否为禁用状态。 |
| ``overlay-class`` | `string | Array | Object=` | - | 参考 [`Overlay`](./overlay) 组件的 [`overlay-class`](./overlay#props-overlay-class) 属性。 |
| ``overlay-style`` | `string | Array | Object=` | - | 参考 [`Overlay`](./overlay) 组件的 [`overlay-style`](./overlay#props-overlay-style) 属性。 |
| ``match`` | `(item, keyword, { ancestors }) => boolean | [number, number] | Array<[number, number]>` | - | 支持自定义高亮逻辑, 默认大小写不敏感,参考 [`Autocomplete`](./Autocomplete#自定义搜索逻辑)。 |
| ``match`` | `(item, keyword, { ancestors }) => boolean | Array<[number, number]>` | - | 支持自定义高亮逻辑, 默认大小写不敏感,参考 [`Autocomplete`](./Autocomplete#自定义搜索逻辑)。 |
| ``filter`` | `(item, keyword, { ancestors, offsets }) => boolean` | - | 支持自定义搜索命中逻辑,参考 [`Autocomplete`](./Autocomplete#自定义搜索逻辑)。 |
^^^ui

View File

@ -41,7 +41,7 @@
| ``expanded`` | `boolean=` | `false` | [^expanded] |
| ``disabled`` | `boolean=` | `false` | 是否为禁用状态。 |
| ``readonly`` | `boolean=` | `false` | 是否为只读状态。 |
| ``match`` | `(item, keyword, { ancestors }) => boolean | [number, number] | Array<[number, number]>` | - | 支持自定义高亮逻辑, 默认大小写不敏感,参考 [`Autocomplete`](./Autocomplete#自定义搜索逻辑)。 |
| ``match`` | `(item, keyword, { ancestors }) => boolean | Array<[number, number]>` | - | 支持自定义高亮逻辑, 默认大小写不敏感,参考 [`Autocomplete`](./Autocomplete#自定义搜索逻辑)。 |
| ``filter`` | `(item, keyword, { ancestors, offsets }) => boolean` | - | 支持自定义搜索命中逻辑,参考 [`Autocomplete`](./Autocomplete#自定义搜索逻辑)。 |
^^^ui

View File

@ -6,9 +6,13 @@
message="恭喜你,你的请求已成功处理"
closable
>
<template slot="title">恭喜你</template>
<template slot="title">
恭喜你
</template>
<template slot="extra">
<veui-button ui="text">查看详情</veui-button>
<veui-button ui="text">
查看详情
</veui-button>
</template>
恭喜你你的请求已成功处理
</veui-alert>

View File

@ -10,7 +10,9 @@
type="success"
>
Your profile has been updated.
<template slot="title">消息标题</template>
<template slot="title">
消息标题
</template>
</veui-alert>
</article>
</template>

View File

@ -101,7 +101,6 @@ export default {
top: 10px;
}
}
</style>
<docs>

View File

@ -132,7 +132,6 @@ export default {
top: 50px;
}
}
</style>
<docs>

View File

@ -108,7 +108,6 @@ export default {
top: 50px;
}
}
</style>
<docs>

View File

@ -46,7 +46,7 @@ export default {
}
</script>
<style lang="less" scoped docs>
<style lang="less" scoped>
h4 {
margin: 0 0 10px;
}

View File

@ -1,22 +1,28 @@
<template>
<article>
<veui-pagination
:page="page"
:total="total"
:to="to"
/>
<veui-pagination
:page="page"
:total="total"
:to="to"
ui="s"
/>
<veui-pagination
:page="page"
:total="total"
:to="to"
ui="xs"
/>
<section>
<veui-pagination
:page="page"
:total="total"
:to="to"
/>
</section>
<section>
<veui-pagination
:page="page"
:total="total"
:to="to"
ui="s"
/>
</section>
<section>
<veui-pagination
:page="page"
:total="total"
:to="to"
ui="xs"
/>
</section>
</article>
</template>

View File

@ -1,7 +1,7 @@
<template>
<article>
<section>
<p>loading<veui-switch v-model="loading"/></p>
<div>loading<veui-switch v-model="loading"/></div>
<veui-table
:data="data"
:loading="loading"

View File

@ -1,12 +1,12 @@
<template>
<article>
<section>
<p>
<div>
允许不排序<veui-switch
v-model="allowFalse"
@change="handleChange"
/>
</p>
</div>
<veui-table
:data="sorted"
key-field="id"

1642
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@
"devDependencies": {
"@docsearch/css": "^3.0.0-alpha.39",
"@docsearch/js": "^3.0.0-alpha.39",
"@justfork/vue-inbrowser-compiler-utils": "^4.42.0",
"@stackblitz/sdk": "^1.5.2",
"babel-eslint": "^10.1.0",
"babel-plugin-lodash": "^3.3.4",
@ -45,10 +46,10 @@
"hastscript": "^3.1.0",
"highlight.js": "^10.7.3",
"js-yaml": "^3.13.1",
"less": "3.9.0",
"less": "^3.13.1",
"less-loader": "^4.1.0",
"less-plugin-est": "^3.0.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lowlight": "^1.9.2",
"mdast-util-to-string": "^2.0.0",
"mkdirp": "^0.5.5",
@ -58,6 +59,7 @@
"preact": "^10.5.14",
"prettier": "^1.16.4",
"prettier-eslint": "^8.8.2",
"prism-theme-night-owl": "^1.4.0",
"raw-loader": "^4.0.2",
"recursive-readdir": "^2.2.2",
"recursive-readdir-sync": "^1.0.6",
@ -71,6 +73,7 @@
"remark-shortcodes": "^0.1.5",
"remark-slug": "^4.2.3",
"short-circuit-loader": "0.0.1-alpha.2",
"splitpanes": "^2.3.8",
"stringify-object": "^3.3.0",
"stylelint": "^13.6.1",
"stylelint-plugin-stylus": "^0.9.0",
@ -84,6 +87,7 @@
"veui-theme-dls-icons": "^2.2.1",
"vue-awesome": "^4.1.0",
"vue-i18n": "^8.16.0",
"vue-live": "^1.17.1",
"vue-windows": "^0.2.4"
},
"dependencies": {

View File

@ -5,6 +5,7 @@ i18n.register(
{
showCode: '显示代码',
hideCode: '隐藏代码',
expandEditor: '展开实时编辑',
playInCodeSandbox: '在 CodeSandbox 中打开',
playInStackBlitz: '在 StackBlitz 中打开'
},
@ -18,6 +19,7 @@ i18n.register(
{
showCode: 'Show code',
hideCode: 'Hide code',
expandEditor: 'Expand Live Editor',
playInCodeSandbox: 'Open in CodeSandbox',
playInStackBlitz: 'Open in StackBlitz'
},
@ -26,6 +28,56 @@ i18n.register(
}
)
i18n.register(
'zh-Hans',
{
copyCode: '复制代码',
copySuccess: '复制成功!',
copyFailed: '复制失败!',
reset: '重置',
dismiss: '关闭'
},
{
ns: 'onelive'
}
)
i18n.register(
'en-US',
{
copyCode: 'Copy code',
copySuccess: 'Copy success!',
copyFailed: 'Copy failed!',
reset: 'Reset',
dismiss: 'Dismiss'
},
{
ns: 'onelive'
}
)
i18n.register(
'zh-Hans',
{
exit: '退出',
liveEdit: '实时编辑'
},
{
ns: 'onerepl'
}
)
i18n.register(
'en-US',
{
exit: 'Exit',
liveEdit: 'Live Edit'
},
{
ns: 'onerepl'
}
)
i18n.register(
'zh-Hans',
{