This repository has been archived on 2023-08-30. You can view files and clone it, but cannot push or open issues/pull-requests.
bytedo
/
wcui
Archived
1
0
Fork 0
wcui/src/layer/index.wc

829 lines
18 KiB
Plaintext

<template>
<div class="layer">
<div class="layer__title noselect"></div>
<div class="layer__content"><slot></slot></div>
<div class="layer__ctrl noselect"></div>
</div>
</template>
<style lang="scss">
:host {
display: none;
justify-content: center;
align-items: center;
position: fixed;
z-index: 65534;
left: 0;
top: 0;
width: 100%;
}
:host([alert]),
:host([confirm]),
:host([prompt]),
:host([frame]),
:host([toast]),
:host([notify]),
:host([common]) {
display: flex;
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
img,
a {
-webkit-user-drag: none;
}
}
.layer {
overflow: hidden;
flex: 0 auto;
position: absolute;
z-index: 65535;
border-radius: 2px;
color: #666;
font-size: 14px;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
transition: opacity 0.2s ease-in-out, left 0.2s ease-in-out,
right 0.2s ease-in-out, top 0.2s ease-in-out, bottom 0.2s ease-in-out;
opacity: 0;
&.scale {
transform: scale(1.01);
transition: transform 0.1s linear;
}
&.blur {
backdrop-filter: blur(5px);
}
&:active {
z-index: 65536;
}
/* 弹层样式 */
&__title {
display: none;
justify-content: space-between;
align-items: center;
width: 100%;
height: 60px;
padding: 15px;
font-size: 16px;
color: var(--color-dark-2);
wc-icon {
--size: 14px;
&:hover {
color: var(--color-red-1);
}
}
}
&__content {
display: flex;
position: relative;
width: 100%;
height: auto;
min-height: 50px;
word-break: break-all;
word-wrap: break-word;
::slotted(&__input) {
flex: 1;
height: 36px;
}
::slotted(&__frame) {
display: flex;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: 0;
resize: none;
background: #fff;
}
::slotted(&__toast) {
display: flex;
align-items: center;
width: 300px;
padding: 0 10px !important;
border-radius: 3px;
font-weight: normal;
text-indent: 8px;
--size: 16px;
color: var(--color-dark-1);
}
::slotted(&__toast.style-info) {
border: 1px solid #ebeef5;
background: #edf2fc;
color: var(--color-grey-3);
}
::slotted(&__toast.style-success) {
border: 1px solid #e1f3d8;
background: #f0f9eb;
color: var(--color-green-3);
}
::slotted(&__toast.style-warning) {
border: 1px solid #faebb4;
background: #faecd8;
color: var(--color-red-1);
}
::slotted(&__toast.style-error) {
border: 1px solid #f5c4c4;
background: #fde2e2;
color: var(--color-red-1);
}
}
&__ctrl {
display: none;
justify-content: flex-end;
width: 100%;
height: 60px;
padding: 15px;
line-height: 30px;
font-size: 14px;
color: #454545;
text-align: right;
button {
min-width: 64px;
height: 30px;
padding: 0 10px;
margin: 0 5px;
border: 1px solid var(--color-plain-3);
border-radius: 2px;
white-space: nowrap;
background: #fff;
font-size: inherit;
font-family: inherit;
outline: none;
color: inherit;
&:hover {
background: var(--color-plain-1);
}
&:active {
border-color: var(--color-grey-1);
}
&:focus {
box-shadow: 0 0 0 2px var(--color-plain-a);
}
&:last-child {
color: #fff;
background: var(--color-teal-2);
border-color: transparent;
&:hover {
background: var(--color-teal-1);
}
&:active {
background: var(--color-teal-3);
}
&:focus {
box-shadow: 0 0 0 2px var(--color-teal-a);
}
}
&::-moz-focus-inner {
border: none;
}
}
}
}
:host([mask]) {
height: 100%;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(5px);
}
:host([alert]),
:host([confirm]),
:host([prompt]) {
.layer {
max-width: 600px;
min-width: 300px;
background: #fff;
&__content {
padding: 0 15px;
}
}
}
:host([notify]) {
.layer {
width: 300px;
height: 120px;
&__content {
padding: 0 15px;
}
}
}
:host([toast]) {
.layer {
box-shadow: none;
&__content {
min-height: 40px;
}
}
}
</style>
<script>
import '../form/input'
import Drag from '../drag/core'
import $ from '../utils'
const LANGUAGES = {
en: {
TITLE: 'Dialog',
BTNS: ['Cancel', 'OK']
},
cn: {
TITLE: '提示',
BTNS: ['取消', '确定']
}
}
LANGUAGES['zh-CN'] = LANGUAGES.cn
const lang =
LANGUAGES[window.__ENV_LANG__ || navigator.language] || LANGUAGES.cn
let uniqueInstance = null // 缓存当前打开的alert/confirm/prompt类型的弹窗
let toastInstance = null // 缓存toast的实例
// 要保证弹层唯一的类型
const UNIQUE_TYPES = ['alert', 'confirm', 'prompt']
function renderBtns(list) {
var html = ''
list.forEach((t, i) => {
html += `<button data-idx="${i}">${t || lang.BTNS[i]}</button>`
})
return html
}
export default class Layer {
props = {
left: 'auto',
right: 'auto',
top: 'auto',
bottom: 'auto',
from: Object.create(null),
to: Object.create(null),
btns: [],
type: '',
title: '',
blur: false,
background: null,
mask: false,
radius: null,
'mask-close': false,
'mask-color': null,
fixed: true //是否固定位置
}
__init__() {
/* render */
this.__TITLE__ = this.root.children[1].firstElementChild
this.__BODY__ = this.root.children[1].children[1]
this.__CTRL__ = this.root.children[1].lastElementChild
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
}
set title(val) {
this.props.title = val
if (val) {
if (this.__TITLE__.firstElementChild) {
this.__TITLE__.insertBefore(
document.createTextNode(val),
this.__TITLE__.firstElementChild
)
} else {
this.__TITLE__.textContent = val
}
this.__TITLE__.style.display = 'flex'
} else {
this.__TITLE__.style.display = ''
}
}
set type(val) {
var { btns } = this.props
if (!val || this._handleBtnClick) {
return
}
switch (val) {
case 'alert':
while (btns.length > 1) {
btns.shift()
}
break
case 'confirm':
case 'prompt':
while (btns.length > 2) {
btns.shift()
}
break
case 'toast':
case 'notify':
case 'frame':
if (val === 'notify') {
var _ico = document.createElement('wc-icon')
_ico.setAttribute('is', 'close')
this.__TITLE__.appendChild(_ico)
}
btns = []
break
default:
val = 'common'
break
}
this.props.type = val
if (btns.length) {
this.__CTRL__.innerHTML = renderBtns(btns)
this.__CTRL__.style.display = 'flex'
} else {
this.__CTRL__.style.display = ''
}
this.setAttribute(val, '')
}
set fixed(val) {
this.props.fixed = !!val
this._updateFixedStat()
}
_updateFixedStat() {
// 这3类弹层不允许拖拽
if (UNIQUE_TYPES.includes(this.props.type)) {
return
}
if (this.props.fixed) {
if (this._dragIns) {
this._dragIns.destroy()
this._dragIns = null
}
} else {
this._dragIns = new Drag(this.root.children[1]).by(this.__TITLE__, {
overflow: this.props.hasOwnProperty('overflow')
? this.props.overflow
: false
})
this.removeAttribute('fixed')
}
}
// 拦截 "确定"按钮的事件
_intercept(input) {
if (this.props.intercept) {
this.props.intercept(input, _ => {
delete this.props.intercept
this.resolve(input)
this.close()
})
} else {
this.resolve(input)
this.close()
}
}
close(force) {
if (this.wrapped === false) {
if (this._dragIns) {
this._dragIns.destroy()
}
if (UNIQUE_TYPES.includes(this.props.type)) {
uniqueInstance = null
}
delete this.promise
$.unbind(this.__CTRL__, 'click', this._handleBtnClick)
// 离场动画
if (this.props.from && !force) {
let _style = 'opacity:0;'
for (let k in this.props.from) {
_style += `${k}:${this.props.from[k]};`
}
this.root.children[1].style.cssText += _style
this.timer = setTimeout(() => {
this.parentNode.removeChild(this)
this.dispatchEvent(new CustomEvent('close'))
}, 200)
} else {
clearTimeout(this.timer)
this.parentNode.removeChild(this)
this.dispatchEvent(new CustomEvent('close'))
}
} else {
this.removeAttribute('common')
this.dispatchEvent(new CustomEvent('close'))
}
}
show() {
if (this.wrapped === false) {
return
}
this.setAttribute('common', '')
}
moveTo(obj = {}) {
var css = ''
for (var k in obj) {
css += `${k}:${obj[k]};`
}
this.root.children[1].style.cssText += css
}
mounted() {
this.type = this.props.type
this.title = this.props.title
this._handleBtnClick = $.bind(this.__CTRL__, 'click', ev => {
if (ev.target.tagName === 'BUTTON') {
var idx = +ev.target.dataset.idx
var { type } = this.props
switch (type) {
case 'alert':
this.resolve()
this.close()
break
case 'confirm':
case 'prompt':
if (idx === 0) {
this.reject()
this.close()
} else {
let inputValue = type === 'prompt' ? this.__INPUT__.value : null
this._intercept(inputValue)
}
break
default:
// 其他类型, 如有按钮, 直接交给拦截器处理
this._intercept(idx)
break
}
}
})
if (this.props.type === 'prompt') {
this.__INPUT__ = this.__BODY__.firstElementChild.assignedNodes().pop()
this._handleSubmit = $.bind(this.__INPUT__, 'submit', ev => {
this._intercept(ev.detail)
})
}
if (this.props.mask) {
this.setAttribute('mask', '')
}
this._updateFixedStat()
/* ------------------------ */
/* ---- 额外的样式 --- */
/* ------------------------ */
if (this.props.mask) {
this._handlMask = $.outside(this.root.children[1], ev => {
// 只作用于当前wc-layer
if (ev.target !== this) {
return
}
if (this.props['mask-close']) {
if (this.wrapped === false) {
this.reject(null)
}
this.close()
} else {
if (UNIQUE_TYPES.includes(this.props.type)) {
this.root.children[1].classList.toggle('scale', true)
setTimeout(_ => {
this.root.children[1].classList.remove('scale')
}, 100)
}
}
})
if (this.props['mask-color']) {
this.style.backgroundColor = this.props['mask-color']
}
}
if (this.props.blur) {
this.root.children[1].classList.toggle('blur', true)
}
let _style = this.props.from ? '' : 'opacity:1;'
if (this.props.background) {
_style += `background: ${this.props.background};`
if (this.props.background === 'transparent') {
_style += 'box-shadow:none;'
}
}
if (this.props.radius || this.props.radius === 0) {
_style += `border-radius: ${this.props.radius};`
}
if (this.props.size) {
for (let k in this.props.size) {
_style += `${k}:${this.props.size[k]};`
}
}
if (this.props.from) {
for (let k in this.props.from) {
_style += `${k}:${this.props.from[k]};`
}
// 进场动画
setTimeout(_ => {
let _nextStyle = 'opacity:1;'
for (let k in this.props.to) {
_nextStyle += `${k}:${this.props.to[k]};`
}
this.root.children[1].style.cssText += _nextStyle
}, 50)
}
if (_style) {
this.root.children[1].style.cssText += _style
}
// 不是prompt类型, 在有按钮时, 自动聚焦最后一个按钮
if (
this.props.type !== 'prompt' &&
this.props.btns.length &&
this.__CTRL__.lastElementChild
) {
// 部分浏览器需要执行一下 focus()才能聚焦成功
this.__CTRL__.lastElementChild.setAttribute('autofocus', '')
setTimeout(_ => this.__CTRL__.lastElementChild.focus(), 10)
}
if (this.props.type === 'toast') {
this.timer = setTimeout(() => {
toastInstance = null
this.close()
}, 3000)
}
if (this.props.type === 'notify') {
this._handleClose = $.bind(this.__TITLE__, 'click', ev => {
if (ev.target.tagName === 'WC-ICON') {
this.close()
}
})
}
}
unmounted() {
$.clearOutside(this._handlMask)
$.unbind(this.__TITLE__, 'click', this._handleClose)
$.unbind(this.__CTRL__, 'click', this._handleBtnClick)
this._handleSubmit && $.unbind(this.__INPUT__, 'submit', this._handleSubmit)
}
watch() {
switch (name) {
case 'title':
case 'type':
this[name] = val
break
case 'mask-color':
case 'background':
this.props[name] = val
break
case 'mask':
case 'mask-close':
case 'blur':
this.props[name] = true
break
case 'radius':
this.props.radius = val
break
case 'left':
case 'right':
case 'top':
case 'bottom':
this.props.from[name] = val
this.props.to = this.props.from
this.removeAttribute(name)
break
case 'fixed':
this.fixed = true
break
}
}
}
function _layer(opt) {
var layDom = document.createElement('wc-layer')
if (!opt.type) {
opt.type = 'common'
}
if (opt.type === 'toast') {
var { type, content } = opt
opt = {
type,
content,
from: { top: 0 },
to: { top: '30px' }
}
if (toastInstance) {
toastInstance.close(true)
}
toastInstance = layDom
} else {
layDom.props.mask = opt.mask
if (opt.btns === false) {
layDom.props.btns = []
} else if (opt.btns && opt.btns.length) {
layDom.props.btns = opt.btns
} else {
layDom.props.btns = lang.BTNS.concat()
}
if (opt.intercept && typeof opt.intercept === 'function') {
layDom.props.intercept = opt.intercept
}
layDom.props.mask = opt.mask
layDom.props['mask-close'] = opt['mask-close']
if (opt.hasOwnProperty('overflow')) {
layDom.props.overflow = opt.overflow
}
/* 额外样式 */
layDom.props['mask-color'] = opt['mask-color']
layDom.props.blur = opt.blur
layDom.props.radius = opt.radius
layDom.props.background = opt.background
if (opt.size && typeof opt.size === 'object') {
layDom.props.size = opt.size
}
// 这3种类型, 只允许同时存在1个, 如果之前有弹出则关闭
if (UNIQUE_TYPES.includes(opt.type)) {
if (uniqueInstance) {
uniqueInstance.close(true)
}
uniqueInstance = layDom
}
}
if (opt.to && typeof opt.to === 'object') {
layDom.props.to = opt.to
if (opt.from && typeof opt.from === 'object') {
layDom.props.from = opt.from
} else {
layDom.props.from = opt.to
}
}
layDom.props.type = opt.type
layDom.props.title = opt.title
if (opt.hasOwnProperty('fixed')) {
layDom.props.fixed = opt.fixed
}
layDom.innerHTML = opt.content
layDom.wrapped = false // 用于区分是API创建的还是包裹现有的节点
document.body.appendChild(layDom)
return layDom.promise
}
Object.assign(_layer, {
alert(content, title = lang.TITLE, btns) {
if (typeof title === 'object') {
btns = title
title = lang.TITLE
}
return this({
type: 'alert',
title,
content,
mask: true,
btns
})
},
confirm(content, title = lang.TITLE, btns) {
if (typeof title === 'object') {
btns = title
title = lang.TITLE
}
return this({
type: 'confirm',
title,
content,
mask: true,
btns
})
},
prompt(title = lang.TITLE, defaultValue = '', intercept) {
if (typeof defaultValue === 'function') {
intercept = defaultValue
defaultValue = ''
}
if (!intercept) {
intercept = function (val, done) {
if (val) {
done()
}
}
}
return this({
type: 'prompt',
title,
content: `<wc-input autofocus class="layer__content__input" value="${defaultValue}"></wc-input>`,
mask: true,
intercept
})
},
frame(url, extra = {}) {
return this({
...extra,
type: 'frame',
content: `<iframe class="layer__content__frame" src="${url}"></iframe>`,
mask: true,
'mask-close': true
})
},
notify(content) {
return this({
type: 'notify',
title: '通知',
content,
blur: true,
from: { right: '-300px', top: 0 },
to: { right: 0 }
})
},
toast(txt, type = 'info') {
var ico = type
switch (type) {
case 'info':
case 'warning':
break
case 'error':
ico = 'deny'
break
case 'success':
ico = 'get'
break
default:
ico = 'info'
}
return this({
content: `
<div class="layer__content__toast style-${type}">
<wc-icon is="${ico}"></wc-icon>
<span class="toast-txt">${txt}</span>
</div>`,
type: 'toast'
})
}
})
window.layer = _layer
</script>
wcui是一套基于`Web Components`的UI组件库, 宗旨是追求简单、实用、不花哨。
JavaScript 95.2%
CSS 4.8%