Refactor popups
This commit is contained in:
parent
4b389582ad
commit
9802473d7c
4
.gitignore
vendored
4
.gitignore
vendored
@ -14,4 +14,6 @@
|
||||
/.idea/
|
||||
/public/*
|
||||
!/public/index.php
|
||||
!/public/manifest.json
|
||||
!/public/manifest.jso
|
||||
|
||||
/yarn-error.log
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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='© <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='© <a href="//osm.org/copyright">OpenStreetMap</a> contributors'/>
|
||||
<l-marker :lat-lng="stop.location"/>
|
||||
</l-map>
|
||||
</div>
|
||||
|
@ -35,4 +35,4 @@
|
||||
margin-left: -100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,3 +109,7 @@ svg.svg-inline--fa {
|
||||
.icon {
|
||||
padding: .5rem 0.75rem;
|
||||
}
|
||||
|
||||
.invalid-feedback p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
5
resources/styles/_fabourites.scss
Normal file
5
resources/styles/_fabourites.scss
Normal file
@ -0,0 +1,5 @@
|
||||
@include media-breakpoint-up('sm') {
|
||||
.favourite-add-form {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
7
resources/styles/_form.scss
Normal file
7
resources/styles/_form.scss
Normal file
@ -0,0 +1,7 @@
|
||||
label {
|
||||
font-weight: bold;
|
||||
font-size: .8rem;
|
||||
margin-bottom: 0;
|
||||
margin-top: -0.2rem;
|
||||
display: block;
|
||||
}
|
@ -114,3 +114,10 @@
|
||||
@include placement("bottom");
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down('sm') {
|
||||
.popper {
|
||||
margin-left: $spacer;
|
||||
margin-right: $spacer;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 () {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 : {
|
||||
|
@ -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) {
|
||||
|
@ -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 %}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user