From 82bb27896ea03ea4d1a8f588bc59163205952b63 Mon Sep 17 00:00:00 2001 From: cookfront Date: Wed, 15 Feb 2017 21:03:07 +0800 Subject: [PATCH] popup component --- components.json | 3 +- docs/examples/popup.md | 84 +++++++++++++ docs/nav.config.json | 4 +- packages/popup/CHANGELOG.md | 8 ++ packages/popup/README.md | 26 ++++ packages/popup/index.js | 3 + packages/popup/package.json | 10 ++ packages/popup/src/popup.vue | 71 +++++++++++ packages/zanui/src/index.pcss | 1 + packages/zanui/src/popup.pcss | 75 ++++++++++++ src/index.js | 5 +- src/mixins/popup/index.js | 194 ++++++++++++++++++++++++++++++ src/mixins/popup/popup-manager.js | 133 ++++++++++++++++++++ src/utils/dom.js | 45 +++++++ src/utils/merge.js | 15 +++ 15 files changed, 673 insertions(+), 4 deletions(-) create mode 100644 docs/examples/popup.md create mode 100644 packages/popup/CHANGELOG.md create mode 100644 packages/popup/README.md create mode 100644 packages/popup/index.js create mode 100644 packages/popup/package.json create mode 100644 packages/popup/src/popup.vue create mode 100644 packages/zanui/src/popup.pcss create mode 100644 src/mixins/popup/index.js create mode 100644 src/mixins/popup/popup-manager.js create mode 100644 src/utils/dom.js create mode 100644 src/utils/merge.js diff --git a/components.json b/components.json index fa7c0daea..a11836bf4 100644 --- a/components.json +++ b/components.json @@ -5,5 +5,6 @@ "radio": "./packages/radio/index.js", "cell": "./packages/cell/index.js", "icon": "./packages/icon/index.js", - "cell-group": "./packages/cell-group/index.js" + "cell-group": "./packages/cell-group/index.js", + "popup": "./packages/popup/index.js" } diff --git a/docs/examples/popup.md b/docs/examples/popup.md new file mode 100644 index 000000000..e5ca59b0b --- /dev/null +++ b/docs/examples/popup.md @@ -0,0 +1,84 @@ + + + + +## Popup组件 + +### 基础用法 + +:::demo +```html +从下方弹出popup + + xxxx + + +从上方方弹出popup + + 更新成功 + + +从右方弹出popup + + 关闭 popup + + +从中间弹出popup + + 一些内容 + +``` +::: + +### API + +| 参数 | 说明 | 类型 | 默认值 | 可选值 | +|-----------|-----------|-----------|-------------|-------------| +| value | 利用`v-model`绑定当前组件是否显示 | Boolean | '' | | \ No newline at end of file diff --git a/docs/nav.config.json b/docs/nav.config.json index 28b555022..38c9aee7c 100644 --- a/docs/nav.config.json +++ b/docs/nav.config.json @@ -77,8 +77,8 @@ "title": "Lazyload" }, { - "path": "/pop", - "title": "Pop" + "path": "/popup", + "title": "Popup" }, { "path": "/swipe", diff --git a/packages/popup/CHANGELOG.md b/packages/popup/CHANGELOG.md new file mode 100644 index 000000000..e88c472b3 --- /dev/null +++ b/packages/popup/CHANGELOG.md @@ -0,0 +1,8 @@ +## 0.0.2 (2017-01-20) + +* 改了bug A +* 加了功能B + +## 0.0.1 (2017-01-10) + +* 第一版 diff --git a/packages/popup/README.md b/packages/popup/README.md new file mode 100644 index 000000000..4c6172563 --- /dev/null +++ b/packages/popup/README.md @@ -0,0 +1,26 @@ +# @youzan/<%= name %> + +!!! 请在此处填写你的文档最简单描述 !!! + +[![version][version-image]][download-url] +[![download][download-image]][download-url] + +[version-image]: http://npm.qima-inc.com/badge/v/@youzan/<%= name %>.svg?style=flat-square +[download-image]: http://npm.qima-inc.com/badge/d/@youzan/<%= name %>.svg?style=flat-square +[download-url]: http://npm.qima-inc.com/package/@youzan/<%= name %> + +## Demo + +## Usage + +## API + +| 参数 | 说明 | 类型 | 默认值 | 可选值 | +|-----------|-----------|-----------|-------------|-------------| +| className | 自定义额外类名 | string | '' | '' | + + + + +## License +[MIT](https://opensource.org/licenses/MIT) diff --git a/packages/popup/index.js b/packages/popup/index.js new file mode 100644 index 000000000..31857d9ef --- /dev/null +++ b/packages/popup/index.js @@ -0,0 +1,3 @@ +import Popup from './src/popup'; + +export default Popup; diff --git a/packages/popup/package.json b/packages/popup/package.json new file mode 100644 index 000000000..7dbfa2900 --- /dev/null +++ b/packages/popup/package.json @@ -0,0 +1,10 @@ +{ + "name": "<%= name %>", + "version": "<%= version %>", + "description": "<%= description %>", + "main": "./lib/index.js", + "author": "<%= author %>", + "license": "<%= license %>", + "devDependencies": {}, + "dependencies": {} +} diff --git a/packages/popup/src/popup.vue b/packages/popup/src/popup.vue new file mode 100644 index 000000000..1f27a2f25 --- /dev/null +++ b/packages/popup/src/popup.vue @@ -0,0 +1,71 @@ + + + diff --git a/packages/zanui/src/index.pcss b/packages/zanui/src/index.pcss index 469e089fc..554cafe0f 100644 --- a/packages/zanui/src/index.pcss +++ b/packages/zanui/src/index.pcss @@ -5,4 +5,5 @@ @import './cell.pcss'; @import './field.pcss'; @import './icon.pcss'; +@import './popup.pcss'; @import './switch.pcss'; diff --git a/packages/zanui/src/popup.pcss b/packages/zanui/src/popup.pcss new file mode 100644 index 000000000..52798154f --- /dev/null +++ b/packages/zanui/src/popup.pcss @@ -0,0 +1,75 @@ +.v-modal { + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.701961); +} + +@component-namespace o2 { + @component popup { + position: fixed; + background-color: #fff; + top: 50%; + left: 50%; + transform: translate3d(-50%, -50%, 0); + backface-visibility: hidden; + transition: .2s ease-out; + + @modifier top { + top: 0; + right: auto; + bottom: auto; + left: 50%; + transform: translate3d(-50%, 0, 0); + } + + @modifier right { + top: 50%; + right: 0; + bottom: auto; + left: auto; + transform: translate3d(0, -50%, 0); + } + + @modifier bottom { + top: auto; + bottom: 0; + right: auto; + left: 50%; + transform: translate3d(-50%, 0, 0); + } + + @modifier left { + top: 50%; + right: auto; + bottom: auto; + left: 0; + transform: translate3d(0, -50%, 0); + } + } +} + +.popup-slide-top-enter, +.popup-slide-top-leave-active { + transform: translate3d(-50%, -100%, 0); +} + +.popup-slide-right-enter, +.popup-slide-right-leave-active { + transform: translate3d(100%, -50%, 0); +} + +.popup-slide-bottom-enter, +.popup-slide-bottom-leave-active { + transform: translate3d(-50%, 100%, 0); +} + +.popup-slide-left-enter, .popup-slide-left-leave-active { + transform: translate3d(-100%, -50%, 0); +} + +.popup-fade-enter, .popup-fade-leave-active { + opacity: 0; +} diff --git a/src/index.js b/src/index.js index 3ffa774fd..42ea5ca67 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ import Radio from '../packages/radio/index.js'; import Cell from '../packages/cell/index.js'; import Icon from '../packages/icon/index.js'; import CellGroup from '../packages/cell-group/index.js'; +import Popup from '../packages/popup/index.js'; // zanui import '../packages/zanui/src/index.pcss'; @@ -18,6 +19,7 @@ const install = function(Vue) { Vue.component(Cell.name, Cell); Vue.component(Icon.name, Icon); Vue.component(CellGroup.name, CellGroup); + Vue.component(Popup.name, Popup); }; // auto install @@ -34,5 +36,6 @@ module.exports = { Radio, Cell, Icon, - CellGroup + CellGroup, + Popup }; diff --git a/src/mixins/popup/index.js b/src/mixins/popup/index.js new file mode 100644 index 000000000..103a1d44a --- /dev/null +++ b/src/mixins/popup/index.js @@ -0,0 +1,194 @@ +import Vue from 'vue'; +import merge from 'src/utils/merge'; +import PopupManager from './popup-manager'; + +let idSeed = 1; + +const getDOM = function(dom) { + if (dom.nodeType === 3) { + dom = dom.nextElementSibling || dom.nextSibling; + getDOM(dom); + } + return dom; +}; + +let scrollBarWidth; +const getScrollBarWidth = () => { + if (Vue.prototype.$isServer) return; + if (scrollBarWidth !== undefined) return scrollBarWidth; + + const outer = document.createElement('div'); + outer.style.visibility = 'hidden'; + outer.style.width = '100px'; + outer.style.position = 'absolute'; + outer.style.top = '-9999px'; + document.body.appendChild(outer); + + const widthNoScroll = outer.offsetWidth; + outer.style.overflow = 'scroll'; + + const inner = document.createElement('div'); + inner.style.width = '100%'; + outer.appendChild(inner); + + const widthWithScroll = inner.offsetWidth; + outer.parentNode.removeChild(outer); + + return widthNoScroll - widthWithScroll; +}; + +export default { + props: { + /** + * popup当前显示状态 + */ + value: { + type: Boolean, + default: false + }, + /** + * 是否显示遮罩层 + */ + overlay: { + type: Boolean, + default: false + }, + /** + * 点击遮罩层是否关闭popup + */ + closeOnClickOverlay: { + type: Boolean, + default: false + }, + zIndex: [String, Number], + /** + * popup滚动时是否body内容也滚动 + * 默认为不滚动 + */ + lockOnScroll: { + type: Boolean, + default: true + } + }, + + watch: { + value(val) { + if (val) { + if (this.opening) return; + + if (!this.rendered) { + this.rendered = true; + Vue.nextTick(() => { + this.open(); + }); + } else { + this.open(); + } + } else { + this.close(); + } + } + }, + + beforeMount() { + this._popupId = 'popup-' + idSeed++; + PopupManager.register(this._popupId, this); + }, + + data() { + return { + opening: false, + opened: false, + closing: false, + bodyOverflow: null, + bodyPaddingRight: null + }; + }, + + methods: { + /** + * 显示popup + */ + open(options) { + if (this.opened) return; + + this.opening = true; + + this.$emit('input', true); + + const dom = getDOM(this.$el); + const props = merge({}, this, options); + const overlay = props.overlay; + const zIndex = props.zIndex; + + // 如果属性中传入了`zIndex`,则覆盖`PopupManager`中对应的`zIndex` + if (zIndex) { + PopupManager.zIndex = zIndex; + } + + // 如果显示遮罩层 + if (overlay) { + if (this.closing) { + PopupManager.closeModal(this._popupId); + this.closing = false; + } + PopupManager.openModal(this._popupId, PopupManager.nextZIndex(), dom); + if (props.lockOnScroll) { + // 将原来的`bodyOverflow`和`bodyPaddingRight`存起来 + if (!this.bodyOverflow) { + this.bodyPaddingRight = document.body.style.paddingRight; + this.bodyOverflow = document.body.style.overflow; + } + scrollBarWidth = getScrollBarWidth(); + let bodyHasOverflow = document.documentElement.clientHeight < document.body.scrollHeight; + if (scrollBarWidth > 0 && bodyHasOverflow) { + document.body.style.paddingRight = scrollBarWidth + 'px'; + } + document.body.style.overlay = 'hidden'; + } + } + + dom.style.zIndex = PopupManager.nextZIndex(); + this.opened = true; + this.opening = false; + }, + + /** + * 关闭popup + */ + close() { + if (this.closing) return; + + this.closing = true; + + this.$emit('input', false); + + if (this.lockOnScroll) { + setTimeout(() => { + if (this.modal && this.bodyOverflow !== 'hidden') { + document.body.style.overflow = this.bodyOverflow; + document.body.style.paddingRight = this.bodyPaddingRight; + } + this.bodyOverflow = null; + this.bodyPaddingRight = null; + }, 200); + } + + PopupManager.closeModal(this._popupId); + this.opened = false; + this.closing = false; + } + }, + + beforeDestroy() { + PopupManager.deregister(this._popupId); + PopupManager.closeModal(this._popupId); + + if (this.modal && this.bodyOverflow !== null && this.bodyOverflow !== 'hidden') { + document.body.style.overflow = this.bodyOverflow; + document.body.style.paddingRight = this.bodyPaddingRight; + } + this.bodyOverflow = null; + this.bodyPaddingRight = null; + } +}; diff --git a/src/mixins/popup/popup-manager.js b/src/mixins/popup/popup-manager.js new file mode 100644 index 000000000..c339df3a9 --- /dev/null +++ b/src/mixins/popup/popup-manager.js @@ -0,0 +1,133 @@ +import { addClass, removeClass } from 'src/utils/dom'; + +let hasModal = false; + +const getModal = function() { + let modalDom = PopupManager.modalDom; + if (modalDom) { + hasModal = true; + } else { + hasModal = false; + modalDom = document.createElement('div'); + PopupManager.modalDom = modalDom; + + modalDom.addEventListener('touchmove', function(event) { + event.preventDefault(); + event.stopPropagation(); + }); + + modalDom.addEventListener('click', function() { + PopupManager.handleOverlayClick && PopupManager.handleOverlayClick(); + }); + } + + return modalDom; +}; + +const instances = {}; + +const PopupManager = { + zIndex: 2000, + + modalStack: [], + + nextZIndex() { + return this.zIndex++; + }, + + getInstance(id) { + return instances[id]; + }, + + register(id, instance) { + if (id && instance) { + instances[id] = instance; + } + }, + + deregister(id) { + if (id) { + instances[id] = null; + delete instances[id]; + } + }, + + handleOverlayClick() { + const topModal = PopupManager.modalStack[PopupManager.modalStack.length - 1]; + if (!topModal) return; + + const instance = PopupManager.getInstance(topModal.id); + if (instance && instance.closeOnClickOverlay) { + instance.close(); + } + }, + + openModal(id, zIndex, dom) { + if (!id || zIndex === undefined) return; + + const modalStack = this.modalStack; + + for (let i = 0, j = modalStack.length; i < j; i++) { + const item = modalStack[i]; + if (item.id === id) { + return; + } + } + + const modalDom = getModal(); + + addClass(modalDom, 'v-modal'); + setTimeout(() => { + removeClass(modalDom, 'v-modal-enter'); + }, 200); + + if (dom && dom.parentNode && dom.parentNode.nodeType !== 11) { + dom.parentNode.appendChild(modalDom); + } else { + document.body.appendChild(modalDom); + } + + if (zIndex) { + modalDom.style.zIndex = zIndex; + } + modalDom.style.display = ''; + + this.modalStack.push({ id: id, zIndex: zIndex }); + }, + + closeModal(id) { + const modalStack = this.modalStack; + const modalDom = getModal(); + + if (modalStack.length > 0) { + const topItem = modalStack[modalStack.length - 1]; + if (topItem.id === id) { + modalStack.pop(); + if (modalStack.length > 0) { + modalDom.style.zIndex = modalStack[modalStack.length - 1].zIndex; + } + } else { + for (let i = modalStack.length - 1; i >= 0; i--) { + if (modalStack[i].id === id) { + modalStack.splice(i, 1); + break; + } + } + } + } + + if (modalStack.length === 0) { + setTimeout(() => { + if (modalStack.length === 0) { + if (modalDom.parentNode) modalDom.parentNode.removeChild(modalDom); + + modalDom.style.display = 'none'; + this.modalDom = undefined; + } + removeClass(modalDom, 'v-modal-leave'); + }, 200); + } + } +}; + +export default PopupManager; diff --git a/src/utils/dom.js b/src/utils/dom.js new file mode 100644 index 000000000..0f7e6cb66 --- /dev/null +++ b/src/utils/dom.js @@ -0,0 +1,45 @@ +/* istanbul ignore next */ +export function addClass(el, cls) { + if (!el) return; + var curClass = el.className; + var classes = (cls || '').split(' '); + + for (var i = 0, j = classes.length; i < j; i++) { + var clsName = classes[i]; + if (!clsName) continue; + + if (el.classList) { + el.classList.add(clsName); + } else { + if (!hasClass(el, clsName)) { + curClass += ' ' + clsName; + } + } + } + if (!el.classList) { + el.className = curClass; + } +}; + +/* istanbul ignore next */ +export function removeClass(el, cls) { + if (!el || !cls) return; + var classes = cls.split(' '); + var curClass = ' ' + el.className + ' '; + + for (var i = 0, j = classes.length; i < j; i++) { + var clsName = classes[i]; + if (!clsName) continue; + + if (el.classList) { + el.classList.remove(clsName); + } else { + if (hasClass(el, clsName)) { + curClass = curClass.replace(' ' + clsName + ' ', ' '); + } + } + } + if (!el.classList) { + el.className = trim(curClass); + } +}; diff --git a/src/utils/merge.js b/src/utils/merge.js new file mode 100644 index 000000000..c0aa39a55 --- /dev/null +++ b/src/utils/merge.js @@ -0,0 +1,15 @@ +export default function(target, ...sources) { + for (let i = 1, j = sources.length; i < j; i++) { + let source = arguments[i] || {}; + for (let prop in source) { + if (source.hasOwnProperty(prop)) { + let value = source[prop]; + if (value !== undefined) { + target[prop] = value; + } + } + } + } + + return target; +};