svelte-layer 基於svelte.js網頁pc端彈窗元件

xiaoyan2017發表於2022-04-18

前幾天有給大家分享一個 svelte+sass實戰微信app介面聊天例項
今天要給大家分享的是全新研發的svelte.js自定義桌面版對話方塊元件SvelteLayer。

如下圖:在lib目錄下新建一個Layer元件目錄。

image.png

引入svelte-layer元件

彈窗元件支援元件式(Layer)+函式式(svLayer)兩種呼叫方式。

import Layer, {svLayer} from '$lib/Layer'

  • 函式式寫法
function handleInfo(e) {
    let el = svLayer({
        title: '標題',
        content: `<div style="padding:20px;">
            <p>函式式呼叫:<em style="color:#999;">svLayer({...})</em></p>
        </div>`,
        resize: true,
        btns: [
            {
                text: '取消',
                click: () => {
                    // 關閉彈窗
                    el.$set({open: false})
                }
            },
            {
                text: '確認',
                style: 'color:#09f;',
                click: () => {
                    svLayer({
                        type: 'toast',
                        icon: 'loading',
                        content: '載入中...',
                        opacity: .2,
                        time: 2
                    })
                }
            },
        ]
    })
}
  • 元件式寫法
<!-- 詢問框 -->
<Layer bind:open={showConfirm} shadeClose="false" title="警告資訊" xclose zIndex="2001" lockScroll={false} resize dragOut
    content="<div style='color:#00e0a1;padding:20px 40px;'>這裡是確認框提示資訊</div>"
    btns={[
        {text: '取消', click: () => showConfirm=false},
        {text: '確定', style: 'color:#e63d23;', click: handleInfo},
    ]}
/>

兩種寫法可以單獨使用,如果是一些複雜的彈窗場景,可考慮兩種寫法混合呼叫。

svelte-layer配置引數

支援如下超過30+引數隨意搭配呼叫。

<script context="module">
    let index = 0 // 用於控制倒數計時臨時索引
    let lockNum = 0 // 用於控制鎖定螢幕臨時索引
</script>

<script>
    // 是否開啟彈窗bind:open={showDialog}
    export let open = false
    // 彈窗標識
    export let id = undefined
    // 標題
    export let title = ''
    // 內容
    export let content = ''
    // 彈窗型別
    export let type = ''
    // 自定義樣式
    export let layerStyle = undefined
    // 自定義類名
    export let customClass = ''
    // toast圖示
    export let icon = ''
    // 是否顯示遮罩層
    export let shade = true
    // 點選遮罩層關閉
    export let shadeClose = true
    // 鎖定螢幕
    export let lockScroll = true
    // 遮罩層透明度
    export let opacity = ''
    // 是否顯示關閉圖示
    export let xclose = false
    // 關閉圖示位置
    export let xposition = 'right'
    // 關閉圖示顏色
    export let xcolor = '#000'
    // 彈窗動畫
    export let anim = 'scaleIn'
    // 彈出位置(auto | ['100px','50px'] | t | r | b | l | lt | rt | lb | rb)
    export let position = 'auto'
    // 抽屜彈窗
    export let drawer = ''
    // 右鍵彈窗定位
    export let follow = null
    // 彈窗自動關閉時間
    export let time = 0
    // 彈窗層級
    export let zIndex = 202204
    // 置頂彈窗
    export let topmost = false
    // 彈窗大小
    export let area = 'auto'
    // 彈窗最大寬度
    export let maxWidth = 375
    // 彈窗是否最大化
    export let maximize = false
    // 彈窗是否全屏
    export let fullscreen = false
    // 是否固定
    export let fixed = true
    // 是否拖拽
    export let drag = '.vlayer__wrap-tit'
    // 是否拖拽螢幕外
    export let dragOut = false
    // 限制拖拽方向 vertical|horizontal
    export let dragDir = ''
    // 拖拽結束回撥 {width: 120, height: 120, x: 100, y: 100}
    export let dragEnd = undefined
    // 是否縮放
    export let resize = false

    // 彈窗按鈕事件
    export let btns = null
    /*export let btns = [
        {text: '取消', style: 'color:red', disabled: true, click: null},
        {text: '確定', style: 'color:blue', click: null}
    ]*/

    // 函式式開啟|關閉回撥
    export let onOpen = undefined
    export let onClose = undefined
    export let beforeClose = undefined

    // 接收函式移除指令
    export let remove = undefined

    import { onMount, afterUpdate, createEventDispatcher, tick } from 'svelte'
    const dispatch = createEventDispatcher()
    
    // ...
</script>

彈窗模板及js邏輯處理模組。

<div class="vui__layer" class:opened class:vui__layer-closed={closeCls} id={id} bind:this={el}>
    <!-- 遮罩層 -->
    {#if bool(shade)}<div class="vlayer__overlay" on:click={shadeClicked} style:opacity></div>{/if}
    <!-- 主體 -->
    <div class="vlayer__wrap {type&&'popui__'+type} anim-{anim}" style="{layerStyle}">
        {#if title}<div class="vlayer__wrap-tit">{@html title}</div>{/if}
        {#if icon&&type=='toast'}<div class="vlayer__toast-icon vlayer__toast-{icon}">{@html toastIcon[icon]}</div>{/if}
        <div class="vlayer__wrap-cntbox">
            <!-- 判斷content插槽是否存在 -->
            {#if $$slots.content}
                <div class="vlayer__wrap-cnt"><slot name="content" /></div>
            {:else}
                {#if content}
                    <!-- iframe -->
                    {#if type=='iframe'}
                    <div class="vlayer__wrap-iframe">
                        <iframe scrolling="auto" allowtransparency="true" frameborder="0" src={content}></iframe>
                    </div>
                    <!-- message|notify|popover -->
                    {:else if type=='message' || type=='notify' || type=='popover'}
                    <div class="vlayer__wrap-cnt">
                        {#if icon}<i class="vlayer-msg__icon {icon}">{@html messageIcon[icon]}</i>{/if}
                        <div class="vlayer-msg__group">
                            {#if title}<div class="vlayer-msg__title">{@html title}</div>{/if}
                            <div class="vlayer-msg__content">{@html content}</div>
                        </div>
                    </div>
                    <!-- 載入動態元件 -->
                    {:else if type == 'component'}
                    <svelte:component this={content}/>
                    {:else}
                    <div class="vlayer__wrap-cnt">{@html content}</div>
                    {/if}
                {/if}
            {/if}
            <slot />
        </div>

        <!-- 按鈕組 -->
        {#if btns}
        <div class="vlayer__wrap-btns">
            {#each btns as btn,index}
                <span class="btn" class:btn-disabled={btn.disabled} style="{btn.style}">{@html btn.text}</span>
            {/each}
        </div>
        {/if}

        {#if xclose}
        <span class="vlayer__xclose" style="color: {xcolor}" on:click={hide}></span>
        {/if}
        {#if maximize}<span class="vlayer__maximize" on:click={maximizeClicked}></span>{/if}
        <!-- 縮放 -->
        {#if resize}
        <span class="vlayer__groupresize">
            <i class="vlayer__resize LT"></i>
            <i class="vlayer__resize RT"></i>
            <i class="vlayer__resize LB"></i>
            <i class="vlayer__resize RB"></i>
        </span>
        {/if}
    </div>
    <!-- 優化拖拽卡頓 -->
    <div class="vlayer__dragfix"></div>
</div>

<script>
    /**
     * @Desc     Svelte.js桌面端對話方塊元件SvelteLayer
     * @Time     andy by 2022-04
     * @About    Q:282310962  wx:xy190310
     */
    
    // ...

    onMount(() => {
        console.log('監聽彈窗開啟')
        window.addEventListener('resize', autopos, false)
        return () => {
            console.log('監聽彈窗關閉')
            window.removeEventListener('resize', autopos, false)
        }
    })

    afterUpdate(() => {
        console.log('監聽彈窗更新')
    })

    // 動態監聽開啟/關閉
    $: if(open) {
        show()
    }else {
        hide()
    }

    /**
     * 開啟彈窗
     */
    async function show() {
        if(opened) return
        opened = true
        dispatch('open')
        typeof onOpen === 'function' && onOpen()

        // 避免獲取彈窗寬高不準確
        await tick()

        zIndex = util.getZIndex(zIndex) + 1

        auto()
    }

    /**
     * 關閉彈窗
     */
    function hide() {
        // ...
    }

    // 彈窗位置
    function auto() {
        autopos()

        // 全屏彈窗
        if(fullscreen) {
            full()
        }

        // 拖拽|縮放
        move()
    }

    // 彈窗定位
    function autopos() {
        if(!opened) return
        let ol, ot
        let pos = position
        let isfixed = bool(fixed)
        let vlayero = el.querySelector('.vlayer__wrap')

        if(!isfixed || follow) {
            vlayero.style.position = 'absolute'
        }

        let area = [util.client('width'), util.client('height'), vlayero.offsetWidth, vlayero.offsetHeight]

        ol = (area[0] - area[2]) / 2
        ot = (area[1] - area[3]) / 2

        if(follow) {
            offset()
        }else {
            typeof pos === 'object' ? (
                ol = parseFloat(pos[0]) || 0, ot = parseFloat(pos[1]) || 0
            ) : (
                pos == 't' ? ot = 0 : 
                pos == 'r' ? ol = area[0] - area[2] : 
                pos == 'b' ? ot = area[1] - area[3] : 
                pos == 'l' ? ol = 0 : 
                pos == 'lt' ? (ol = 0, ot = 0) : 
                pos == 'rt' ? (ol = area[0] - area[2], ot = 0) : 
                pos == 'lb' ? (ol = 0, ot = area[1] - area[3]) :
                pos == 'rb' ? (ol = area[0] - area[2], ot = area[1] - area[3]) : 
                null
            )

            vlayero.style.left = parseFloat(isfixed ? ol : util.scroll('left') + ol) + 'px'
            vlayero.style.top = parseFloat(isfixed ? ot : util.scroll('top') + ot) + 'px'
        }
    }

    // 跟隨定位
    function offset() {
        let ow, oh, ps
        let vlayero = el.querySelector('.vlayer__wrap')

        ow = vlayero.offsetWidth
        oh = vlayero.offsetHeight
        ps = util.getFollowRect(follow, ow, oh)
        tipArrow = ps[2]

        vlayero.style.left = ps[0] + 'px'
        vlayero.style.top = ps[1] + 'px'
    }

    // 最大化彈窗
    async function full() {
        // ...
    }

    // 復位彈窗
    async function restore() {
        // ...
    }

    // 拖拽縮放
    function move() {
        let isfixed = bool(fixed)
        let isdragOut = bool(dragOut)
        let c = {}

        let vlayero = el.querySelector('.vlayer__wrap')
        let otit = el.querySelector('.vlayer__wrap-tit')
        let ocnt = el.querySelector('.vlayer__wrap-cntbox')
        let obtn = el.querySelector('.vlayer__wrap-btns')
        let odrag = el.querySelector(drag)
        let oresize = el.querySelectorAll('.vlayer__resize')
        let ofix = el.querySelector('.vlayer__dragfix')

        // 拖拽
        if(odrag) {
            odrag.style.cursor = util.isIE() ? 'move' : 'grab'
            util.on(odrag, 'mousedown', function(e) {
                // ...
            })
        }

        util.on(document, 'mousemove', function(e) {
            if(c.dragTrigger) {
                let iL = e.clientX - c.pos[0] + c.area[0]
                let iT = e.clientY - c.pos[1] + c.area[1]
                let fixL = isfixed ? 0 : c.scroll[1]
                let fixT = isfixed ? 0 : c.scroll[2]
                let iMaxL = c.client[0] + fixL - c.area[2]
                let iMaxT = c.client[1] + fixT - c.area[3]
                let oMaxT = c.scroll[0] - c.area[3]

                // 邊界檢測
                if(isdragOut) {
                    iT = iT < 0 ? 0 : iT
                    iT = (iT > oMaxT) ? oMaxT : iT
                }else {
                    iL = (iL < fixL) ? fixL : iL
                    iL = (iL > iMaxL) ? iMaxL : iL
                    iT = (iT < fixT) ? fixT : iT
                    iT = (iT > iMaxT) ? iMaxT : iT
                }

                // 記錄拖拽彈窗座標
                c.dragPosition = {
                    width: c.area[2],
                    height: c.area[3],
                    x: iL,
                    y: iT
                }

                // 限制拖拽方向
                dragDir == 'horizontal' ? (vlayero.style.left = iL + 'px')
                :
                dragDir == 'vertical' ? (vlayero.style.top = iT + 'px')
                :
                (vlayero.style.left = iL + 'px', vlayero.style.top = iT + 'px')
            }

            // 邊角縮放
            if(c.resizeTrigger && c.elem) {
                // ...
            }
        })

        util.on(document, 'mouseup', function() {
            c.dragTrigger && (
                delete c.dragTrigger, ofix.style.display = 'none',
                typeof dragEnd === 'function' && dragEnd(dragPosition)
            )
            c.resizeTrigger && (
                delete c.resizeTrigger, ofix.style.display = 'none'
            )
            document.onmouseup = null
        })
    }

    // 點選最大化按鈕
    function maximizeClicked(e) {
        let o = e.target
        if(o.classList.contains('maximized')) {
            restore()
        }else {
            full()
        }
    }
    
    //...
</script>

另外svelte-layer還支援動態引入外部元件。

import Counter from '$lib/Counter.svelte'

// 動態載入元件(函式式呼叫)
function showComponentLayer() {
    svLayer({
        type: 'component',
        title: '動態載入元件',
        content: Counter,
        resize: true,
        xclose: true,
        maximize: true,
        area: ['360px', '250px']
    })
}

<!-- 元件呼叫 -->
<Layer bind:open={showComponent} content="這裡是內容資訊" resize drag=".vlayer__wrap-cnt"
    btns={[
        {text: '確認', style: 'color:#f60;', click: () => showComponent=false},
    ]}
    on:open={handleOpen} on:close={handleClose}
>
    <svelte:fragment slot="content">
        <Counter />
    </svelte:fragment>
</Layer>

另外還封裝了類似element-ui種message/notify/popover彈窗型別。

/**
 * Message提示訊息
 * @param {object} options
 * @param {string} icon圖示(info/success/error/warning/loading)
 */
svLayer.message = function(options) {
    typeof options == 'string' && (options = {content: options})
    return svLayer({
        icon: 'info',
        anim: 'fadeInUp',
        position: 't',
        lockScroll: false,
        shade: false,
        time: 2,
        layerStyle: 'margin-top: 10px',
        ...options,
        title: null,
        type: 'message'
    })
}

/**
 * Notification通知訊息
 * @param {object} options
 * @param {string} icon圖示(info/success/error/warning/loading)
 */
svLayer.notify = function(options) {
    typeof options == 'string' && (options = {content: options})
    return svLayer({
        anim: 'fadeInRight',
        position: 'rt',
        lockScroll: false,
        shade: false,
        xclose: true,
        time: 5,
        layerStyle: 'margin: 20px 0 0 -20px',
        ...options,
        type: 'notify'
    })
}

/**
 * Popover氣泡框
 * @param {object} options
 * @param {string} icon圖示(info/success/error/warning/loading)
 */
svLayer.popover = function(options) {
    return svLayer({
        anim: 'fadeIn',
        lockScroll: false,
        shade: false,
        xclose: true,
        ...options,
        type: 'popover'
    })
}

好了,以上就是svelte.js開發自定義pc端彈窗外掛的一些分享。後續還會分享一些svelte自定義元件及實戰案例專案。

相關文章