My first posting went off with an unpleasant aftertaste. I promised to correct this misunderstanding and present your first article-lesson on VueJs to your court. I hope it will be useful. A lot of thoughts, a lot of experience too. All my life I learn from other people's articles, lessons. It's time to share knowledge too.And we will create modal windows. Yes, again they are. But not as simple as described in my first (not my) publication.
<template> <button @click="visible = true">Show modal</button> <modal :show="visible"> <div> </div> </modal> </template> <script> export default { data() { return { visible: false } } </script>
{ name: 'simple-modal', props: ['test'], template: "<div>{{test}}</div>" }
import Bus from './utils/bus'; import ModalWrapper from './modal-wrapper'; import Modal from './modal' const VuModal = {} VuModal.install = (Vue) => { Vue.prototype.$modals = new Vue({ name: '$modals', created() { Bus.$on('opened', data => { this.$emit('modals:opened', data); }); Bus.$on('closed', data => { this.$emit('modals:closed', data); }); Bus.$on('destroyed', data => { this.$emit('modals:destroyed', data); }); this.$on('new', options => { this.open(options); }); this.$on('close', data => { this.close(data); }); this.$on('dismiss', index => { this.dismiss(index || null); }); }, methods: { open(options = null) { Bus.$emit('new', options); }, close(data = null) { Bus.$emit('close', data); }, dismiss(index = null) { Bus.$emit('dismiss', index); } } }); Vue.mixin({ created() { this.$on('modals:new', options => { Bus.$emit('new', options); }); this.$on('modals:close', data => { Bus.$emit('close', data); }); this.$on('modals:dismiss', index => { Bus.$emit('dismiss', index); }); } }); } if (typeof window !== 'undefined' && window.Vue) { window.Vue.use(VuModal); } export default VuModal; export { ModalWrapper, Modal, Bus }
let instance = null; class EventBus { constructor() { if (!instance) { this.events = {}; instance = this; } return instance; } $emit(event, message) { if (!this.events[event]) return; const callbacks = this.events[event]; for (let i = 0, l = callbacks.length; i < l; i++) { const callback = callbacks[i]; callback.call(this, message); } } $on(event, callback) { if (!this.events[event]) this.events[event] = []; this.events[event].push(callback); } } export default new EventBus();
Vue.prototype.$modals = new Vue({ name: '$modals', created() { Bus.$on('opened', data => { this.$emit('modals:opened', data); }); Bus.$on('closed', data => { this.$emit('modals:closed', data); }); Bus.$on('destroyed', data => { this.$emit('modals:destroyed', data); }); this.$on('new', options => { this.open(options); }); this.$on('close', data => { this.close(data); }); this.$on('dismiss', index => { this.dismiss(index || null); }); }, methods: { open(options = null) { Bus.$emit('new', options); }, close(data = null) { Bus.$emit('close', data); }, dismiss(index = null) { Bus.$emit('dismiss', index); } } });
Vue.mixin({ created() { this.$on('modals:new', options => { Bus.$emit('new', options); }); this.$on('modals:close', data => { Bus.$emit('close', data); }); this.$on('modals:dismiss', index => { Bus.$emit('dismiss', index); }); } });
this.$modals.open(options)
this.$modals.$emit('new', options)
this.$emit('modals:new', options)
Bus.$emit('new', options)
props: { title: { // type: String, default: '' }, className: { // css- type: String, default: '' }, isScroll: { // , - type: Boolean, default: false }, escapable: { // dismiss() Esc- type: Boolean, default: false }, dismissable: { // dismiss() () type: Boolean, default: true }, fullscreen: { // type: Boolean, default: false }, isTop: { // type: Boolean, default: false }, isBottom: { // type: Boolean, default: false }, isLeft: { // type: Boolean, default: false }, isRight: { // type: Boolean, default: false }, center: { // type: Boolean, default: false }, size: { // () type: String, default: 'md' }, bodyPadding: { // padding- body - , type: Boolean, default: true } },
import CloseIcon from './close-icon' export default { name: 'vu-modal', componentName: 'vu-modal', ... }
export default { name: 'close-icon', functional: true, render(h) { return h('svg', { attrs: { width: '12px', height: '12px', viewBox: '0 0 12 12', xmlSpace: 'preserve' } }, [ h('line', { attrs: { x1: 1, y1: 11, x2: 11, y2: 1 }, style: { strokeLinecap: 'round', strokeLinejoin: 'round', } }), h('line', { attrs: { x1: 1, y1: 1, x2: 11, y2: 11 }, style: { strokeLinecap: 'round', strokeLinejoin: 'round', } }) ]) } }
export default { ... computed: { propsData() { return (this.$parent.$vnode.data.props && this.$parent.$vnode.data.props.vModal) ? this.$parent.$vnode.data.props.vModal : this.$props } } }
<template lang="html"> <vu-modal title="Test modal" :isScroll="true" size="p50" :center="true" :escapable="true"> <div slot="header" style="display: flex; justify-content: left; align-items: center;" v-if="header"> <div style="padding-left: 10px">Simple modal</div> </div> <div> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quia consequuntur minus sint quidem ut tenetur dicta sunt voluptates numquam. Eum totam ex maxime aut recusandae quae laborum fugit ab autem.</p> </div> <div slot="footer"> <button class="uk-button uk-button-smaller uk-button-primary" @click="close">Cancel</button> </div> </vu-modal> </template> <script> export default { name: 'simple-modal', props: { lorem: { type: Boolean, default: true } } }; </script>
import SimpleModal from './modals/simple' ... this.$modals.open({ component: SimpleModal })
import SimpleModal from './modals/simple' ... this.$modals.open({ component: SimpleModal, center: false })
export default { ... computed: { propsData() { return (this.$parent.$vnode.data.props && this.$parent.$vnode.data.props.vModal) ? this.$parent.$vnode.data.props.vModal : this.$props } } }
render(h) { const { dismissable, title, isScroll, fullscreen, isTop, isBottom, isLeft, isRight, center, size, className, bodyPadding } = this.propsData const closeBtn = dismissable ? h('div', { class: 'vu-modal__close-btn', on: { click: () => {this.$modals.dismiss()} } }, [h(CloseIcon)]) : null const headerContent = this.$slots.header ? this.$slots.header : title ? h('span', {class: ['vu-modal__cmp-header-title']}, title) : null const header = headerContent ? h('div', { class: ['vu-modal__cmp-header'] }, [ headerContent ]) : null const body = h('div', { class: ['vu-modal__cmp-body'], style: { overflowY: isScroll ? 'auto' : null, padding: bodyPadding ? '1em' : 0 } }, [ this.$slots.default ]) const footer = this.$slots.footer ? h('div', { class: ['vu-modal__cmp-footer'] }, [ this.$slots.footer ]) : null let style = {} let translateX = '-50%' let translateY = '0' if(center) { translateX = '-50%' translateY = '-50%' } if(isRight || isLeft) { translateX = '0%' } if((isTop || isBottom) && !isScroll && !center) { translateY = '0%' } style.transform = `translate(${translateX}, ${translateY})` return h('div', { style, class: ['vu-modal__cmp', { 'vu-modal__cmp--is-fullscreen': fullscreen, 'vu-modal__cmp--is-center': center, 'vu-modal__cmp--is-top': isTop && !isScroll && !center, 'vu-modal__cmp--is-bottom': isBottom && !isScroll && !center, 'vu-modal__cmp--is-left': isLeft, 'vu-modal__cmp--is-right': isRight }, isScroll && fullscreen && 'vu-modal__cmp--is-scroll-fullscreen', isScroll && !fullscreen && 'vu-modal__cmp--is-scroll', !fullscreen && `vu-modal__cmp--${size}`, className ], on: {click: (event) => {event.stopPropagation()}} }, [ closeBtn, header, body, footer ]) }
body.modals-open { overflow: hidden; } .vu-modal { &__wrapper { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 5000; overflow-x: hidden; overflow-y: auto; transition: opacity .4s ease; } &__mask { background-color: rgba(0, 0, 0, .5); position: absolute; width: 100%; height: 100%; overflow-y: scroll; &--disabled { background-color: rgba(0, 0, 0, 0); } } &__cmp { display: flex; flex-direction: column; border-radius: 0px; background: #FFF; box-shadow: 3px 5px 20px #333; margin: 30px auto; position: absolute; left: 50%; transform: translateX(-50%); width: 650px; &--is-center { margin: auto; top: 50%; } &--is-scroll { max-height: 90%; } &--is-scroll-fullscreen { max-height: 100%; } &--is-fullscreen { width: 100%; min-height: 100%; margin: 0 0; } &--is-bottom { bottom: 0; } &--is-top { top: 0; } &--is-right { right: 0; margin-right: 30px; } &--is-left { left: 0; margin-left: 30px; } &--xl { width: 1024px; } &--lg { width: 850px; } &--md { width: 650px; } &--sm { width: 550px; } &--xs { width: 350px; } &--p50 { width: 50%; } &--p70 { width: 70%; } &--p90 { width: 90%; } &-body { padding: 1em; &::-webkit-scrollbar { width: 6px; } &::-webkit-scrollbar-track { -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); } &::-webkit-scrollbar-thumb { background-color: darkgrey; outline: 1px solid slategrey; } } &-header { user-select: none; border-bottom: 1px solid #EEE; padding: 1em; text-align: left; &-title { font-size: 16px; font-weight: 800; } } &-footer { border-top: solid 1px #EEE; user-select: none; padding: 1em; text-align: right; } } &__close-btn { user-select: none; position: absolute; right: 12px; top: 5px; line { stroke: grey; stroke-width: 2; } &:hover { cursor: pointer; line { stroke: black; } } } }
handleEscapeKey(e) { if (e.keyCode === 27 && this.modals.length) { if (!this.modals.length) return; if (this.current.options.escapable) this.dismiss(); } }
created() { Bus.$on('new', options => { // , const defaults = { // , Modal title: '', dismissable: true, center: false, fullscreen: false, isTop: false, isBottom: false, isLeft: false, isRight: false, isScroll: false, className: '', size: 'md', escapable: false, bodyPadding: true }; // ! // props- . . // rendered , options, Modal (vu-modal) let instance = {} // , modals let rendered if(options.component.template) { // "" template, Modal , . . . . rendered = false } else { // render , componentOptions rendered = options.component.render.call(this, this.$createElement) } // componentName Modal. 'vu-modal', Modal (vu-modal) if(rendered && rendered.componentOptions && rendered.componentOptions.Ctor.extendOptions.componentName === 'vu-modal') { // props-, template- vu-modal const propsData = rendered.componentOptions.propsData instance = { isVmodal: true, // , options: Object.assign(defaults, propsData, options) // } } else { instance = { isVmodal: false, // vu-modal options: Object.assign(defaults, options) // } } rendered = null this.modals.push(instance); // modals Bus.$emit('opened', { // EventBus c index: this.$last, // , instance // }); this.body && this.body.classList.add('modals-open'); // body });
Bus.$on('close', data => { // , close let index = null; if (data && data.$index) index = data.$index; // if (index === null) index = this.$last; // , this.close(data, index); // close }); Bus.$on('dismiss', index => { // dismiss, if (index === null) index = this.$last; // , this.dismiss(index); // dismiss });
methods: { splice(index = null) { // , if (index === -1) return; if (!this.modals.length) return; if (index === null) // , this.modals.pop(); else this.modals.splice(index, 1); if (!this.modals.length) { // this.body && this.body.classList.remove('modals-open'); // body 'modals-open' Bus.$emit('destroyed'); // , EventBus, , } } }
doClose(index) { // modals , splice, if (!this.modals.length) return; if (!this.modals[index]) return; this.splice(index); }, // , , close. close(data = null, index = null) { if (this.modals.length === 0) return; let localIndex = index; // index , , . , - if (index && typeof index === 'function') { localIndex = index(data, this.modals); } if (typeof localIndex !== 'number') localIndex = this.$last; // // , , callback- onClose, , // onClose - , false, if (localIndex !== false && this.modals[localIndex]) { if(this.modals[localIndex].options.onClose(data) === false) { return } } Bus.$emit('closed', { // 'closed' index: localIndex, // instance: this.modals[index], // data // , }); // , , modals, this.doClose(localIndex); },
dismiss(index = null) { let localIndex = index; if (index && typeof index === 'function') localIndex = index(this.$last); if (typeof localIndex !== 'number') localIndex = this.$last; if (this.modals[localIndex].options.onDismiss() === false) return; Bus.$emit('dismissed', { index: localIndex, instance: this.modals[localIndex] }); this.doClose(localIndex); },
computed: { current() { // return this.modals[this.$last]; }, $last() { // () return this.modals.length - 1; }, body() { // body, , / 'modals-open' if (typeof document !== 'undefined') { return document.querySelector('body'); } } }
render(h) { // , if(!this.modals.length) { return null }; // let modals = this.modals.map((modal, index) => { // let modalComponent // if(modal.isVmodal) { // Modal (vu-modal) // props-, vModal c vu-modal props- , modalComponent = h(modal.options.component, { props: Object.assign({}, {vModal: modal.options}, modal.options.props) }) } else { // Modal , props-, , modalComponent = h(ModalCmp, { props: modal.options }, [ h(modal.options.component, { props: modal.options.props }) ]) } // , - css // dismissable true, , return h('div', { class: ['vu-modal__mask', {'vu-modal__mask--disabled': index != this.$last }], on: {click: () => {modal.options.dismissable && this.dismiss()}}, key: index }, [ modalComponent // ]) }) // return h('div', { class: 'vu-modal__wrapper', }, [ modals ]) } // ! :)
this.$modals.open({ title: 'Modal with callbacks', component: Example, props: { lorem: true, test: true }, onDismiss() { console.log('Dismiss ok!') } onClose(data) { if(data.ended) return false console.log('Ok!') } })
this.$modals.close({ ended: true })
this.$modals.dismiss()
Source: https://habr.com/ru/post/350232/
All Articles