kopia lustrzana https://github.com/FacilMap/facilmap
Status commit
rodzic
98079eb28e
commit
cc87787a37
|
@ -0,0 +1,19 @@
|
|||
.fm-about {
|
||||
ul {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 180px);
|
||||
gap: 5px;
|
||||
|
||||
li {
|
||||
border: 1px solid rgba(0,0,0,.125);
|
||||
display: flex;
|
||||
|
||||
a {
|
||||
flex-grow: 1;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import packageJson from "../../../package.json";
|
|||
import WithRender from "./about.vue";
|
||||
import { baseLayers, overlays } from "facilmap-leaflet";
|
||||
import { Prop } from "vue-property-decorator";
|
||||
import "./about.scss";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
|
|
|
@ -1,17 +1,48 @@
|
|||
<b-modal :id="id" :title="`About FacilMap ${fmVersion}`" ok-only ok-title="Close" size="lg">
|
||||
<b-modal :id="id" :title="`About FacilMap ${fmVersion}`" ok-only ok-title="Close" size="lg" dialog-class="fm-about">
|
||||
<p><a :href="fmHomepage" target="_blank"><strong>FacilMap</strong></a> is available under the <a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">GNU Affero General Public License, Version 3</a>.</p>
|
||||
<p>If something does not work or you have a suggestion for improvement, please report on the <a :href="fmBugTracker" target="_blank">issue tracker</a>.</p>
|
||||
<h4>Privacy</h4>
|
||||
<p>
|
||||
FacilMap aims to be a privacy-friendly open-source alternative to commercial maps that track your data. When you use FacilMap,
|
||||
anything you do on the map (for example pan/zoom the map, search for something, calculate a route, add a marker) is sent to the
|
||||
FacilMap server and persisted there only if necessary for the interaction. No personally identifiable information is persisted
|
||||
(for example your IP address). FacilMap does not set any cookies.
|
||||
</p>
|
||||
<p>
|
||||
FacilMap combines multiple third-party services (listed below under “Map data”) into one versatile map. When you use FacilMap,
|
||||
your browser will retrieve the information provided by these services directly from there. FacilMap will send the current position
|
||||
of the map and (for the search/route service) the search terms to those services. Your browser will send your IP address and
|
||||
potentially some cookies that these services have set. It remains at the discretion of those third-party services what they
|
||||
do with this data.
|
||||
</p>
|
||||
<h4>Map data</h4>
|
||||
<dl class="row">
|
||||
<template v-for="layer in layers" v-if="layer.options.attribution">
|
||||
<dt class="col-sm-3">{{layer.options.fmName}}</dt>
|
||||
<dd class="col-sm-9" v-html="layer.options.attribution"></dd>
|
||||
</template>
|
||||
|
||||
<dt class="col-sm-3">Search</dt>
|
||||
<dd class="col-sm-9"><a href="https://nominatim.openstreetmap.org/" target="_blank">Nominatim</a> / <a href="http://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a></dd>
|
||||
|
||||
<dt class="col-sm-3">Directions</dt>
|
||||
<dd class="col-sm-9"><a href="https://www.mapbox.com/api-documentation/#directions">Mapbox Directions API</a> / <a href="https://openrouteservice.org/">OpenRouteService</a> / <a href="http://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a></dd>
|
||||
|
||||
<dt class="col-sm-3">GeoIP</dt>
|
||||
<dd class="col-sm-9">This product includes GeoLite2 data created by MaxMind, available from <a href="https://www.maxmind.com">https://www.maxmind.com</a>.</dd>
|
||||
</dl>
|
||||
<h4>Programs/libraries</h4>
|
||||
<ul class="list-inline">
|
||||
<ul>
|
||||
<li><a href="https://nodejs.org/" target="_blank">Node.js</a></li>
|
||||
<li><a href="http://sequelizejs.com/" target="_blank">Sequelize</a></li>
|
||||
<li><a href="https://sequelize.org/" target="_blank">Sequelize</a></li>
|
||||
<li><a href="https://socket.io/" target="_blank">socket.io</a></li>
|
||||
<li><a href="https://www.typescriptlang.org/" target="_blank">TypeScript</a></li>
|
||||
<li><a href="https://webpack.js.org/" target="_blank">Webpack</a></li>
|
||||
<li><a href="https://jquery.com/" target="_blank">jQuery</a></li>
|
||||
<li><a href="https://angularjs.org/" target="_blank">AngularJS</a></li>
|
||||
<li><a href="https://vuejs.org/" target="_blank">Vue.js</a></li>
|
||||
<li><a href="https://github.com/chjj/marked" target="_blank">Marked</a></li>
|
||||
<li><a href="https://getbootstrap.com/" target="_blank">Bootstrap</a></li>
|
||||
<li><a href="https://angular-ui.github.io/bootstrap/" target="_blank">AngularUI Bootstrap</a></li>
|
||||
<li><a href="https://bootstrap-vue.org/" target="_blank">BootstrapVue</a></li>
|
||||
<li><a href="https://leafletjs.com/" target="_blank">Leaflet</a></li>
|
||||
<li><a href="http://project-osrm.org/" target="_blank">OSRM</a></li>
|
||||
<li><a href="https://openrouteservice.org/" target="_blank">OpenRouteService</a></li>
|
||||
|
@ -19,25 +50,9 @@
|
|||
<li><a href="https://github.com/joewalnes/filtrex" target="_blank">Filtrex</a></li>
|
||||
</ul>
|
||||
<h4>Icons</h4>
|
||||
<ul class="list-inline">
|
||||
<ul>
|
||||
<li><a href="https://github.com/twain47/Open-SVG-Map-Icons/" target="_blank">Open SVG Map Icons</a></li>
|
||||
<li><a href="https://glyphicons.com/" target="_blank">Glyphicons</a></li>
|
||||
<li><a href="https://zavoloklom.github.io/material-design-iconic-font/index.html" target="_blank">Material Design Iconic Font</a></li>
|
||||
</ul>
|
||||
<h4>Map data</h4>
|
||||
<dl class="dl-horizontal">
|
||||
<template v-for="layer in layers" v-if="layer.attribution">
|
||||
<dt>{{layer.name}}</dt>
|
||||
<dd v-html="layer.attribution"></dd>
|
||||
</template>
|
||||
|
||||
<dt>Search</dt>
|
||||
<dd><a href="https://nominatim.openstreetmap.org/" target="_blank">Nominatim</a> / <a href="http://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a></dd>
|
||||
|
||||
<dt>Directions</dt>
|
||||
<dd><a href="https://www.mapbox.com/api-documentation/#directions">Mapbox Directions API</a> / <a href="https://openrouteservice.org/">OpenRouteService</a> / <a href="http://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a></dd>
|
||||
|
||||
<dt>GeoIP</dt>
|
||||
<dd>This product includes GeoLite2 data created by MaxMind, available from <a href="https://www.maxmind.com">https://www.maxmind.com</a>.</dd>
|
||||
</dl>
|
||||
</b-modal>
|
|
@ -5,24 +5,11 @@ import "./client.scss";
|
|||
import WithRender from "./client.vue";
|
||||
|
||||
const CLIENT_KEY = "fm-client";
|
||||
const CLIENT_CONTEXT_KEY = "fm-client-context";
|
||||
|
||||
export function InjectClient() {
|
||||
return InjectReactive(CLIENT_KEY);
|
||||
}
|
||||
|
||||
export function InjectClientContext() {
|
||||
return InjectReactive(CLIENT_CONTEXT_KEY);
|
||||
}
|
||||
|
||||
export interface ClientContext {
|
||||
loading: number;
|
||||
/* padId: string | undefined;
|
||||
disconnected: boolean;
|
||||
serverError: Error | undefined;
|
||||
deleted: boolean; */
|
||||
}
|
||||
|
||||
@WithRender
|
||||
@Component({})
|
||||
export class ClientProvider extends Vue {
|
||||
|
@ -30,22 +17,14 @@ export class ClientProvider extends Vue {
|
|||
@Prop({ type: String, default: "/" }) readonly serverUrl!: string;
|
||||
@Prop({ type: String }) readonly padId: string | undefined;
|
||||
|
||||
@ProvideReactive(CLIENT_KEY) client!: Client;
|
||||
@ProvideReactive(CLIENT_CONTEXT_KEY) clientContext: ClientContext = {
|
||||
loading: 0
|
||||
};
|
||||
@ProvideReactive(CLIENT_KEY) client: Client = null as any;
|
||||
|
||||
created(): void {
|
||||
const client = new Client(this.serverUrl, this.padId);
|
||||
client._set = Vue.set;
|
||||
client._delete = Vue.delete;
|
||||
|
||||
this.client = client;
|
||||
|
||||
client.on("loadStart", () => {
|
||||
this.clientContext.loading++;
|
||||
});
|
||||
client.on("loadEnd", () => {
|
||||
this.clientContext.loading--;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -1,17 +1,25 @@
|
|||
import { decodeQueryString, encodeQueryString } from "facilmap-utils";
|
||||
import Vue from "vue";
|
||||
|
||||
const queryParams = decodeQueryString(location.search);
|
||||
const toBoolean = (val: string, def: boolean) => (val == null ? def : val != "0" && val != "false" && val != "no");
|
||||
|
||||
const context = {
|
||||
const isNarrow = () => window.innerWidth < 768;
|
||||
|
||||
const context = Vue.observable({
|
||||
activePadId: decodeURIComponent(location.pathname.match(/[^/]*$/)![0]),
|
||||
urlPrefix: location.protocol + "//" + location.host + location.pathname.replace(/[^/]*$/, ""),
|
||||
toolbox: toBoolean(queryParams.toolbox, true),
|
||||
search: toBoolean(queryParams.search, true),
|
||||
autofocus: toBoolean(queryParams.autofocus, parent === window),
|
||||
legend: toBoolean(queryParams.legend, true),
|
||||
interactive: toBoolean(queryParams.interactive, parent === window)
|
||||
};
|
||||
interactive: toBoolean(queryParams.interactive, parent === window),
|
||||
isNarrow: isNarrow()
|
||||
});
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
context.isNarrow = isNarrow();
|
||||
});
|
||||
|
||||
export default context;
|
||||
|
||||
|
|
|
@ -47,6 +47,13 @@
|
|||
text-shadow: 0 0 3px #fff, 0 0 5px #fff, 0 0 10px #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-control-locate.leaflet-control-locate a {
|
||||
font-size: inherit;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -75,20 +82,21 @@
|
|||
|
||||
.fm-leaflet-map-spinner {
|
||||
position:absolute;
|
||||
bottom: 7px;
|
||||
left: 102px;
|
||||
height:28px;
|
||||
width:29px;
|
||||
padding:16px 16px 16px 15px;
|
||||
background:url(./spinner-background.png);
|
||||
box-sizing: content-box;
|
||||
bottom: 20px;
|
||||
left: 115px;
|
||||
color: #00272a;
|
||||
}
|
||||
|
||||
.fm-attribution-icon {
|
||||
.fm-logo {
|
||||
position: absolute;
|
||||
bottom: -19px;
|
||||
bottom: 0;
|
||||
left: -25px;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
margin-bottom: -19px;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-open-external {
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import WithRender from "./leaflet-map.vue";
|
||||
import Vue from "vue";
|
||||
import { Component, Inject, InjectReactive, Provide, ProvideReactive } from "vue-property-decorator";
|
||||
import { Component, InjectReactive, ProvideReactive } from "vue-property-decorator";
|
||||
import Client from 'facilmap-client';
|
||||
import "./leaflet-map.scss";
|
||||
import { ClientContext, InjectClient, InjectClientContext } from "../client/client";
|
||||
import L, { LatLng, Map } from "leaflet";
|
||||
import { InjectClient } from "../client/client";
|
||||
import L, { Control, LatLng, Map } from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { displayView, getInitialView, getVisibleLayers, HashHandler, setVisibleLayers, VisibleLayers } from "facilmap-leaflet";
|
||||
import { createSymbolHtml, displayView, getInitialView, getVisibleLayers, HashHandler, VisibleLayers } from "facilmap-leaflet";
|
||||
import "leaflet.locatecontrol";
|
||||
import "leaflet.locatecontrol/dist/L.Control.Locate.css";
|
||||
import "leaflet-graphicscale";
|
||||
import "leaflet-graphicscale/src/Leaflet.GraphicScale.scss";
|
||||
import "leaflet-mouse-position";
|
||||
import "leaflet-mouse-position/src/L.Control.MousePosition.css";
|
||||
import $ from "jquery";
|
||||
|
||||
const MAP_COMPONENTS_KEY = "fm-map-components";
|
||||
const MAP_CONTEXT_KEY = "fm-map-context";
|
||||
|
@ -25,6 +26,24 @@ export function InjectMapContext() {
|
|||
return InjectReactive(MAP_CONTEXT_KEY);
|
||||
}
|
||||
|
||||
function createButton(symbol: string, onClick: () => void): Control {
|
||||
return Object.assign(new Control(), {
|
||||
onAdd() {
|
||||
const div = document.createElement('div');
|
||||
div.className = "leaflet-bar";
|
||||
const a = document.createElement('a');
|
||||
a.href = "javascript:";
|
||||
a.innerHTML = createSymbolHtml("currentColor", "1.5em", symbol);
|
||||
a.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
});
|
||||
div.appendChild(a);
|
||||
return div;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export interface MapComponents {
|
||||
map: Map;
|
||||
hashHandler: HashHandler;
|
||||
|
@ -36,6 +55,7 @@ export interface MapContext {
|
|||
layers: VisibleLayers;
|
||||
filter: string | undefined;
|
||||
hash: string;
|
||||
showToolbox: boolean;
|
||||
}
|
||||
|
||||
@WithRender
|
||||
|
@ -45,13 +65,12 @@ export interface MapContext {
|
|||
export default class LeafletMap extends Vue {
|
||||
|
||||
@InjectClient() client!: Client;
|
||||
@InjectClientContext() clientContext!: ClientContext;
|
||||
|
||||
@ProvideReactive(MAP_COMPONENTS_KEY) mapComponents!: MapComponents;
|
||||
@ProvideReactive(MAP_CONTEXT_KEY) mapContext: MapContext = null as any;
|
||||
|
||||
isInFrame = (parent !== window);
|
||||
loaded = true;
|
||||
loaded = false;
|
||||
|
||||
get selfUrl() {
|
||||
return `${location.origin}${location.pathname}${this.mapContext.hash ? `#${this.mapContext.hash}` : ''}`;
|
||||
|
@ -71,6 +90,8 @@ export default class LeafletMap extends Vue {
|
|||
iconLoading: "a"
|
||||
}).addTo(map);
|
||||
|
||||
$(locateControl._container).find("a").append(createSymbolHtml("currentColor", "1.5em", "screenshot"));
|
||||
|
||||
// $compile($('<fm-icon fm-icon="screenshot" alt="Locate"/>').appendTo($("a", locateControl._container)))($scope);
|
||||
|
||||
L.control.mousePosition({
|
||||
|
@ -84,27 +105,35 @@ export default class LeafletMap extends Vue {
|
|||
position: "bottomcenter"
|
||||
}).addTo(map);
|
||||
|
||||
/* createButton("menu-hamburger", () => {
|
||||
this.mapContext.showToolbox = true;
|
||||
}).addTo(map); */
|
||||
|
||||
this.mapComponents = {
|
||||
map,
|
||||
hashHandler
|
||||
};
|
||||
|
||||
if (!map._loaded) {
|
||||
// Initial view was not set by hash handler
|
||||
getInitialView(this.client).then((view) => {
|
||||
displayView(map, view);
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
// TODO
|
||||
});
|
||||
}
|
||||
(async () => {
|
||||
if (!map._loaded) {
|
||||
try {
|
||||
// Initial view was not set by hash handler
|
||||
displayView(map, await getInitialView(this.client));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
displayView(map);
|
||||
}
|
||||
}
|
||||
this.loaded = true;
|
||||
})();
|
||||
|
||||
this.mapContext = {
|
||||
center: map._loaded ? map.getCenter() : L.latLng(0, 0),
|
||||
zoom: map._loaded ? map.getZoom() : 1,
|
||||
layers: getVisibleLayers(map),
|
||||
filter: map.fmFilter,
|
||||
hash: location.hash.replace(/^#/, "")
|
||||
hash: location.hash.replace(/^#/, ""),
|
||||
showToolbox: false
|
||||
};
|
||||
|
||||
map.on("moveend", () => {
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
<div class="fm-leaflet-map"></div>
|
||||
|
||||
<a v-if="isInFrame" :href="selfUrl" target="_blank" class="fm-open-external" uib-tooltip="Open FacilMap in full size" tooltip-placement="right"></a>
|
||||
<img src="./logo.png" class="fm-attribution-icon"/>
|
||||
<div class="fm-leaflet-map-spinner" v-show="clientContext.loading > 0">
|
||||
<img src="./spinner.gif" alt="Loading…">
|
||||
</div>
|
||||
|
||||
<div class="fm-logo">
|
||||
<img src="./logo.png"/>
|
||||
</div>
|
||||
<b-spinner class="fm-leaflet-map-spinner" v-show="client.loading > 0"></b-spinner>
|
||||
|
||||
<slot v-if="mapContext"></slot>
|
||||
|
||||
<!-- <div class="fm-leaflet-map-disabled-cover" v-show="client.padId && (client.disconnected || client.serverError || client.deleted)"></div>
|
||||
<div class="fm-leaflet-map-disabled-cover" v-show="client.padId && (client.disconnected || client.serverError || client.deleted)"></div>
|
||||
<div class="fm-leaflet-map-loading" v-show="!loaded && !client.serverError">
|
||||
Loading...
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 2.1 KiB |
Plik binarny nie jest wyświetlany.
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 3.5 KiB |
Plik binarny nie jest wyświetlany.
|
@ -7,10 +7,11 @@ import "./main.scss";
|
|||
import { InjectClient } from "../client/client";
|
||||
import Toolbox from "../toolbox/toolbox";
|
||||
import context from "../context";
|
||||
import SearchBox from "../search-box/search-box";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
components: { LeafletMap, Toolbox }
|
||||
components: { LeafletMap, SearchBox, Toolbox }
|
||||
})
|
||||
export default class Main extends Vue {
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="fm-main">
|
||||
<leaflet-map>
|
||||
<toolbox v-if="context.toolbox" :interactive="context.interactive"></toolbox>
|
||||
<!-- <search v-if="context.search" :autofocus="context.autofocus"></search>
|
||||
<legend v-if="context.legend"></legend> -->
|
||||
</leaflet-map>
|
||||
<LeafletMap>
|
||||
<Toolbox v-if="context.toolbox" :interactive="context.interactive"></Toolbox>
|
||||
<SearchBox v-if="context.search" :autofocus="context.autofocus"></SearchBox>
|
||||
<!--<Legend v-if="context.legend"></Legend> -->
|
||||
</LeafletMap>
|
||||
</div>
|
File diff suppressed because one or more lines are too long
|
@ -8,8 +8,12 @@ import context from './context';
|
|||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css";
|
||||
import withRender from "./map.vue";
|
||||
import Vue2TouchEvents from 'vue2-touch-events'
|
||||
import PortalVue from 'portal-vue'
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
Vue.use(Vue2TouchEvents);
|
||||
Vue.use(PortalVue);
|
||||
|
||||
/*fm.app.config(function($compileProvider, $uibTooltipProvider) {
|
||||
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|javascript):/);
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
.fm-search-box {
|
||||
position: absolute;
|
||||
|
||||
&:not(.isNarrow) {
|
||||
top: 10px;
|
||||
left: 52px;
|
||||
transition: opacity .7s,width .7s,max-width .7s,background-color .7s,border-color .7s;
|
||||
opacity: .7;
|
||||
|
||||
&:hover,&.fm-hasFocus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.isNarrow {
|
||||
top: calc(100% - 70px);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import WithRender from "./search-box.vue";
|
||||
import Vue from "vue";
|
||||
import { Component } from "vue-property-decorator";
|
||||
import "./search-box.scss";
|
||||
import context from "../context";
|
||||
import $ from "jquery";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
components: { }
|
||||
})
|
||||
export default class SearchBox extends Vue {
|
||||
|
||||
tab: number = 0;
|
||||
touchStartY: number | null = null;
|
||||
|
||||
get isNarrow() {
|
||||
return context.isNarrow;
|
||||
}
|
||||
|
||||
handleTouchStart(event: TouchEvent) {
|
||||
if(context.isNarrow && event.touches && event.touches[0] && $(event.target as EventTarget).closest("[draggable=true]").length == 0) {
|
||||
const top = (this.$el as HTMLElement).offsetTop;
|
||||
this.touchStartY = event.touches[0].clientY - top;
|
||||
$(this.$el).css("top", `${top}px`);
|
||||
}
|
||||
}
|
||||
|
||||
handleTouchMove(event: TouchEvent) {
|
||||
if(this.touchStartY != null && event.touches[0]) {
|
||||
const minTop = Math.max(0, ((this.$el as HTMLElement).offsetParent as HTMLElement).offsetHeight - (this.$el as HTMLElement).scrollHeight);
|
||||
const maxTop = ((this.$el as HTMLElement).offsetParent as HTMLElement).offsetHeight - 70;
|
||||
const top = Math.max(minTop, Math.min(maxTop, event.touches[0].clientY - this.touchStartY));
|
||||
$(this.$el).css("top", `${top}px`);
|
||||
}
|
||||
}
|
||||
|
||||
handleTouchEnd(event: TouchEvent) {
|
||||
if(this.touchStartY != null && event.changedTouches[0]) {
|
||||
this.touchStartY = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<div class="fm-search-box" :class="{ isNarrow }" v-model="tab" v-touch:start="handleTouchStart" v-touch:moving="handleTouchMove" v-touch:end="handleTouchEnd">
|
||||
<b-card no-body>
|
||||
<b-tabs card align="center">
|
||||
<b-tab title="" title-item-class="d-none"></b-tab>
|
||||
<b-tab title="Test 1">
|
||||
<p>Test 1</p>
|
||||
</b-tab>
|
||||
<b-tab title="Test 2">
|
||||
<p>Test 2</p>
|
||||
</b-tab>
|
||||
<portal-target name="fm-search-box"></portal-target>
|
||||
</b-tabs>
|
||||
</b-card>
|
||||
</div>
|
|
@ -2,23 +2,30 @@
|
|||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
opacity: .5;
|
||||
transition: opacity .7s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
.fm-toolbox-toggle {
|
||||
color: #444;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: #f4f4f4;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-menu-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
.fm-sidebar:not(.isNarrow) {
|
||||
opacity: .5;
|
||||
transition: opacity .7s;
|
||||
|
||||
.navbar {
|
||||
margin-bottom: 0; /* So that the legend can calculate the height of the toolbox properly */
|
||||
}
|
||||
|
||||
a.disabled {
|
||||
color: #aaa !important;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
|
|
@ -2,17 +2,20 @@ import Component from "vue-class-component";
|
|||
import Vue from "vue";
|
||||
import WithRender from "./toolbox.vue";
|
||||
import "./toolbox.scss";
|
||||
import { Prop } from "vue-property-decorator";
|
||||
import { Prop, Ref } from "vue-property-decorator";
|
||||
import Client from "facilmap-client";
|
||||
import { InjectClient } from "../client/client";
|
||||
import { InjectMapComponents, InjectMapContext, MapComponents, MapContext } from "../leaflet-map/leaflet-map";
|
||||
import { baseLayers, displayView, overlays, setBaseLayer, toggleOverlay } from "facilmap-leaflet";
|
||||
import { View } from "facilmap-types";
|
||||
import About from "../about/about";
|
||||
import Sidebar from "../ui/sidebar/sidebar";
|
||||
import Icon from "../ui/icon/icon";
|
||||
import context from "../context";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
components: { About }
|
||||
components: { About, Icon, Sidebar }
|
||||
})
|
||||
export default class Toolbox extends Vue {
|
||||
|
||||
|
@ -22,7 +25,11 @@ export default class Toolbox extends Vue {
|
|||
@Prop({ type: Boolean, default: true }) readonly interactive!: boolean;
|
||||
|
||||
hasImportUi = true; // TODO
|
||||
|
||||
|
||||
get isNarrow() {
|
||||
return context.isNarrow;
|
||||
}
|
||||
|
||||
get links() {
|
||||
const v = this.mapContext;
|
||||
return {
|
||||
|
@ -121,4 +128,5 @@ export default class Toolbox extends Vue {
|
|||
showHistory() {
|
||||
map.historyUi.openHistoryDialog();
|
||||
} */
|
||||
|
||||
}
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
<div class="fm-toolbox">
|
||||
<b-navbar variant="light">
|
||||
<b-navbar-nav>
|
||||
<b-nav-item v-if="!client.padId && interactive" @click="startPad()">Start collaborative map</b-nav-item>
|
||||
<a v-if="isNarrow" href="javascript:" class="fm-toolbox-toggle" v-b-toggle.fm-toolbox-sidebar><Icon icon="menu-hamburger"></Icon></a>
|
||||
|
||||
<Sidebar id="fm-toolbox-sidebar">
|
||||
<b-nav-item v-if="!client.padId && interactive" href="javascript:" @click="startPad()">Start collaborative map</b-nav-item>
|
||||
<b-nav-item-dropdown v-if="!client.readonly && client.padId" text="Add" :disabled="!!client.interaction" right>
|
||||
<b-dropdown-item v-for="type in client.types" :disabled="!!client.interaction" @click="addObject(type)">{{type.name}}</b-dropdown-item>
|
||||
<b-dropdown-item v-for="type in client.types" :disabled="!!client.interaction" href="javascript:" @click="addObject(type)">{{type.name}}</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="client.writable == 2"></b-dropdown-divider>
|
||||
<b-dropdown-item v-if="client.writable == 2" :disabled="!!client.interaction" @click="editObjectTypes()">Manage types</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.writable == 2" :disabled="!!client.interaction" href="javascript:" @click="editObjectTypes()">Manage types</b-dropdown-item>
|
||||
</b-nav-item-dropdown>
|
||||
<b-nav-item-dropdown v-if="client.padId && (!client.readonly || (client.views | fmPropertyCount) != 0)" text="Views" right>
|
||||
<b-dropdown-item v-for="(id, view) in client.views" @click="displayView(view)">{{view.name}}</b-dropdown-item>
|
||||
<b-dropdown-item v-for="(id, view) in client.views" href="javascript:" @click="displayView(view)">{{view.name}}</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="client.writable == 2"></b-dropdown-divider>
|
||||
<b-dropdown-item v-if="client.writable == 2" @click="saveView()">Save current view</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.writable == 2" @click="manageViews()">Manage views</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.writable == 2" href="javascript:" @click="saveView()">Save current view</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.writable == 2" href="javascript:" @click="manageViews()">Manage views</b-dropdown-item>
|
||||
</b-nav-item-dropdown>
|
||||
<b-nav-item-dropdown text="Map style" right>
|
||||
<b-dropdown-item v-for="layerInfo in baseLayers" :active="layerInfo.active" @click="setBaseLayer(layerInfo.key)">{{layerInfo.name}}</b-dropdown-item>
|
||||
<b-dropdown-item v-for="layerInfo in baseLayers" :active="layerInfo.active" href="javascript:" @click="setBaseLayer(layerInfo.key)">{{layerInfo.name}}</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="baseLayers.length > 0 && overlays.length > 0"></b-dropdown-divider>
|
||||
<b-dropdown-item v-for="layerInfo in overlays" :active="layerInfo.active" @click="toggleOverlay(layerInfo.key)">{{layerInfo.name}}</b-dropdown-item>
|
||||
<b-dropdown-item v-for="layerInfo in overlays" :active="layerInfo.active" href="javascript:" @click="toggleOverlay(layerInfo.key)">{{layerInfo.name}}</b-dropdown-item>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item :href="links.osm" target="_blank">Open this on OpenStreetMap</b-dropdown-item>
|
||||
<b-dropdown-item :href="links.google" target="_blank">Open this on Google Maps</b-dropdown-item>
|
||||
|
@ -24,22 +25,20 @@
|
|||
</b-nav-item-dropdown>
|
||||
<b-nav-item-dropdown text="Tools" right>
|
||||
<!--<b-dropdown-item v-if="!client.readonly" @click="openDialog('copy-pad-dialog')">Copy pad</b-dropdown-item>-->
|
||||
<b-dropdown-item v-if="hasImportUi && interactive" @click="importFile()">Open file</b-dropdown-item>
|
||||
<b-dropdown-item v-if="hasImportUi && interactive" href="javascript:" @click="importFile()">Open file</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.padId" :href="`${client.padData.id}/geojson${filterQuery.q}`" title="GeoJSON files store all map information and can thus be used for map backups and be re-imported without any loss.">Export as GeoJSON</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.padId" :href="`${client.padData.id}/gpx?useTracks=1${filterQuery.a}`" title="GPX files can be opened with most navigation software. In track mode, any calculated routes are saved in the file.">Export as GPX (tracks)</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.padId" :href="`${client.padData.id}/gpx?useTracks=0${filterQuery.a}`" title="GPX files can be opened with most navigation software. In route mode, only the start/end/via points are saved in the file, and the navigation software needs to recalculate the routes.">Export as GPX (routes)</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.padId" :href="`${client.padData.id}/table${filterQuery.q}`" target="_blank">Export as table</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="client.padId"></b-dropdown-divider>
|
||||
<b-dropdown-item v-if="client.padId" @click="filter()">Filter</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.writable == 2 && client.padId" @click="editPadSettings()">Settings</b-dropdown-item>
|
||||
<b-dropdown-item v-if="!client.readonly && client.padId" @click="showHistory()">Show edit history</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.padId" href="javascript:" @click="filter()">Filter</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.writable == 2 && client.padId" href="javascript:" @click="editPadSettings()">Settings</b-dropdown-item>
|
||||
<b-dropdown-item v-if="!client.readonly && client.padId" href="javascript:" @click="showHistory()">Show edit history</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="client.padId"></b-dropdown-divider>
|
||||
<b-dropdown-item v-b-modal.fm-toolbox-about>About FacilMap</b-dropdown-item>
|
||||
<b-dropdown-item v-b-modal.fm-toolbox-about v-b-toggle.fm-toolbox-sidebar href="javascript:">About FacilMap</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.padId" :href="links.facilmap">Exit collaborative map</b-dropdown-item>
|
||||
</b-nav-item-dropdown>
|
||||
</b-nav>
|
||||
</b-navbar-nav>
|
||||
</b-navbar>
|
||||
</Sidebar>
|
||||
|
||||
<About id="fm-toolbox-about"></About>
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
import WithRender from "./icon.vue";
|
||||
import Vue from "vue";
|
||||
import { Component, Prop } from "vue-property-decorator";
|
||||
import { createSymbolHtml } from "facilmap-leaflet";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
components: { }
|
||||
})
|
||||
export default class Icon extends Vue {
|
||||
|
||||
@Prop({ type: String }) icon!: string;
|
||||
|
||||
get iconCode() {
|
||||
return createSymbolHtml("currentColor", "1.5em", this.icon);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<span class="fm-icon" v-html="iconCode"></span>
|
|
@ -0,0 +1,55 @@
|
|||
import WithRender from "./sidebar.vue";
|
||||
import Vue from "vue";
|
||||
import { Component, Prop, Ref } from "vue-property-decorator";
|
||||
import context from "../../context";
|
||||
import $ from "jquery";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
components: { }
|
||||
})
|
||||
export default class Sidebar extends Vue {
|
||||
|
||||
@Prop({ type: String }) readonly id!: string;
|
||||
|
||||
touchStartX: number | null = null;
|
||||
sidebarVisible = false;
|
||||
|
||||
get isNarrow() {
|
||||
return context.isNarrow;
|
||||
}
|
||||
|
||||
handleTouchStart(event: TouchEvent) {
|
||||
if(event.touches && event.touches[0] && $(event.target as EventTarget).closest("[draggable=true]").length == 0) {
|
||||
this.touchStartX = event.touches[0].clientX;
|
||||
$(this.$el).find(".b-sidebar").css("transition", "none");
|
||||
}
|
||||
}
|
||||
|
||||
handleTouchMove(event: TouchEvent) {
|
||||
if(this.touchStartX != null && event.touches[0]) {
|
||||
const right = Math.min(this.touchStartX - event.touches[0].clientX, 0);
|
||||
$(this.$el).find(".b-sidebar").css("margin-right", `${right}px`);
|
||||
}
|
||||
}
|
||||
|
||||
handleTouchEnd(event: TouchEvent) {
|
||||
if(this.touchStartX != null && event.changedTouches[0]) {
|
||||
const right = Math.min(this.touchStartX - event.changedTouches[0].clientX, 0);
|
||||
if(right < -($(this.$el).find(".b-sidebar").width() as number / 2)) {
|
||||
this.sidebarVisible = false;
|
||||
setTimeout(() => {
|
||||
$(this.$el).find(".b-sidebar").css("margin-right", "");
|
||||
}, 0);
|
||||
} else {
|
||||
$(this.$el).find(".b-sidebar").css({
|
||||
"transition": "margin-right 0.4s",
|
||||
"margin-right": ""
|
||||
});
|
||||
}
|
||||
|
||||
this.touchStartX = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<div class="fm-sidebar" :class="{ isNarrow }">
|
||||
<b-sidebar shadow backdrop right v-if="isNarrow" :id="id" v-touch:start="handleTouchStart" v-touch:moving="handleTouchMove" v-touch:end="handleTouchEnd" v-model="sidebarVisible">
|
||||
<b-navbar toggleable="true">
|
||||
<b-navbar-nav>
|
||||
<slot></slot>
|
||||
</b-navbar-nav>
|
||||
</b-navbar>
|
||||
</b-sidebar>
|
||||
|
||||
<b-navbar variant="light" v-if="!isNarrow">
|
||||
<b-navbar-nav>
|
||||
<slot></slot>
|
||||
</b-navbar-nav>
|
||||
</b-navbar>
|
||||
</div>
|
|
@ -11,6 +11,10 @@ declare module "leaflet" {
|
|||
_loaded?: true;
|
||||
}
|
||||
|
||||
interface Control {
|
||||
_container: HTMLElement;
|
||||
}
|
||||
|
||||
namespace control {
|
||||
export const graphicScale: any;
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue