/** * {弹窗组件} * @author yutent * @date 2023/03/06 15:17:25 */ import { css, html, Component, bind, unbind, nextTick, styleMap } from '@bd/core' import '../form/input.js' import Drag from '../drag/core.js' let uniqueInstance = null // 缓存当前打开的alert/confirm/prompt类型的弹窗 let toastInstance = null // 缓存toast的实例 const LANG_TITLE = '提示' const LANG_BTNS = ['取消', '确定'] // 要保证弹层唯一的类型 const UNIQUE_TYPES = ['alert', 'confirm', 'prompt'] class Layer extends Component { static props = { type: '', mask: false, title: { type: String, default: LANG_TITLE, attribute: false }, content: { type: String, default: '', attribute: false }, btns: [] } static styles = [ css` :host { display: none; justify-content: center; align-items: center; position: fixed; z-index: 65534; left: 0; top: 0; width: 100%; } :host([type]) { display: flex; } .noselect { -webkit-touch-callout: none; user-select: none; img, a { -webkit-user-drag: none; } } `, css` .layer { overflow: hidden; flex: 0 auto; position: relative; z-index: 65535; border-radius: 3px; 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; &.scale { transform: scale(1.01); transition: transform 0.1s linear; } &.blur { backdrop-filter: blur(5px); } &:active { z-index: 65536; } } `, /* 弹层样式 */ css` .layer { &__title { display: flex; 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; } ::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: flex; 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: 3px; 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; } } } } `, css` :host([mask]) { height: 100%; background: rgba(255, 255, 255, 0.15); backdrop-filter: blur(5px); } :host([type='alert']), :host([type='confirm']), :host([type='prompt']) { .layer { max-width: 600px; min-width: 300px; background: #fff; &__content { padding: 0 15px; } } } :host([type='notify']) { .layer { width: 300px; height: 120px; &__content { padding: 0 15px; } } } :host([type='toast']) { .layer { box-shadow: none; &__content { min-height: 40px; } } } ` ] #wrapped = null #dragIns = null #resolve = null #reject = null constructor() { super() this.promise = new Promise((resolve, reject) => { this.#resolve = resolve this.#reject = reject }) this.promise.host = this } #intercept(value) { if (this.intercept) { this.intercept(value, _ => { delete this.intercept this.#resolve(value) this.$refs.box.$animate(true).then(_ => this.close()) }) } else { this.#resolve(value) this.$refs.box.$animate(true).then(_ => this.close()) } } mounted() { if (this.type === 'prompt') { this.$refs.input = this.firstElementChild bind(this.$refs.input, 'submit', ev => { this.#intercept(ev.target.value) }) } this.$refs.box.$animate() } updated() { this.$refs.box.$animate() } /** * 关闭实例 * @param force {Boolean} 是否强制关闭 */ close(force) { // if (this.#wrapped) { this.removeAttribute('common') this.$emit('close') } else { // 有拖拽实例, 先销毁 if (this.#dragIns) { this.#dragIns.destroy() } // 不允许多开的类型, 需要清除 if (UNIQUE_TYPES.includes(this.type)) { uniqueInstance = null } // 离场动画 if (this.from && !force) { let _style = 'opacity:0;' for (let k in this.from) { _style += `${k}:${this.from[k]};` } this.$refs.box.style.cssText += _style this.timer = setTimeout(() => { this.$emit('close') this.remove() }, 200) } else { clearTimeout(this.timer) this.$emit('close') this.remove() } } } // 按钮的点击事件 handleBtnClick(ev) { if (ev.target.tagName === 'BUTTON') { let idx = +ev.target.dataset.idx switch (this.type) { case 'alert': this.$refs.box.$animate(true).then(_ => this.close()) break case 'confirm': case 'prompt': if (idx === 0) { this.#reject() this.$refs.box.$animate(true).then(_ => this.close()) } else { let value = this.type === 'prompt' ? this.$refs.input.value : null this.#intercept(value) } break default: // 其他类型, 如有按钮, 直接交给拦截器处理 this.#intercept(idx) break } } } render() { return html`
${this.title}${this.type === 'notify' ? html`` : ''}
${this.btns.map((s, i) => html``)}
` } } function layer(opt = {}) { let layDom = document.createElement('wc-layer') let { type = 'common', content = '' } = opt if (type === 'toast') { opt = { type, content, from: { top: 0 }, to: { top: '30px' } } if (toastInstance) { toastInstance.close(true) } toastInstance = layDom } else { layDom.mask = opt.mask if (opt.btns === false) { layDom.btns = [] } else if (opt.btns && opt.btns.length) { layDom.btns = opt.btns } else { layDom.btns = LANG_BTNS.concat() } if (opt.intercept && typeof opt.intercept === 'function') { layDom.intercept = opt.intercept } layDom.mask = opt.mask layDom['mask-close'] = opt['mask-close'] if (opt.hasOwnProperty('overflow')) { layDom.overflow = opt.overflow } /* 额外样式 */ layDom['mask-color'] = opt['mask-color'] layDom.blur = opt.blur layDom.radius = opt.radius layDom.background = opt.background if (opt.size && typeof opt.size === 'object') { layDom.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.to = opt.to if (opt.from && typeof opt.from === 'object') { layDom.from = opt.from } else { layDom.from = opt.to } } layDom.type = opt.type layDom.title = opt.title if (opt.hasOwnProperty('fixed')) { layDom.fixed = opt.fixed } layDom.innerHTML = content layDom.wrapped = false // 用于区分是API创建的还是包裹现有的节点 document.body.appendChild(layDom) return layDom.promise } layer.alert = function ( content, title = LANG_TITLE, btns = LANG_BTNS.slice(1) ) { if (typeof title === 'object') { btns = title title = LANG_TITLE } return this({ type: 'alert', title, content, mask: true, btns }) } layer.confirm = function ( content, title = LANG_TITLE, btns = LANG_BTNS.concat() ) { if (typeof title === 'object') { btns = title title = LANG_TITLE } return this({ type: 'confirm', title, content, mask: true, btns }) } layer.prompt = function (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: ``, mask: true, intercept, btns: LANG_BTNS.concat() }) } layer.notify = function (content) { return this({ type: 'notify', title: '通知', content, blur: true, from: { right: '-300px', top: 0 }, to: { right: 0 } }) } layer.toast = function (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: `
${txt}
`, type: 'toast' }) } Layer.reg('layer') window.layer = layer