diff --git a/packages/devui-vue/devui-cli/commands/build.js b/packages/devui-vue/devui-cli/commands/build.js index c9f61a08a6..337e15b34d 100644 --- a/packages/devui-vue/devui-cli/commands/build.js +++ b/packages/devui-vue/devui-cli/commands/build.js @@ -1,28 +1,28 @@ -const path = require('path') -const fs = require('fs') -const fsExtra = require('fs-extra') -const { defineConfig, build } = require('vite') -const vue = require('@vitejs/plugin-vue') -const vueJsx = require('@vitejs/plugin-vue-jsx') -const nuxtBuild = require('./build-nuxt-auto-import') +const path = require('path'); +const fs = require('fs'); +const fsExtra = require('fs-extra'); +const { defineConfig, build } = require('vite'); +const vue = require('@vitejs/plugin-vue'); +const vueJsx = require('@vitejs/plugin-vue-jsx'); +const nuxtBuild = require('./build-nuxt-auto-import'); -const entryDir = path.resolve(__dirname, '../../devui') -const outputDir = path.resolve(__dirname, '../../build') +const entryDir = path.resolve(__dirname, '../../devui'); +const outputDir = path.resolve(__dirname, '../../build'); const baseConfig = defineConfig({ configFile: false, publicDir: false, - plugins: [vue(), vueJsx()] -}) + plugins: [vue(), vueJsx()], +}); const rollupOptions = { - external: ['vue', 'vue-router'], + external: ['vue', 'vue-router', '@vueuse/core'], output: { globals: { - vue: 'Vue' - } - } -} + vue: 'Vue', + }, + }, +}; const buildSingle = async (name) => { await build( @@ -34,13 +34,13 @@ const buildSingle = async (name) => { entry: path.resolve(entryDir, name), name: 'index', fileName: 'index', - formats: ['es', 'umd'] + formats: ['es', 'umd'], }, - outDir: path.resolve(outputDir, name) - } + outDir: path.resolve(outputDir, name), + }, }) - ) -} + ); +}; const buildAll = async () => { await build( @@ -52,13 +52,13 @@ const buildAll = async () => { entry: path.resolve(entryDir, 'vue-devui.ts'), name: 'VueDevui', fileName: 'vue-devui', - formats: ['es', 'umd'] + formats: ['es', 'umd'], }, - outDir: outputDir - } + outDir: outputDir, + }, }) - ) -} + ); +}; const createPackageJson = (name) => { const fileStr = `{ @@ -67,25 +67,25 @@ const createPackageJson = (name) => { "main": "index.umd.js", "module": "index.es.js", "style": "style.css" -}` +}`; - fsExtra.outputFile(path.resolve(outputDir, `${name}/package.json`), fileStr, 'utf-8') -} + fsExtra.outputFile(path.resolve(outputDir, `${name}/package.json`), fileStr, 'utf-8'); +}; exports.build = async () => { - await buildAll() + await buildAll(); const components = fs.readdirSync(entryDir).filter((name) => { - const componentDir = path.resolve(entryDir, name) - const isDir = fs.lstatSync(componentDir).isDirectory() - return isDir && fs.readdirSync(componentDir).includes('index.ts') - }) + const componentDir = path.resolve(entryDir, name); + const isDir = fs.lstatSync(componentDir).isDirectory(); + return isDir && fs.readdirSync(componentDir).includes('index.ts'); + }); for (const name of components) { - await buildSingle(name) - createPackageJson(name) - nuxtBuild.createAutoImportedComponent(name) + await buildSingle(name); + createPackageJson(name); + nuxtBuild.createAutoImportedComponent(name); } - nuxtBuild.createNuxtPlugin() -} + nuxtBuild.createNuxtPlugin(); +}; diff --git a/packages/devui-vue/devui/drawer/index.ts b/packages/devui-vue/devui/drawer/index.ts index c0ac81fb9b..f0b0a21e7d 100644 --- a/packages/devui-vue/devui/drawer/index.ts +++ b/packages/devui-vue/devui/drawer/index.ts @@ -1,25 +1,21 @@ -import type { App } from 'vue' -import Drawer from './src/drawer' -import DrawerService from './src/drawer-service' +import type { App } from 'vue'; +import Drawer from './src/drawer'; +import DrawerService from './src/drawer-service'; -Drawer.install = function(app: App): void { - app.component(Drawer.name, Drawer) -} - -export { Drawer, DrawerService } +export { Drawer, DrawerService }; // TODO: no-service model exists memory leak // rest tasks -// 1. draggable width -// 2. function of the 1st icon in header-component -// 3. rest service-model api +// 1. draggable width +// 2. function of the 1st icon in header-component +// 3. rest service-model api // 4. typescript type of props export default { title: 'Drawer 抽屉板', category: '反馈', status: '75%', install(app: App): void { - app.use(Drawer as any) - app.config.globalProperties.$drawerService = DrawerService - } -} + app.component(Drawer.name, Drawer); + app.config.globalProperties.$drawerService = new DrawerService(); + }, +}; diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-body.scss b/packages/devui-vue/devui/drawer/src/components/drawer-body.scss deleted file mode 100644 index 7aaba13f43..0000000000 --- a/packages/devui-vue/devui/drawer/src/components/drawer-body.scss +++ /dev/null @@ -1,43 +0,0 @@ -@import '../../../style/devui.scss'; - -.devui-drawer { - position: fixed; - top: 0; - left: 0; - height: 100vh; -} - -.devui-overlay-wrapper { - display: flex; - justify-content: center; - align-items: center; - position: absolute; - top: 0; - bottom: 0; - width: 100vw; -} - -.devui-overlay-backdrop { - position: absolute; - top: 0; - left: 0; - background: $devui-shadow; - width: 100vw; - height: 100vh; -} - -.devui-drawer-nav { - position: absolute; - top: 0; - bottom: 0; - border-radius: $devui-border-radius; - background: $devui-base-bg; -} - -.devui-drawer-content { - border-radius: $devui-border-radius; - overflow: auto; - box-shadow: $devui-shadow-length-fullscreen-overlay $devui-shadow; - padding: 20px; - height: 100vh; -} diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-body.tsx b/packages/devui-vue/devui/drawer/src/components/drawer-body.tsx deleted file mode 100644 index 6fba8f93e9..0000000000 --- a/packages/devui-vue/devui/drawer/src/components/drawer-body.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { defineComponent, inject, computed, Transition } from 'vue' - -import './drawer-body.scss' - -export default defineComponent({ - name: 'DrawerBody', - setup(props, { slots }) { - const isFullScreen: any = inject('isFullScreen') - const closeDrawer: any = inject('closeDrawer') - const zindex: number = inject('zindex') - const isCover: boolean = inject('isCover') - const position: any = inject('position') - const width: any = inject('width') - const visible: boolean = inject('visible') - const backdropCloseable: any = inject('backdropCloseable') - const destroyOnHide: any = inject('destroyOnHide') - const showAnimation: any = inject('showAnimation') - - const navRight = computed(() => position.value === 'right' ? { 'right': 0 } : { 'left': 0 }) - const navWidth = computed(() => isFullScreen.value ? '100vw' : width.value) - - const clickContent = (e) => { - e.stopPropagation() - } - - const handleDrawerClose = () => { - if (!backdropCloseable.value) return; - closeDrawer(); - } - - return { - zindex, - slots, - isCover, - navRight, - navWidth, - visible, - position, - showAnimation, - clickContent, - handleDrawerClose, - destroyOnHide, - } - }, - - render() { - const { - zindex, slots, isCover, navRight, navWidth, showAnimation, - visible, handleDrawerClose, destroyOnHide, position } = this - - if (destroyOnHide.value && !visible) { - return null - } - - const transitionName = showAnimation ? position : 'none' - - return ( -
- {isCover ?
: null} - -
-
-
- {slots.default ? slots.default() : null} -
-
-
-
-
- ) - } -}) \ No newline at end of file diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-container.tsx b/packages/devui-vue/devui/drawer/src/components/drawer-container.tsx deleted file mode 100644 index f348d456e2..0000000000 --- a/packages/devui-vue/devui/drawer/src/components/drawer-container.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { defineComponent, inject } from 'vue' - -export default defineComponent({ - name: 'DrawerContainer', - setup() { - const visible = inject('visible') - const destroyOnHide = inject('destroyOnHide') - return { visible, destroyOnHide } - }, - render() { - const { visible, destroyOnHide } = this - - if (destroyOnHide.value && !visible) { - return null - } - return
内容区域
- } -}) \ No newline at end of file diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-header.scss b/packages/devui-vue/devui/drawer/src/components/drawer-header.scss deleted file mode 100644 index ee1776a5d0..0000000000 --- a/packages/devui-vue/devui/drawer/src/components/drawer-header.scss +++ /dev/null @@ -1,11 +0,0 @@ -@import '../drawer.scss'; - -.devui-drawer-header { - display: flex; - flex-direction: row; - justify-content: flex-end; - - & .devui-drawer-header-item + .devui-drawer-header-item { - padding-left: 12px; - } -} diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-header.tsx b/packages/devui-vue/devui/drawer/src/components/drawer-header.tsx deleted file mode 100644 index 9adbe932db..0000000000 --- a/packages/devui-vue/devui/drawer/src/components/drawer-header.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { defineComponent, ref, inject, computed } from 'vue' - -import './drawer-header.scss' - -export default defineComponent({ - name: 'DrawerHeader', - emits: ['toggleFullScreen', 'close'], - setup(props, ctx) { - const isFullScreen = ref(false) - - const visible = inject('visible') - const destroyOnHide = inject('destroyOnHide') - - const fullScreenClassName = computed(() => isFullScreen.value ? 'icon icon-minimize' : 'icon icon-maxmize') - - const handleFullScreen = (e) => { - e.stopPropagation() - isFullScreen.value = !isFullScreen.value - ctx.emit('toggleFullScreen') - } - - const handleDrawerClose = () => { - ctx.emit('close') - } - - return { fullScreenClassName, visible, handleFullScreen, handleDrawerClose, destroyOnHide } - }, - render() { - const { - handleFullScreen, handleDrawerClose, visible, - fullScreenClassName, destroyOnHide - } = this - - if (destroyOnHide.value && !visible) { - return null - } - - return ( -
-
- -
-
- -
-
- -
-
- ) - } -}) diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-overlay.scss b/packages/devui-vue/devui/drawer/src/components/drawer-overlay.scss new file mode 100644 index 0000000000..1cec622700 --- /dev/null +++ b/packages/devui-vue/devui/drawer/src/components/drawer-overlay.scss @@ -0,0 +1,20 @@ +@import '../../../styles-var/devui-var.scss'; + +.devui-drawer-overlay { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: $devui-shadow; +} + +.drawer-overlay-fade-enter-active, +.drawer-overlay-fade-leave-active { + transition: opacity 0.1s linear; +} + +.drawer-overlay-fade-enter-from, +.drawer-overlay-fade-leave-to { + opacity: 0; +} diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-overlay.tsx b/packages/devui-vue/devui/drawer/src/components/drawer-overlay.tsx new file mode 100644 index 0000000000..973e4b3a0d --- /dev/null +++ b/packages/devui-vue/devui/drawer/src/components/drawer-overlay.tsx @@ -0,0 +1,18 @@ +import { defineComponent, Transition } from 'vue'; +import type { SetupContext } from 'vue'; +import { drawerOverlayProps, DrawerOverlayProps } from '../drawer-types'; +import './drawer-overlay.scss'; + +export default defineComponent({ + name: 'DDrawerOverlay', + props: drawerOverlayProps, + emits: ['click'], + setup(props: DrawerOverlayProps, ctx: SetupContext) { + const handleClick = (e: Event) => { + ctx.emit('click', e); + }; + return () => ( + {props.visible &&
}
+ ); + }, +}); diff --git a/packages/devui-vue/devui/drawer/src/drawer-service.tsx b/packages/devui-vue/devui/drawer/src/drawer-service.tsx index 2fe7bec3be..22dbae350d 100644 --- a/packages/devui-vue/devui/drawer/src/drawer-service.tsx +++ b/packages/devui-vue/devui/drawer/src/drawer-service.tsx @@ -1,77 +1,58 @@ -import { createApp } from 'vue' -import { DrawerProps } from './drawer-types' - -import DDrawer from './drawer' -import { omit } from 'lodash' - -interface drawerInstance { - hide(): void - hideDirectly(): void - destroy(): void -} - -function createDrawerApp(props: DrawerProps, drawer: drawerInstance, el: HTMLElement) { - if (drawer) { - return drawer - } - const restProps = omit(props, ['header', 'content', 'visible']) - - const res = createApp( - // BUG: this function generates a new app, v-model instructor of template is like not working - // TODO: could be fixed by using self-defined header slot - {{ header: props.header, content: props.content }} - ) - res.mount(el) - return res +import { createApp, nextTick, onUnmounted, reactive } from 'vue'; +import type { App } from 'vue'; +import Drawer from './drawer'; +import { DrawerOptions } from './drawer-types'; + +const defaultOptions: DrawerOptions = { + modelValue: false, + content: '', + zIndex: 1000, + showOverlay: true, + escKeyCloseable: true, + position: 'right', + lockScroll: true, + closeOnClickOverlay: true, +}; + +function initInstance(state: DrawerOptions): App { + const container = document.createElement('div'); + const content = state.content; + delete state.content; + + const app: App = createApp({ + setup() { + const handleVisibleChange = () => { + state.modelValue = false; + }; + onUnmounted(() => { + console.log(111); + document.body.removeChild(container); + }); + return () => ( + + {content} + + ); + }, + }); + + document.body.appendChild(container); + app.mount(container); + return app; } export default class DrawerService { - static create(props: DrawerProps, drawer: drawerInstance): drawerInstance { - if (!drawer) { - drawer = new Drawer(props) - } - return drawer - } -} - -class Drawer { - private drawer: any = null - private div: HTMLElement = null - private props: DrawerProps = null - - constructor(props: DrawerProps) { - this.props = props - } - - public show(): void { - if (!this.drawer) { - this.div = document.createElement('div') - this.drawer = createDrawerApp(this.props, this.drawer, this.div) - } - // TODO: this is a hack, need to find a better way. (the row 62) - this.drawer._instance.props.visible = true - } - - public hide = async (): Promise => { - const beforeHidden = this.props.beforeHidden - let result = (typeof beforeHidden === 'function' ? beforeHidden() : beforeHidden) ?? false - if (result instanceof Promise) { - result = await result - } - if (!result) this.hideDirectly() - } - - public hideDirectly = (): void => { - this.drawer._instance.props.visible = false - // this.div.remove() - } - - public destroy = (): void => { - // when drawer is null, it has been destroyed already and no need to destroy again - if (this.drawer) { - this.drawer.unmount() - this.drawer = null - this.div.remove() - } + open(options: DrawerOptions): { close: () => void } { + const state: DrawerOptions = reactive({ ...defaultOptions, ...options }); + const app = initInstance(state); + + state.modelValue = true; + + return { + close: () => { + state.modelValue = false; + app.unmount(); + }, + }; } } diff --git a/packages/devui-vue/devui/drawer/src/drawer-types.ts b/packages/devui-vue/devui/drawer/src/drawer-types.ts index 6ff7e1f959..498f09c187 100644 --- a/packages/devui-vue/devui/drawer/src/drawer-types.ts +++ b/packages/devui-vue/devui/drawer/src/drawer-types.ts @@ -1,51 +1,58 @@ -import type { ExtractPropTypes, PropType } from 'vue' +import type { ExtractPropTypes, PropType, Slot, Ref } from 'vue'; export const drawerProps = { - width: { // 宽度 - type: String, - default: '300px', - }, - visible: { // 是否可见 + modelValue: { type: Boolean, default: false, }, - zIndex: { // 层级 + zIndex: { type: Number, default: 1000, }, - isCover: { // 是否有遮罩层 + showOverlay: { type: Boolean, default: true, }, - escKeyCloseable: { // 是否可通过esc关闭 + escKeyCloseable: { type: Boolean, default: true, }, - position: { // 位置 只有左和右 + position: { type: String as PropType<'left' | 'right'>, - default: 'left', + default: 'right', }, - backdropCloseable: { // 点击遮罩层是否可关闭 + lockScroll: { type: Boolean, default: true, }, - destroyOnHide: { // 是否在隐藏时销毁 - type: Boolean, - default: false, - }, - showAnimation: { // 是否启用动效 + closeOnClickOverlay: { type: Boolean, default: true, }, - beforeHidden: { // 关闭前的回调 - type: [Promise, Function] as PropType | (() => boolean | Promise)>, + beforeClose: { + type: Function as PropType<(done: () => void) => void>, }, - content: { // 默认内容插槽 - type: Object, - }, - header: { // 头部内容插槽 - type: Object, +}; + +export const drawerOverlayProps = { + visible: { + type: Boolean, + default: false, }, -} as const +}; + +type DrawerEmitEvent = 'update:modelValue' | 'close' | 'open'; + +export type DrawerEmit = (event: DrawerEmitEvent, result?: unknown) => void; + +export type DrawerProps = ExtractPropTypes; + +export type DrawerOverlayProps = ExtractPropTypes; + +export type DrawerOptions = Partial & { content?: string | Slot }; -export type DrawerProps = ExtractPropTypes +export type UseDrawerFn = { + drawerRef: Ref; + drawerClasses: Ref>; + handleOverlayClick: () => void; +}; diff --git a/packages/devui-vue/devui/drawer/src/drawer.scss b/packages/devui-vue/devui/drawer/src/drawer.scss index 49caf1ee62..3afef767e4 100644 --- a/packages/devui-vue/devui/drawer/src/drawer.scss +++ b/packages/devui-vue/devui/drawer/src/drawer.scss @@ -1,41 +1,52 @@ -.devui-drawer{ - &-left-enter-active { - animation: left-inout 0.3s; +@import '../../styles-var/devui-var.scss'; + +.devui-drawer { + position: fixed; + top: 0; + bottom: 0; + width: 300px; + border-radius: $devui-border-radius; + background-color: $devui-base-bg; + transform: translateX(0); + opacity: 1; + overflow: auto; + box-shadow: $devui-shadow-length-fullscreen-overlay $devui-shadow; + + &-left { + left: 0; } - &-left-leave-active { - animation: left-inout 0.3s reverse; + &-right { + right: 0; } +} +.drawer-fly { &-right-enter-active { - animation: right-inout 0.3s; + transition: all 0.3s cubic-bezier(0.16, 0.75, 0.5, 1); } &-right-leave-active { - animation: right-inout 0.3s reverse; + transition: all 0.3s cubic-bezier(0.5, 0, 0.84, 0.25); } -} -@keyframes right-inout { - 0% { - transform: translateX(100px); + &-right-enter-from, + &-right-leave-to { opacity: 0; + transform: translateX(100%); } - 100% { - transform: translateX(0); - opacity: 1; + &-left-enter-active { + transition: all 0.3s cubic-bezier(0.16, 0.75, 0.5, 1); } -} -@keyframes left-inout { - 0% { - transform: translateX(-100px); - opacity: 0; + &-left-leave-active { + transition: all 0.3s cubic-bezier(0.5, 0, 0.84, 0.25); } - 100% { - transform: translateX(0); - opacity: 1; + &-left-enter-from, + &-left-leave-to { + opacity: 0; + transform: translateX(-100%); } -} \ No newline at end of file +} diff --git a/packages/devui-vue/devui/drawer/src/drawer.tsx b/packages/devui-vue/devui/drawer/src/drawer.tsx index 9bde30a3d5..37e7c09b6f 100644 --- a/packages/devui-vue/devui/drawer/src/drawer.tsx +++ b/packages/devui-vue/devui/drawer/src/drawer.tsx @@ -1,103 +1,27 @@ -import { defineComponent, ref, toRefs, watch, onUnmounted, Teleport, provide } from 'vue' -import { drawerProps, DrawerProps } from './drawer-types' - -import DrawerHeader from './components/drawer-header' -import DrawerContainer from './components/drawer-container' -import DrawerBody from './components/drawer-body' - -import './drawer.scss' +import { defineComponent, Teleport, Transition } from 'vue'; +import { drawerProps, DrawerProps } from './drawer-types'; +import DrawerOverlay from './components/drawer-overlay'; +import { useDrawer } from './use-drawer'; +import './drawer.scss'; export default defineComponent({ name: 'DDrawer', + inheritAttrs: false, props: drawerProps, - emits: ['close', 'update:visible', 'afterOpened'], - setup(props: DrawerProps, { emit, slots }) { - const { - width, visible, zIndex, isCover, escKeyCloseable, position, - backdropCloseable, destroyOnHide, showAnimation - } = toRefs(props) - const isFullScreen = ref(false) - - const fullscreen = () => { - isFullScreen.value = !isFullScreen.value - } - - const closeDrawer = async () => { - const beforeHidden = props.beforeHidden; - let result = (typeof beforeHidden === 'function' ? beforeHidden(): beforeHidden) ?? false; - if (result instanceof Promise) { - result = await result; - } - if (result) return; - - // BUG: this is not working when use service model - emit('update:visible', false) - emit('close') - } - - const escCloseDrawer = (e) => { - if (e.code === 'Escape') { - closeDrawer() - } - } - - watch(visible, (val) => { - if (val) { - emit('afterOpened') - // TODO: destroy-model should reset props, this function should be extracted - if (destroyOnHide.value) { - isFullScreen.value = false - } - } - if (escKeyCloseable && val) { - document.addEventListener('keyup', escCloseDrawer) - } else { - document.removeEventListener('keyup', escCloseDrawer) - } - }) - - // TODO: need to handle these params again - // 1. should be provided by params' value (eg: provide('closeDrawer', closeDrawer.value)) - // 2. which param should be provided - provide('closeDrawer', closeDrawer) - provide('zindex', zIndex) - provide('isCover', isCover) - provide('position', position) - provide('width', width) - provide('visible', visible) - provide('isFullScreen', isFullScreen) - provide('backdropCloseable', backdropCloseable) - provide('destroyOnHide', destroyOnHide) - provide('showAnimation', showAnimation) - - onUnmounted(() => { - document.removeEventListener('keyup', escCloseDrawer) - }) - - return { - isFullScreen, - visible, - slots, - fullscreen, - closeDrawer, - } - }, - render() { - const { fullscreen, closeDrawer, visible, destroyOnHide } = this; - - if (destroyOnHide.value && !visible) { - return null - } - - return ( - - - {this.slots.header ? this.slots.header({fullscreen, closeDrawer}) : - - } - {this.slots.content ? this.slots.content() : } - + emits: ['close', 'update:modelValue', 'open'], + setup(props: DrawerProps, { emit, slots, attrs }) { + const { drawerRef, drawerClasses, handleOverlayClick } = useDrawer(props, emit); + return () => ( + + {props.showOverlay && } + + {props.modelValue && ( +
+ {slots.default?.()} +
+ )} +
- ) - } -}) + ); + }, +}); diff --git a/packages/devui-vue/devui/drawer/src/use-drawer.ts b/packages/devui-vue/devui/drawer/src/use-drawer.ts new file mode 100644 index 0000000000..f328cc1201 --- /dev/null +++ b/packages/devui-vue/devui/drawer/src/use-drawer.ts @@ -0,0 +1,50 @@ +import { computed, onUnmounted, ref, watch } from 'vue'; +import { onClickOutside } from '@vueuse/core'; +import { DrawerEmit, DrawerProps, UseDrawerFn } from './drawer-types'; +import { lockScroll } from '../../shared/util/lock-scroll'; + +export function useDrawer(props: DrawerProps, emit: DrawerEmit): UseDrawerFn { + const drawerRef = ref(); + const drawerClasses = computed(() => ({ + 'devui-drawer': true, + [`devui-drawer-${props.position}`]: true, + })); + const close = () => { + emit('update:modelValue', false); + emit('close'); + }; + let lockScrollCb: () => void; + const execClose = () => { + props.beforeClose ? props.beforeClose(close) : close(); + }; + const handleOverlayClick = () => { + props.closeOnClickOverlay && execClose(); + }; + const handleEscClose = (e: KeyboardEvent) => { + e.code === 'Escape' && execClose(); + }; + + onClickOutside(drawerRef, execClose); + + const removeBodyAdditions = () => { + lockScrollCb?.(); + document.removeEventListener('keyup', handleEscClose); + }; + + watch( + () => props.modelValue, + (val) => { + if (val) { + emit('open'); + props.lockScroll && (lockScrollCb = lockScroll()); + props.escKeyCloseable && document.addEventListener('keyup', handleEscClose); + } else { + removeBodyAdditions(); + } + } + ); + + onUnmounted(removeBodyAdditions); + + return { drawerRef, drawerClasses, handleOverlayClick }; +} diff --git a/packages/devui-vue/devui/shared/util/lock-scroll.ts b/packages/devui-vue/devui/shared/util/lock-scroll.ts new file mode 100644 index 0000000000..16123eeef9 --- /dev/null +++ b/packages/devui-vue/devui/shared/util/lock-scroll.ts @@ -0,0 +1,19 @@ +export function lockScroll(): () => void | undefined { + if (document.documentElement.scrollHeight > document.documentElement.clientHeight) { + const scrollTop = document.documentElement.scrollTop; + const style = document.documentElement.getAttribute('style'); + document.documentElement.style.position = 'fixed'; + document.documentElement.style.top = `-${scrollTop}px`; + document.documentElement.style.width = document.documentElement.style.width || '100%'; + document.documentElement.style.overflowY = 'scroll'; + return () => { + if (style) { + document.documentElement.setAttribute('style', style); + } else { + document.documentElement.removeAttribute('style'); + } + document.documentElement.scrollTop = scrollTop; + }; + } + return; +} diff --git a/packages/devui-vue/docs/components/drawer/index.md b/packages/devui-vue/docs/components/drawer/index.md index b1aaa8b68e..c6a9fab12e 100644 --- a/packages/devui-vue/docs/components/drawer/index.md +++ b/packages/devui-vue/docs/components/drawer/index.md @@ -2,7 +2,7 @@ 屏幕边缘滑出的浮层面板组件。 -### 何时使用 +#### 何时使用 1. 抽屉从父窗体边缘滑入,覆盖住部分父窗体内容。用户在抽屉内操作时不必离开当前任务,操作完成后,可以平滑地回到到原任务。 2. 当需要一个附加的面板来控制父窗体内容,这个面板在需要时呼出。比如,控制界面展示样式,往界面中添加内容。 @@ -10,215 +10,161 @@ ### 基本用法 -

基本用法,可以控制全屏、关闭和设置宽度。

- -:::demo +:::demo 默认从右侧滑出,宽度为`300px`。 ```vue +``` - const drawerAfterOpened = () => { - console.log('open'); - }; +::: - const beforeHidden = () => { - return new Promise((resolve) => { - resolve(false); - }); - }; +### 左侧弹出 - return { - isDrawerShow, - btnName, - drawerWidth, - drawerShow, - drawerClose, - drawerAfterOpened, - isCover, - backdropCloseable, - beforeHidden - }; - } -}; +:::demo 通过`position`设置左侧滑出。 + +```vue + + ``` ::: -### 自定义模板 +### 背景滚动 -

自定义抽屉板模板。

+:::demo drawer 滑出之后,默认背景滚动会被锁定,可通过`lock-scroll`设置为`false`来解锁。 -:::demo +```vue + + + +``` + +::: + +### 关闭前回调 + +:::demo `before-close`在用户关闭 drawer 时会被调用,可在完成某些异步操作后,通过执行`done`函数关闭。 ```vue + ``` ::: -### 以服务的方式调用 +### 服务方式 -:::demo +:::demo 组件在全局注册了`$drawerService`,可通过服务的方式使用,drawer 的内容通过`content`参数传入。服务返回了用于关闭 drawer 的`close`方法。 ```vue + ``` ::: -### 参数及 API - -| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | -| :---------------: | :-------------------: | :-----: | :------------------------------------------ | --------------------- | -| v-model:visible | `Boolean` | `false` | 必选,设置抽屉板是否可见 | [基本用法](#基本用法) | -| width | `String` | `300px` | 可选,设置抽屉板宽度 | [基本用法](#基本用法) | -| zIndex | `Number` | `1000` | 可选,设置 drawer 的 z-index 值 | [基本用法](#基本用法) | -| isCover | `Boolean` | `true` | 可选,是否有遮罩层 | [基本用法](#基本用法) | -| escKeyCloseable | `Boolean` | `true` | 可选,设置可否通过 esc 按键来关闭 drawer 层 | [基本用法](#基本用法) | -| position | `String` | 'right' | 可选,抽屉板出现的位置,'left'或者'right' | [基本用法](#基本用法) | -| backdropCloseable | `Boolean` | true | 可选,设置可否通过点击背景来关闭 drawer 层 | [基本用法](#基本用法) | -| destroyOnHide | `Boolean` | true | 可选,设置是否在隐藏时销毁 drawer 层 | [基本用法](#基本用法) | -| beforeHidden | `Function \| Promise` | -- | 可选,关闭窗口之前的回调 | [基本用法](#基本用法) | -| onClose | `Function` | -- | 可选,关闭 drawer 时候调用 | [基本用法](#基本用法) | -| onAfterOpened | `Function` | -- | 可选,打开 drawer 后时候调用 | [基本用法](#基本用法) | -| showAnimation | `boolean` | true | 可选,是否开启动效 | [基本用法](#基本用法) | - -### 插槽 - -| 名称 | 类型 | 说明 | 跳转 Demo | -| :-----: | :--------: | :--------: | :-----------------------: | -| content | 具名插槽 | 抽屉板内容 | [自定义模板](#自定义模板) | -| header | 作用域插槽 | 抽屉板头部 | [自定义模板](#自定义模板) | - -#### 作用域插槽参数 - -| 名称 | 作用 | 说明 | 跳转 Demo | -| :---------: | :--------: | :---------------------------------------------------------------------: | :-----------------------: | -| fullscreen | 切换全屏 | -- | [自定义模板](#自定义模板) | -| closeDrawer | 关闭抽屉板 | 在关闭抽屉板时必须调用该方法,否则 `beforeHidden` 和 `close` 属性不生效 | [自定义模板](#自定义模板) | +### d-drawer 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | +| ---------------------- | ---------------- | ------- | ------------------------------------------------- | ------------------------- | +| v-model | `Boolean` | `false` | 可选,设置抽屉板是否可见 | [基本用法](#基本用法) | +| position | `String` | `right` | 可选,抽屉板出现的位置,'left'或者'right' | [左侧弹出](#左侧弹出) | +| show-overlay | `Boolean` | `true` | 可选,是否有遮罩层 | [基本用法](#基本用法) | +| lock-scroll | `Boolean` | `true` | 可选,是否锁定滚动 | [背景滚动](#背景滚动) | +| z-index | `Number` | `1000` | 可选,设置 drawer 的 z-index 值 | [基本用法](#基本用法) | +| esc-key-closeable | `Boolean` | `true` | 可选,设置可否通过 esc 按键来关闭 drawer 层 | [基本用法](#基本用法) | +| close-on-click-overlay | `Boolean` | `true` | 可选,设置可否通过点击背景来关闭 drawer 层 | [基本用法](#基本用法) | +| before-close | `(done) => void` | `-` | 可选,关闭窗口前的回调,调用 `done` 可关闭 drawer | [关闭前回调](#关闭前回调) | + +### d-drawer 事件 + +| 事件名 | 类型 | 说明 | +| ------ | ---- | ----------------- | +| open | `-` | drawer 打开时触发 | +| close | `-` | drawer 关闭时触发 | + +### d-drawer 插槽 + +| 名称 | 类型 | 说明 | 跳转 Demo | +| ------- | ---- | ---------- | --------------------- | +| default | 默认 | 抽屉板内容 | [基本用法](#基本用法) | diff --git a/packages/devui-vue/package.json b/packages/devui-vue/package.json index 25f35e8274..a12e943acf 100644 --- a/packages/devui-vue/package.json +++ b/packages/devui-vue/package.json @@ -35,6 +35,7 @@ "dependencies": { "@devui-design/icons": "^1.3.0", "@types/lodash-es": "^4.17.4", + "@vueuse/core": "^7.7.1", "async-validator": "^4.0.2", "devui-theme": "workspace:^0.0.1", "fs-extra": "^10.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96659d6697..9dbe5a10c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,7 @@ importers: '@vue/test-utils': ^2.0.0-rc.9 '@vuedx/typecheck': ^0.4.1 '@vuedx/typescript-plugin-vue': ^0.4.1 + '@vueuse/core': ^7.7.1 async-validator: ^4.0.2 babel-jest: ^27.0.2 chalk: ^4.1.2 @@ -142,6 +143,7 @@ importers: dependencies: '@devui-design/icons': 1.3.0 '@types/lodash-es': 4.17.6 + '@vueuse/core': 7.7.1_vue@3.2.31 async-validator: 4.0.7 devui-theme: link:../devui-theme fs-extra: 10.0.0 @@ -2930,6 +2932,37 @@ packages: - supports-color dev: true + /@vueuse/core/7.7.1_vue@3.2.31: + resolution: {integrity: sha512-PRRgbATMpoeUmkCEBtUeJgOwtew8s+4UsEd+Pm7MhkjL2ihCNrSqxNVtM6NFE4uP2sWnkGcZpCjPuNSxowJ1Ow==} + peerDependencies: + '@vue/composition-api': ^1.1.0 + vue: ^2.6.0 || ^3.2.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + vue: + optional: true + dependencies: + '@vueuse/shared': 7.7.1_vue@3.2.31 + vue: 3.2.31 + vue-demi: 0.12.1_vue@3.2.31 + dev: false + + /@vueuse/shared/7.7.1_vue@3.2.31: + resolution: {integrity: sha512-rN2qd22AUl7VdBxihagWyhUNHCyVk9IpvBTTfHoLH9G7rGE552X1f+zeCfehuno0zXif13jPw+icW/wn2a0rnQ==} + peerDependencies: + '@vue/composition-api': ^1.1.0 + vue: ^2.6.0 || ^3.2.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + vue: + optional: true + dependencies: + vue: 3.2.31 + vue-demi: 0.12.1_vue@3.2.31 + dev: false + /JSONStream/1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -8767,6 +8800,21 @@ packages: deprecated: This package has been renamed to @vscode/web-custom-data, please update to the new name dev: true + /vue-demi/0.12.1_vue@3.2.31: + resolution: {integrity: sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + vue: 3.2.31 + dev: false + /vue-eslint-parser/7.11.0_eslint@7.32.0: resolution: {integrity: sha512-qh3VhDLeh773wjgNTl7ss0VejY9bMMa0GoDG2fQVyDzRFdiU3L7fw74tWZDHNQXdZqxO3EveQroa9ct39D2nqg==} engines: {node: '>=8.10'}