From e0574615a7dc77df78205c89524c9ac7ad7a4325 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sat, 26 Sep 2020 16:07:58 +0200 Subject: [PATCH] Basic Modal system --- resources/components/favourites/save.html | 2 +- resources/components/picker/stop.html | 4 +- resources/components/popper.html | 4 - resources/components/tooltip.html | 4 +- resources/components/ui/dialog.html | 29 +++ resources/styles/_map.scss | 2 +- resources/styles/main.scss | 16 +- resources/styles/page/_provider-picker.scss | 2 +- resources/styles/ui/_modal.scss | 58 ++++++ .../styles/{_popper.scss => ui/_popup.scss} | 20 +- resources/ts/app.ts | 4 + resources/ts/components/ui/dialog.ts | 187 ++++++++++++++++++ resources/ts/components/ui/icon.ts | 15 +- resources/ts/components/ui/index.ts | 1 + resources/ts/components/utils.ts | 106 ---------- resources/ts/filters.ts | 16 +- templates/app.html.twig | 16 +- 17 files changed, 341 insertions(+), 145 deletions(-) delete mode 100644 resources/components/popper.html create mode 100644 resources/components/ui/dialog.html create mode 100644 resources/styles/ui/_modal.scss rename resources/styles/{_popper.scss => ui/_popup.scss} (93%) create mode 100644 resources/ts/components/ui/dialog.ts diff --git a/resources/components/favourites/save.html b/resources/components/favourites/save.html index 544dc83..8607252 100644 --- a/resources/components/favourites/save.html +++ b/resources/components/favourites/save.html @@ -1,4 +1,4 @@ -
+
diff --git a/resources/components/picker/stop.html b/resources/components/picker/stop.html index 1f42643..b8d8997 100644 --- a/resources/components/picker/stop.html +++ b/resources/components/picker/stop.html @@ -35,8 +35,8 @@ - + - +
diff --git a/resources/components/popper.html b/resources/components/popper.html deleted file mode 100644 index 84c1c94..0000000 --- a/resources/components/popper.html +++ /dev/null @@ -1,4 +0,0 @@ -
-
- -
diff --git a/resources/components/tooltip.html b/resources/components/tooltip.html index 65805b0..033eeb5 100644 --- a/resources/components/tooltip.html +++ b/resources/components/tooltip.html @@ -1,9 +1,9 @@ - + diff --git a/resources/components/ui/dialog.html b/resources/components/ui/dialog.html new file mode 100644 index 0000000..7adf4cb --- /dev/null +++ b/resources/components/ui/dialog.html @@ -0,0 +1,29 @@ +
+
+
+
+ +
{{ title }}
+
+
+ +
+ + +
+
+
+
+
+ +
+ + +
+ diff --git a/resources/styles/_map.scss b/resources/styles/_map.scss index ab177bf..89f34fc 100644 --- a/resources/styles/_map.scss +++ b/resources/styles/_map.scss @@ -1,5 +1,5 @@ .map__label-box { - @extend .popper; + @extend .ui-popup; padding: .5rem; background: white; diff --git a/resources/styles/main.scss b/resources/styles/main.scss index 5304c5c..83d2198 100644 --- a/resources/styles/main.scss +++ b/resources/styles/main.scss @@ -75,12 +75,24 @@ $grid-gutter-width: $spacer * 2; } } +@mixin position($position, $top: inherit, $right: inherit, $bottom: inherit, $left: inherit) { + $right: if($right == inherit, $top, $right); + $bottom: if($bottom == inherit, $top, $bottom); + $left: if($left == inherit, $right, $left); + + position: $position; + + top: $top; + right: $right; + left: $left; + bottom: $bottom; +} + @import "common"; @import "stop"; @import "departure"; @import "line"; @import "controls"; -@import "popper"; @import "animations"; @import "form"; @import "favourites"; @@ -89,6 +101,8 @@ $grid-gutter-width: $spacer * 2; @import "map"; @import "ui/switch"; +@import "ui/popup"; +@import "ui/modal"; @import "page/provider-picker"; diff --git a/resources/styles/page/_provider-picker.scss b/resources/styles/page/_provider-picker.scss index 75bc5bb..939910d 100644 --- a/resources/styles/page/_provider-picker.scss +++ b/resources/styles/page/_provider-picker.scss @@ -8,7 +8,7 @@ } .provider-picker { - @extend .popper; + @extend .ui-popup; padding: 1rem; margin: 3rem; } diff --git a/resources/styles/ui/_modal.scss b/resources/styles/ui/_modal.scss new file mode 100644 index 0000000..2764a52 --- /dev/null +++ b/resources/styles/ui/_modal.scss @@ -0,0 +1,58 @@ +.ui-backdrop { + @include position(fixed, 0); + background: rgba(black, .75); + padding: $spacer; + display: flex; + flex-direction: column; + align-items: center; + overflow-y: auto; + overscroll-behavior-y: contain; + + &::after { + height: 1rem; + display: block; + content: ""; + } +} + +$dialog-margin: 1rem; +$dialog-sizes: ( + medium: 480px, + small: 320px, + large: 640px, +) +; + +.ui-modal { + padding: $dialog-margin; + background: white; + margin: auto; + box-shadow: rgba(black, .7) 0 1px 3px; + border-radius: 1px; + + @each $size, $width in $dialog-sizes { + .ui-modal--#{$size} { + width: $width; + } + } +} + +.ui-modal__close { + margin-right: -$dialog-margin; + padding: $dialog-margin $dialog-margin 0; + margin-top: -$dialog-margin; +} + +.ui-modal__header { + flex: 1 1 auto; +} + +.ui-modal__title { + font-weight: bold; + font-size: 0.875rem; +} + +.ui-modal__top-bar { + display: flex; + margin-bottom: $dialog-margin * 0.75; +} diff --git a/resources/styles/_popper.scss b/resources/styles/ui/_popup.scss similarity index 93% rename from resources/styles/_popper.scss rename to resources/styles/ui/_popup.scss index 7b9ea5b..384d6c2 100644 --- a/resources/styles/_popper.scss +++ b/resources/styles/ui/_popup.scss @@ -54,7 +54,7 @@ @mixin triangle-left($size, $color, $border: none) { @include triangle(left, $size, $color, $border); } @mixin triangle-right($size, $color, $border: none) { @include triangle(right, $size, $color, $border); } -.popper { +.ui-popup { $arrow-base: 8px; $arrow-color: white; $arrow-border: rgba(black, 0.2); @@ -74,17 +74,17 @@ border-radius: 2px; - .popper__arrow { + .ui-popup__arrow { position: absolute; width: 0; height: 0; } - &.popper--no-padding { + &.ui-popup--no-padding { padding: 0; } - .popper__heading { + .ui-popup__heading { font-size: $font-size-sm; font-weight: bold; margin-bottom: .5rem; @@ -105,7 +105,7 @@ &[x-placement*="#{$placement}"] { margin-#{map-get($opposite, $placement)}: $arrow-base; - .popper__arrow { + .ui-popup__arrow { #{map-get($opposite, $placement)}: 0; @include triangle(map-get($opposite, $placement), $arrow-base, $arrow-color, $arrow-border); } @@ -119,11 +119,11 @@ @include placement("bottom"); } - &.popper--arrow { + &.ui-popup--arrow { @include arrows; } - &.popper--tooltip { + &.ui-popup--tooltip { background: $dark; color: white; padding: .5rem .75rem; @@ -132,14 +132,14 @@ min-width: 0; box-shadow: none; - &.popper--arrow { + &.ui-popup--arrow { $arrow-color: $dark; $arrow-border: none; $arrow-base: 6px; @include arrows; - .popper__arrow::before { + .ui-popup__arrow::before { border: none; } } @@ -147,7 +147,7 @@ } @include media-breakpoint-down('sm') { - .popper { + .ui-popup { margin-left: $spacer; margin-right: $spacer; } diff --git a/resources/ts/app.ts b/resources/ts/app.ts index 71804a7..a201127 100644 --- a/resources/ts/app.ts +++ b/resources/ts/app.ts @@ -31,10 +31,14 @@ Vue.use(VueMoment, { moment }); declare module 'vue/types/vue' { interface Vue { $isTouch: boolean; + $hasSlot: (slot: string) => string; } } Vue.prototype.$isTouch = 'ontouchstart' in window || navigator.msMaxTouchPoints > 0; +Vue.prototype.$hasSlot = function (this: Vue, slot: string): boolean { + return !!this.$slots[slot] || !!this.$scopedSlots[slot]; +} Component.registerHooks(['removed']); diff --git a/resources/ts/components/ui/dialog.ts b/resources/ts/components/ui/dialog.ts new file mode 100644 index 0000000..4ecd057 --- /dev/null +++ b/resources/ts/components/ui/dialog.ts @@ -0,0 +1,187 @@ +import Vue from "vue"; +import { Component, Prop, Watch } from "vue-property-decorator"; +import Popper, { Placement } from "popper.js"; +import { defaultBreakpoints } from "../../filters"; + +/** + * How popup will be presented to user: + * - "modal" - modal window + * - "popup" - simple popup + */ +export type DialogBehaviour = "modal" | "popup"; + +@Component({ + template: require('../../../components/ui/dialog.html'), + inheritAttrs: false, +}) +export default class UiDialog extends Vue { + @Prop({ type: String, default: "popup" }) + private behaviour: DialogBehaviour; + + @Prop({ type: String }) + private mobileBehaviour: DialogBehaviour; + + @Prop([String, HTMLElement]) + public reference: string | HTMLElement; + + @Prop(Object) + public refs: string; + + @Prop({ type: String, default: "auto" }) + public placement: Placement; + + @Prop(Boolean) + public arrow: boolean; + + @Prop({ type: Boolean, default: true }) + public responsive: boolean; + + @Prop(String) + public title: string; + + private isMobile: boolean = false; + + private _focusOutEvent; + private _resizeEvent; + + private _popper; + + get currentBehaviour(): DialogBehaviour { + if (!this.mobileBehaviour) { + return this.behaviour; + } + + return this.isMobile ? this.mobileBehaviour : this.behaviour; + } + + get hasFooter() { + return this.$hasSlot('footer') + } + + get hasHeader() { + return this.$hasSlot('header') + } + + private getReferenceElement() { + const isInWrapper = this.$parent.$options.name == 'portalTarget'; + + if (typeof this.reference === 'string') { + if (this.refs) { + return this.refs[this.reference]; + } + + if (isInWrapper) { + return this.$parent.$parent.$refs[this.reference]; + } + + return this.$parent.$refs[this.reference]; + } + + if (this.reference instanceof HTMLElement) { + return this.reference; + } + + return isInWrapper ? this.$parent.$el : this.$el.parentElement; + } + + focusOut(event: MouseEvent) { + if (this.$el.contains(event.target as Node)) { + return; + } + + this.$emit('leave', event); + } + + mounted() { + this.handleWindowResize(); + + if (this.behaviour === 'popup') { + this.initPopper(); + } + + window.addEventListener('resize', this._resizeEvent = this.handleWindowResize.bind(this)); + } + + private initPopper() { + const reference = this.getReferenceElement(); + + this._popper = new Popper(reference, this.$el, { + placement: this.placement, + modifiers: { + arrow: { enabled: this.arrow, element: this.$refs['arrow'] as Element }, + responsive: { + enabled: this.responsive, + order: 890, + fn(data) { + if (window.innerWidth < 560) { + data.instance.options.placement = 'top'; + data.styles.transform = `translate3d(0, ${ data.offsets.popper.top }px, 0)`; + data.styles.right = '0'; + data.styles.left = '0'; + data.styles.width = 'auto'; + data.arrowStyles.left = `${ data.offsets.popper.left + data.offsets.arrow.left }px`; + } + + return data; + } + } + } + }); + + this.$nextTick(() => { + this._popper && this._popper.update(); + document.addEventListener('click', this._focusOutEvent = this.focusOut.bind(this), { capture: true }); + }); + } + + private removePopper() { + this._popper.destroy() + this._popper = null; + } + + updated() { + if (this._popper) { + this._popper.update(); + } + } + + beforeDestroy() { + this._focusOutEvent && document.removeEventListener('click', this._focusOutEvent, { capture: true }); + } + + removed() { + if (this._popper) { + this.removePopper(); + } + } + + private handleBackdropClick(ev: Event) { + const target = ev.target as HTMLElement; + + if (target.classList.contains("ui-backdrop")) { + this.$emit('leave'); + } + } + + private handleCloseClick() { + this.$emit('leave'); + this.$emit('close'); + } + + private handleWindowResize() { + this.isMobile = screen.width < defaultBreakpoints.md; + } + + @Watch('currentBehaviour') + private handleBehaviourChange(newBehaviour: DialogBehaviour, oldBehaviour: DialogBehaviour) { + if (oldBehaviour === 'popup') { + this.removePopper(); + } + + if (newBehaviour === 'popup') { + this.$nextTick(() => this.initPopper()); + } + } +} + +Vue.component("ui-dialog", UiDialog); diff --git a/resources/ts/components/ui/icon.ts b/resources/ts/components/ui/icon.ts index 11863a0..b25535a 100644 --- a/resources/ts/components/ui/icon.ts +++ b/resources/ts/components/ui/icon.ts @@ -7,7 +7,8 @@ import { faCheck, faCheckDouble, faChevronCircleUp, - faChevronDown, faChevronUp, + faChevronDown, + faChevronUp, faClock, faCog, faExclamationTriangle, @@ -15,7 +16,8 @@ import { faInfoCircle, faMapMarkerAlt, faMoon, - faQuestionCircle, faQuestionSquare, + faQuestionCircle, + faQuestionSquare, faSearch, faSign, faStar, @@ -23,7 +25,13 @@ import { faTimes, faTrashAlt } from "@fortawesome/pro-light-svg-icons"; -import { faClock as faClockBold, faCodeCommit, faMinus, faPlus, faSpinnerThird } from "@fortawesome/pro-regular-svg-icons"; +import { + faClock as faClockBold, + faCodeCommit, + faMinus, + faPlus, + faSpinnerThird +} from "@fortawesome/pro-regular-svg-icons"; import { faExclamationTriangle as faSolidExclamationTriangle, faWalking } from "@fortawesome/pro-solid-svg-icons"; import { fac } from "../../icons"; import { FontAwesomeIcon, FontAwesomeLayers, FontAwesomeLayersText } from "@fortawesome/vue-fontawesome"; @@ -88,6 +96,7 @@ const definitions: Dictionary = { {icon: faClockBold}, {icon: faSolidExclamationTriangle, transform: "shrink-5 down-4 right-6"} ]), + 'close': simple(faTimes), ...lineTypeIcons, ...messageTypeIcons, }; diff --git a/resources/ts/components/ui/index.ts b/resources/ts/components/ui/index.ts index 8814273..1909ea7 100644 --- a/resources/ts/components/ui/index.ts +++ b/resources/ts/components/ui/index.ts @@ -1,3 +1,4 @@ export * from './switch'; export * from './icon'; export * from './numeric-input' +export * from './dialog' diff --git a/resources/ts/components/utils.ts b/resources/ts/components/utils.ts index 9dc6837..464fc68 100644 --- a/resources/ts/components/utils.ts +++ b/resources/ts/components/utils.ts @@ -1,111 +1,6 @@ import Vue from 'vue'; import { Component, Prop, Watch } from "vue-property-decorator"; -import Popper, { Placement } from "popper.js"; -import vueRemovedHookMixin from "vue-removed-hook-mixin"; -@Component({ - template: require("../../components/popper.html"), - mixins: [ vueRemovedHookMixin ] -}) -export class PopperComponent extends Vue { - @Prop([ String, HTMLElement ]) - public reference: string | HTMLElement; - - @Prop(Object) - public refs: string; - - @Prop({ type: String, default: "auto" }) - public placement: Placement; - - @Prop(Boolean) - public arrow: boolean; - - @Prop({ type: Boolean, default: true }) - public responsive: boolean; - - private _event; - private _popper; - - private getReferenceElement() { - const isInPortal = this.$parent.$options.name == 'portalTarget'; - - if (typeof this.reference === 'string') { - if (this.refs) { - return this.refs[this.reference]; - } - - if (isInPortal) { - return this.$parent.$parent.$refs[this.reference]; - } - - return this.$parent.$refs[this.reference]; - } - - if (this.reference instanceof HTMLElement) { - return this.reference; - } - - return isInPortal ? this.$parent.$el : this.$el.parentElement; - } - - focusOut(event: MouseEvent) { - if (this.$el.contains(event.target as Node)) { - return; - } - - this.$emit('leave', event); - } - - mounted() { - const reference = this.getReferenceElement(); - - this._popper = new Popper(reference, this.$el, { - placement: this.placement, - modifiers: { - arrow: { enabled: this.arrow, element: this.$refs['arrow'] as Element }, - responsive: { - enabled: this.responsive, - order: 890, - fn(data) { - if (window.innerWidth < 560) { - data.instance.options.placement = 'top'; - data.styles.transform = `translate3d(0, ${data.offsets.popper.top}px, 0)`; - data.styles.right = '0'; - data.styles.left = '0'; - data.styles.width = 'auto'; - data.arrowStyles.left = `${data.offsets.popper.left + data.offsets.arrow.left}px`; - } - - return data; - } - } - } - }); - - this.$nextTick(() => { - this._popper.update(); - document.addEventListener('click', this._event = this.focusOut.bind(this), { capture: true }); - }); - } - - updated() { - this._popper.update(); - } - - @Watch('visible') - private onVisibilityUpdate() { - this._popper.update(); - window.dispatchEvent(new Event('resize')); - } - - beforeDestroy() { - this._event && document.removeEventListener('click', this._event, { capture: true }); - } - - removed() { - this._popper.destroy() - } -} @Component({ template: require('../../components/fold.html') }) export class FoldComponent extends Vue { @@ -151,7 +46,6 @@ export class LazyComponent extends Vue { } } -Vue.component('Popper', PopperComponent); Vue.component('Fold', FoldComponent); Vue.component('Lazy', LazyComponent); diff --git a/resources/ts/filters.ts b/resources/ts/filters.ts index 3a0e040..b3fd1e4 100644 --- a/resources/ts/filters.ts +++ b/resources/ts/filters.ts @@ -2,6 +2,14 @@ import { set, signed } from "./utils"; import Vue from 'vue'; import { condition } from "./decorators"; +export const defaultBreakpoints = { + 'xs': 0, + 'sm': 576, + 'md': 768, + 'lg': 1024, + 'xl': 1200, +} + Vue.filter('signed', signed); Vue.directive('hover', { @@ -61,13 +69,7 @@ Vue.directive('autofocus', { Vue.directive('responsive', { inserted(el, binding) { - const breakpoints = typeof binding.value === 'object' ? binding.value : { - 'xs': 0, - 'sm': 576, - 'md': 768, - 'lg': 1024, - 'xl': 1200, - }; + const breakpoints = typeof binding.value === 'object' ? binding.value : defaultBreakpoints; const resize = binding['resize'] = () => { const width = el.scrollWidth; diff --git a/templates/app.html.twig b/templates/app.html.twig index a8143e6..214d70c 100644 --- a/templates/app.html.twig +++ b/templates/app.html.twig @@ -29,9 +29,9 @@ - + - + @@ -54,9 +54,9 @@ - + - + @@ -105,9 +105,11 @@
- - - + + + + +