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
+
+
+从上方方弹出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;
+};