diff --git a/build/bin/build-entry.js b/build/bin/build-entry.js index 2419df433..a976ee92e 100644 --- a/build/bin/build-entry.js +++ b/build/bin/build-entry.js @@ -51,7 +51,7 @@ ComponentNames.forEach(name => { 'Lazyload', // services - 'MessageBox', + 'Dialog', 'Toast', 'Indicator' ].indexOf(componentName) === -1) { diff --git a/components.json b/components.json index fa7c0daea..1ed3e41c2 100644 --- a/components.json +++ b/components.json @@ -5,5 +5,7 @@ "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", + "dialog": "./packages/dialog/index.js" } diff --git a/docs/examples/dialog.md b/docs/examples/dialog.md new file mode 100644 index 000000000..46305c766 --- /dev/null +++ b/docs/examples/dialog.md @@ -0,0 +1,46 @@ + + +## Dialog组件 + +### 基础用法 + +:::demo +```html +alert + +confirm +``` +::: + +### API + +| 参数 | 说明 | 类型 | 默认值 | 可选值 | +|-----------|-----------|-----------|-------------|-------------| +| title | 标题 | String | '' | | +| message | 内容 | String | '' | | diff --git a/docs/examples/popup.md b/docs/examples/popup.md new file mode 100644 index 000000000..3778b530e --- /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 | '' | | diff --git a/docs/nav.config.json b/docs/nav.config.json index 28b555022..6046705d1 100644 --- a/docs/nav.config.json +++ b/docs/nav.config.json @@ -77,8 +77,12 @@ "title": "Lazyload" }, { - "path": "/pop", - "title": "Pop" + "path": "/popup", + "title": "Popup" + }, + { + "path": "/dialog", + "title": "Dialog" }, { "path": "/swipe", diff --git a/packages/dialog/CHANGELOG.md b/packages/dialog/CHANGELOG.md new file mode 100644 index 000000000..e88c472b3 --- /dev/null +++ b/packages/dialog/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/dialog/README.md b/packages/dialog/README.md new file mode 100644 index 000000000..4c6172563 --- /dev/null +++ b/packages/dialog/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/dialog/index.js b/packages/dialog/index.js new file mode 100644 index 000000000..cb8dcd8d5 --- /dev/null +++ b/packages/dialog/index.js @@ -0,0 +1,3 @@ +import Dialog from './src/dialog.js'; + +export default Dialog; diff --git a/packages/dialog/package.json b/packages/dialog/package.json new file mode 100644 index 000000000..7dbfa2900 --- /dev/null +++ b/packages/dialog/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/dialog/src/dialog.js b/packages/dialog/src/dialog.js new file mode 100644 index 000000000..fa295ac15 --- /dev/null +++ b/packages/dialog/src/dialog.js @@ -0,0 +1,98 @@ +import Vue from 'vue'; +import Dialog from './dialog.vue'; +import merge from 'src/utils/merge'; + +const DialogConstructor = Vue.extend(Dialog); + +let currentDialog; +let instance; +let dialogQueue = []; + +const defaultCallback = action => { + if (currentDialog) { + let callback = currentDialog.callback; + + if (typeof callback === 'function') { + callback(action); + } + + if (currentDialog.resolve && action === 'confirm') { + currentDialog.resolve(action); + } else if (currentDialog.reject && action === 'cancel') { + currentDialog.reject(action); + } + } +}; + +const initInstance = () => { + instance = new DialogConstructor({ + el: document.createElement('div') + }); + + instance.callback = defaultCallback; +}; + +const showNextDialog = () => { + if (!instance) { + initInstance(); + } + + if (!instance.value && dialogQueue.length > 0) { + currentDialog = dialogQueue.shift(); + + let options = currentDialog.options; + + for (let prop in options) { + if (options.hasOwnProperty(prop)) { + instance[prop] = options[prop]; + } + } + + if (options.callback === undefined) { + instance.callback = defaultCallback; + } + + document.body.appendChild(instance.$el); + + Vue.nextTick(() => { + instance.value = true; + }); + } +}; + +var DialogBox = options => { + return new Promise((resolve, reject) => { // eslint-disable-line + dialogQueue.push({ + options: merge({}, options), + callback: options.callback, + resolve: resolve, + reject: reject + }); + + showNextDialog(); + }); +}; + +DialogBox.alert = function(options) { + return DialogBox(merge({ + type: 'alert', + closeOnClickOverlay: false, + showCancelButton: false + }, options)); +}; + +DialogBox.confirm = function(options) { + return DialogBox(merge({ + type: 'confirm', + closeOnClickOverlay: true, + showCancelButton: true + }, options)); +}; + +DialogBox.close = function() { + instance.value = false; + dialogQueue = []; + currentDialog = null; +}; + +export default DialogBox; diff --git a/packages/dialog/src/dialog.vue b/packages/dialog/src/dialog.vue new file mode 100644 index 000000000..bac5b3437 --- /dev/null +++ b/packages/dialog/src/dialog.vue @@ -0,0 +1,85 @@ + + + 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..1740fc049 --- /dev/null +++ b/packages/popup/src/popup.vue @@ -0,0 +1,71 @@ + + + diff --git a/packages/zanui/src/dialog.pcss b/packages/zanui/src/dialog.pcss new file mode 100644 index 000000000..a8ed0db0a --- /dev/null +++ b/packages/zanui/src/dialog.pcss @@ -0,0 +1,95 @@ +@import "./mixins/border_retina.pcss"; + +@component-namespace o2 { + @component dialog-wrapper { + position: absolute; + } + + @component dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate3d(-50%, -50%, 0); + background-color: #fff; + width: 85%; + border-radius: 4px; + font-size: 16px; + overflow: hidden; + backface-visibility: hidden; + transition: .2s; + + @descendent header { + padding: 15px 0 0; + } + + @descendent content { + padding: 15px 20px; + min-height: 36px; + position: relative; + + &::after { + @mixin border-retina (bottom); + } + } + + @descendent title { + text-align: center; + padding-left: 0; + margin-bottom: 0; + font-size: 16px; + color: #333; + } + + @descendent message { + color: #999; + margin: 0; + font-size: 14px; + line-height: 1.5; + } + + @descendent footer { + font-size: 14px; + overflow: hidden; + } + + .is-twobtn { + .o2-dialog-btn { + width: 50%; + } + + .o2-dialog-cancel { + &::after { + @mixin border-retina (right); + } + } + } + + @descendent btn { + line-height: 40px; + border: 0; + background-color: #fff; + float: left; + box-sizing: border-box; + text-align: center; + position: relative; + } + + @descendent cancel { + color: #333; + } + + @descendent confirm { + color: #00C000; + width: 100%; + } + } +} + +.dialog-bounce-enter { + opacity: 0; + transform: translate3d(-50%, -50%, 0) scale(0.7); +} +.dialog-bounce-leave-active { + opacity: 0; + transform: translate3d(-50%, -50%, 0) scale(0.9); +} diff --git a/packages/zanui/src/index.pcss b/packages/zanui/src/index.pcss index 469e089fc..64007adf1 100644 --- a/packages/zanui/src/index.pcss +++ b/packages/zanui/src/index.pcss @@ -3,6 +3,8 @@ */ @import './button.pcss'; @import './cell.pcss'; +@import './dialog.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..b3dd83c70 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,8 @@ 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'; +import Dialog from '../packages/dialog/index.js'; // zanui import '../packages/zanui/src/index.pcss'; @@ -18,6 +20,8 @@ const install = function(Vue) { Vue.component(Cell.name, Cell); Vue.component(Icon.name, Icon); Vue.component(CellGroup.name, CellGroup); + Vue.component(Popup.name, Popup); + // Vue.component(Dialog.name, Dialog); }; // auto install @@ -34,5 +38,7 @@ module.exports = { Radio, Cell, Icon, - CellGroup + CellGroup, + Popup, + Dialog }; diff --git a/src/mixins/popup/index.js b/src/mixins/popup/index.js new file mode 100644 index 000000000..cf0636e9e --- /dev/null +++ b/src/mixins/popup/index.js @@ -0,0 +1,195 @@ +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 (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; + this.open(); + } else { + if (this.closing) return; + 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(); + + // 页面是否`overflow` + 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); + } + + this.opened = false; + this.doAfterClose(); + }, + + doAfterClose() { + this.closing = false; + PopupManager.closeModal(this._popupId); + } + }, + + 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..9ba799094 --- /dev/null +++ b/src/mixins/popup/popup-manager.js @@ -0,0 +1,136 @@ +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]; + } + }, + + /** + * 遮罩层点击回调,`closeOnClickOverlay`为`true`时会关闭当前`popup` + */ + 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..ede0951ec --- /dev/null +++ b/src/utils/dom.js @@ -0,0 +1,57 @@ +const trim = function(string) { + return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, ''); +}; + +export function hasClass(el, cls) { + if (!el || !cls) return false; + if (cls.indexOf(' ') !== -1) throw new Error('className should not contain space.'); + if (el.classList) { + return el.classList.contains(cls); + } else { + return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1; + } +}; + +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; + } +}; + +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..564a989a4 --- /dev/null +++ b/src/utils/merge.js @@ -0,0 +1,15 @@ +export default function(target, ...sources) { + for (let i = 0; i < sources.length; i++) { + let source = sources[i] || {}; + for (let prop in source) { + if (source.hasOwnProperty(prop)) { + let value = source[prop]; + if (value !== undefined) { + target[prop] = value; + } + } + } + } + + return target; +};