@square/maker
Version:
Maker Design System by Square
1 lines • 16 kB
Source Map (JSON)
{"version":3,"file":"styles.css","mappings":"AAkLA,gBAIA,8DADA,gDAFA,YACA,cAGA,+BACA,CAEA,oCACA,gBAGA,8BADA,iBADA,WAGA,CACA,CCxLA,gBACA,YACA,CCuWA,gBACA,kBACA,SACA,CAEA,gBAOA,mBADA,aAEA,sBACA,CAEA,gCAPA,SACA,OAJA,eAEA,QADA,KAgBA,CAPA,gBAMA,qDACA,CAEA,gBACA,wBACA,CAEA,gBACA,eACA,CAEA,gBAGA,YACA,gBAHA,kBACA,UAGA,CAEA,oCACA,gBAIA,2DACA,kCAJA,qBAEA,YADA,UAIA,CACA","sources":["webpack://@square/maker/./src/components/Modal/src/Modal.vue","webpack://@square/maker/./src/components/Modal/src/ModalContent.vue","webpack://@square/maker/./src/components/Modal/src/ModalLayer.vue"],"sourcesContent":["<template>\n\t<m-touch-capture\n\t\tref=\"modal\"\n\t\t:class=\"$s.Modal\"\n\t\t:style=\"style\"\n\t\t:prevent-default=\"preventDefault\"\n\t\tv-bind=\"$attrs\"\n\t\t:aria-labelledby=\"ariaLabelledby\"\n\t\t@scroll.native=\"onScroll\"\n\t\t@on-drag-down=\"onDragDown\"\n\t\t@on-drag-end=\"onDragEnd\"\n\t\t@on-swipe-down=\"onSwipeDown\"\n\t\tv-on=\"$listeners\"\n\t>\n\t\t<!-- @slot Modal content -->\n\t\t<slot />\n\t</m-touch-capture>\n</template>\n\n<script>\nimport cssValidator from '@square/maker/utils/css-validator';\nimport { throttle } from 'lodash';\nimport { MThemeKey, defaultTheme, resolveThemeableProps } from '@square/maker/components/Theme';\nimport { MTouchCapture } from '@square/maker/components/TouchCapture';\nimport modalApi from './modal-api';\n\nexport default {\n\tname: 'Modal',\n\n\tcomponents: {\n\t\tMTouchCapture,\n\t},\n\n\tinject: {\n\t\tmodalApi,\n\t\ttheme: {\n\t\t\tdefault: defaultTheme(),\n\t\t\tfrom: MThemeKey,\n\t\t},\n\t},\n\n\tinheritAttrs: false,\n\n\tprops: {\n\t\t/**\n\t\t * Before close hook, can block closing\n\t\t */\n\t\tbeforeClose: {\n\t\t\ttype: Function,\n\t\t\tdefault: undefined,\n\t\t},\n\t\t/**\n\t\t * Background color of modal\n\t\t */\n\t\tbgColor: {\n\t\t\ttype: String,\n\t\t\tdefault: undefined,\n\t\t\tvalidator: cssValidator('color'),\n\t\t},\n\t\t/**\n\t\t * Text color of modal\n\t\t */\n\t\tcolor: {\n\t\t\ttype: String,\n\t\t\tdefault: undefined,\n\t\t\tvalidator: cssValidator('color'),\n\t\t},\n\n\t\t/**\n\t\t * Toggle to allow swiping the dialog away\n\t\t */\n\t\tcloseOnSwipeDown: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * The ID of the element that labels the modal.\n\t\t */\n\t\tariaLabelledby: {\n\t\t\ttype: String,\n\t\t\tdefault: undefined,\n\t\t},\n\t},\n\n\tdata() {\n\t\tconst scrollCheckDelay = 800;\n\t\treturn {\n\t\t\tmodalStyles: {},\n\t\t\tisScrolledToTop: true,\n\t\t\tonScroll: throttle(this.setScrollTop, scrollCheckDelay),\n\t\t\tpreventDefault: false,\n\t\t};\n\t},\n\n\tcomputed: {\n\t\t...resolveThemeableProps('modal', [\n\t\t\t'bgColor',\n\t\t\t'color',\n\t\t]),\n\n\t\tstyle() {\n\t\t\treturn {\n\t\t\t\t'--bg-color': this.resolvedBgColor,\n\t\t\t\t'--color': this.resolvedColor,\n\t\t\t\t...this.modalStyles,\n\t\t\t};\n\t\t},\n\t},\n\n\twatch: {\n\t\tbeforeClose: {\n\t\t\timmediate: true,\n\t\t\thandler(hook) {\n\t\t\t\tthis.modalApi.registerBeforeCloseHook(hook);\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\tsetScrollTop() {\n\t\t\tif (!this.closeOnSwipeDown) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst scrollTop = this.$refs?.modal?.$el?.scrollTop || 0;\n\t\t\tthis.isScrolledToTop = scrollTop <= 0;\n\t\t},\n\n\t\tonSwipeDown() {\n\t\t\tif (!this.closeOnSwipeDown) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (this.isScrolledToTop) {\n\t\t\t\tthis.preventDefault = true;\n\t\t\t\tthis.modalApi.close();\n\t\t\t}\n\t\t},\n\n\t\tonDragDown(gesture) {\n\t\t\tif (!this.closeOnSwipeDown) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (this.isScrolledToTop) {\n\t\t\t\tthis.preventDefault = true;\n\t\t\t\tconst transform = `translateY(${gesture.changeY}px)`;\n\t\t\t\tthis.modalStyles = {\n\t\t\t\t\ttransform,\n\t\t\t\t\t'backface-visibility': 'hidden',\n\t\t\t\t\toverflow: 'hidden',\n\t\t\t\t\ttransition: 'none',\n\t\t\t\t};\n\t\t\t}\n\t\t},\n\n\t\tonDragEnd(gesture) {\n\t\t\tif (!this.closeOnSwipeDown) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// percent of window height modal must be dragged to close on release\n\t\t\tconst minDragCloseDistance = 0.3;\n\t\t\tconst minDragThreshold = window.innerHeight * minDragCloseDistance;\n\t\t\tif (this.isScrolledToTop\n\t\t\t&& gesture.changeY > minDragThreshold) {\n\t\t\t\tthis.modalApi.close();\n\t\t\t} else {\n\t\t\t\tthis.preventDefault = false;\n\t\t\t\tthis.modalStyles = {};\n\t\t\t}\n\t\t},\n\t},\n};\n</script>\n\n<style module=\"$s\">\n.Modal {\n\theight: 100%;\n\toverflow: auto;\n\tcolor: var(--color, $maker-color-body);\n\tbackground: var(--bg-color, $maker-color-background);\n\ttransition: transform 0.2s linear;\n}\n\n@media screen and (--for-tablet-landscape-up) {\n\t.Modal {\n\t\twidth: 600px;\n\t\tmin-height: 180px;\n\t\tmax-height: calc(100vh - 64px);\n\t}\n}\n</style>\n","<template>\n\t<div :class=\"$s.ModalContent\">\n\t\t<!-- @slot Modal Content content (gets correct padding) -->\n\t\t<slot />\n\t</div>\n</template>\n\n<style module=\"$s\">\n.ModalContent {\n\tpadding: 24px;\n}\n</style>\n","<template>\n\t<!--\n\t\ta modal is always opened from the \"parent\"\n\t\twhich is why every layer's template references\n\t\tthe parentModalApi instead of the modalApi\n\t-->\n\t<div :class=\"$s.Layer\">\n\t\t<m-transition-fade-in>\n\t\t\t<div\n\t\t\t\tv-if=\"parentModalApi.state.renderFn\"\n\t\t\t\t:class=\"[\n\t\t\t\t\t$s.Translucent,\n\t\t\t\t\t{ [$s.Transparent]: parentModalApi.state.isStacked },\n\t\t\t\t]\"\n\t\t\t/>\n\t\t</m-transition-fade-in>\n\t\t<m-transition-responsive :transitions=\"transitions\">\n\t\t\t<div\n\t\t\t\tv-if=\"parentModalApi.state.renderFn\"\n\t\t\t\tref=\"baseModalLayer\"\n\t\t\t\t:class=\"$s.ModalLayer\"\n\t\t\t\t@click.capture=\"closeOnClickOutside\"\n\t\t\t>\n\t\t\t\t<pseudo-window\n\t\t\t\t\tbody\n\t\t\t\t\t:class=\"$s.disableScroll\"\n\t\t\t\t/>\n\t\t\t\t<pseudo-window\n\t\t\t\t\tdocument\n\t\t\t\t\t@keyup.esc=\"closeOnEsc\"\n\t\t\t\t/>\n\t\t\t\t<div\n\t\t\t\t\tref=\"modal\"\n\t\t\t\t\t:class=\"$s.Container\"\n\t\t\t\t\trole=\"dialog\"\n\t\t\t\t\taria-modal=\"true\"\n\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t>\n\t\t\t\t\t<render-fn :render-fn=\"parentModalApi.state.renderFn\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</m-transition-responsive>\n\t\t<modal-layer v-if=\"parentModalApi.state.renderFn\" />\n\t</div>\n</template>\n\n<script>\nimport { inject, provide, reactive } from 'vue';\nimport PseudoWindow from 'vue-pseudo-window';\nimport assert from '@square/maker/utils/assert';\nimport { MTransitionFadeIn } from '@square/maker/components/TransitionFadeIn';\nimport { MTransitionResponsive } from '@square/maker/components/TransitionResponsive';\nimport {\n\tfadeOutFn,\n\tspringUpFn,\n\tspringDownFn,\n\tspringDelay,\n\tfloatUpFn,\n\tfloatDownFn,\n\tdelayedFloatUpFn,\n\tdelayedFadeInFn,\n\tmobileMinWidth,\n\ttabletMinWidth,\n} from '@square/maker/utils/transitions';\nimport RenderFn from '@square/maker/utils/RenderFn';\nimport modalApi from './modal-api';\n\nconst ONE = 1;\n\nfunction isPromise(thing) {\n\treturn typeof thing === 'object' && typeof thing.then === 'function';\n}\n\n/**\n * This function can be simplified if refactored into an async function\n * HOWEVER it was intentionally written using explicit Promises because\n * it allows us to optimize for the HAPPY PATH where all of the steps\n * can be executed synchronously which closes a stack of modals much\n * faster and with fewer visual hiccups which creates a better user\n * experience. So please do not attempt to refactor this into an async\n * function to \"clean up\" the code because you'll be making it slower.\n * @return {boolean} true if no modal or modal was closed, false otherwise\n */\nasync function closeModal(api) {\n\t// no api means no layer means no modal to close\n\tif (!api) {\n\t\treturn true;\n\t}\n\n\t// can't close modal if there is another modal stacked\n\t// on top if it, must close that stacked modal first\n\tif (api.state.children > ONE) {\n\t\treturn false;\n\t}\n\n\t// check beforeCloseHook set via Modal's beforeClose prop\n\tif (typeof api.state.localBeforeCloseHook === 'function') {\n\t\tconst canCloseLocal = api.state.localBeforeCloseHook();\n\t\t// check if result is a promise, i.e. hook is async\n\t\tif (isPromise(canCloseLocal)) {\n\t\t\t// resolve async result\n\t\t\treturn canCloseLocal\n\t\t\t\t.then((resolvedCanCloseLocal) => {\n\t\t\t\t\t// block closing if returned false\n\t\t\t\t\tif (!resolvedCanCloseLocal) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\t// check beforeClose hook set via options in api.open\n\t\t\t\t\tif (typeof api.state.options.beforeCloseHook === 'function') {\n\t\t\t\t\t\treturn api.state.options.beforeCloseHook();\n\t\t\t\t\t}\n\t\t\t\t\t// return true to signal that we can close\n\t\t\t\t\treturn true;\n\t\t\t\t}).then((resolvedCanCloseOpt) => {\n\t\t\t\t\t// block closing if returned false\n\t\t\t\t\tif (!resolvedCanCloseOpt) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\t// async close modal\n\t\t\t\t\tapi.state.renderFn = undefined;\n\t\t\t\t\tapi.uncountChild();\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\t\t}\n\t\t// canCloseLocal is not a promise, i.e. hook is sync\n\t\t// sync block closing if beforeClose returned false\n\t\tif (!canCloseLocal) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// sync check beforeCloseHook set via api options\n\tif (typeof api.state.options.beforeCloseHook === 'function') {\n\t\tconst canCloseOpt = api.state.options.beforeCloseHook();\n\t\t// check if result is a promise, i.e. hook is async\n\t\tif (isPromise(canCloseOpt)) {\n\t\t\t// resolve async result\n\t\t\treturn canCloseOpt\n\t\t\t\t.then((resolvedCanCloseOpt) => {\n\t\t\t\t\t// block closing if returned false\n\t\t\t\t\tif (!resolvedCanCloseOpt) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\t// async close modal\n\t\t\t\t\tapi.state.renderFn = undefined;\n\t\t\t\t\tapi.uncountChild();\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\t\t}\n\t\t// canCloseOpt is not a promise, i.e. hook is sync\n\t\t// sync block closing if beforeClose returned false\n\t\tif (!canCloseOpt) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// execution can only reach this point if:\n\t// 1. there were no registered beforeClose hooks or\n\t// 2. all beforeClose hooks were sync and returned true\n\n\t// sync close modal\n\tapi.state.renderFn = undefined;\n\tapi.uncountChild();\n\treturn true;\n}\n\nfunction createModalApi(parentModalApi) {\n\treturn {\n\t\tstate: reactive({\n\t\t\trenderFn: undefined,\n\t\t\tlocalBeforeCloseHook: undefined,\n\t\t\tchildren: 0,\n\t\t\toptions: {},\n\t\t\tisStacked: !!parentModalApi,\n\t\t}),\n\n\t\topen(renderFn, options = {}) {\n\t\t\tif (this.state.renderFn) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tthis.state.renderFn = renderFn;\n\t\t\tthis.state.options = options;\n\t\t\tthis.countChild();\n\n\t\t\treturn () => {\n\t\t\t\t// close specific opened modal\n\t\t\t\tif (this.state.renderFn === renderFn) {\n\t\t\t\t\tcloseModal(this);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\t// if no modal is open then \"closing\"\n\t\t\t\t// it trivially \"successful\"\n\t\t\t\tif (!this.state.renderFn) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\t// modal not closed\n\t\t\t\treturn false;\n\t\t\t};\n\t\t},\n\n\t\tcountChild() {\n\t\t\tthis.state.children += ONE;\n\t\t\tparentModalApi?.countChild();\n\t\t},\n\n\t\tuncountChild() {\n\t\t\tthis.state.children -= ONE;\n\t\t\tparentModalApi?.uncountChild();\n\t\t},\n\n\t\tregisterBeforeCloseHook(hook) {\n\t\t\tif (!parentModalApi) {\n\t\t\t\t// no hook, no problem, fail silently\n\t\t\t\tif (!hook) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// hook but no layer, big problem, fail loudly\n\t\t\t\tassert.error(false, 'Cannot set the beforeClose prop on a Modal if it is mounted outside of an ModalLayer', 'Modal');\n\t\t\t}\n\t\t\tparentModalApi.state.localBeforeCloseHook = hook;\n\t\t},\n\n\t\tasync close() {\n\t\t\treturn closeModal(parentModalApi);\n\t\t},\n\n\t\tasync closeAll() {\n\t\t\tconst closed = await this.close();\n\n\t\t\tif (!closed) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tif (parentModalApi) {\n\t\t\t\treturn await parentModalApi.closeAll();\n\t\t\t}\n\n\t\t\treturn true;\n\t\t},\n\t};\n}\n\nexport const useModalLayer = () => {\n\tconst parentModalApi = inject(modalApi, undefined);\n\tconst api = createModalApi(parentModalApi);\n\n\tprovide(modalApi, api);\n\n\treturn { modalApi: parentModalApi || api };\n};\n\nconst apiMixin = {\n\t// parentModalApi contains the renderFn, options,\n\t// and beforeClose hooks to be used for this layer\n\tinject: {\n\t\tparentModalApi: {\n\t\t\tdefault: undefined,\n\t\t\tfrom: modalApi,\n\t\t},\n\t},\n\n\tprovide() {\n\t\tconst vm = this;\n\t\tconst api = createModalApi(vm.parentModalApi);\n\n\t\tif (!this.modalApi) {\n\t\t\tthis.modalApi = api;\n\t\t}\n\n\t\treturn {\n\t\t\t[modalApi]: api,\n\t\t};\n\t},\n};\n\nexport default {\n\tname: 'ModalLayer',\n\n\tcomponents: {\n\t\tRenderFn,\n\t\tPseudoWindow,\n\t\tMTransitionFadeIn,\n\t\tMTransitionResponsive,\n\t},\n\n\tmixins: [\n\t\tapiMixin,\n\t],\n\n\tinheritAttrs: false,\n\n\tapiMixin,\n\tuseModalLayer,\n\n\tdata() {\n\t\tlet tabletEnterFn = floatUpFn;\n\t\tlet tabletLeaveFn = floatDownFn;\n\t\tif (this.parentModalApi.state.isStacked) {\n\t\t\ttabletEnterFn = delayedFloatUpFn;\n\t\t\ttabletLeaveFn = floatDownFn;\n\t\t}\n\t\treturn {\n\t\t\ttransitions: [{\n\t\t\t\tminWidth: mobileMinWidth,\n\t\t\t\tenter: springUpFn,\n\t\t\t\tleave: springDownFn,\n\t\t\t}, {\n\t\t\t\tminWidth: tabletMinWidth,\n\t\t\t\tenter: tabletEnterFn,\n\t\t\t\tleave: tabletLeaveFn,\n\t\t\t}],\n\t\t};\n\t},\n\n\tmounted() {\n\t\tconst vm = this;\n\t\tthis.unwatchStackedModal = this.$watch(() => vm.modalApi.state.renderFn, () => {\n\t\t\tconst isTablet = window.innerWidth >= tabletMinWidth;\n\t\t\tconst isMobile = !isTablet;\n\t\t\tconst isOpeningStackedModal = !!vm.modalApi.state.renderFn;\n\t\t\tconst isClosingStackedModal = !isOpeningStackedModal;\n\t\t\tconst element = this.$refs.baseModalLayer;\n\n\t\t\t// element can be undefined when closing child modal and parent modal\n\t\t\t// at once so we check if element exists before applying transitions\n\t\t\tif (!element) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (isTablet && isOpeningStackedModal) {\n\t\t\t\tfadeOutFn({ element });\n\t\t\t} else if (isTablet && isClosingStackedModal) {\n\t\t\t\tdelayedFadeInFn({ element });\n\t\t\t} else if (isMobile && isOpeningStackedModal) {\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\telement.style.opacity = '0%';\n\t\t\t\t}, springDelay);\n\t\t\t} else if (isMobile && isClosingStackedModal) {\n\t\t\t\telement.style.removeProperty('opacity');\n\t\t\t}\n\t\t});\n\t},\n\n\tdestroyed() {\n\t\tthis.unwatchStackedModal();\n\t},\n\n\tmethods: {\n\t\tcloseOnClickOutside(event) {\n\t\t\tconst { closeOnClickOutside } = this.parentModalApi.state.options;\n\t\t\tconst { modal } = this.$refs;\n\t\t\tif (modal && closeOnClickOutside && !modal.contains(event.target)) {\n\t\t\t\tthis.modalApi.close();\n\t\t\t}\n\t\t},\n\t\tcloseOnEsc() {\n\t\t\tconst { closeOnEsc } = this.parentModalApi.state.options;\n\t\t\tconst { modal } = this.$refs;\n\n\t\t\tif (modal && closeOnEsc) {\n\t\t\t\tthis.modalApi.close();\n\t\t\t}\n\t\t},\n\t},\n};\n</script>\n\n<style module=\"$s\">\n.Layer {\n\tposition: relative;\n\tz-index: 1;\n}\n\n.ModalLayer {\n\tposition: fixed;\n\ttop: 0;\n\tright: 0;\n\tbottom: 0;\n\tleft: 0;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n.Translucent {\n\tposition: fixed;\n\ttop: 0;\n\tright: 0;\n\tbottom: 0;\n\tleft: 0;\n\tbackground-color: $maker-color-overlay;\n}\n\n.Transparent {\n\tbackground-color: transparent;\n}\n\n.disableScroll {\n\toverflow: hidden;\n}\n\n.Container {\n\tposition: relative;\n\twidth: 100%;\n\theight: 100%;\n\toverflow: hidden;\n}\n\n@media screen and (--for-tablet-landscape-up) {\n\t.Container {\n\t\tdisplay: inline-block;\n\t\twidth: auto;\n\t\theight: auto;\n\t\tborder-radius: $maker-shape-default-border-radius;\n\t\tbox-shadow: 0 0 24px 8px rgba(0, 0, 0, 0.3);\n\t}\n}\n</style>\n"],"names":[],"sourceRoot":""}