Refactor popups

This commit is contained in:
Kacper Donat 2020-01-19 18:03:38 +01:00
parent 4b389582ad
commit 9802473d7c
21 changed files with 184 additions and 77 deletions

4
.gitignore vendored
View File

@ -14,4 +14,6 @@
/.idea/
/public/*
!/public/index.php
!/public/manifest.json
!/public/manifest.jso
/yarn-error.log

View File

@ -40,6 +40,7 @@
"copy-webpack-plugin": "^4.5.2",
"imagemin-webpack-plugin": "^2.3.0",
"mini-css-extract-plugin": "^0.4.2",
"portal-vue": "^2.1.7",
"vue2-leaflet": "^1.0.2",
"vuex": "^3.0.1",
"vuex-class": "^0.3.1",

View File

@ -1,6 +1,14 @@
<div class="input-group">
<input class="form-control form-control-sm" placeholder="nazwa" v-model="name"/>
<button class="btn btn-sm btn-dark" @click="save">
<fa :icon="['fal', 'check']"></fa>
</button>
</div>
<form class="favourite-add-form" @submit="save">
<label for="favourite_add_name">Nazwa</label>
<div class="input-group">
<input class="form-control form-control-sm" placeholder="np. Z pracy"
:class="{ 'is-invalid': errors.name.length > 0 }" id="favourite_add_name"
v-model="name" v-autofocus/>
<button class="btn btn-sm btn-dark" type="submit">
<fa :icon="['fal', 'check']"></fa>
</button>
<div v-if="errors.name.length > 0" class="invalid-feedback">
<p v-for="error in errors.name">{{ error }}</p>
</div>
</div>
</form>

View File

@ -1,8 +1,8 @@
<div class="finder">
<input class="form-control" v-model="filter" placeholder="Zacznij pisać nazwę aby szukać..."/>
<input class="form-control" :value="filter" @input="filter = $event.target.value" placeholder="Zacznij pisać nazwę aby szukać..."/>
<div v-if="filter.length < 3" class="mt-2">
<favourites></favourites>
<favourites />
</div>
<div v-if="state === 'fetching'" class="text-center p-4">

View File

@ -1,8 +1,6 @@
<div class="d-flex flex-wrap">
<stop :stop="stop" />
<slot/>
<div class="stop__actions flex-space-left">
<slot name="actions">
<button class="btn btn-action" ref="action-info" @click="details = !details">
@ -19,9 +17,9 @@
<stop-details :stop="stop"/>
</fold>
<popper reference="action-map" :visible="map" arrow class="popper--no-padding" style="width: 500px;" placement="right-start">
<div style="height: 300px">
<stop-map :stop="stop" />
</div>
<popper reference="action-map" v-show="showMap" arrow class="popper--no-padding" style="width: 500px;" placement="right-start" v-hover:inMap>
<lazy :activate="showMap">
<stop-map :stop="stop" style="height: 300px"/>
</lazy>
</popper>
</div>

View File

@ -1,7 +1,4 @@
<div class="popper" :class="{ 'popper--arrow': arrow, 'd-none': !show }" @focusin="focused = true" @focusout="focused = false" v-hover="hovered">
<div class="popper" :class="{ 'popper--arrow': arrow }" v-on="listeners">
<div class="popper__arrow" ref="arrow" v-if="arrow"></div>
<lazy v-if="lazy" :activate="show">
<slot></slot>
</lazy>
<slot v-else></slot>
</div>
<slot />
</div>

View File

@ -1,5 +1,7 @@
<l-map :center="stop.location" :zoom=17 :options="{ zoomControl: false, dragging: false }">
<l-tile-layer url="//{s}.tile.osm.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="//osm.org/copyright">OpenStreetMap</a> contributors'/>
<l-marker :lat-lng="stop.location"/>
</l-map>
<div>
<l-map :center="stop.location" :zoom=17 :options="{ zoomControl: false, dragging: false }">
<l-tile-layer url="//{s}.tile.osm.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="//osm.org/copyright">OpenStreetMap</a> contributors'/>
<l-marker :lat-lng="stop.location"/>
</l-map>
</div>

View File

@ -35,4 +35,4 @@
margin-left: -100%;
}
}
}
}

View File

@ -109,3 +109,7 @@ svg.svg-inline--fa {
.icon {
padding: .5rem 0.75rem;
}
.invalid-feedback p {
margin-bottom: 0;
}

View File

@ -0,0 +1,5 @@
@include media-breakpoint-up('sm') {
.favourite-add-form {
width: 250px;
}
}

View File

@ -0,0 +1,7 @@
label {
font-weight: bold;
font-size: .8rem;
margin-bottom: 0;
margin-top: -0.2rem;
display: block;
}

View File

@ -114,3 +114,10 @@
@include placement("bottom");
}
}
@include media-breakpoint-down('sm') {
.popper {
margin-left: $spacer;
margin-right: $spacer;
}
}

View File

@ -10,7 +10,6 @@ $primary: #005ea8;
$custom-control-indicator-checked-bg: $dark;
$custom-control-indicator-active-bg: $dark;
$line-types: (
'trolleybus': #419517,
'tram': #cd2e12,
@ -27,6 +26,7 @@ $headings-margin-bottom: $default-spacing;
$container-max-widths: map-merge($container-max-widths, ( xl: 1320px ));
$link-color: #005ea8;
$grid-gutter-width: $spacer * 2;
@import "~bootstrap/scss/bootstrap";
@ -49,6 +49,8 @@ $link-color: #005ea8;
@import "controls";
@import "popper";
@import "animations";
@import "form";
@import "fabourites";
body {
min-height: 100vh;

View File

@ -13,9 +13,11 @@ window['Popper'] = Popper;
// dependencies
import Vue from "vue";
import Vuex from 'vuex';
import PortalVue from 'portal-vue';
import { Workbox } from "workbox-window";
Vue.use(Vuex);
Vue.use(PortalVue);
// async dependencies
(async function () {

View File

@ -19,6 +19,7 @@ export class FavouritesComponent extends Vue {
@Component({ template: require('../../components/favourites/save.html' )})
export class FavouritesAdderComponent extends Vue {
private name = "";
private errors = { name: [] };
@Mutation add: (fav: Favourite) => void;
@ -28,10 +29,28 @@ export class FavouritesAdderComponent extends Vue {
const favourite: Favourite = { name, state };
this.add(favourite);
this.name = '';
if (this.validate(favourite)) {
this.add(favourite);
this.name = '';
this.$emit('saved', favourite);
this.$emit('saved', favourite);
}
}
private validate(favourite: Favourite) {
let errors = { name: [] };
if (favourite.name.length == 0) {
errors.name.push("Musisz podać nazwę.");
}
if (this.$store.state.favourites.favourites.filter(other => other.name == favourite.name).length > 0) {
errors.name.push("Istnieje już zapisana grupa przystanków o takiej nazwie.");
}
this.errors = errors;
return Object.entries(errors).map(a => a[1]).reduce((acc, cur) => [ ...acc, ...cur ]).length == 0;
}
}

View File

@ -13,6 +13,11 @@ export class PickerStopComponent extends Vue {
details: boolean = false;
map: boolean = false;
inMap: boolean = false;
get showMap() {
return this.inMap || this.map;
}
}
@Component({

View File

@ -1,35 +1,37 @@
import Vue from 'vue';
import { Component, Prop, Watch } from "vue-property-decorator";
import Popper, { Placement } from "popper.js";
import { Portal } from "portal-vue";
@Component({ template: require("../../components/popper.html") })
@Component({
template: require("../../components/popper.html")
})
export class PopperComponent extends Vue {
@Prop(String)
public reference: string;
@Prop(Object)
public refs: string;
@Prop({ type: String, default: "auto" })
public placement: Placement;
@Prop(Boolean)
public arrow: boolean;
@Prop({ type: Boolean, default: false })
public visible: boolean;
@Prop(Boolean)
public lazy: boolean;
public hovered: boolean = false;
public focused: boolean = false;
private _event;
private _popper;
get show() {
return this.visible || this.hovered || this.focused;
focusOut(event: MouseEvent) {
if (this.$el.contains(event.target as Node)) {
return;
}
this.$emit('leave', event);
}
mounted() {
const reference = this.$parent.$refs[this.reference] as HTMLElement;
const reference = this.refsSource[this.reference] as HTMLElement;
this._popper = new Popper(reference, this.$el, {
placement: this.placement,
@ -42,8 +44,6 @@ export class PopperComponent extends Vue {
if (window.innerWidth < 560) {
data.instance.options.placement = 'bottom';
data.styles.transform = `translate3d(0, ${data.offsets.popper.top}px, 0)`;
data.styles.width = '100%';
data.styles.margin = '0';
data.styles.right = '0';
data.styles.left = '0';
data.styles.width = 'auto';
@ -56,13 +56,20 @@ export class PopperComponent extends Vue {
}
});
this.$nextTick(() => this._popper.update())
this.$nextTick(() => {
this._popper.update();
document.addEventListener('click', this._event = this.focusOut.bind(this), { capture: true });
});
}
updated() {
this._popper.update();
}
get listeners() {
return { ...this.$listeners, focusout: this.focusOut }
}
@Watch('visible')
private onVisibilityUpdate() {
this._popper.update();
@ -71,6 +78,19 @@ export class PopperComponent extends Vue {
beforeDestroy() {
this._popper.destroy();
this._event && document.removeEventListener('click', this._event, { capture: true });
}
get refsSource() {
if (this.refs) {
return this.refs;
}
if (this.$parent.$options.name == 'portalTarget') {
return this.$parent.$parent.$refs;
}
return this.$parent.$refs
}
}

View File

@ -45,6 +45,20 @@ Vue.directive('hover', {
}
});
Vue.directive('autofocus', {
inserted(el, binding) {
if (binding.value !== undefined) {
const value = binding.value;
if ((typeof value === "boolean" && !value) || (typeof value === "function" && !value(el))) {
return;
}
}
el.focus();
}
});
Vue.directive('responsive', {
inserted(el, binding) {
const breakpoints = typeof binding.value === 'object' ? binding.value : {

View File

@ -51,7 +51,13 @@ class ZtmGdanskDepartureRepository implements DepartureRepository
private function getRealDepartures(Stop $stop)
{
$estimates = json_decode(file_get_contents(static::ESTIMATES_URL . "?stopId=" . $stop->getId()), true)['delay'];
try {
$estimates = file_get_contents(static::ESTIMATES_URL . "?stopId=" . $stop->getId());
$estimates = json_decode($estimates, true)['delay'];
} catch (\Error $e) {
return collect();
}
$estimates = collect($estimates);
$lines = $estimates->map(function ($delay) {

View File

@ -11,7 +11,7 @@
<fa :icon="['fal', 'bullhorn']" fixed-width class="mr-2"></fa>
Komunikaty <span class="ml-2 badge badge-pill badge-dark">{{ '{{ messages.count }}' }}</span>
</h2>
<button class="btn btn-action flex-space-left" ref="settings-messages" v-hover="visibility.messages">
<button class="btn btn-action flex-space-left" ref="settings-messages" @click="visibility.messages = true">
<fa :icon="['fal', 'cog']" fixed-width></fa>
</button>
<button class="btn btn-action" @click="updateMessages" ref="btn-messages-refresh">
@ -21,7 +21,7 @@
<fa :icon="['fal', sections.messages ? 'chevron-up' : 'chevron-down']" fixed-width/>
</button>
<popper reference="settings-messages" :visible="visibility.messages" arrow placement="left-start">
<popper reference="settings-messages" v-if="visibility.messages" arrow placement="left-start" @leave="visibility.messages = false">
<h3 class="popper__heading flex">
<fa :icon="['far', 'cog']"></fa>
<label class="text" for="messages-auto-refresh">autoodświeżanie</label>
@ -47,26 +47,27 @@
<span class="text">Odjazdy</span>
</h2>
<button class="btn btn-action flex-space-left" ref="settings-departures" v-hover="visibility.departures">
<button class="btn btn-action flex-space-left" ref="settings-departures" @click="visibility.departures = true">
<fa :icon="['fal', 'cog']" fixed-width></fa>
</button>
<button class="btn btn-action" @click="updateDepartures({ stops })">
<fa :icon="['fal', 'sync']" :spin="departures.state === 'fetching'" fixed-width></fa>
</button>
<popper reference="settings-departures" :visible="visibility.departures" arrow placement="left-start">
<h3 class="popper__heading flex">
<fa :icon="['far', 'sync']" fixed-width></fa>
<label class="text" for="messages-auto-refresh">autoodświeżanie</label>
<input type="checkbox" class="flex-space-left" id="messages-auto-refresh" v-model="autorefresh.departures.active"/>
</h3>
<div class="flex" v-show="autorefresh.messages.active">
<span class="text">co</span>
<label class="sr-only" for="messages-auto-refresh-interval">częstotliwość odświeżania</label>
<input type="text" class="form-control form-control-sm form-control-simple" id="messages-auto-refresh-interval" v-model="autorefresh.departures.interval"/>
<span class="text">s</span>
</div>
</popper>
<portal to="popups">
<popper reference="settings-departures" v-if="visibility.departures" arrow placement="left-start" @leave="visibility.departures = false">
<h3 class="popper__heading flex">
<fa :icon="['far', 'sync']" fixed-width></fa>
<label class="text" for="messages-auto-refresh">autoodświeżanie</label>
<input type="checkbox" class="flex-space-left" id="messages-auto-refresh" v-model="autorefresh.departures.active"/>
</h3>
<div class="flex" v-show="autorefresh.messages.active">
<span class="text">co</span>
<label class="sr-only" for="messages-auto-refresh-interval">częstotliwość odświeżania</label>
<input type="text" class="form-control form-control-sm form-control-simple" id="messages-auto-refresh-interval" v-model="autorefresh.departures.interval"/>
<span class="text">s</span>
</div>
</popper>
</portal>
</header>
<departures :stops="stops"></departures>
{% if provider.attribution %}
@ -87,17 +88,6 @@
<button class="btn btn-action flex-space-left" @click="clear">
<fa :icon="['fal', 'trash-alt']" fixed-width></fa>
</button>
<button class="btn btn-action" @click="visibility.save = true" @focusout="visibility.save = false" ref="save">
<fa :icon="['fal', 'star']" fixed-width></fa>
</button>
<popper reference="save" :visible="visibility.save" arrow>
<h3 class="popper__heading flex">
<fa :icon="['far', 'star']" fixed-width></fa>
<span class="text">Dodaj do ulubionych</span>
</h3>
<favourites-adder></favourites-adder>
</popper>
</header>
<ul class="picker__stops list-underlined">
@ -108,6 +98,17 @@
<picker-stop :stop="stop" class="flex-grow-1"></picker-stop>
</li>
</ul>
<div class="d-flex mt-2">
<button class="btn btn-action btn-sm flex-space-left" @click="visibility.save = true" ref="save">
<fa :icon="['fal', 'star']" fixed-width></fa>
zapisz jako...
</button>
</div>
<popper reference="save" v-if="visibility.save" arrow tabindex="-1" @leave="visibility.save = false">
<favourites-adder @saved="visibility.save = false"/>
</popper>
</section>
<section class="section picker">
<header class="section__title flex">
@ -130,7 +131,7 @@
</button>
</template>
</header>
<div class="transition-box">
<div class="transition-box" style="overflow: hidden;">
<transition name="fade">
<keep-alive>
<stop-finder @select="add" :blacklist="stops" v-if="visibility.picker === 'search'"></stop-finder>
@ -141,6 +142,8 @@
</section>
</div>
</div>
<portal-target name="popups" multiple></portal-target>
{% endblock %}
{% block javascripts %}

View File

@ -4782,6 +4782,11 @@ popper.js@*, popper.js@^1.14.1, popper.js@^1.14.4:
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3"
integrity sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw==
portal-vue@^2.1.7:
version "2.1.7"
resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.7.tgz#ea08069b25b640ca08a5b86f67c612f15f4e4ad4"
integrity sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g==
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"