829 lines
18 KiB
Plaintext
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>
|
JavaScript
95.2%
CSS
4.8%