Compare commits

68 Commits

Author SHA1 Message Date
c3569d6b98 Merge pull request 'dev/patrick' (#1) from dev/patrick into main
Reviewed-on: #1
2026-04-20 16:32:31 +02:00
7dea362c89 added moderation portal with admin authentification and seperate styling 2026-04-20 16:01:10 +02:00
11a062dd84 added ende attribution in footer 2026-04-20 15:31:49 +02:00
aec6a9bfb6 commented new vote function 2026-04-20 15:21:58 +02:00
94d4308d3f added visual vote deefback without sweet alert 2026-04-20 15:19:56 +02:00
a37c1ffe01 likes and dislikes changable if citizen changes oppinion 2026-04-20 15:06:07 +02:00
8151390835 warning message portal still in development in welcome modal 2026-04-20 14:55:24 +02:00
99cf34671a changed language of geoman plugin to german 2026-04-20 14:48:16 +02:00
f9187a3e84 pinned version of sweetalert 2026-04-20 14:45:31 +02:00
94100b9371 removed sweetalert duplicate 2026-04-20 14:43:38 +02:00
84ce0de870 sweetalert font override 2026-04-20 14:40:28 +02:00
391cec07c8 custom GPS button styling 2026-04-20 14:38:14 +02:00
d3cfcbab25 custom mouse position styling 2026-04-20 14:30:43 +02:00
1eafc27c53 dynamic categories in contribution modal dropdown 2026-04-20 14:21:11 +02:00
dbacae3f2e removed login with key press funcitonality 2026-04-20 14:10:18 +02:00
luptmoor
de9724b820 extension.md extended 2026-04-19 16:55:54 +02:00
556c5ea4b9 bugfixe sweet alert showed behind login modal 2026-04-19 16:55:19 +02:00
1dfffd93e5 added map boundaries based on municipality center 2026-04-19 16:49:31 +02:00
b3879d812f Merge branch 'dev/patrick' of https://git.endex-geodaten.de/lukas.uptmoor/webgis-lohne into dev/patrick 2026-04-19 16:43:37 +02:00
f0a88b13d1 categories searchable in contribution list 2026-04-19 16:42:25 +02:00
luptmoor
7aa0cad5fb basic instructions for scalability started in EXTENSION.md 2026-04-19 16:38:02 +02:00
e459a86edb geocoder can search outside map boundaries, prioritizes results inside map boundaries 2026-04-19 16:36:37 +02:00
adc2b71eb7 bugfix users can now only edit and delete own contributions 2026-04-19 16:28:09 +02:00
b6bedc788b bugfix opens create modal after login if geometry was drawn before 2026-04-19 16:23:44 +02:00
583bbcd27d bugfix opens create modal after login if geometry was drawn before 2026-04-19 16:21:05 +02:00
2a24f486b5 bugfix popups and tooltips of invisible layers can no loger be activated per mouse 2026-04-19 16:16:43 +02:00
d29f484993 added user name submit with enter key 2026-04-19 16:05:57 +02:00
3f72ef3bc4 commented app.js and added text blocks 2026-04-19 14:03:06 +02:00
a0cbe29f97 changed path to scripts in api folder 2026-04-19 13:51:37 +02:00
luptmoor
15705dac97 moved api folder to public 2026-04-19 13:45:13 +02:00
c8f4832a95 commented app.js 2026-04-19 13:44:08 +02:00
1714e33fa7 commented app.js 2026-04-19 13:43:38 +02:00
luptmoor
5e66e73db6 scripts added to gitignore 2026-04-19 13:09:21 +02:00
1337b0dca3 commented styles.css 2026-04-19 13:03:29 +02:00
765b74ceec commented index.php and modified text blocks 2026-04-19 13:00:47 +02:00
871e43aef5 added icon-municipality.png 2026-04-19 12:32:48 +02:00
bfc21d8fb6 modified connection settings in init.php 2026-04-19 12:32:08 +02:00
luptmoor
250ca9909d hardcoded port and hostname matching name of docker setup 2026-04-19 12:26:03 +02:00
c249c8e049 commented index.php 2026-04-19 12:21:48 +02:00
958f15a7a4 added company and muncipality logos 2026-04-18 20:53:38 +02:00
855b69f95d removed obsolete local plugins, now included with CDN 2026-04-17 20:52:09 +02:00
77df35926d addes app.js with map initialization, CRUD workflow, sidebar and modal logic 2026-04-17 20:45:03 +02:00
65ef7f07c9 rebuild styles.css with mobile-first layout and municipality theming 2026-04-17 20:35:37 +02:00
6eca88e941 rebuild index.php with header, map, sidebar, footer and modals 2026-04-17 20:33:56 +02:00
801131985d commented action handlers 2026-04-17 20:15:05 +02:00
4707e73421 commented READ action handler 2026-04-17 19:59:23 +02:00
241ec75323 added contributions API endpoint with CRUD and voting with prepared statements 2026-04-17 19:32:50 +02:00
d3297d2a3c added comments to db 2026-04-17 19:29:56 +02:00
c7e9444903 added comments to init 2026-04-17 18:37:33 +02:00
72315b4030 added database helper including JSON response and input validation utilities 2026-04-17 16:36:16 +02:00
403d81b132 added database helper including JSON response and input validation utilities 2026-04-17 16:24:39 +02:00
4f35ddeafe added .gitattributes to specify line feed line endings for .sh and .sql files 2026-04-17 15:49:21 +02:00
19b038d4f5 improve backup script preflight checks and file size validation 2026-04-17 15:42:02 +02:00
4554ea3ff0 added votes index and documented future migration tasks 2026-04-17 15:22:15 +02:00
0083a05482 added votes index and documented future migration tasks 2026-04-17 15:17:45 +02:00
041d1603dc shortened .env.example 2026-04-16 17:16:44 +02:00
b3a4ba6d4a added database backup script with daily, weekly and monthly rotation 2026-04-16 17:12:46 +02:00
04dc118598 added initial database schema migration 2026-04-16 16:44:24 +02:00
dec36d4053 fixed .env path in init.php 2026-04-16 16:14:43 +02:00
d2f2b577be added README.md 2026-04-16 16:12:57 +02:00
a640ed1b78 commented example .env 2026-04-16 16:08:34 +02:00
7c0c0b5048 added example env. 2026-04-16 16:07:38 +02:00
50035a524d created project structureapi /, public/, migrations/, scripts/, legacy/ 2026-04-16 16:00:35 +02:00
e8ce6c6f36 init adapted to server 2026-04-16 15:23:17 +02:00
97ab6a52ab commented init.php 2026-04-15 15:28:28 +02:00
b8f1c32a22 init.php connection to db with ssh 2026-04-15 14:56:44 +02:00
0aeee9a168 gitignore added 2026-04-15 14:42:07 +02:00
luptmoor
1f8e3935bb hostname as var 2026-04-15 16:23:18 +02:00
91 changed files with 3946 additions and 6461 deletions

7
.env.example Normal file
View File

@@ -0,0 +1,7 @@
# Example Environment Configfile
POSTGRES_HOSTNAME=postgres_host
POSTGRES_PORT=postgres_port
POSTGRES_DB=postgres_database
POSTGRES_USER=postgres_user
POSTGRES_PASSWORD=
ADMIN_PASSWORD=

8
.gitattributes vendored Normal file
View File

@@ -0,0 +1,8 @@
# Specifies Line Feed (LF) Line Endings for Shell Scripts
*.sh text eol=lf
# # Specifies Line Feed (LF) Line Endings for SQL Files
*.sql text eol=lf
# Letd Git decide for other Files
* text=auto

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.env
.vscode/
*.log
scripts

78
EXTENSION.md Normal file
View File

@@ -0,0 +1,78 @@
## Neue Ideenkarte anlegen
1. DNS record ```<name>``` A 195.59.32.237 600s
2. Nginx Weiterleitung in ```default.conf```:
```
server {
listen 443 ssl;
server_name <name>.endex-geodaten.de;
ssl_certificate /etc/letsencrypt/live/endex-geodaten.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/endex-geodaten.de/privkey.pem;
root /var/www/webgis-<name>/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass webgis-<name>-php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
```
3. Docker container für UI
```
webgis-<name>-php:
build: php-docker/
container_name: webgis-<name>-php
volumes:
- ./webgis-<name>:/var/www/webgis-<name>
networks:
- frontend
- webgis-<name>-nw
```
und Datenbank anlegen.
```
webgis-<name>db:
image: postgis/postgis:15-3.3
container_name: webgis-<name>-db
restart: always
ports:
- "127.0.0.1:543<ID>:5432" # inside the container always 5432
environment:
- POSTGRES_USER=${WEBGIS_DB_USER} # maybe go back to default username
- POSTGRES_PASSWORD=${WEBGIS_DB_PW} # must be secure and unique
- POSTGRES_DB=${WEBGIS_DB_NAME} #same as container name
volumes:
- ./webgis-<name>-data:/var/lib/postgresql/data
networks:
- webgis-<name>-nw
```
4. nginx Volume für neue Stadt in ```docker-compose.yml``` anlegen
```
./webgis-<name>:/var/www/webgis-<name>
```
5. Frontend source code nach ```webgis-<name>``` klonen
```
git submodule add -b <branch-name> https://git.endex-geodaten.de/lukas.uptmoor/webgis-<name>.git
```
Jede Kommune sollte ein eigenes Repo kriegen, da Features am Anfang variieren.
6. Mit der Datenbank verbinden über SSH-Tunnel
```
ssh -L 5433:localhost:543<ID> root@endex-geodaten.de
```
und Datenbank für Anwendung vorbereiten.

1631
index.php

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
<?php
$host = 'webgis-db'; // Matches the service name in docker-compose
$db = getenv('POSTGRES_DB');
$user = getenv('POSTGRES_USER');
$pass = getenv('POSTGRES_PASSWORD');
ob_start();
session_start();
try {
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
];
$dsn = "pgsql:host=localhost;dbname=$db;port=5432";
$pdo = new PDO($dsn, $user, $pass, $opt);
// Error-Message
} catch(PDOException $e) {
echo "Error: ".$e->getMessage();
}
?>

View File

@@ -0,0 +1,169 @@
-- =====================================================================
-- WebGIS Citizen Participation Portal — Initial Schema
-- Migration: 001_initial_schema.sql
-- Description: Creates Core Tables for a multi-tenant Citizen
-- Participation Platform with Point/Line/Polygon
-- Contributions, Voting, and Moderation Workflow.
-- =====================================================================
-- ---------------------------------------------------------------------
-- Block 1: Checks PostGIS Extension
-- ---------------------------------------------------------------------
CREATE EXTENSION IF NOT EXISTS postgis;
-- ---------------------------------------------------------------------
-- Block 2: Creates Table "municipalities"
-- One Row per Municipalitiy using the Portal (multi-tenant setup).
-- ---------------------------------------------------------------------
CREATE TABLE municipalities (
municipality_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE, -- Municipalitiy Name
slug VARCHAR(50) NOT NULL UNIQUE, -- URL-safe Identifier, e.g. "lohne"
center_lat DOUBLE PRECISION NOT NULL, -- Map Center Latitude
center_lng DOUBLE PRECISION NOT NULL, -- Map Center Longitude
default_zoom SMALLINT NOT NULL DEFAULT 13, -- Map Default Zoom Level
logo_path VARCHAR(255), -- Relative Path to Municipality Logo
primary_color VARCHAR(7) DEFAULT '#6a6a6a', -- HexColor for UI Theme
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE municipalities IS 'Configuration Per Municipality (Tenant) using the Citizen Participation Portal.';
-- ---------------------------------------------------------------------
-- Block 3: Table "contributions"
-- Aitizen and Administration Contributions as Points, Lines, and
-- Polygons stored together in one mixed-geometry Column.
-- ---------------------------------------------------------------------
CREATE TABLE contributions (
contribution_id SERIAL PRIMARY KEY,
municipality_id INTEGER NOT NULL REFERENCES municipalities(municipality_id) ON DELETE CASCADE,
geom GEOMETRY(Geometry, 4326) NOT NULL, -- Mixed Geometry: Point, Line, Polygon, ... (WGS84)
geom_type VARCHAR(20) NOT NULL, -- 'point' | 'line' | 'polygon'
category VARCHAR(50) NOT NULL, -- Contribution Category
title VARCHAR(200) NOT NULL,
description TEXT,
author_name VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
likes_count INTEGER NOT NULL DEFAULT 0,
dislikes_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT contributions_geom_type_check
CHECK (geom_type IN ('point', 'line', 'polygon')),
CONSTRAINT contributions_status_check
CHECK (status IN ('pending', 'approved', 'rejected', 'in_progress', 'done'))
);
COMMENT ON TABLE contributions IS 'Citizen and Administration Contributions with mixed Geometry Types.';
-- ---------------------------------------------------------------------
-- Block 4: Indexes for fast Queries
-- ---------------------------------------------------------------------
CREATE INDEX contributions_geom_idx ON contributions USING GIST (geom);
CREATE INDEX contributions_municipality_idx ON contributions (municipality_id);
CREATE INDEX contributions_status_idx ON contributions (status);
CREATE INDEX contributions_category_idx ON contributions (category);
-- ---------------------------------------------------------------------
-- Block 5: Table "votes"
-- Individual like and dislike Records. UNIQUE Constraint prevents the
-- same voter from liking or disliking the same contribution multiple times.
-- ---------------------------------------------------------------------
CREATE TABLE votes (
vote_id SERIAL PRIMARY KEY,
contribution_id INTEGER NOT NULL REFERENCES contributions(contribution_id) ON DELETE CASCADE,
voter_name VARCHAR(100) NOT NULL, -- ToDo: Replace with user_id once Authentification exists
vote_type VARCHAR(10) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT votes_unique_per_voter UNIQUE (contribution_id, voter_name),
CONSTRAINT votes_vote_type_check CHECK (vote_type IN ('like', 'dislike'))
);
COMMENT ON TABLE votes IS 'Individual Votes to prevent duplicate Likes and Dislikes.';
-- ---------------------------------------------------------------------
-- Block 6: Trigger Functions
-- ---------------------------------------------------------------------
-- Automatically Refresh updated_at on every UPDATE.
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER contributions_updated_at
BEFORE UPDATE ON contributions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TRIGGER municipalities_updated_at
BEFORE UPDATE ON municipalities
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- Keeps likes_count / dislikes_count synchronized with the votes Table.
CREATE OR REPLACE FUNCTION update_vote_counts()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
IF NEW.vote_type = 'like' THEN
UPDATE contributions SET likes_count = likes_count + 1
WHERE contribution_id = NEW.contribution_id;
ELSE
UPDATE contributions SET dislikes_count = dislikes_count + 1
WHERE contribution_id = NEW.contribution_id;
END IF;
ELSIF TG_OP = 'DELETE' THEN
IF OLD.vote_type = 'like' THEN
UPDATE contributions SET likes_count = GREATEST(likes_count - 1, 0)
WHERE contribution_id = OLD.contribution_id;
ELSE
UPDATE contributions SET dislikes_count = GREATEST(dislikes_count - 1, 0)
WHERE contribution_id = OLD.contribution_id;
END IF;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER votes_count_sync
AFTER INSERT OR DELETE ON votes
FOR EACH ROW EXECUTE FUNCTION update_vote_counts();
-- ---------------------------------------------------------------------
-- Block 7: Typed Geometry Views for QGIS
-- QGIS handles mixed-geometry Tables awkwardly, so one View per
-- Geometry Type is created. Reflects live Data from the Contributions Table.
-- ---------------------------------------------------------------------
CREATE VIEW contributions_points AS
SELECT * FROM contributions WHERE geom_type = 'point';
CREATE VIEW contributions_lines AS
SELECT * FROM contributions WHERE geom_type = 'line';
CREATE VIEW contributions_polygons AS
SELECT * FROM contributions WHERE geom_type = 'polygon';
-- ---------------------------------------------------------------------
-- Block 8: Seed Data — Initial Municipality
-- ---------------------------------------------------------------------
INSERT INTO municipalities (name, slug, center_lat, center_lng, default_zoom, primary_color)
VALUES ('Lohne (Oldenburg)', 'lohne', 52.66639, 8.23306, 14, '#00376D');
-- =====================================================================
-- End of migration 001_initial_schema.sql
-- =====================================================================

View File

@@ -0,0 +1,48 @@
-- =====================================================================
-- WebGIS Citizen Participation Portal
-- Migration: 002_add_votes_index.sql
-- Description: Adds missing Index on votes.contribution_id for fast
-- Vote Lookups per Contribution.
-- =====================================================================
-- ---------------------------------------------------------------------
-- Block 1: Index for fast Queries
-- The UNIQUE Constraint on contribution_id and voter_name creates a
-- composite Index, but Queries filtering only by contribution_id
-- cannot use it efficiently. This single-column Index covers that Case.
-- ---------------------------------------------------------------------
CREATE INDEX votes_contribution_idx ON votes (contribution_id);
-- =====================================================================
-- ToDo's for future Migrations
-- =====================================================================
--
-- 1. Categories Table
-- Create a "categories" Table with municipality_id, slug, label,
-- icon (FontAwesome), color, and sort_order. Replace the free-text
-- "category" Column in Contributions with a Foreign Key Reference.
-- This prevents Typos and inconsistent Category Names, and allows
-- each Municipality to define its own Set of Categories.
--
-- 2. Soft Delete
-- Add "deleted_at TIMESTAMPTZ DEFAULT NULL" to Contributions.
-- Instead of DELETE, set deleted_at = NOW(). Filter all Queries
-- with "WHERE deleted_at IS NULL". Allows Moderation Audit Trail
-- and accidental Deletion Recovery.
--
-- 3. Audit Log
-- Create an "audit_log" Table recording who changed what and when.
-- Columns: audit_id, table_name, record_id, action (insert/update/
-- delete), changed_by, old_values (JSONB), new_values (JSONB),
-- created_at. Populate via Triggers on Contributions and Votes.
--
-- 4. Geometry Validation
-- Add CHECK Constraint "ST_IsValid(geom)" on Contributions, or
-- validate in the API Layer before Insert. Prevents self-crossing
-- Polygons and other invalid Geometries.
--
-- =====================================================================
-- End of migration 002_add_votes_index.sql
-- =====================================================================

View File

@@ -1,575 +0,0 @@
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
'use strict';
var immediate = require('immediate');
/* istanbul ignore next */
function INTERNAL() {}
var handlers = {};
var REJECTED = ['REJECTED'];
var FULFILLED = ['FULFILLED'];
var PENDING = ['PENDING'];
module.exports = exports = Promise;
function Promise(resolver) {
if (typeof resolver !== 'function') {
throw new TypeError('resolver must be a function');
}
this.state = PENDING;
this.queue = [];
this.outcome = void 0;
if (resolver !== INTERNAL) {
safelyResolveThenable(this, resolver);
}
}
Promise.prototype["catch"] = function (onRejected) {
return this.then(null, onRejected);
};
Promise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled !== 'function' && this.state === FULFILLED ||
typeof onRejected !== 'function' && this.state === REJECTED) {
return this;
}
var promise = new this.constructor(INTERNAL);
if (this.state !== PENDING) {
var resolver = this.state === FULFILLED ? onFulfilled : onRejected;
unwrap(promise, resolver, this.outcome);
} else {
this.queue.push(new QueueItem(promise, onFulfilled, onRejected));
}
return promise;
};
function QueueItem(promise, onFulfilled, onRejected) {
this.promise = promise;
if (typeof onFulfilled === 'function') {
this.onFulfilled = onFulfilled;
this.callFulfilled = this.otherCallFulfilled;
}
if (typeof onRejected === 'function') {
this.onRejected = onRejected;
this.callRejected = this.otherCallRejected;
}
}
QueueItem.prototype.callFulfilled = function (value) {
handlers.resolve(this.promise, value);
};
QueueItem.prototype.otherCallFulfilled = function (value) {
unwrap(this.promise, this.onFulfilled, value);
};
QueueItem.prototype.callRejected = function (value) {
handlers.reject(this.promise, value);
};
QueueItem.prototype.otherCallRejected = function (value) {
unwrap(this.promise, this.onRejected, value);
};
function unwrap(promise, func, value) {
immediate(function () {
var returnValue;
try {
returnValue = func(value);
} catch (e) {
return handlers.reject(promise, e);
}
if (returnValue === promise) {
handlers.reject(promise, new TypeError('Cannot resolve promise with itself'));
} else {
handlers.resolve(promise, returnValue);
}
});
}
handlers.resolve = function (self, value) {
var result = tryCatch(getThen, value);
if (result.status === 'error') {
return handlers.reject(self, result.value);
}
var thenable = result.value;
if (thenable) {
safelyResolveThenable(self, thenable);
} else {
self.state = FULFILLED;
self.outcome = value;
var i = -1;
var len = self.queue.length;
while (++i < len) {
self.queue[i].callFulfilled(value);
}
}
return self;
};
handlers.reject = function (self, error) {
self.state = REJECTED;
self.outcome = error;
var i = -1;
var len = self.queue.length;
while (++i < len) {
self.queue[i].callRejected(error);
}
return self;
};
function getThen(obj) {
// Make sure we only access the accessor once as required by the spec
var then = obj && obj.then;
if (obj && typeof obj === 'object' && typeof then === 'function') {
return function appyThen() {
then.apply(obj, arguments);
};
}
}
function safelyResolveThenable(self, thenable) {
// Either fulfill, reject or reject with error
var called = false;
function onError(value) {
if (called) {
return;
}
called = true;
handlers.reject(self, value);
}
function onSuccess(value) {
if (called) {
return;
}
called = true;
handlers.resolve(self, value);
}
function tryToUnwrap() {
thenable(onSuccess, onError);
}
var result = tryCatch(tryToUnwrap);
if (result.status === 'error') {
onError(result.value);
}
}
function tryCatch(func, value) {
var out = {};
try {
out.value = func(value);
out.status = 'success';
} catch (e) {
out.status = 'error';
out.value = e;
}
return out;
}
exports.resolve = resolve;
function resolve(value) {
if (value instanceof this) {
return value;
}
return handlers.resolve(new this(INTERNAL), value);
}
exports.reject = reject;
function reject(reason) {
var promise = new this(INTERNAL);
return handlers.reject(promise, reason);
}
exports.all = all;
function all(iterable) {
var self = this;
if (Object.prototype.toString.call(iterable) !== '[object Array]') {
return this.reject(new TypeError('must be an array'));
}
var len = iterable.length;
var called = false;
if (!len) {
return this.resolve([]);
}
var values = new Array(len);
var resolved = 0;
var i = -1;
var promise = new this(INTERNAL);
while (++i < len) {
allResolver(iterable[i], i);
}
return promise;
function allResolver(value, i) {
self.resolve(value).then(resolveFromAll, function (error) {
if (!called) {
called = true;
handlers.reject(promise, error);
}
});
function resolveFromAll(outValue) {
values[i] = outValue;
if (++resolved === len && !called) {
called = true;
handlers.resolve(promise, values);
}
}
}
}
exports.race = race;
function race(iterable) {
var self = this;
if (Object.prototype.toString.call(iterable) !== '[object Array]') {
return this.reject(new TypeError('must be an array'));
}
var len = iterable.length;
var called = false;
if (!len) {
return this.resolve([]);
}
var i = -1;
var promise = new this(INTERNAL);
while (++i < len) {
resolver(iterable[i]);
}
return promise;
function resolver(value) {
self.resolve(value).then(function (response) {
if (!called) {
called = true;
handlers.resolve(promise, response);
}
}, function (error) {
if (!called) {
called = true;
handlers.reject(promise, error);
}
});
}
}
},{"immediate":2}],2:[function(require,module,exports){
(function (global){
'use strict';
var Mutation = global.MutationObserver || global.WebKitMutationObserver;
var scheduleDrain;
{
if (Mutation) {
var called = 0;
var observer = new Mutation(nextTick);
var element = global.document.createTextNode('');
observer.observe(element, {
characterData: true
});
scheduleDrain = function () {
element.data = (called = ++called % 2);
};
} else if (!global.setImmediate && typeof global.MessageChannel !== 'undefined') {
var channel = new global.MessageChannel();
channel.port1.onmessage = nextTick;
scheduleDrain = function () {
channel.port2.postMessage(0);
};
} else if ('document' in global && 'onreadystatechange' in global.document.createElement('script')) {
scheduleDrain = function () {
// Create a <script> element; its readystatechange event will be fired asynchronously once it is inserted
// into the document. Do so, thus queuing up the task. Remember to clean up once it's been called.
var scriptEl = global.document.createElement('script');
scriptEl.onreadystatechange = function () {
nextTick();
scriptEl.onreadystatechange = null;
scriptEl.parentNode.removeChild(scriptEl);
scriptEl = null;
};
global.document.documentElement.appendChild(scriptEl);
};
} else {
scheduleDrain = function () {
setTimeout(nextTick, 0);
};
}
}
var draining;
var queue = [];
//named nextTick for less confusing stack traces
function nextTick() {
draining = true;
var i, oldQueue;
var len = queue.length;
while (len) {
oldQueue = queue;
queue = [];
i = -1;
while (++i < len) {
oldQueue[i]();
}
len = queue.length;
}
draining = false;
}
module.exports = immediate;
function immediate(task) {
if (queue.push(task) === 1 && !draining) {
scheduleDrain();
}
}
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],3:[function(require,module,exports){
(function (global){
'use strict';
var jsonp = require('./jsonp');
var Promise = require('lie');
module.exports = function (url, options) {
options = options || {};
if (options.jsonp) {
return jsonp(url, options);
}
var request;
var cancel;
var out = new Promise(function (resolve, reject) {
cancel = reject;
if (global.XMLHttpRequest === undefined) {
reject('XMLHttpRequest is not supported');
}
var response;
request = new global.XMLHttpRequest();
request.open('GET', url);
if (options.headers) {
Object.keys(options.headers).forEach(function (key) {
request.setRequestHeader(key, options.headers[key]);
});
}
request.onreadystatechange = function () {
if (request.readyState === 4) {
if ((request.status < 400 && options.local) || request.status === 200) {
if (global.JSON) {
response = JSON.parse(request.responseText);
} else {
reject(new Error('JSON is not supported'));
}
resolve(response);
} else {
if (!request.status) {
reject('Attempted cross origin request without CORS enabled');
} else {
reject(request.statusText);
}
}
}
};
request.send();
});
out.catch(function (reason) {
request.abort();
return reason;
});
out.abort = cancel;
return out;
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./jsonp":5,"lie":1}],4:[function(require,module,exports){
(function (global){
'use strict';
var L = global.L || require('leaflet');
var Promise = require('lie');
var ajax = require('./ajax');
L.GeoJSON.AJAX = L.GeoJSON.extend({
defaultAJAXparams: {
dataType: 'json',
callbackParam: 'callback',
local: false,
middleware: function (f) {
return f;
}
},
initialize: function (url, options) {
this.urls = [];
if (url) {
if (typeof url === 'string') {
this.urls.push(url);
} else if (typeof url.pop === 'function') {
this.urls = this.urls.concat(url);
} else {
options = url;
url = undefined;
}
}
var ajaxParams = L.Util.extend({}, this.defaultAJAXparams);
for (var i in options) {
if (this.defaultAJAXparams.hasOwnProperty(i)) {
ajaxParams[i] = options[i];
}
}
this.ajaxParams = ajaxParams;
this._layers = {};
L.Util.setOptions(this, options);
this.on('data:loaded', function () {
if (this.filter) {
this.refilter(this.filter);
}
}, this);
var self = this;
if (this.urls.length > 0) {
new Promise(function (resolve) {
resolve();
}).then(function () {
self.addUrl();
});
}
},
clearLayers: function () {
this.urls = [];
L.GeoJSON.prototype.clearLayers.call(this);
return this;
},
addUrl: function (url) {
var self = this;
if (url) {
if (typeof url === 'string') {
self.urls.push(url);
} else if (typeof url.pop === 'function') {
self.urls = self.urls.concat(url);
}
}
var loading = self.urls.length;
var done = 0;
self.fire('data:loading');
self.urls.forEach(function (url) {
if (self.ajaxParams.dataType.toLowerCase() === 'json') {
ajax(url, self.ajaxParams).then(function (d) {
var data = self.ajaxParams.middleware(d);
self.addData(data);
self.fire('data:progress', data);
}, function (err) {
self.fire('data:progress', {
error: err
});
});
} else if (self.ajaxParams.dataType.toLowerCase() === 'jsonp') {
L.Util.jsonp(url, self.ajaxParams).then(function (d) {
var data = self.ajaxParams.middleware(d);
self.addData(data);
self.fire('data:progress', data);
}, function (err) {
self.fire('data:progress', {
error: err
});
});
}
});
self.on('data:progress', function () {
if (++done === loading) {
self.fire('data:loaded');
}
});
},
refresh: function (url) {
url = url || this.urls;
this.clearLayers();
this.addUrl(url);
},
refilter: function (func) {
if (typeof func !== 'function') {
this.filter = false;
this.eachLayer(function (a) {
a.setStyle({
stroke: true,
clickable: true
});
});
} else {
this.filter = func;
this.eachLayer(function (a) {
if (func(a.feature)) {
a.setStyle({
stroke: true,
clickable: true
});
} else {
a.setStyle({
stroke: false,
clickable: false
});
}
});
}
}
});
L.Util.Promise = Promise;
L.Util.ajax = ajax;
L.Util.jsonp = require('./jsonp');
L.geoJson.ajax = function (geojson, options) {
return new L.GeoJSON.AJAX(geojson, options);
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./ajax":3,"./jsonp":5,"leaflet":undefined,"lie":1}],5:[function(require,module,exports){
(function (global){
'use strict';
var L = global.L || require('leaflet');
var Promise = require('lie');
module.exports = function (url, options) {
options = options || {};
var head = document.getElementsByTagName('head')[0];
var scriptNode = L.DomUtil.create('script', '', head);
var cbName, ourl, cbSuffix, cancel;
var out = new Promise(function (resolve, reject) {
cancel = reject;
var cbParam = options.cbParam || 'callback';
if (options.callbackName) {
cbName = options.callbackName;
} else {
cbSuffix = '_' + ('' + Math.random()).slice(2);
cbName = '_leafletJSONPcallbacks.' + cbSuffix;
}
scriptNode.type = 'text/javascript';
if (cbSuffix) {
if (!global._leafletJSONPcallbacks) {
global._leafletJSONPcallbacks = {
length: 0
};
}
global._leafletJSONPcallbacks.length++;
global._leafletJSONPcallbacks[cbSuffix] = function (data) {
head.removeChild(scriptNode);
delete global._leafletJSONPcallbacks[cbSuffix];
global._leafletJSONPcallbacks.length--;
if (!global._leafletJSONPcallbacks.length) {
delete global._leafletJSONPcallbacks;
}
resolve(data);
};
}
if (url.indexOf('?') === -1) {
ourl = url + '?' + cbParam + '=' + cbName;
} else {
ourl = url + '&' + cbParam + '=' + cbName;
}
scriptNode.src = ourl;
}).then(null, function (reason) {
head.removeChild(scriptNode);
delete L.Util.ajax.cb[cbSuffix];
return reason;
});
out.abort = cancel;
return out;
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"leaflet":undefined,"lie":1}]},{},[4]);

View File

@@ -1 +0,0 @@
.leaflet-control-minimap{border:rgba(255,255,255,1) solid;box-shadow:0 1px 5px rgba(0,0,0,.65);border-radius:3px;background:#f8f8f9;transition:all .6s}.leaflet-control-minimap a{background-color:rgba(255,255,255,1);background-repeat:no-repeat;z-index:99999;transition:all .6s}.leaflet-control-minimap a.minimized-bottomright{-webkit-transform:rotate(180deg);transform:rotate(180deg);border-radius:0}.leaflet-control-minimap a.minimized-topleft{-webkit-transform:rotate(0deg);transform:rotate(0deg);border-radius:0}.leaflet-control-minimap a.minimized-bottomleft{-webkit-transform:rotate(270deg);transform:rotate(270deg);border-radius:0}.leaflet-control-minimap a.minimized-topright{-webkit-transform:rotate(90deg);transform:rotate(90deg);border-radius:0}.leaflet-control-minimap-toggle-display{background-image:url(images/toggle.svg);background-size:cover;position:absolute;border-radius:3px 0 0}.leaflet-oldie .leaflet-control-minimap-toggle-display{background-image:url(images/toggle.png)}.leaflet-control-minimap-toggle-display-bottomright{bottom:0;right:0}.leaflet-control-minimap-toggle-display-topleft{top:0;left:0;-webkit-transform:rotate(180deg);transform:rotate(180deg)}.leaflet-control-minimap-toggle-display-bottomleft{bottom:0;left:0;-webkit-transform:rotate(90deg);transform:rotate(90deg)}.leaflet-control-minimap-toggle-display-topright{top:0;right:0;-webkit-transform:rotate(270deg);transform:rotate(270deg)}.leaflet-oldie .leaflet-control-minimap{border:1px solid #999}.leaflet-oldie .leaflet-control-minimap a{background-color:#fff}.leaflet-oldie .leaflet-control-minimap a.minimized{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2)}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="18" width="18"><defs><marker orient="auto" overflow="visible"><path d="M-2.6-2.828L-5.428 0-2.6 2.828.228 0-2.6-2.828z" fill-rule="evenodd" stroke="#000" stroke-width=".4pt"/></marker><marker orient="auto" overflow="visible"><g fill="none" stroke="#000" stroke-width=".8" stroke-linecap="round"><path d="M4.566 4.75L-.652 0"/><path d="M1.544 4.75L-3.674 0"/><path d="M-1.566 4.75L-6.784 0"/><path d="M4.566-5.013L-.652-.263"/><path d="M1.544-5.013l-5.218 4.75"/><path d="M-1.566-5.013l-5.218 4.75"/></g></marker><marker orient="auto" overflow="visible"><path d="M-5.6-5.657L-11.257 0-5.6 5.657.057 0-5.6-5.657z" fill-rule="evenodd" stroke="#000" stroke-width=".8pt"/></marker><marker orient="auto" overflow="visible"><path d="M4.616 0l-6.92 4v-8l6.92 4z" fill-rule="evenodd" stroke="#000" stroke-width=".8pt"/></marker><marker orient="auto" overflow="visible"><path d="M-10.69-4.437L1.328-.017l-12.018 4.42c1.92-2.61 1.91-6.18 0-8.84z" font-size="12" fill-rule="evenodd" stroke-width=".6875" stroke-linejoin="round"/></marker><marker orient="auto" overflow="visible"><path d="M-4.616 0l6.92-4v8l-6.92-4z" fill-rule="evenodd" stroke="#000" stroke-width=".8pt"/></marker><marker orient="auto" overflow="visible"><path d="M10 0l4-4L0 0l14 4-4-4z" fill-rule="evenodd" stroke="#000" stroke-width=".8pt"/></marker><marker orient="auto" overflow="visible"><path d="M10.69 4.437L-1.328.017l12.018-4.42c-1.92 2.61-1.91 6.18 0 8.84z" font-size="12" fill-rule="evenodd" stroke-width=".6875" stroke-linejoin="round"/></marker></defs><path d="M13.18 13.146v-5.81l-5.81 5.81h5.81z" stroke="#000" stroke-width="1.643"/><path d="M12.762 12.727l-6.51-6.51" fill="none" stroke="#000" stroke-width="2.482" stroke-linecap="round"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,9 +0,0 @@
.leaflet-container .leaflet-control-mouseposition {
background-color: rgba(255, 255, 255, 0.7);
box-shadow: 0 0 5px #bbb;
padding: 0 5px;
margin:0;
color: #333;
font: 11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}

View File

@@ -1,48 +0,0 @@
L.Control.MousePosition = L.Control.extend({
options: {
position: 'bottomleft',
separator: ' : ',
emptyString: 'Unavailable',
lngFirst: false,
numDigits: 5,
lngFormatter: undefined,
latFormatter: undefined,
prefix: ""
},
onAdd: function (map) {
this._container = L.DomUtil.create('div', 'leaflet-control-mouseposition');
L.DomEvent.disableClickPropagation(this._container);
map.on('mousemove', this._onMouseMove, this);
this._container.innerHTML=this.options.emptyString;
return this._container;
},
onRemove: function (map) {
map.off('mousemove', this._onMouseMove)
},
_onMouseMove: function (e) {
var lng = this.options.lngFormatter ? this.options.lngFormatter(e.latlng.lng) : L.Util.formatNum(e.latlng.lng, this.options.numDigits);
var lat = this.options.latFormatter ? this.options.latFormatter(e.latlng.lat) : L.Util.formatNum(e.latlng.lat, this.options.numDigits);
var value = this.options.lngFirst ? lng + this.options.separator + lat : lat + this.options.separator + lng;
var prefixAndValue = this.options.prefix + ' ' + value;
this._container.innerHTML = prefixAndValue;
}
});
L.Map.mergeOptions({
positionControl: false
});
L.Map.addInitHook(function () {
if (this.options.positionControl) {
this.positionControl = new L.Control.MousePosition();
this.addControl(this.positionControl);
}
});
L.control.mousePosition = function (options) {
return new L.Control.MousePosition(options);
};

View File

@@ -1 +0,0 @@
*.min.js

View File

@@ -1,19 +0,0 @@
module.exports = {
extends: 'eslint:recommended',
parserOptions: {
ecmaVersion: 2018,
},
env: {
browser: true,
},
rules: {
'no-unused-vars': ['error', { args: 'none' }]
},
overrides: [{
files: ['gulpfile.js'],
env: {
browser: false,
node: true,
}
}]
};

View File

@@ -1,2 +0,0 @@
github: Turbo87
custom: https://paypal.me/tobiasbieniek

View File

@@ -1,21 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
time: "04:00"
open-pull-requests-limit: 10
versioning-strategy: increase
ignore:
- dependency-name: eslint
versions:
- 7.18.0
- 7.19.0
- 7.20.0
- 7.21.0
- 7.22.0
- 7.23.0
- 7.24.0
commit-message:
prefix: ""

View File

@@ -1,20 +0,0 @@
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
pids
logs
results
npm-debug.log
node_modules
dist
build
.eslintcache

View File

@@ -1,11 +0,0 @@
module.exports = {
plugins: [
'stylelint-scss',
],
extends: [
'stylelint-config-recommended-scss',
],
rules: {
'no-descending-specificity': null,
},
};

View File

@@ -1,24 +0,0 @@
language: node_js
node_js: stable
sudo: false
cache:
yarn: true
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH=$HOME/.yarn/bin:$PATH
install:
- yarn install
script:
- yarn lint:css
- yarn lint:js
# build the processed/minified assets
- yarn gulp
# check that they results match the committed state
- git diff --exit-code

View File

@@ -1,44 +0,0 @@
# Changelog
## v0.4.0 (2017-10-25)
- ol3 version without jquery dependency ([#97](https://github.com/Turbo87/sidebar-v2/pull/97))
- Allow non-tab links in the sidebar
- add type definition for leaflet-sidebar ([#112](https://github.com/Turbo87/sidebar-v2/pull/112))
- Remove reference to L.Mixin.Evented ([#124](https://github.com/Turbo87/sidebar-v2/pull/124))
- Fix Chrome 62 list-style-type bug ([#127](https://github.com/Turbo87/sidebar-v2/pull/127))
## v0.3.1 (2016-11-01)
- fix `ol3` example map layer ([#77](https://github.com/Turbo87/sidebar-v2/pull/77))
- leaflet: deprecate `removeFrom()` in favor of `remove()` ([#73](https://github.com/Turbo87/sidebar-v2/pull/73))
- leaflet: allow non-tab links on the sidebar ([#87](https://github.com/Turbo87/sidebar-v2/pull/87))
- leaflet: fix CDN location on example pages ([#94](https://github.com/Turbo87/sidebar-v2/pull/94))
- ol3: move "scale-line" together with the zoom controls ([#93](https://github.com/Turbo87/sidebar-v2/pull/93))
## v0.3.0 (2016-01-19)
- ol2: move scale line control too when sidebar opens/closes
- hide scrollbars when collapsed ([#21](https://github.com/Turbo87/sidebar-v2/issues/21))
- fix tab clicking on devices with touch screen *and* mouse ([#34](https://github.com/Turbo87/sidebar-v2/issues/35))
- new `.sidebar-header` CSS class for styled headings
- new `.sidebar-close` CSS class for close buttons in headings
- fix broken Google Maps code (until Google changes things again...)
- allow `.disabled` on `<li>` elements in `.sidebar-tabs` element
- allow second tabbar at the bottom
- new `position: 'right'` option
## v0.2.1 (2014-09-29)
- ol2, ol3: fixed sidebar content scrolling
## v0.2.0 (2014-09-29)
- jQuery API and events
## v0.1.0 (2014-09-12)
- first beta release

View File

@@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013 Tobias Bieniek
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,42 +0,0 @@
# sidebar-v2
A responsive sidebar for mapping libraries like [Leaflet](#leaflet) or [OpenLayers](#openlayers-3).
It is more or less a successor of the [leaflet-sidebar](https://github.com/turbo87/leaflet-sidebar/) plugin, thus the `v2` suffix.
<a href="https://flattr.com/submit/auto?user_id=turbo&url=https%3A%2F%2Fgithub.com%2FTurbo87%2Fsidebar-v2" target="_blank"><img src="https://api.flattr.com/button/flattr-badge-large.png" alt="Flattr this" title="Flattr this" border="0"></a>
![Demo](doc/sidebar-v2.gif)
## [Leaflet](http://leafletjs.com/)
![Sidebar collapsed](doc/leaflet-1.png) ![Sidebar extended](doc/leaflet-2.png)
Example code at [`examples/index.html`](examples/index.html) ([Preview](http://turbo87.github.io/sidebar-v2/examples/index.html))
## [OpenLayers 3](http://openlayers.org/)
![Sidebar collapsed](doc/ol3-1.png) ![Sidebar extended](doc/ol3-2.png)
Example code at [`examples/ol3.html`](examples/ol3.html) ([Preview](http://turbo87.github.io/sidebar-v2/examples/ol3.html))
## [OpenLayers 2](http://openlayers.org/two/)
![Sidebar collapsed](doc/ol2-1.png) ![Sidebar extended](doc/ol2-2.png)
Example code at [`examples/ol2.html`](examples/ol2.html) ([Preview](http://turbo87.github.io/sidebar-v2/examples/ol2.html))
## [Google Maps](https://developers.google.com/maps/)
![Sidebar collapsed](doc/gmaps-1.png) ![Sidebar extended](doc/gmaps-2.png)
Example code at [`examples/gmaps.html`](examples/gmaps.html) ([Preview](http://turbo87.github.io/sidebar-v2/examples/gmaps.html))
## License
sidebar-v2 is free software, and may be redistributed under the [MIT license](LICENSE).

View File

@@ -1,30 +0,0 @@
{
"name": "sidebar-v2",
"version": "0.3.1",
"homepage": "https://github.com/Turbo87/sidebar-v2",
"authors": [
"Tobias Bieniek <tobias.bieniek@qsc.de>"
],
"description": "A responsive sidebar for mapping libraries like Leaflet or OpenLayers",
"main": [
"css/gmaps-sidebar.css",
"css/leaflet-sidebar.css",
"css/ol2-sidebar.css",
"css/ol3-sidebar.css",
"js/jquery-sidebar.js",
"js/leaflet-sidebar.js",
"js/ol3-sidebar.js"
],
"keywords": [
"gis",
"leaflet",
"openlayers",
"map"
],
"license": "MIT",
"ignore": [
"doc",
"examples",
".gitignore"
]
}

View File

@@ -1,205 +0,0 @@
.sidebar {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
overflow: hidden;
z-index: 2000; }
.sidebar.collapsed {
width: 40px; }
@media (min-width: 768px) {
.sidebar {
top: 10px;
bottom: 10px;
transition: width 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar {
width: 305px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar {
width: 390px; } }
@media (min-width: 1200px) {
.sidebar {
width: 460px; } }
.sidebar-left {
left: 0; }
@media (min-width: 768px) {
.sidebar-left {
left: 10px; } }
.sidebar-right {
right: 0; }
@media (min-width: 768px) {
.sidebar-right {
right: 10px; } }
.sidebar-tabs {
top: 0;
bottom: 0;
height: 100%;
background-color: #fff; }
.sidebar-left .sidebar-tabs {
left: 0; }
.sidebar-right .sidebar-tabs {
right: 0; }
.sidebar-tabs, .sidebar-tabs > ul {
position: absolute;
width: 40px;
margin: 0;
padding: 0;
list-style-type: none; }
.sidebar-tabs > li, .sidebar-tabs > ul > li {
width: 100%;
height: 40px;
color: #666;
font-size: 12pt;
overflow: hidden;
transition: all 80ms; }
.sidebar-tabs > li:hover, .sidebar-tabs > ul > li:hover {
color: #000;
background-color: #fff9e6; }
.sidebar-tabs > li.active, .sidebar-tabs > ul > li.active {
color: #000;
background-color: #febf00; }
.sidebar-tabs > li.disabled, .sidebar-tabs > ul > li.disabled {
color: rgba(102, 102, 102, 0.4); }
.sidebar-tabs > li.disabled:hover, .sidebar-tabs > ul > li.disabled:hover {
background: transparent; }
.sidebar-tabs > li.disabled > a, .sidebar-tabs > ul > li.disabled > a {
cursor: default; }
.sidebar-tabs > li > a, .sidebar-tabs > ul > li > a {
display: block;
width: 100%;
height: 100%;
line-height: 40px;
color: inherit;
text-decoration: none;
text-align: center; }
.sidebar-tabs > ul + ul {
bottom: 0; }
.sidebar-content {
position: absolute;
top: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.95);
overflow-x: hidden;
overflow-y: auto; }
.sidebar-left .sidebar-content {
left: 40px;
right: 0; }
.sidebar-right .sidebar-content {
left: 0;
right: 40px; }
.sidebar.collapsed > .sidebar-content {
overflow-y: hidden; }
.sidebar-pane {
display: none;
left: 0;
right: 0;
box-sizing: border-box;
padding: 10px 20px; }
.sidebar-pane.active {
display: block; }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-pane {
min-width: 265px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-pane {
min-width: 350px; } }
@media (min-width: 1200px) {
.sidebar-pane {
min-width: 420px; } }
.sidebar-header {
margin: -10px -20px 0;
height: 40px;
padding: 0 20px;
line-height: 40px;
font-size: 14.4pt;
color: #000;
background-color: #febf00; }
.sidebar-right .sidebar-header {
padding-left: 40px; }
.sidebar-close {
position: absolute;
top: 0;
width: 40px;
height: 40px;
text-align: center;
cursor: pointer; }
.sidebar-left .sidebar-close {
right: 0; }
.sidebar-right .sidebar-close {
left: 0; }
.sidebar-left ~ .sidebar-map {
margin-left: 40px; }
@media (min-width: 768px) {
.sidebar-left ~ .sidebar-map {
margin-left: 0; } }
.sidebar-right ~ .sidebar-map {
margin-right: 40px; }
@media (min-width: 768px) {
.sidebar-right ~ .sidebar-map {
margin-right: 0; } }
.sidebar {
border-right: 0;
box-shadow: rgba(0, 0, 0, 0.298039) 0 1px 4px -1px; }
@media (min-width: 768px) {
.sidebar {
border: 0;
border-radius: 2px; } }
@media (min-width: 768px) {
.sidebar-left {
bottom: 35px; } }
@media (min-width: 768px) {
.sidebar-left ~ .sidebar-map .gm-style > div.gmnoprint[style*="left: 0px"] {
transition: margin-left 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-left ~ .sidebar-map .gm-style > div.gmnoprint[style*="left: 0px"] {
margin-left: 325px !important; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-left ~ .sidebar-map .gm-style > div.gmnoprint[style*="left: 0px"] {
margin-left: 410px !important; } }
@media (min-width: 1200px) {
.sidebar-left ~ .sidebar-map .gm-style > div.gmnoprint[style*="left: 0px"] {
margin-left: 480px !important; } }
@media (min-width: 768px) {
.sidebar-left.collapsed ~ .sidebar-map .gm-style > div.gmnoprint[style*="left: 0px"] {
margin-left: 60px !important; } }
@media (min-width: 768px) {
.sidebar-right {
bottom: 24px; } }
@media (min-width: 768px) {
.sidebar-right ~ .sidebar-map .gm-style > div.gmnoprint[style*="right: 28px"] {
transition: margin-right 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-right ~ .sidebar-map .gm-style > div.gmnoprint[style*="right: 28px"] {
margin-right: 325px !important; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-right ~ .sidebar-map .gm-style > div.gmnoprint[style*="right: 28px"] {
margin-right: 410px !important; } }
@media (min-width: 1200px) {
.sidebar-right ~ .sidebar-map .gm-style > div.gmnoprint[style*="right: 28px"] {
margin-right: 480px !important; } }
@media (min-width: 768px) {
.sidebar-right.collapsed ~ .sidebar-map .gm-style > div.gmnoprint[style*="right: 28px"] {
margin-right: 60px !important; } }

View File

@@ -1 +0,0 @@
.sidebar{position:absolute;top:0;bottom:0;width:100%;overflow:hidden;z-index:2000;border-right:0;box-shadow:rgba(0,0,0,.298039) 0 1px 4px -1px}.sidebar.collapsed{width:40px}@media (min-width:768px) and (max-width:991px){.sidebar{width:305px}.sidebar-pane{min-width:265px}}@media (min-width:992px) and (max-width:1199px){.sidebar{width:390px}}@media (min-width:1200px){.sidebar{width:460px}}.sidebar-left{left:0}.sidebar-right{right:0}@media (min-width:768px){.sidebar{top:10px;bottom:10px;transition:width .5s}.sidebar-left{left:10px}.sidebar-right{right:10px}}.sidebar-tabs{top:0;bottom:0;height:100%;background-color:#fff}.sidebar-left .sidebar-tabs{left:0}.sidebar-right .sidebar-tabs{right:0}.sidebar-tabs,.sidebar-tabs>ul{position:absolute;width:40px;margin:0;padding:0;list-style-type:none}.sidebar-tabs>li,.sidebar-tabs>ul>li{width:100%;height:40px;color:#666;font-size:12pt;overflow:hidden;transition:80ms}.sidebar-tabs>li:hover,.sidebar-tabs>ul>li:hover{color:#000;background-color:#fff9e6}.sidebar-tabs>li.active,.sidebar-tabs>ul>li.active{color:#000;background-color:#febf00}.sidebar-tabs>li.disabled,.sidebar-tabs>ul>li.disabled{color:rgba(102,102,102,.4)}.sidebar-tabs>li.disabled:hover,.sidebar-tabs>ul>li.disabled:hover{background:0 0}.sidebar-tabs>li.disabled>a,.sidebar-tabs>ul>li.disabled>a{cursor:default}.sidebar-tabs>li>a,.sidebar-tabs>ul>li>a{display:block;width:100%;height:100%;line-height:40px;color:inherit;text-decoration:none;text-align:center}.sidebar-tabs>ul+ul{bottom:0}.sidebar-content{position:absolute;top:0;bottom:0;background-color:rgba(255,255,255,.95);overflow-x:hidden;overflow-y:auto}.sidebar-left .sidebar-content{left:40px;right:0}.sidebar-right .sidebar-content{left:0;right:40px}.sidebar.collapsed>.sidebar-content{overflow-y:hidden}.sidebar-pane{display:none;left:0;right:0;box-sizing:border-box;padding:10px 20px}.sidebar-pane.active{display:block}.sidebar-header{margin:-10px -20px 0;height:40px;padding:0 20px;line-height:40px;font-size:14.4pt;color:#000;background-color:#febf00}.sidebar-right .sidebar-header{padding-left:40px}.sidebar-close{position:absolute;top:0;width:40px;height:40px;text-align:center;cursor:pointer}.sidebar-left .sidebar-close{right:0}.sidebar-right .sidebar-close{left:0}.sidebar-left~.sidebar-map{margin-left:40px}.sidebar-right~.sidebar-map{margin-right:40px}@media (min-width:768px) and (max-width:991px){.sidebar-left~.sidebar-map .gm-style>div.gmnoprint[style*="left: 0px"]{margin-left:325px!important}.sidebar-right~.sidebar-map .gm-style>div.gmnoprint[style*="right: 28px"]{margin-right:325px!important}}@media (min-width:992px) and (max-width:1199px){.sidebar-pane{min-width:350px}.sidebar-left~.sidebar-map .gm-style>div.gmnoprint[style*="left: 0px"]{margin-left:410px!important}.sidebar-right~.sidebar-map .gm-style>div.gmnoprint[style*="right: 28px"]{margin-right:410px!important}}@media (min-width:1200px){.sidebar-pane{min-width:420px}.sidebar-left~.sidebar-map .gm-style>div.gmnoprint[style*="left: 0px"]{margin-left:480px!important}.sidebar-right~.sidebar-map .gm-style>div.gmnoprint[style*="right: 28px"]{margin-right:480px!important}}@media (min-width:768px){.sidebar-left~.sidebar-map{margin-left:0}.sidebar-right~.sidebar-map{margin-right:0}.sidebar{border:0;border-radius:2px}.sidebar-left{bottom:35px}.sidebar-left~.sidebar-map .gm-style>div.gmnoprint[style*="left: 0px"]{transition:margin-left .5s}.sidebar-left.collapsed~.sidebar-map .gm-style>div.gmnoprint[style*="left: 0px"]{margin-left:60px!important}.sidebar-right{bottom:24px}.sidebar-right~.sidebar-map .gm-style>div.gmnoprint[style*="right: 28px"]{transition:margin-right .5s}.sidebar-right.collapsed~.sidebar-map .gm-style>div.gmnoprint[style*="right: 28px"]{margin-right:60px!important}}

View File

@@ -1,200 +0,0 @@
.sidebar {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
overflow: hidden;
z-index: 2000; }
.sidebar.collapsed {
width: 40px; }
@media (min-width: 768px) {
.sidebar {
top: 10px;
bottom: 10px;
transition: width 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar {
width: 305px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar {
width: 390px; } }
@media (min-width: 1200px) {
.sidebar {
width: 460px; } }
.sidebar-left {
left: 0; }
@media (min-width: 768px) {
.sidebar-left {
left: 10px; } }
.sidebar-right {
right: 0; }
@media (min-width: 768px) {
.sidebar-right {
right: 10px; } }
.sidebar-tabs {
top: 0;
bottom: 0;
height: 100%;
background-color: #fff; }
.sidebar-left .sidebar-tabs {
left: 0; }
.sidebar-right .sidebar-tabs {
right: 0; }
.sidebar-tabs, .sidebar-tabs > ul {
position: absolute;
width: 40px;
margin: 0;
padding: 0;
list-style-type: none; }
.sidebar-tabs > li, .sidebar-tabs > ul > li {
width: 100%;
height: 40px;
color: #333;
font-size: 12pt;
overflow: hidden;
transition: all 80ms; }
.sidebar-tabs > li:hover, .sidebar-tabs > ul > li:hover {
color: #000;
background-color: #eee; }
.sidebar-tabs > li.active, .sidebar-tabs > ul > li.active {
color: #fff;
background-color: #0074d9; }
.sidebar-tabs > li.disabled, .sidebar-tabs > ul > li.disabled {
color: rgba(51, 51, 51, 0.4); }
.sidebar-tabs > li.disabled:hover, .sidebar-tabs > ul > li.disabled:hover {
background: transparent; }
.sidebar-tabs > li.disabled > a, .sidebar-tabs > ul > li.disabled > a {
cursor: default; }
.sidebar-tabs > li > a, .sidebar-tabs > ul > li > a {
display: block;
width: 100%;
height: 100%;
line-height: 40px;
color: inherit;
text-decoration: none;
text-align: center; }
.sidebar-tabs > ul + ul {
bottom: 0; }
.sidebar-content {
position: absolute;
top: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.95);
overflow-x: hidden;
overflow-y: auto; }
.sidebar-left .sidebar-content {
left: 40px;
right: 0; }
.sidebar-right .sidebar-content {
left: 0;
right: 40px; }
.sidebar.collapsed > .sidebar-content {
overflow-y: hidden; }
.sidebar-pane {
display: none;
left: 0;
right: 0;
box-sizing: border-box;
padding: 10px 20px; }
.sidebar-pane.active {
display: block; }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-pane {
min-width: 265px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-pane {
min-width: 350px; } }
@media (min-width: 1200px) {
.sidebar-pane {
min-width: 420px; } }
.sidebar-header {
margin: -10px -20px 0;
height: 40px;
padding: 0 20px;
line-height: 40px;
font-size: 14.4pt;
color: #fff;
background-color: #0074d9; }
.sidebar-right .sidebar-header {
padding-left: 40px; }
.sidebar-close {
position: absolute;
top: 0;
width: 40px;
height: 40px;
text-align: center;
cursor: pointer; }
.sidebar-left .sidebar-close {
right: 0; }
.sidebar-right .sidebar-close {
left: 0; }
.sidebar-left ~ .sidebar-map {
margin-left: 40px; }
@media (min-width: 768px) {
.sidebar-left ~ .sidebar-map {
margin-left: 0; } }
.sidebar-right ~ .sidebar-map {
margin-right: 40px; }
@media (min-width: 768px) {
.sidebar-right ~ .sidebar-map {
margin-right: 0; } }
.sidebar {
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); }
.sidebar.leaflet-touch {
box-shadow: none;
border-right: 2px solid rgba(0, 0, 0, 0.2); }
@media (min-width: 768px) {
.sidebar {
border-radius: 4px; }
.sidebar.leaflet-touch {
border: 2px solid rgba(0, 0, 0, 0.2); } }
@media (min-width: 768px) {
.sidebar-left ~ .sidebar-map .leaflet-left {
transition: left 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-left ~ .sidebar-map .leaflet-left {
left: 315px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-left ~ .sidebar-map .leaflet-left {
left: 400px; } }
@media (min-width: 1200px) {
.sidebar-left ~ .sidebar-map .leaflet-left {
left: 470px; } }
@media (min-width: 768px) {
.sidebar-left.collapsed ~ .sidebar-map .leaflet-left {
left: 50px; } }
@media (min-width: 768px) {
.sidebar-right ~ .sidebar-map .leaflet-right {
transition: right 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-right ~ .sidebar-map .leaflet-right {
right: 315px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-right ~ .sidebar-map .leaflet-right {
right: 400px; } }
@media (min-width: 1200px) {
.sidebar-right ~ .sidebar-map .leaflet-right {
right: 470px; } }
@media (min-width: 768px) {
.sidebar-right.collapsed ~ .sidebar-map .leaflet-right {
right: 50px; } }

View File

@@ -1 +0,0 @@
.sidebar{position:absolute;top:0;bottom:0;width:100%;overflow:hidden;z-index:2000;box-shadow:0 1px 5px rgba(0,0,0,.65)}.sidebar.collapsed{width:40px}@media (min-width:768px) and (max-width:991px){.sidebar{width:305px}.sidebar-pane{min-width:265px}}@media (min-width:992px) and (max-width:1199px){.sidebar{width:390px}}@media (min-width:1200px){.sidebar{width:460px}}.sidebar-left{left:0}.sidebar-right{right:0}@media (min-width:768px){.sidebar{top:10px;bottom:10px;transition:width .5s}.sidebar-left{left:10px}.sidebar-right{right:10px}}.sidebar-tabs{top:0;bottom:0;height:100%;background-color:#fff}.sidebar-left .sidebar-tabs{left:0}.sidebar-right .sidebar-tabs{right:0}.sidebar-tabs,.sidebar-tabs>ul{position:absolute;width:40px;margin:0;padding:0;list-style-type:none}.sidebar-tabs>li,.sidebar-tabs>ul>li{width:100%;height:40px;color:#333;font-size:12pt;overflow:hidden;transition:80ms}.sidebar-tabs>li:hover,.sidebar-tabs>ul>li:hover{color:#000;background-color:#eee}.sidebar-tabs>li.active,.sidebar-tabs>ul>li.active{color:#fff;background-color:#0074d9}.sidebar-tabs>li.disabled,.sidebar-tabs>ul>li.disabled{color:rgba(51,51,51,.4)}.sidebar-tabs>li.disabled:hover,.sidebar-tabs>ul>li.disabled:hover{background:0 0}.sidebar-tabs>li.disabled>a,.sidebar-tabs>ul>li.disabled>a{cursor:default}.sidebar-tabs>li>a,.sidebar-tabs>ul>li>a{display:block;width:100%;height:100%;line-height:40px;color:inherit;text-decoration:none;text-align:center}.sidebar-tabs>ul+ul{bottom:0}.sidebar-content{position:absolute;top:0;bottom:0;background-color:rgba(255,255,255,.95);overflow-x:hidden;overflow-y:auto}.sidebar-left .sidebar-content{left:40px;right:0}.sidebar-right .sidebar-content{left:0;right:40px}.sidebar.collapsed>.sidebar-content{overflow-y:hidden}.sidebar-pane{display:none;left:0;right:0;box-sizing:border-box;padding:10px 20px}.sidebar-pane.active{display:block}.sidebar-header{margin:-10px -20px 0;height:40px;padding:0 20px;line-height:40px;font-size:14.4pt;color:#fff;background-color:#0074d9}.sidebar-right .sidebar-header{padding-left:40px}.sidebar-close{position:absolute;top:0;width:40px;height:40px;text-align:center;cursor:pointer}.sidebar-left .sidebar-close{right:0}.sidebar-right .sidebar-close{left:0}.sidebar-left~.sidebar-map{margin-left:40px}.sidebar-right~.sidebar-map{margin-right:40px}.sidebar.leaflet-touch{box-shadow:none;border-right:2px solid rgba(0,0,0,.2)}@media (min-width:768px) and (max-width:991px){.sidebar-left~.sidebar-map .leaflet-left{left:315px}.sidebar-right~.sidebar-map .leaflet-right{right:315px}}@media (min-width:992px) and (max-width:1199px){.sidebar-pane{min-width:350px}.sidebar-left~.sidebar-map .leaflet-left{left:400px}.sidebar-right~.sidebar-map .leaflet-right{right:400px}}@media (min-width:1200px){.sidebar-pane{min-width:420px}.sidebar-left~.sidebar-map .leaflet-left{left:470px}.sidebar-right~.sidebar-map .leaflet-right{right:470px}}@media (min-width:768px){.sidebar-left~.sidebar-map{margin-left:0}.sidebar-right~.sidebar-map{margin-right:0}.sidebar{border-radius:4px}.sidebar.leaflet-touch{border:2px solid rgba(0,0,0,.2)}.sidebar-left~.sidebar-map .leaflet-left{transition:left .5s}.sidebar-left.collapsed~.sidebar-map .leaflet-left{left:50px}.sidebar-right~.sidebar-map .leaflet-right{transition:right .5s}.sidebar-right.collapsed~.sidebar-map .leaflet-right{right:50px}}

View File

@@ -1,218 +0,0 @@
.sidebar {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
overflow: hidden;
z-index: 2000; }
.sidebar.collapsed {
width: 40px; }
@media (min-width: 768px) {
.sidebar {
top: 8px;
bottom: 8px;
transition: width 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar {
width: 305px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar {
width: 390px; } }
@media (min-width: 1200px) {
.sidebar {
width: 460px; } }
.sidebar-left {
left: 0; }
@media (min-width: 768px) {
.sidebar-left {
left: 8px; } }
.sidebar-right {
right: 0; }
@media (min-width: 768px) {
.sidebar-right {
right: 8px; } }
.sidebar-tabs {
top: 0;
bottom: 0;
height: 100%;
background-color: rgba(0, 60, 136, 0.5); }
.sidebar-left .sidebar-tabs {
left: 0; }
.sidebar-right .sidebar-tabs {
right: 0; }
.sidebar-tabs, .sidebar-tabs > ul {
position: absolute;
width: 40px;
margin: 0;
padding: 0;
list-style-type: none; }
.sidebar-tabs > li, .sidebar-tabs > ul > li {
width: 100%;
height: 40px;
color: #fff;
font-size: 12pt;
overflow: hidden;
transition: all 80ms; }
.sidebar-tabs > li:hover, .sidebar-tabs > ul > li:hover {
color: #fff;
background-color: rgba(0, 60, 136, 0.6); }
.sidebar-tabs > li.active, .sidebar-tabs > ul > li.active {
color: #fff;
background-color: #0074d9; }
.sidebar-tabs > li.disabled, .sidebar-tabs > ul > li.disabled {
color: rgba(255, 255, 255, 0.4); }
.sidebar-tabs > li.disabled:hover, .sidebar-tabs > ul > li.disabled:hover {
background: transparent; }
.sidebar-tabs > li.disabled > a, .sidebar-tabs > ul > li.disabled > a {
cursor: default; }
.sidebar-tabs > li > a, .sidebar-tabs > ul > li > a {
display: block;
width: 100%;
height: 100%;
line-height: 40px;
color: inherit;
text-decoration: none;
text-align: center; }
.sidebar-tabs > ul + ul {
bottom: 0; }
.sidebar-content {
position: absolute;
top: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.95);
overflow-x: hidden;
overflow-y: auto; }
.sidebar-left .sidebar-content {
left: 40px;
right: 0; }
.sidebar-right .sidebar-content {
left: 0;
right: 40px; }
.sidebar.collapsed > .sidebar-content {
overflow-y: hidden; }
.sidebar-pane {
display: none;
left: 0;
right: 0;
box-sizing: border-box;
padding: 10px 20px; }
.sidebar-pane.active {
display: block; }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-pane {
min-width: 265px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-pane {
min-width: 350px; } }
@media (min-width: 1200px) {
.sidebar-pane {
min-width: 420px; } }
.sidebar-header {
margin: -10px -20px 0;
height: 40px;
padding: 0 20px;
line-height: 40px;
font-size: 14.4pt;
color: #fff;
background-color: #0074d9; }
.sidebar-right .sidebar-header {
padding-left: 40px; }
.sidebar-close {
position: absolute;
top: 0;
width: 40px;
height: 40px;
text-align: center;
cursor: pointer; }
.sidebar-left .sidebar-close {
right: 0; }
.sidebar-right .sidebar-close {
left: 0; }
.sidebar {
background-color: rgba(255, 255, 255, 0.4); }
@media (min-width: 768px) {
.sidebar {
border: 3px solid transparent;
border-radius: 4px; } }
.sidebar-left {
border-right: 3px solid transparent; }
.sidebar-right {
border-left: 3px solid transparent; }
.sidebar-tabs {
overflow: hidden; }
@media (min-width: 768px) {
.sidebar-tabs {
border-radius: 4px 0 0 4px; }
.collapsed .sidebar-tabs {
border-radius: 4px; } }
@media (min-width: 768px) {
.sidebar-content {
border-radius: 0 4px 4px 0; } }
.sidebar-left ~ .sidebar-map .olControlZoom,
.sidebar-left ~ .sidebar-map .olScaleLine {
margin-left: 46px; }
@media (min-width: 768px) {
.sidebar-left ~ .sidebar-map .olControlZoom,
.sidebar-left ~ .sidebar-map .olScaleLine {
transition: margin-left 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-left ~ .sidebar-map .olControlZoom,
.sidebar-left ~ .sidebar-map .olScaleLine {
margin-left: 319px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-left ~ .sidebar-map .olControlZoom,
.sidebar-left ~ .sidebar-map .olScaleLine {
margin-left: 404px; } }
@media (min-width: 1200px) {
.sidebar-left ~ .sidebar-map .olControlZoom,
.sidebar-left ~ .sidebar-map .olScaleLine {
margin-left: 474px; } }
@media (min-width: 768px) {
.sidebar-left.collapsed ~ .sidebar-map .olControlZoom,
.sidebar-left.collapsed ~ .sidebar-map .olScaleLine {
margin-left: 54px; } }
.sidebar-right ~ .sidebar-map .olControlAttribution,
.sidebar-right ~ .sidebar-map .olControlPermalink,
.sidebar-right ~ .sidebar-map .olControlMousePosition {
margin-right: 46px; }
@media (min-width: 768px) {
.sidebar-right ~ .sidebar-map .olControlAttribution,
.sidebar-right ~ .sidebar-map .olControlPermalink,
.sidebar-right ~ .sidebar-map .olControlMousePosition {
transition: margin-right 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-right ~ .sidebar-map .olControlAttribution,
.sidebar-right ~ .sidebar-map .olControlPermalink,
.sidebar-right ~ .sidebar-map .olControlMousePosition {
margin-right: 319px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-right ~ .sidebar-map .olControlAttribution,
.sidebar-right ~ .sidebar-map .olControlPermalink,
.sidebar-right ~ .sidebar-map .olControlMousePosition {
margin-right: 404px; } }
@media (min-width: 1200px) {
.sidebar-right ~ .sidebar-map .olControlAttribution,
.sidebar-right ~ .sidebar-map .olControlPermalink,
.sidebar-right ~ .sidebar-map .olControlMousePosition {
margin-right: 474px; } }
@media (min-width: 768px) {
.sidebar-right.collapsed ~ .sidebar-map .olControlAttribution,
.sidebar-right.collapsed ~ .sidebar-map .olControlPermalink,
.sidebar-right.collapsed ~ .sidebar-map .olControlMousePosition {
margin-right: 54px; } }

View File

@@ -1 +0,0 @@
.sidebar{position:absolute;top:0;bottom:0;width:100%;overflow:hidden;z-index:2000}.sidebar.collapsed{width:40px}@media (min-width:768px) and (max-width:991px){.sidebar{width:305px}.sidebar-pane{min-width:265px}}@media (min-width:992px) and (max-width:1199px){.sidebar{width:390px}}@media (min-width:1200px){.sidebar{width:460px}}.sidebar-left{left:0;border-right:3px solid transparent}.sidebar-right{right:0;border-left:3px solid transparent}@media (min-width:768px){.sidebar{top:8px;bottom:8px;transition:width .5s;border:3px solid transparent;border-radius:4px}.sidebar-left{left:8px}.sidebar-right{right:8px}}.sidebar-tabs{top:0;bottom:0;height:100%;background-color:rgba(0,60,136,.5)}.sidebar-left .sidebar-tabs{left:0}.sidebar-right .sidebar-tabs{right:0}.sidebar-tabs,.sidebar-tabs>ul{position:absolute;width:40px;margin:0;padding:0;list-style-type:none}.sidebar-tabs>li,.sidebar-tabs>ul>li{width:100%;height:40px;color:#fff;font-size:12pt;overflow:hidden;transition:80ms}.sidebar-tabs>li:hover,.sidebar-tabs>ul>li:hover{color:#fff;background-color:rgba(0,60,136,.6)}.sidebar-tabs>li.active,.sidebar-tabs>ul>li.active{color:#fff;background-color:#0074d9}.sidebar-tabs>li.disabled,.sidebar-tabs>ul>li.disabled{color:rgba(255,255,255,.4)}.sidebar-tabs>li.disabled:hover,.sidebar-tabs>ul>li.disabled:hover{background:0 0}.sidebar-tabs>li.disabled>a,.sidebar-tabs>ul>li.disabled>a{cursor:default}.sidebar-tabs>li>a,.sidebar-tabs>ul>li>a{display:block;width:100%;height:100%;line-height:40px;color:inherit;text-decoration:none;text-align:center}.sidebar-tabs>ul+ul{bottom:0}.sidebar-content{position:absolute;top:0;bottom:0;background-color:rgba(255,255,255,.95);overflow-x:hidden;overflow-y:auto}.sidebar-left .sidebar-content{left:40px;right:0}.sidebar-right .sidebar-content{left:0;right:40px}.sidebar.collapsed>.sidebar-content{overflow-y:hidden}.sidebar-pane{display:none;left:0;right:0;box-sizing:border-box;padding:10px 20px}.sidebar-pane.active{display:block}.sidebar-header{margin:-10px -20px 0;height:40px;padding:0 20px;line-height:40px;font-size:14.4pt;color:#fff;background-color:#0074d9}.sidebar-right .sidebar-header{padding-left:40px}.sidebar-close{position:absolute;top:0;width:40px;height:40px;text-align:center;cursor:pointer}.sidebar-left .sidebar-close{right:0}.sidebar-right .sidebar-close{left:0}.sidebar{background-color:rgba(255,255,255,.4)}.sidebar-tabs{overflow:hidden}.sidebar-left~.sidebar-map .olControlZoom,.sidebar-left~.sidebar-map .olScaleLine{margin-left:46px}.sidebar-right~.sidebar-map .olControlAttribution,.sidebar-right~.sidebar-map .olControlMousePosition,.sidebar-right~.sidebar-map .olControlPermalink{margin-right:46px}@media (min-width:768px) and (max-width:991px){.sidebar-left~.sidebar-map .olControlZoom,.sidebar-left~.sidebar-map .olScaleLine{margin-left:319px}.sidebar-right~.sidebar-map .olControlAttribution,.sidebar-right~.sidebar-map .olControlMousePosition,.sidebar-right~.sidebar-map .olControlPermalink{margin-right:319px}}@media (min-width:992px) and (max-width:1199px){.sidebar-pane{min-width:350px}.sidebar-left~.sidebar-map .olControlZoom,.sidebar-left~.sidebar-map .olScaleLine{margin-left:404px}.sidebar-right~.sidebar-map .olControlAttribution,.sidebar-right~.sidebar-map .olControlMousePosition,.sidebar-right~.sidebar-map .olControlPermalink{margin-right:404px}}@media (min-width:1200px){.sidebar-pane{min-width:420px}.sidebar-left~.sidebar-map .olControlZoom,.sidebar-left~.sidebar-map .olScaleLine{margin-left:474px}.sidebar-right~.sidebar-map .olControlAttribution,.sidebar-right~.sidebar-map .olControlMousePosition,.sidebar-right~.sidebar-map .olControlPermalink{margin-right:474px}}@media (min-width:768px){.sidebar-tabs{border-radius:4px 0 0 4px}.collapsed .sidebar-tabs{border-radius:4px}.sidebar-content{border-radius:0 4px 4px 0}.sidebar-left~.sidebar-map .olControlZoom,.sidebar-left~.sidebar-map .olScaleLine{transition:margin-left .5s}.sidebar-left.collapsed~.sidebar-map .olControlZoom,.sidebar-left.collapsed~.sidebar-map .olScaleLine{margin-left:54px}.sidebar-right~.sidebar-map .olControlAttribution,.sidebar-right~.sidebar-map .olControlMousePosition,.sidebar-right~.sidebar-map .olControlPermalink{transition:margin-right .5s}.sidebar-right.collapsed~.sidebar-map .olControlAttribution,.sidebar-right.collapsed~.sidebar-map .olControlMousePosition,.sidebar-right.collapsed~.sidebar-map .olControlPermalink{margin-right:54px}}

View File

@@ -1,212 +0,0 @@
.sidebar {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
overflow: hidden;
z-index: 2000; }
.sidebar.collapsed {
width: 40px; }
@media (min-width: 768px) {
.sidebar {
top: 6px;
bottom: 6px;
transition: width 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar {
width: 305px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar {
width: 390px; } }
@media (min-width: 1200px) {
.sidebar {
width: 460px; } }
.sidebar-left {
left: 0; }
@media (min-width: 768px) {
.sidebar-left {
left: 6px; } }
.sidebar-right {
right: 0; }
@media (min-width: 768px) {
.sidebar-right {
right: 6px; } }
.sidebar-tabs {
top: 0;
bottom: 0;
height: 100%;
background-color: rgba(0, 60, 136, 0.5); }
.sidebar-left .sidebar-tabs {
left: 0; }
.sidebar-right .sidebar-tabs {
right: 0; }
.sidebar-tabs, .sidebar-tabs > ul {
position: absolute;
width: 40px;
margin: 0;
padding: 0;
list-style-type: none; }
.sidebar-tabs > li, .sidebar-tabs > ul > li {
width: 100%;
height: 40px;
color: #fff;
font-size: 12pt;
overflow: hidden;
transition: all 80ms; }
.sidebar-tabs > li:hover, .sidebar-tabs > ul > li:hover {
color: #fff;
background-color: rgba(0, 60, 136, 0.6); }
.sidebar-tabs > li.active, .sidebar-tabs > ul > li.active {
color: #fff;
background-color: #0074d9; }
.sidebar-tabs > li.disabled, .sidebar-tabs > ul > li.disabled {
color: rgba(255, 255, 255, 0.4); }
.sidebar-tabs > li.disabled:hover, .sidebar-tabs > ul > li.disabled:hover {
background: transparent; }
.sidebar-tabs > li.disabled > a, .sidebar-tabs > ul > li.disabled > a {
cursor: default; }
.sidebar-tabs > li > a, .sidebar-tabs > ul > li > a {
display: block;
width: 100%;
height: 100%;
line-height: 40px;
color: inherit;
text-decoration: none;
text-align: center; }
.sidebar-tabs > ul + ul {
bottom: 0; }
.sidebar-content {
position: absolute;
top: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.95);
overflow-x: hidden;
overflow-y: auto; }
.sidebar-left .sidebar-content {
left: 40px;
right: 0; }
.sidebar-right .sidebar-content {
left: 0;
right: 40px; }
.sidebar.collapsed > .sidebar-content {
overflow-y: hidden; }
.sidebar-pane {
display: none;
left: 0;
right: 0;
box-sizing: border-box;
padding: 10px 20px; }
.sidebar-pane.active {
display: block; }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-pane {
min-width: 265px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-pane {
min-width: 350px; } }
@media (min-width: 1200px) {
.sidebar-pane {
min-width: 420px; } }
.sidebar-header {
margin: -10px -20px 0;
height: 40px;
padding: 0 20px;
line-height: 40px;
font-size: 14.4pt;
color: #fff;
background-color: #0074d9; }
.sidebar-right .sidebar-header {
padding-left: 40px; }
.sidebar-close {
position: absolute;
top: 0;
width: 40px;
height: 40px;
text-align: center;
cursor: pointer; }
.sidebar-left .sidebar-close {
right: 0; }
.sidebar-right .sidebar-close {
left: 0; }
.sidebar {
background-color: rgba(255, 255, 255, 0.4); }
@media (min-width: 768px) {
.sidebar {
border: 3px solid transparent;
border-radius: 4px; } }
.sidebar-left {
border-right: 3px solid transparent; }
.sidebar-right {
border-left: 3px solid transparent; }
.sidebar-tabs {
overflow: hidden; }
@media (min-width: 768px) {
.sidebar-tabs {
border-radius: 2px 0 0 2px; }
.collapsed .sidebar-tabs {
border-radius: 2px; } }
@media (min-width: 768px) {
.sidebar-content {
border-radius: 0 2px 2px 0; } }
.sidebar-left ~ .sidebar-map .ol-zoom, .sidebar-left ~ .sidebar-map .ol-scale-line {
margin-left: 46px; }
@media (min-width: 768px) {
.sidebar-left ~ .sidebar-map .ol-zoom, .sidebar-left ~ .sidebar-map .ol-scale-line {
transition: margin-left 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-left ~ .sidebar-map .ol-zoom, .sidebar-left ~ .sidebar-map .ol-scale-line {
margin-left: 317px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-left ~ .sidebar-map .ol-zoom, .sidebar-left ~ .sidebar-map .ol-scale-line {
margin-left: 402px; } }
@media (min-width: 1200px) {
.sidebar-left ~ .sidebar-map .ol-zoom, .sidebar-left ~ .sidebar-map .ol-scale-line {
margin-left: 472px; } }
@media (min-width: 768px) {
.sidebar-left.collapsed ~ .sidebar-map .ol-zoom, .sidebar-left.collapsed ~ .sidebar-map .ol-scale-line {
margin-left: 52px; } }
.sidebar-right ~ .sidebar-map .ol-rotate,
.sidebar-right ~ .sidebar-map .ol-attribution,
.sidebar-right ~ .sidebar-map .ol-full-screen {
margin-right: 46px; }
@media (min-width: 768px) {
.sidebar-right ~ .sidebar-map .ol-rotate,
.sidebar-right ~ .sidebar-map .ol-attribution,
.sidebar-right ~ .sidebar-map .ol-full-screen {
transition: margin-right 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-right ~ .sidebar-map .ol-rotate,
.sidebar-right ~ .sidebar-map .ol-attribution,
.sidebar-right ~ .sidebar-map .ol-full-screen {
margin-right: 317px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-right ~ .sidebar-map .ol-rotate,
.sidebar-right ~ .sidebar-map .ol-attribution,
.sidebar-right ~ .sidebar-map .ol-full-screen {
margin-right: 402px; } }
@media (min-width: 1200px) {
.sidebar-right ~ .sidebar-map .ol-rotate,
.sidebar-right ~ .sidebar-map .ol-attribution,
.sidebar-right ~ .sidebar-map .ol-full-screen {
margin-right: 472px; } }
@media (min-width: 768px) {
.sidebar-right.collapsed ~ .sidebar-map .ol-rotate,
.sidebar-right.collapsed ~ .sidebar-map .ol-attribution,
.sidebar-right.collapsed ~ .sidebar-map .ol-full-screen {
margin-right: 52px; } }

View File

@@ -1 +0,0 @@
.sidebar{position:absolute;top:0;bottom:0;width:100%;overflow:hidden;z-index:2000}.sidebar.collapsed{width:40px}@media (min-width:768px) and (max-width:991px){.sidebar{width:305px}.sidebar-pane{min-width:265px}}@media (min-width:992px) and (max-width:1199px){.sidebar{width:390px}}@media (min-width:1200px){.sidebar{width:460px}}.sidebar-left{left:0;border-right:3px solid transparent}.sidebar-right{right:0;border-left:3px solid transparent}@media (min-width:768px){.sidebar{top:6px;bottom:6px;transition:width .5s;border:3px solid transparent;border-radius:4px}.sidebar-left{left:6px}.sidebar-right{right:6px}}.sidebar-tabs{top:0;bottom:0;height:100%;background-color:rgba(0,60,136,.5)}.sidebar-left .sidebar-tabs{left:0}.sidebar-right .sidebar-tabs{right:0}.sidebar-tabs,.sidebar-tabs>ul{position:absolute;width:40px;margin:0;padding:0;list-style-type:none}.sidebar-tabs>li,.sidebar-tabs>ul>li{width:100%;height:40px;color:#fff;font-size:12pt;overflow:hidden;transition:80ms}.sidebar-tabs>li:hover,.sidebar-tabs>ul>li:hover{color:#fff;background-color:rgba(0,60,136,.6)}.sidebar-tabs>li.active,.sidebar-tabs>ul>li.active{color:#fff;background-color:#0074d9}.sidebar-tabs>li.disabled,.sidebar-tabs>ul>li.disabled{color:rgba(255,255,255,.4)}.sidebar-tabs>li.disabled:hover,.sidebar-tabs>ul>li.disabled:hover{background:0 0}.sidebar-tabs>li.disabled>a,.sidebar-tabs>ul>li.disabled>a{cursor:default}.sidebar-tabs>li>a,.sidebar-tabs>ul>li>a{display:block;width:100%;height:100%;line-height:40px;color:inherit;text-decoration:none;text-align:center}.sidebar-tabs>ul+ul{bottom:0}.sidebar-content{position:absolute;top:0;bottom:0;background-color:rgba(255,255,255,.95);overflow-x:hidden;overflow-y:auto}.sidebar-left .sidebar-content{left:40px;right:0}.sidebar-right .sidebar-content{left:0;right:40px}.sidebar.collapsed>.sidebar-content{overflow-y:hidden}.sidebar-pane{display:none;left:0;right:0;box-sizing:border-box;padding:10px 20px}.sidebar-pane.active{display:block}.sidebar-header{margin:-10px -20px 0;height:40px;padding:0 20px;line-height:40px;font-size:14.4pt;color:#fff;background-color:#0074d9}.sidebar-right .sidebar-header{padding-left:40px}.sidebar-close{position:absolute;top:0;width:40px;height:40px;text-align:center;cursor:pointer}.sidebar-left .sidebar-close{right:0}.sidebar-right .sidebar-close{left:0}.sidebar{background-color:rgba(255,255,255,.4)}.sidebar-tabs{overflow:hidden}.sidebar-left~.sidebar-map .ol-scale-line,.sidebar-left~.sidebar-map .ol-zoom{margin-left:46px}.sidebar-right~.sidebar-map .ol-attribution,.sidebar-right~.sidebar-map .ol-full-screen,.sidebar-right~.sidebar-map .ol-rotate{margin-right:46px}@media (min-width:768px) and (max-width:991px){.sidebar-left~.sidebar-map .ol-scale-line,.sidebar-left~.sidebar-map .ol-zoom{margin-left:317px}.sidebar-right~.sidebar-map .ol-attribution,.sidebar-right~.sidebar-map .ol-full-screen,.sidebar-right~.sidebar-map .ol-rotate{margin-right:317px}}@media (min-width:992px) and (max-width:1199px){.sidebar-pane{min-width:350px}.sidebar-left~.sidebar-map .ol-scale-line,.sidebar-left~.sidebar-map .ol-zoom{margin-left:402px}.sidebar-right~.sidebar-map .ol-attribution,.sidebar-right~.sidebar-map .ol-full-screen,.sidebar-right~.sidebar-map .ol-rotate{margin-right:402px}}@media (min-width:1200px){.sidebar-pane{min-width:420px}.sidebar-left~.sidebar-map .ol-scale-line,.sidebar-left~.sidebar-map .ol-zoom{margin-left:472px}.sidebar-right~.sidebar-map .ol-attribution,.sidebar-right~.sidebar-map .ol-full-screen,.sidebar-right~.sidebar-map .ol-rotate{margin-right:472px}}@media (min-width:768px){.sidebar-tabs{border-radius:2px 0 0 2px}.collapsed .sidebar-tabs{border-radius:2px}.sidebar-content{border-radius:0 2px 2px 0}.sidebar-left~.sidebar-map .ol-scale-line,.sidebar-left~.sidebar-map .ol-zoom{transition:margin-left .5s}.sidebar-left.collapsed~.sidebar-map .ol-scale-line,.sidebar-left.collapsed~.sidebar-map .ol-zoom{margin-left:52px}.sidebar-right~.sidebar-map .ol-attribution,.sidebar-right~.sidebar-map .ol-full-screen,.sidebar-right~.sidebar-map .ol-rotate{transition:margin-right .5s}.sidebar-right.collapsed~.sidebar-map .ol-attribution,.sidebar-right.collapsed~.sidebar-map .ol-full-screen,.sidebar-right.collapsed~.sidebar-map .ol-rotate{margin-right:52px}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 559 KiB

View File

@@ -1,202 +0,0 @@
# sidebar-v2 Documentation
## Installation
### NPM
```
npm install sidebar-v2 --save
```
### CDN hosted
OpenLayers 3+
```HTML
<!-- inside the <head> element -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Turbo87/sidebar-v2@v0.4.0/css/ol3-sidebar.css">
<!-- at the end of the <body> element -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/gh/Turbo87/sidebar-v2@v0.4.0/js/ol3-sidebar.js"></script>
```
### Self hosted
Download the [latest release](https://github.com/Turbo87/sidebar-v2/releases/latest),
unpack the downloaded file, and load the CSS and JavaScript into your
document, for instance (OpenLayers 3+):
```HTML
<!-- inside the <head> element -->
<link rel="stylesheet" href="sidebar-v2/css/ol3-sidebar.css">
<!-- at the end of the <body> element -->
<script type="text/javascript" src="sidebar-v2/js/ol3-sidebar.js"></script>
```
## Usage
In your HTML ensure that you have loaded the
[OpenLayers](https://openlayers.org/) and `sidebar-v2` CSS. In the example
code below we also use [FontAwesome](https://fontawesome.com/) so that nice
symbols can be used for the sidebar's buttons.
```HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v5.3.2/css/ol.css" type="text/css">
<link rel="stylesheet" href="sidebar-v2/css/ol3-sidebar.css">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
```
Then create the `div` element within the HTML `body` for the map similarly
to how one would for plain OpenLayers maps. However note that you need to
use `class="sidebar-map"` instead of `class="map"` and the map `div` needs
to *follow* the `div` for the sidebar:
```HTML
<!-- follows sidebar div -->
<div id="map" class="sidebar-map"></div>
```
Now define the sidebar (by default in a collapsed state) via the `sidebar`
and `collapsed` classes:
```HTML
<div id="sidebar" class="sidebar collapsed">
</div>
```
Each sidebar element consists of a navigation tab connected to a tab pane
containing the content of the sidebar element.
The navigation tabs are a simple unordered list of anchors linking to the
respective tab pane:
```HTML
<!-- navigation tabs -->
<div class="sidebar-tabs">
<ul role="tablist">
<li><a href="#home" role="tab"><i class="fa fa-bars"></i></a></li>
</ul>
</div>
```
The content of a given tab is contained in a sidebar tab pane (note the `id`
attribute pointing back to the relevant navigation tab). A pane includes a
header (via the `sidebar-header` class), which contains the `span` element
needed to close the pane, and then simple HTML text, for instance `p`
elements:
```HTML
<!-- tab panes -->
<div class="sidebar-content">
<div class="sidebar-pane" id="home">
<h1 class="sidebar-header">
Pane header text
<span class="sidebar-close"><i class="fa fa-caret-left"></i></span>
</h1>
<p>Pane text</p>
</div>
</div>
```
Now that the HTML has been set up, we can add the sidebar to the OpenLayers
map within JavaScript by adding a `script` element at the end of the `body`.
Don't forget to load the OpenLayers and sidebar-v2 JavaScript:
```HTML
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v5.3.2/build/ol.js"></script>
<script src="sidebar-v2/js/ol3-sidebar.js"></script>
```
Then set up the OpenLayers map, in this case using an
[OpenStreetMap](https://www.openstreetmap.org/) source:
```HTML
<script>
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.transform([7, 51.2], 'EPSG:4326', 'EPSG:3857'),
zoom: 4
})
});
</script>
```
To add the sidebar, simply create a new `Sidebar` object which links to the
sidebar `div` created above, and then add it as a new control to the map:
```javascript
var sidebar = new ol.control.Sidebar({ element: 'sidebar', position: 'left' });
map.addControl(sidebar);
```
Putting it all together we get:
```HTML
<!DOCTYPE html>
<html lang="en">
<head>
<title>sidebar-v2 usage example</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<link rel="stylesheet" href="https://openlayers.org/en/v4.6.5/css/ol.css" type="text/css">
<link rel="stylesheet" href="sidebar-v2/css/ol3-sidebar.css">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
<div id="sidebar" class="sidebar collapsed">
<!-- navigation tabs -->
<div class="sidebar-tabs">
<ul role="tablist">
<li><a href="#home" role="tab"><i class="fa fa-bars"></i></a></li>
</ul>
</div>
<!-- tab panes -->
<div class="sidebar-content">
<div class="sidebar-pane" id="home">
<h1 class="sidebar-header">
Pane header text
<span class="sidebar-close"><i class="fa fa-caret-left"></i></span>
</h1>
<p>Pane text</p>
</div>
</div>
</div>
<div id="map" class="sidebar-map"></div>
<script src="https://openlayers.org/en/v4.6.5/build/ol.js" type="text/javascript"></script>
<script src="sidebar-v2/js/ol3-sidebar.js" type="text/javascript"></script>
<script>
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.transform([7, 51.2], 'EPSG:4326', 'EPSG:3857'),
zoom: 4
})
});
var sidebar = new ol.control.Sidebar({ element: 'sidebar', position: 'left' });
map.addControl(sidebar);
</script>
</body>
</html>
```
For a more complete examples, have a look at the files in the `examples/`
directory of the distribution.

View File

@@ -1,99 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>sidebar-v2 example</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="../css/gmaps-sidebar.css" />
<style>
body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
font: 10pt "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.lorem {
font-style: italic;
color: #AAA;
}
</style>
</head>
<body onload="initialize()">
<div id="sidebar" class="sidebar collapsed">
<!-- Nav tabs -->
<div class="sidebar-tabs">
<ul role="tablist">
<li><a href="#home" role="tab"><i class="fa fa-bars"></i></a></li>
<li><a href="#profile" role="tab"><i class="fa fa-user"></i></a></li>
<li class="disabled"><a href="#messages" role="tab"><i class="fa fa-envelope"></i></a></li>
<li><a href="https://github.com/Turbo87/sidebar-v2" role="tab" target="_blank"><i class="fa fa-github"></i></a></li>
</ul>
<ul role="tablist">
<li><a href="#settings" role="tab"><i class="fa fa-gear"></i></a></li>
</ul>
</div>
<!-- Tab panes -->
<div class="sidebar-content">
<div class="sidebar-pane" id="home">
<h1 class="sidebar-header">
sidebar-v2
<span class="sidebar-close"><i class="fa fa-caret-left"></i></span>
</h1>
<p>A responsive sidebar for mapping libraries like <a href="http://leafletjs.com/">Leaflet</a> or <a href="http://openlayers.org/">OpenLayers</a>.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
</div>
<div class="sidebar-pane" id="profile">
<h1 class="sidebar-header">Profile<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</div>
<div class="sidebar-pane" id="messages">
<h1 class="sidebar-header">Messages<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</div>
<div class="sidebar-pane" id="settings">
<h1 class="sidebar-header">Settings<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</div>
</div>
</div>
<div id="map" class="sidebar-map"></div>
<a href="https://github.com/Turbo87/sidebar-v2/"><img style="position: fixed; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a>
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="https://maps.googleapis.com/maps/api/js" type="text/javascript"></script>
<script src="../js/jquery-sidebar.js"></script>
<script>
function initialize() {
var map = new google.maps.Map(document.getElementById("map"), {
center: new google.maps.LatLng(51.2, 7),
zoom: 5,
mapTypeId: google.maps.MapTypeId.TERRAIN,
});
}
var sidebar = $('#sidebar').sidebar();
</script>
</body>
</html>

View File

@@ -1,102 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>sidebar-v2 example</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.1/dist/leaflet.css" />
<!--[if lte IE 8]><link rel="stylesheet" href="https://cdn.leafletjs.com/leaflet-0.7.2/leaflet.ie.css" /><![endif]-->
<link rel="stylesheet" href="../css/leaflet-sidebar.css" />
<style>
body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
font: 10pt "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.lorem {
font-style: italic;
color: #AAA;
}
</style>
</head>
<body>
<div id="sidebar" class="sidebar collapsed">
<!-- Nav tabs -->
<div class="sidebar-tabs">
<ul role="tablist">
<li><a href="#home" role="tab"><i class="fa fa-bars"></i></a></li>
<li><a href="#profile" role="tab"><i class="fa fa-user"></i></a></li>
<li class="disabled"><a href="#messages" role="tab"><i class="fa fa-envelope"></i></a></li>
<li><a href="https://github.com/Turbo87/sidebar-v2" role="tab" target="_blank"><i class="fa fa-github"></i></a></li>
</ul>
<ul role="tablist">
<li><a href="#settings" role="tab"><i class="fa fa-gear"></i></a></li>
</ul>
</div>
<!-- Tab panes -->
<div class="sidebar-content">
<div class="sidebar-pane" id="home">
<h1 class="sidebar-header">
sidebar-v2
<span class="sidebar-close"><i class="fa fa-caret-left"></i></span>
</h1>
<p>A responsive sidebar for mapping libraries like <a href="http://leafletjs.com/">Leaflet</a> or <a href="http://openlayers.org/">OpenLayers</a>.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
</div>
<div class="sidebar-pane" id="profile">
<h1 class="sidebar-header">Profile<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</div>
<div class="sidebar-pane" id="messages">
<h1 class="sidebar-header">Messages<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</div>
<div class="sidebar-pane" id="settings">
<h1 class="sidebar-header">Settings<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</div>
</div>
</div>
<div id="map" class="sidebar-map"></div>
<a href="https://github.com/Turbo87/sidebar-v2/"><img style="position: fixed; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a>
<script src="https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"></script>
<script src="../js/leaflet-sidebar.js"></script>
<script>
var map = L.map('map');
map.setView([51.2, 7], 9);
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: 'Map data &copy; OpenStreetMap contributors'
}).addTo(map);
var marker = L.marker([51.2, 7]).addTo(map);
var sidebar = L.control.sidebar('sidebar').addTo(map);
</script>
</body>
</html>

View File

@@ -1,103 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>sidebar-v2 example</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="../css/ol2-sidebar.css" />
<style>
body {
padding: 0;
margin: 0;
overflow: hidden;
}
html, body, #map {
height: 100%;
font: 10pt "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.lorem {
font-style: italic;
color: #AAA;
}
</style>
</head>
<body>
<div id="sidebar" class="sidebar collapsed">
<!-- Nav tabs -->
<div class="sidebar-tabs">
<ul role="tablist">
<li><a href="#home" role="tab"><i class="fa fa-bars"></i></a></li>
<li><a href="#profile" role="tab"><i class="fa fa-user"></i></a></li>
<li class="disabled"><a href="#messages" role="tab"><i class="fa fa-envelope"></i></a></li>
<li><a href="https://github.com/Turbo87/sidebar-v2" role="tab" target="_blank"><i class="fa fa-github"></i></a></li>
</ul>
<ul role="tablist">
<li><a href="#settings" role="tab"><i class="fa fa-gear"></i></a></li>
</ul>
</div>
<!-- Tab panes -->
<div class="sidebar-content">
<div class="sidebar-pane" id="home">
<h1 class="sidebar-header">
sidebar-v2
<span class="sidebar-close"><i class="fa fa-caret-left"></i></span>
</h1>
<p>A responsive sidebar for mapping libraries like <a href="http://leafletjs.com/">Leaflet</a> or <a href="http://openlayers.org/">OpenLayers</a>.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
</div>
<div class="sidebar-pane" id="profile">
<h1 class="sidebar-header">Profile<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</div>
<div class="sidebar-pane" id="messages">
<h1 class="sidebar-header">Messages<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</div>
<div class="sidebar-pane" id="settings">
<h1 class="sidebar-header">Settings<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</div>
</div>
</div>
<div id="map" class="sidebar-map"></div>
<a href="https://github.com/Turbo87/sidebar-v2/"><img style="position: fixed; top: 0; right: 0; border: 0; z-index: 5000" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a>
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="https://openlayers.org/api/OpenLayers.js" type="text/javascript"></script>
<script src="../js/jquery-sidebar.js"></script>
<script>
var map = new OpenLayers.Map('map');
var wms = new OpenLayers.Layer.WMS(
"OpenLayers WMS",
"http://vmap0.tiles.osgeo.org/wms/vmap0", {
layers: 'basic'
});
map.addLayer(wms);
map.setCenter([7, 51.2], 6);
var sidebar = $('#sidebar').sidebar();
</script>
</body>
</html>

View File

@@ -1,107 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>sidebar-v2 example</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/css/ol.css" type="text/css">
<link rel="stylesheet" href="../css/ol3-sidebar.css" />
<style>
body {
padding: 0;
margin: 0;
overflow: hidden;
}
html, body, #map {
height: 100%;
font: 10pt "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.lorem {
font-style: italic;
color: #AAA;
}
</style>
</head>
<body>
<div id="sidebar" class="sidebar collapsed">
<!-- Nav tabs -->
<div class="sidebar-tabs">
<ul role="tablist">
<li><a href="#home" role="tab"><i class="fa fa-bars"></i></a></li>
<li><a href="#profile" role="tab"><i class="fa fa-user"></i></a></li>
<li class="disabled"><a href="#messages" role="tab"><i class="fa fa-envelope"></i></a></li>
<li><a href="https://github.com/Turbo87/sidebar-v2" role="tab" target="_blank"><i class="fa fa-github"></i></a></li>
</ul>
<ul role="tablist">
<li><a href="#settings" role="tab"><i class="fa fa-gear"></i></a></li>
</ul>
</div>
<!-- Tab panes -->
<div class="sidebar-content">
<div class="sidebar-pane" id="home">
<h1 class="sidebar-header">
sidebar-v2
<span class="sidebar-close"><i class="fa fa-caret-left"></i></span>
</h1>
<p>A responsive sidebar for mapping libraries like <a href="http://leafletjs.com/">Leaflet</a> or <a href="http://openlayers.org/">OpenLayers</a>.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
</div>
<div class="sidebar-pane" id="profile">
<h1 class="sidebar-header">Profile<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</div>
<div class="sidebar-pane" id="messages">
<h1 class="sidebar-header">Messages<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</div>
<div class="sidebar-pane" id="settings">
<h1 class="sidebar-header">Settings<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</div>
</div>
</div>
<div id="map" class="sidebar-map"></div>
<a href="https://github.com/Turbo87/sidebar-v2/"><img style="position: fixed; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/build/ol.js"></script>
<script src="../js/ol3-sidebar.js"></script>
<script>
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.transform([7, 51.2], 'EPSG:4326', 'EPSG:3857'),
zoom: 4
})
});
var sidebar = new ol.control.Sidebar({ element: 'sidebar', position: 'left' });
map.addControl(sidebar);
</script>
</body>
</html>

View File

@@ -1,102 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>sidebar-v2 example</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.1/dist/leaflet.css" />
<!--[if lte IE 8]><link rel="stylesheet" href="https://cdn.leafletjs.com/leaflet-0.7.2/leaflet.ie.css" /><![endif]-->
<link rel="stylesheet" href="../css/leaflet-sidebar.css" />
<style>
body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
font: 10pt "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.lorem {
font-style: italic;
color: #AAA;
}
</style>
</head>
<body>
<div id="sidebar" class="sidebar collapsed">
<!-- Nav tabs -->
<div class="sidebar-tabs">
<ul role="tablist">
<li><a href="#home" role="tab"><i class="fa fa-bars"></i></a></li>
<li><a href="#profile" role="tab"><i class="fa fa-user"></i></a></li>
<li class="disabled"><a href="#messages" role="tab"><i class="fa fa-envelope"></i></a></li>
<li><a href="https://github.com/Turbo87/sidebar-v2" role="tab" target="_blank"><i class="fa fa-github"></i></a></li>
</ul>
<ul role="tablist">
<li><a href="#settings" role="tab"><i class="fa fa-gear"></i></a></li>
</ul>
</div>
<!-- Tab panes -->
<div class="sidebar-content">
<div class="sidebar-pane" id="home">
<h1 class="sidebar-header">
sidebar-v2
<span class="sidebar-close"><i class="fa fa-caret-right"></i></span>
</h1>
<p>A responsive sidebar for mapping libraries like <a href="http://leafletjs.com/">Leaflet</a> or <a href="http://openlayers.org/">OpenLayers</a>.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p class="lorem">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
</div>
<div class="sidebar-pane" id="profile">
<h1 class="sidebar-header">Profile<span class="sidebar-close"><i class="fa fa-caret-right"></i></span></h1>
</div>
<div class="sidebar-pane" id="messages">
<h1 class="sidebar-header">Messages<span class="sidebar-close"><i class="fa fa-caret-right"></i></span></h1>
</div>
<div class="sidebar-pane" id="settings">
<h1 class="sidebar-header">Settings<span class="sidebar-close"><i class="fa fa-caret-right"></i></span></h1>
</div>
</div>
</div>
<div id="map" class="sidebar-map"></div>
<a href="https://github.com/Turbo87/sidebar-v2/"><img style="position: fixed; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a>
<script src="https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"></script>
<script src="../js/leaflet-sidebar.js"></script>
<script>
var map = L.map('map');
map.setView([51.2, 7], 9);
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: 'Map data &copy; OpenStreetMap contributors'
}).addTo(map);
var marker = L.marker([51.2, 7]).addTo(map);
var sidebar = L.control.sidebar('sidebar', {position: 'right'}).addTo(map);
</script>
</body>
</html>

View File

@@ -1,39 +0,0 @@
var gulp = require('gulp');
var cleanCSS = require('gulp-clean-css');
var sass = require('gulp-sass');
var rename = require('gulp-rename');
var uglify = require('gulp-uglify');
// SASS compilation
gulp.task('sass', function() {
return gulp.src('scss/*sidebar.scss')
.pipe(sass())
.on('error', sass.logError)
.pipe(gulp.dest('css'));
});
// Minify JS + CSS
gulp.task('minify:js', function() {
return gulp.src('js/*sidebar.js')
.pipe(rename({ suffix: '.min' }))
.pipe(uglify())
.pipe(gulp.dest('js'));
});
gulp.task('minify:css', gulp.series('sass', function() {
return gulp.src('css/*sidebar.css')
.pipe(rename({ suffix: '.min' }))
.pipe(cleanCSS({level: 2}))
.pipe(gulp.dest('css'));
}));
gulp.task('minify', gulp.parallel('minify:js', 'minify:css'));
// Watch JS + CSS Files
gulp.task('watch', gulp.series('minify', function() {
gulp.watch('js/*.js', ['minify:js']);
gulp.watch('scss/*.scss', ['minify:css']);
}));
// Default
gulp.task('default', gulp.series('minify'));

View File

@@ -1,84 +0,0 @@
/* global $ */
/**
* Create a new sidebar on this jQuery object.
*
* @example
* var sidebar = $('#sidebar').sidebar();
*
* @param {Object} [options] - Optional options object
* @param {string} [options.position=left] - Position of the sidebar: 'left' or 'right'
* @returns {jQuery}
*/
$.fn.sidebar = function(options) {
var $sidebar = this;
var $tabs = $sidebar.find('ul.sidebar-tabs, .sidebar-tabs > ul');
var $container = $sidebar.children('.sidebar-content').first();
options = $.extend({
position: 'left'
}, options || {});
$sidebar.addClass('sidebar-' + options.position);
$tabs.children('li').children('a[href^="#"]').on('click', function(e) {
e.preventDefault();
var $tab = $(this).closest('li');
if ($tab.hasClass('active'))
$sidebar.close();
else if (!$tab.hasClass('disabled'))
$sidebar.open(this.hash.slice(1), $tab);
});
$sidebar.find('.sidebar-close').on('click', function() {
$sidebar.close();
});
/**
* Open sidebar (if necessary) and show the specified tab.
*
* @param {string} id - The id of the tab to show (without the # character)
* @param {jQuery} [$tab] - The jQuery object representing the tab node (used internally for efficiency)
*/
$sidebar.open = function(id, $tab) {
if (typeof $tab === 'undefined')
$tab = $tabs.find('li > a[href="#' + id + '"]').parent();
// hide old active contents
$container.children('.sidebar-pane.active').removeClass('active');
// show new content
$container.children('#' + id).addClass('active');
// remove old active highlights
$tabs.children('li.active').removeClass('active');
// set new highlight
$tab.addClass('active');
$sidebar.trigger('content', { 'id': id });
if ($sidebar.hasClass('collapsed')) {
// open sidebar
$sidebar.trigger('opening');
$sidebar.removeClass('collapsed');
}
};
/**
* Close the sidebar (if necessary).
*/
$sidebar.close = function() {
// remove old active highlights
$tabs.children('li.active').removeClass('active');
if (!$sidebar.hasClass('collapsed')) {
// close sidebar
$sidebar.trigger('closing');
$sidebar.addClass('collapsed');
}
};
return $sidebar;
};

View File

@@ -1 +0,0 @@
$.fn.sidebar=function(e){var s=this,i=s.find("ul.sidebar-tabs, .sidebar-tabs > ul"),a=s.children(".sidebar-content").first();return e=$.extend({position:"left"},e||{}),s.addClass("sidebar-"+e.position),i.children("li").children('a[href^="#"]').on("click",function(e){e.preventDefault();var i=$(this).closest("li");i.hasClass("active")?s.close():i.hasClass("disabled")||s.open(this.hash.slice(1),i)}),s.find(".sidebar-close").on("click",function(){s.close()}),s.open=function(e,l){void 0===l&&(l=i.find('li > a[href="#'+e+'"]').parent()),a.children(".sidebar-pane.active").removeClass("active"),a.children("#"+e).addClass("active"),i.children("li.active").removeClass("active"),l.addClass("active"),s.trigger("content",{id:e}),s.hasClass("collapsed")&&(s.trigger("opening"),s.removeClass("collapsed"))},s.close=function(){i.children("li.active").removeClass("active"),s.hasClass("collapsed")||(s.trigger("closing"),s.addClass("collapsed"))},s};

View File

@@ -1,216 +0,0 @@
/* global L */
/**
* @name Sidebar
* @class L.Control.Sidebar
* @extends L.Control
* @param {string} id - The id of the sidebar element (without the # character)
* @param {Object} [options] - Optional options object
* @param {string} [options.position=left] - Position of the sidebar: 'left' or 'right'
* @see L.control.sidebar
*/
L.Control.Sidebar = L.Control.extend(/** @lends L.Control.Sidebar.prototype */ {
includes: (L.Evented.prototype || L.Mixin.Events),
options: {
position: 'left'
},
initialize: function (id, options) {
var i, child;
L.setOptions(this, options);
// Find sidebar HTMLElement
this._sidebar = L.DomUtil.get(id);
// Attach .sidebar-left/right class
L.DomUtil.addClass(this._sidebar, 'sidebar-' + this.options.position);
// Attach touch styling if necessary
if (L.Browser.touch)
L.DomUtil.addClass(this._sidebar, 'leaflet-touch');
// Find sidebar > div.sidebar-content
for (i = this._sidebar.children.length - 1; i >= 0; i--) {
child = this._sidebar.children[i];
if (child.tagName == 'DIV' &&
L.DomUtil.hasClass(child, 'sidebar-content'))
this._container = child;
}
// Find sidebar ul.sidebar-tabs > li, sidebar .sidebar-tabs > ul > li
this._tabitems = this._sidebar.querySelectorAll('ul.sidebar-tabs > li, .sidebar-tabs > ul > li');
for (i = this._tabitems.length - 1; i >= 0; i--) {
this._tabitems[i]._sidebar = this;
}
// Find sidebar > div.sidebar-content > div.sidebar-pane
this._panes = [];
this._closeButtons = [];
for (i = this._container.children.length - 1; i >= 0; i--) {
child = this._container.children[i];
if (child.tagName == 'DIV' &&
L.DomUtil.hasClass(child, 'sidebar-pane')) {
this._panes.push(child);
var closeButtons = child.querySelectorAll('.sidebar-close');
for (var j = 0, len = closeButtons.length; j < len; j++)
this._closeButtons.push(closeButtons[j]);
}
}
},
/**
* Add this sidebar to the specified map.
*
* @param {L.Map} map
* @returns {Sidebar}
*/
addTo: function (map) {
var i, child;
this._map = map;
for (i = this._tabitems.length - 1; i >= 0; i--) {
child = this._tabitems[i];
var sub = child.querySelector('a');
if (sub.hasAttribute('href') && sub.getAttribute('href').slice(0,1) == '#') {
L.DomEvent
.on(sub, 'click', L.DomEvent.preventDefault )
.on(sub, 'click', this._onClick, child);
}
}
for (i = this._closeButtons.length - 1; i >= 0; i--) {
child = this._closeButtons[i];
L.DomEvent.on(child, 'click', this._onCloseClick, this);
}
return this;
},
/**
* @deprecated - Please use remove() instead of removeFrom(), as of Leaflet 0.8-dev, the removeFrom() has been replaced with remove()
* Removes this sidebar from the map.
* @param {L.Map} map
* @returns {Sidebar}
*/
removeFrom: function(map) {
console.log('removeFrom() has been deprecated, please use remove() instead as support for this function will be ending soon.');
this.remove(map);
},
/**
* Remove this sidebar from the map.
*
* @param {L.Map} map
* @returns {Sidebar}
*/
remove: function (map) {
var i, child;
this._map = null;
for (i = this._tabitems.length - 1; i >= 0; i--) {
child = this._tabitems[i];
L.DomEvent.off(child.querySelector('a'), 'click', this._onClick);
}
for (i = this._closeButtons.length - 1; i >= 0; i--) {
child = this._closeButtons[i];
L.DomEvent.off(child, 'click', this._onCloseClick, this);
}
return this;
},
/**
* Open sidebar (if necessary) and show the specified tab.
*
* @param {string} id - The id of the tab to show (without the # character)
*/
open: function(id) {
var i, child;
// hide old active contents and show new content
for (i = this._panes.length - 1; i >= 0; i--) {
child = this._panes[i];
if (child.id == id)
L.DomUtil.addClass(child, 'active');
else if (L.DomUtil.hasClass(child, 'active'))
L.DomUtil.removeClass(child, 'active');
}
// remove old active highlights and set new highlight
for (i = this._tabitems.length - 1; i >= 0; i--) {
child = this._tabitems[i];
if (child.querySelector('a').hash == '#' + id)
L.DomUtil.addClass(child, 'active');
else if (L.DomUtil.hasClass(child, 'active'))
L.DomUtil.removeClass(child, 'active');
}
this.fire('content', { id: id });
// open sidebar (if necessary)
if (L.DomUtil.hasClass(this._sidebar, 'collapsed')) {
this.fire('opening');
L.DomUtil.removeClass(this._sidebar, 'collapsed');
}
return this;
},
/**
* Close the sidebar (if necessary).
*/
close: function() {
// remove old active highlights
for (var i = this._tabitems.length - 1; i >= 0; i--) {
var child = this._tabitems[i];
if (L.DomUtil.hasClass(child, 'active'))
L.DomUtil.removeClass(child, 'active');
}
// close sidebar
if (!L.DomUtil.hasClass(this._sidebar, 'collapsed')) {
this.fire('closing');
L.DomUtil.addClass(this._sidebar, 'collapsed');
}
return this;
},
/**
* @private
*/
_onClick: function() {
if (L.DomUtil.hasClass(this, 'active'))
this._sidebar.close();
else if (!L.DomUtil.hasClass(this, 'disabled'))
this._sidebar.open(this.querySelector('a').hash.slice(1));
},
/**
* @private
*/
_onCloseClick: function () {
this.close();
}
});
/**
* Creates a new sidebar.
*
* @example
* var sidebar = L.control.sidebar('sidebar').addTo(map);
*
* @param {string} id - The id of the sidebar element (without the # character)
* @param {Object} [options] - Optional options object
* @param {string} [options.position=left] - Position of the sidebar: 'left' or 'right'
* @returns {Sidebar} A new sidebar instance
*/
L.control.sidebar = function (id, options) {
return new L.Control.Sidebar(id, options);
};

View File

@@ -1 +0,0 @@
L.Control.Sidebar=L.Control.extend({includes:L.Evented.prototype||L.Mixin.Events,options:{position:"left"},initialize:function(t,s){var i,e;for(L.setOptions(this,s),this._sidebar=L.DomUtil.get(t),L.DomUtil.addClass(this._sidebar,"sidebar-"+this.options.position),L.Browser.touch&&L.DomUtil.addClass(this._sidebar,"leaflet-touch"),i=this._sidebar.children.length-1;i>=0;i--)"DIV"==(e=this._sidebar.children[i]).tagName&&L.DomUtil.hasClass(e,"sidebar-content")&&(this._container=e);for(this._tabitems=this._sidebar.querySelectorAll("ul.sidebar-tabs > li, .sidebar-tabs > ul > li"),i=this._tabitems.length-1;i>=0;i--)this._tabitems[i]._sidebar=this;for(this._panes=[],this._closeButtons=[],i=this._container.children.length-1;i>=0;i--)if("DIV"==(e=this._container.children[i]).tagName&&L.DomUtil.hasClass(e,"sidebar-pane")){this._panes.push(e);for(var o=e.querySelectorAll(".sidebar-close"),a=0,l=o.length;a<l;a++)this._closeButtons.push(o[a])}},addTo:function(t){var s,i;for(this._map=t,s=this._tabitems.length-1;s>=0;s--){var e=(i=this._tabitems[s]).querySelector("a");e.hasAttribute("href")&&"#"==e.getAttribute("href").slice(0,1)&&L.DomEvent.on(e,"click",L.DomEvent.preventDefault).on(e,"click",this._onClick,i)}for(s=this._closeButtons.length-1;s>=0;s--)i=this._closeButtons[s],L.DomEvent.on(i,"click",this._onCloseClick,this);return this},removeFrom:function(t){console.log("removeFrom() has been deprecated, please use remove() instead as support for this function will be ending soon."),this.remove(t)},remove:function(t){var s,i;for(this._map=null,s=this._tabitems.length-1;s>=0;s--)i=this._tabitems[s],L.DomEvent.off(i.querySelector("a"),"click",this._onClick);for(s=this._closeButtons.length-1;s>=0;s--)i=this._closeButtons[s],L.DomEvent.off(i,"click",this._onCloseClick,this);return this},open:function(t){var s,i;for(s=this._panes.length-1;s>=0;s--)(i=this._panes[s]).id==t?L.DomUtil.addClass(i,"active"):L.DomUtil.hasClass(i,"active")&&L.DomUtil.removeClass(i,"active");for(s=this._tabitems.length-1;s>=0;s--)(i=this._tabitems[s]).querySelector("a").hash=="#"+t?L.DomUtil.addClass(i,"active"):L.DomUtil.hasClass(i,"active")&&L.DomUtil.removeClass(i,"active");return this.fire("content",{id:t}),L.DomUtil.hasClass(this._sidebar,"collapsed")&&(this.fire("opening"),L.DomUtil.removeClass(this._sidebar,"collapsed")),this},close:function(){for(var t=this._tabitems.length-1;t>=0;t--){var s=this._tabitems[t];L.DomUtil.hasClass(s,"active")&&L.DomUtil.removeClass(s,"active")}return L.DomUtil.hasClass(this._sidebar,"collapsed")||(this.fire("closing"),L.DomUtil.addClass(this._sidebar,"collapsed")),this},_onClick:function(){L.DomUtil.hasClass(this,"active")?this._sidebar.close():L.DomUtil.hasClass(this,"disabled")||this._sidebar.open(this.querySelector("a").hash.slice(1))},_onCloseClick:function(){this.close()}}),L.control.sidebar=function(t,s){return new L.Control.Sidebar(t,s)};

View File

@@ -1,132 +0,0 @@
/* global ol */
ol.control.Sidebar = function (settings) {
var defaults = {
element: null,
position: 'left'
}, i, child;
this._options = Object.assign({}, defaults, settings);
ol.control.Control.call(this, {
element: document.getElementById(this._options.element),
target: this._options.target
});
// Attach .sidebar-left/right class
this.element.classList.add('sidebar-' + this._options.position);
// Find sidebar > div.sidebar-content
for (i = this.element.children.length - 1; i >= 0; i--) {
child = this.element.children[i];
if (child.tagName === 'DIV' &&
child.classList.contains('sidebar-content')) {
this._container = child;
}
}
// Find sidebar ul.sidebar-tabs > li, sidebar .sidebar-tabs > ul > li
this._tabitems = this.element.querySelectorAll('ul.sidebar-tabs > li, .sidebar-tabs > ul > li');
for (i = this._tabitems.length - 1; i >= 0; i--) {
this._tabitems[i]._sidebar = this;
}
// Find sidebar > div.sidebar-content > div.sidebar-pane
this._panes = [];
this._closeButtons = [];
for (i = this._container.children.length - 1; i >= 0; i--) {
child = this._container.children[i];
if (child.tagName == 'DIV' &&
child.classList.contains('sidebar-pane')) {
this._panes.push(child);
var closeButtons = child.querySelectorAll('.sidebar-close');
for (var j = 0, len = closeButtons.length; j < len; j++) {
this._closeButtons.push(closeButtons[j]);
}
}
}
};
if ('inherits' in ol) {
ol.inherits(ol.control.Sidebar, ol.control.Control);
} else {
ol.control.Sidebar.prototype = Object.create(ol.control.Control.prototype);
ol.control.Sidebar.prototype.constructor = ol.control.Sidebar;
}
ol.control.Sidebar.prototype.setMap = function(map) {
var i, child;
for (i = this._tabitems.length - 1; i >= 0; i--) {
child = this._tabitems[i];
var sub = child.querySelector('a');
if (sub.hasAttribute('href') && sub.getAttribute('href').slice(0,1) == '#') {
sub.onclick = this._onClick.bind(child);
}
}
for (i = this._closeButtons.length - 1; i >= 0; i--) {
child = this._closeButtons[i];
child.onclick = this._onCloseClick.bind(this);
}
};
ol.control.Sidebar.prototype.open = function(id) {
var i, child;
// hide old active contents and show new content
for (i = this._panes.length - 1; i >= 0; i--) {
child = this._panes[i];
if (child.id == id)
child.classList.add('active');
else if (child.classList.contains('active'))
child.classList.remove('active');
}
// remove old active highlights and set new highlight
for (i = this._tabitems.length - 1; i >= 0; i--) {
child = this._tabitems[i];
if (child.querySelector('a').hash == '#' + id)
child.classList.add('active');
else if (child.classList.contains('active'))
child.classList.remove('active');
}
// open sidebar (if necessary)
if (this.element.classList.contains('collapsed')) {
this.element.classList.remove('collapsed');
}
return this;
};
ol.control.Sidebar.prototype.close = function() {
// remove old active highlights
for (var i = this._tabitems.length - 1; i >= 0; i--) {
var child = this._tabitems[i];
if (child.classList.contains('active'))
child.classList.remove('active');
}
// close sidebar
if (!this.element.classList.contains('collapsed')) {
this.element.classList.add('collapsed');
}
return this;
};
ol.control.Sidebar.prototype._onClick = function(evt) {
evt.preventDefault();
if (this.classList.contains('active')) {
this._sidebar.close();
} else if (!this.classList.contains('disabled')) {
this._sidebar.open(this.querySelector('a').hash.slice(1));
}
};
ol.control.Sidebar.prototype._onCloseClick = function() {
this.close();
};

View File

@@ -1 +0,0 @@
ol.control.Sidebar=function(t){var e,s;for(this._options=Object.assign({},{element:null,position:"left"},t),ol.control.Control.call(this,{element:document.getElementById(this._options.element),target:this._options.target}),this.element.classList.add("sidebar-"+this._options.position),e=this.element.children.length-1;e>=0;e--)"DIV"===(s=this.element.children[e]).tagName&&s.classList.contains("sidebar-content")&&(this._container=s);for(this._tabitems=this.element.querySelectorAll("ul.sidebar-tabs > li, .sidebar-tabs > ul > li"),e=this._tabitems.length-1;e>=0;e--)this._tabitems[e]._sidebar=this;for(this._panes=[],this._closeButtons=[],e=this._container.children.length-1;e>=0;e--)if("DIV"==(s=this._container.children[e]).tagName&&s.classList.contains("sidebar-pane")){this._panes.push(s);for(var i=s.querySelectorAll(".sidebar-close"),o=0,l=i.length;o<l;o++)this._closeButtons.push(i[o])}},"inherits"in ol?ol.inherits(ol.control.Sidebar,ol.control.Control):(ol.control.Sidebar.prototype=Object.create(ol.control.Control.prototype),ol.control.Sidebar.prototype.constructor=ol.control.Sidebar),ol.control.Sidebar.prototype.setMap=function(t){var e,s;for(e=this._tabitems.length-1;e>=0;e--){var i=(s=this._tabitems[e]).querySelector("a");i.hasAttribute("href")&&"#"==i.getAttribute("href").slice(0,1)&&(i.onclick=this._onClick.bind(s))}for(e=this._closeButtons.length-1;e>=0;e--)(s=this._closeButtons[e]).onclick=this._onCloseClick.bind(this)},ol.control.Sidebar.prototype.open=function(t){var e,s;for(e=this._panes.length-1;e>=0;e--)(s=this._panes[e]).id==t?s.classList.add("active"):s.classList.contains("active")&&s.classList.remove("active");for(e=this._tabitems.length-1;e>=0;e--)(s=this._tabitems[e]).querySelector("a").hash=="#"+t?s.classList.add("active"):s.classList.contains("active")&&s.classList.remove("active");return this.element.classList.contains("collapsed")&&this.element.classList.remove("collapsed"),this},ol.control.Sidebar.prototype.close=function(){for(var t=this._tabitems.length-1;t>=0;t--){var e=this._tabitems[t];e.classList.contains("active")&&e.classList.remove("active")}return this.element.classList.contains("collapsed")||this.element.classList.add("collapsed"),this},ol.control.Sidebar.prototype._onClick=function(t){t.preventDefault(),this.classList.contains("active")?this._sidebar.close():this.classList.contains("disabled")||this._sidebar.open(this.querySelector("a").hash.slice(1))},ol.control.Sidebar.prototype._onCloseClick=function(){this.close()};

View File

@@ -1,40 +0,0 @@
{
"name": "sidebar-v2",
"version": "0.4.0",
"description": "A responsive sidebar for mapping libraries like Leaflet or OpenLayers",
"keywords": [
"gis",
"leaflet",
"map",
"openlayers"
],
"homepage": "https://github.com/turbo87/sidebar-v2",
"bugs": "https://github.com/turbo87/sidebar-v2/issues",
"license": "MIT",
"author": "Tobias Bieniek <tobias.bieniek@gmx.de>",
"files": [
"css",
"js",
"scss"
],
"repository": {
"type": "git",
"url": "https://github.com/turbo87/sidebar-v2.git"
},
"scripts": {
"lint:css": "stylelint **/*.scss",
"lint:js": "eslint . --cache"
},
"devDependencies": {
"eslint": "^6.8.0",
"gulp": "~4.0.0",
"gulp-clean-css": "~4.3.0",
"gulp-concat": "~2.6.1",
"gulp-rename": "~2.0.0",
"gulp-sass": "^4.1.0",
"gulp-uglify": "~3.0.2",
"stylelint": "^13.12.0",
"stylelint-config-recommended-scss": "^4.2.0",
"stylelint-scss": "^3.19.0"
}
}

View File

@@ -1,261 +0,0 @@
$threshold-lg: 1200px !default;
$threshold-md: 992px !default;
$threshold-sm: 768px !default;
$width-lg: 460px !default;
$width-md: 390px !default;
$width-sm: 305px !default;
$width-xs: 100% !default;
$sidebar-z-index: 2000 !default;
$sidebar-transition: 500ms !default;
$tab-size: 40px !default;
$tab-font-size: 12pt !default;
$tab-bg: null !default;
$tab-transition: 80ms !default;
$header-fg: $tab-active-fg !default;
$header-bg: $tab-active-bg !default;
$content-bg: rgba(255, 255, 255, 0.95) !default;
$content-padding-vertical: 10px !default;
$content-padding-horizontal: 20px !default;
$move-map-in-xs: true !default;
.sidebar {
position: absolute;
top: 0;
bottom: 0;
width: $width-xs;
overflow: hidden;
z-index: $sidebar-z-index;
&.collapsed {
width: $tab-size;
}
@media(min-width:$threshold-sm) {
top: $sidebar-margins;
bottom: $sidebar-margins;
transition: width $sidebar-transition;
}
@media(min-width:$threshold-sm) and (max-width:$threshold-md - 1px) {
width: $width-sm;
}
@media(min-width:$threshold-md) and (max-width:$threshold-lg - 1px) {
width: $width-md;
}
@media(min-width:$threshold-lg) {
width: $width-lg;
}
}
.sidebar-left {
left: 0;
@media(min-width:$threshold-sm) {
left: $sidebar-margins;
}
}
.sidebar-right {
right: 0;
@media(min-width:$threshold-sm) {
right: $sidebar-margins;
}
}
.sidebar-tabs {
top: 0;
bottom: 0;
height: 100%;
.sidebar-left & {
left: 0;
}
.sidebar-right & {
right: 0;
}
background-color: $tabs-bg;
&, & > ul {
position: absolute;
width: $tab-size;
margin: 0;
padding: 0;
list-style-type: none;
& > li {
width: 100%;
height: $tab-size;
color: $tab-fg;
@if $tab-bg { background: $tab-bg; }
font-size: $tab-font-size;
overflow: hidden;
transition: all $tab-transition;
&:hover {
color: $tab-hover-fg;
background-color: $tab-hover-bg;
}
&.active {
color: $tab-active-fg;
background-color: $tab-active-bg;
}
&.disabled {
color: fade-out($tab-fg, 0.6);
&:hover {
@if $tab-bg {
background: $tab-bg;
} @else {
background: transparent;
}
}
& > a {
cursor: default;
}
}
& > a {
display: block;
width: 100%;
height: 100%;
line-height: $tab-size;
color: inherit;
text-decoration: none;
text-align: center;
}
}
}
& > ul + ul {
bottom: 0;
}
}
.sidebar-content {
position: absolute;
.sidebar-left & {
left: $tab-size;
right: 0;
}
.sidebar-right & {
left: 0;
right: $tab-size;
}
top: 0;
bottom: 0;
background-color: $content-bg;
overflow-x: hidden;
overflow-y: auto;
.sidebar.collapsed > & {
overflow-y: hidden;
}
}
.sidebar-pane {
display: none;
left: 0;
right: 0;
box-sizing: border-box;
padding: $content-padding-vertical $content-padding-horizontal;
&.active {
display: block;
}
@media(min-width:$threshold-sm) and (max-width:$threshold-md - 1px) {
min-width: $width-sm - $tab-size;
}
@media(min-width:$threshold-md) and (max-width:$threshold-lg - 1px) {
min-width: $width-md - $tab-size;
}
@media(min-width:$threshold-lg) {
min-width: $width-lg - $tab-size;
}
}
.sidebar-header {
margin: (-$content-padding-vertical) (-$content-padding-horizontal) 0;
height: $tab-size;
padding: 0 $content-padding-horizontal;
line-height: $tab-size;
font-size: $tab-font-size * 1.2;
color: $header-fg;
background-color: $header-bg;
.sidebar-right & {
padding-left: $tab-size;
}
}
.sidebar-close {
position: absolute;
top: 0;
width: $tab-size;
height: $tab-size;
text-align: center;
cursor: pointer;
.sidebar-left & {
right: 0;
}
.sidebar-right & {
left: 0;
}
}
@if $move-map-in-xs {
.sidebar-left ~ .sidebar-map {
margin-left: $tab-size;
@media(min-width:$threshold-sm) {
margin-left: 0;
}
}
.sidebar-right ~ .sidebar-map {
margin-right: $tab-size;
@media(min-width:$threshold-sm) {
margin-right: 0;
}
}
}

View File

@@ -1,50 +0,0 @@
$sidebar-bg: rgba(255, 255, 255, 0.4) !default;
$sidebar-border-width: 3px !default;
$sidebar-border-radius: 4px !default;
$sidebar-border: $sidebar-border-width solid transparent !default;
$tab-fg: #fff !default;
$tabs-bg: rgba(0, 60, 136, 0.5) !default;
$tab-hover-fg: #fff !default;
$tab-hover-bg: rgba(0, 60, 136, 0.6) !default;
$tab-active-fg: #fff !default;
$tab-active-bg: #0074d9 !default;
$move-map-in-xs: false !default;
@import 'base';
.sidebar {
background-color: $sidebar-bg;
@media(min-width:$threshold-sm) {
border: $sidebar-border;
border-radius: $sidebar-border-radius;
}
}
.sidebar-left {
border-right: $sidebar-border;
}
.sidebar-right {
border-left: $sidebar-border;
}
.sidebar-tabs {
overflow: hidden;
@media(min-width:$threshold-sm) {
border-radius: $sidebar-inner-border-radius 0 0 $sidebar-inner-border-radius;
.collapsed & {
border-radius: $sidebar-inner-border-radius;
}
}
}
.sidebar-content {
@media(min-width:$threshold-sm) {
border-radius: 0 $sidebar-inner-border-radius $sidebar-inner-border-radius 0;
}
}

View File

@@ -1,86 +0,0 @@
$sidebar-margins: 10px !default;
$sidebar-left-bottom-margin: $sidebar-margins + 25px !default;
$sidebar-right-bottom-margin: $sidebar-margins + 14px !default;
$sidebar-border: 0 !default;
$sidebar-border-radius: 2px !default;
$sidebar-shadow: rgba(0, 0, 0, 0.298039) 0 1px 4px -1px !default;
$tab-fg: #666 !default;
$tabs-bg: #fff !default;
$tab-active-fg: #000 !default;
$tab-active-bg: #febf00 !default;
$tab-hover-fg: #000 !default;
$tab-hover-bg: ($tabs-bg * 9 + $tab-active-bg) / 10 !default;
@import 'base';
.sidebar {
border-right: $sidebar-border;
box-shadow: $sidebar-shadow;
@media(min-width:$threshold-sm) {
border: $sidebar-border;
border-radius: $sidebar-border-radius;
}
}
.sidebar-left {
@media(min-width:$threshold-sm) {
bottom: $sidebar-left-bottom-margin;
}
& ~ .sidebar-map .gm-style > div.gmnoprint[style*="left: 0px"] {
@media(min-width:$threshold-sm) {
transition: margin-left $sidebar-transition;
}
@media(min-width:$threshold-sm) and (max-width:$threshold-md - 1px) {
margin-left: $width-sm + $sidebar-margins * 2 !important;
}
@media(min-width:$threshold-md) and (max-width:$threshold-lg - 1px) {
margin-left: $width-md + $sidebar-margins * 2 !important;
}
@media(min-width:$threshold-lg) {
margin-left: $width-lg + $sidebar-margins * 2 !important;
}
}
@media(min-width:$threshold-sm) {
&.collapsed ~ .sidebar-map .gm-style > div.gmnoprint[style*="left: 0px"] {
margin-left: $tab-size + $sidebar-margins * 2 !important;
}
}
}
.sidebar-right {
@media(min-width:$threshold-sm) {
bottom: $sidebar-right-bottom-margin;
}
& ~ .sidebar-map .gm-style > div.gmnoprint[style*="right: 28px"] {
@media(min-width:$threshold-sm) {
transition: margin-right $sidebar-transition;
}
@media(min-width:$threshold-sm) and (max-width:$threshold-md - 1px) {
margin-right: $width-sm + $sidebar-margins * 2 !important;
}
@media(min-width:$threshold-md) and (max-width:$threshold-lg - 1px) {
margin-right: $width-md + $sidebar-margins * 2 !important;
}
@media(min-width:$threshold-lg) {
margin-right: $width-lg + $sidebar-margins * 2 !important;
}
}
@media(min-width:$threshold-sm) {
&.collapsed ~ .sidebar-map .gm-style > div.gmnoprint[style*="right: 28px"] {
margin-right: $tab-size + $sidebar-margins * 2 !important;
}
}
}

View File

@@ -1,84 +0,0 @@
$sidebar-margins: 10px !default;
$sidebar-border-radius: 4px !default;
$sidebar-touch-border: 2px solid rgba(0, 0, 0, 0.2) !default;
$sidebar-shadow: 0 1px 5px rgba(0, 0, 0, 0.65) !default;
$tab-fg: #333 !default;
$tabs-bg: #fff !default;
$tab-hover-fg: #000 !default;
$tab-hover-bg: #eee !default;
$tab-active-fg: #fff !default;
$tab-active-bg: #0074d9 !default;
@import 'base';
.sidebar {
box-shadow: $sidebar-shadow;
&.leaflet-touch {
box-shadow: none;
border-right: $sidebar-touch-border;
}
@media(min-width:$threshold-sm) {
border-radius: $sidebar-border-radius;
&.leaflet-touch {
border: $sidebar-touch-border;
}
}
}
.sidebar-left {
& ~ .sidebar-map .leaflet-left {
@media(min-width:$threshold-sm) {
transition: left $sidebar-transition;
}
@media(min-width:$threshold-sm) and (max-width:$threshold-md - 1px) {
left: $width-sm + $sidebar-margins;
}
@media(min-width:$threshold-md) and (max-width:$threshold-lg - 1px) {
left: $width-md + $sidebar-margins;
}
@media(min-width:$threshold-lg) {
left: $width-lg + $sidebar-margins;
}
}
&.collapsed ~ .sidebar-map .leaflet-left {
@media(min-width:$threshold-sm) {
left: $tab-size + $sidebar-margins;
}
}
}
.sidebar-right {
& ~ .sidebar-map .leaflet-right {
@media(min-width:$threshold-sm) {
transition: right $sidebar-transition;
}
@media(min-width:$threshold-sm) and (max-width:$threshold-md - 1px) {
right: $width-sm + $sidebar-margins;
}
@media(min-width:$threshold-md) and (max-width:$threshold-lg - 1px) {
right: $width-md + $sidebar-margins;
}
@media(min-width:$threshold-lg) {
right: $width-lg + $sidebar-margins;
}
}
&.collapsed ~ .sidebar-map .leaflet-right {
@media(min-width:$threshold-sm) {
right: $tab-size + $sidebar-margins;
}
}
}

View File

@@ -1,80 +0,0 @@
$sidebar-margins: 8px !default;
$sidebar-inner-border-radius: 4px !default;
@import 'ol-base';
.sidebar-left {
& ~ .sidebar-map {
.olControlZoom,
.olScaleLine {
margin-left: $tab-size + $sidebar-border-width * 2;
@media(min-width: $threshold-sm) {
transition: margin-left $sidebar-transition;
}
@media(min-width: $threshold-sm) and (max-width: $threshold-md - 1px) {
margin-left: $width-sm + $sidebar-margins + $sidebar-border-width * 2;
}
@media(min-width: $threshold-md) and (max-width: $threshold-lg - 1px) {
margin-left: $width-md + $sidebar-margins + $sidebar-border-width * 2;
}
@media(min-width: $threshold-lg) {
margin-left: $width-lg + $sidebar-margins + $sidebar-border-width * 2;
}
}
}
&.collapsed ~ .sidebar-map {
.olControlZoom,
.olScaleLine {
@media(min-width:$threshold-sm) {
margin-left: $tab-size + $sidebar-margins + $sidebar-border-width * 2;
}
}
}
}
.sidebar-right {
& ~ .sidebar-map {
.olControlAttribution,
.olControlPermalink,
.olControlMousePosition {
margin-right: $tab-size + $sidebar-border-width * 2;
@media(min-width: $threshold-sm) {
transition: margin-right $sidebar-transition;
}
@media(min-width: $threshold-sm) and (max-width: $threshold-md - 1px) {
margin-right: $width-sm + $sidebar-margins + $sidebar-border-width * 2;
}
@media(min-width: $threshold-md) and (max-width: $threshold-lg - 1px) {
margin-right: $width-md + $sidebar-margins + $sidebar-border-width * 2;
}
@media(min-width: $threshold-lg) {
margin-right: $width-lg + $sidebar-margins + $sidebar-border-width * 2;
}
}
}
&.collapsed ~ .sidebar-map {
.olControlAttribution,
.olControlPermalink,
.olControlMousePosition {
@media(min-width:$threshold-sm) {
margin-right: $tab-size + $sidebar-margins + $sidebar-border-width * 2;
}
}
}
}

View File

@@ -1,81 +0,0 @@
$sidebar-margins: 6px !default;
$sidebar-inner-border-radius: 2px !default;
@import 'ol-base';
.sidebar-left {
& ~ .sidebar-map {
.ol-zoom, .ol-scale-line {
margin-left: $tab-size + $sidebar-border-width * 2;
@media(min-width: $threshold-sm) {
transition: margin-left $sidebar-transition;
}
@media(min-width: $threshold-sm) and (max-width: $threshold-md - 1px) {
margin-left: $width-sm + $sidebar-margins + $sidebar-border-width * 2;
}
@media(min-width: $threshold-md) and (max-width: $threshold-lg - 1px) {
margin-left: $width-md + $sidebar-margins + $sidebar-border-width * 2;
}
@media(min-width: $threshold-lg) {
margin-left: $width-lg + $sidebar-margins + $sidebar-border-width * 2;
}
}
}
&.collapsed ~ .sidebar-map {
.ol-zoom, .ol-scale-line {
@media(min-width:$threshold-sm) {
margin-left: $tab-size + $sidebar-margins + $sidebar-border-width * 2;
}
}
}
}
.sidebar-right {
& ~ .sidebar-map {
.ol-rotate,
.ol-attribution,
.ol-full-screen {
margin-right: $tab-size + $sidebar-border-width * 2;
@media(min-width: $threshold-sm) {
transition: margin-right $sidebar-transition;
}
@media(min-width: $threshold-sm) and (max-width: $threshold-md - 1px) {
margin-right: $width-sm + $sidebar-margins + $sidebar-border-width * 2;
}
@media(min-width: $threshold-md) and (max-width: $threshold-lg - 1px) {
margin-right: $width-md + $sidebar-margins + $sidebar-border-width * 2;
}
@media(min-width: $threshold-lg) {
margin-right: $width-lg + $sidebar-margins + $sidebar-border-width * 2;
}
}
}
&.collapsed ~ .sidebar-map {
.ol-rotate,
.ol-attribution,
.ol-full-screen {
@media(min-width:$threshold-sm) {
margin-right: $tab-size + $sidebar-margins + $sidebar-border-width * 2;
}
}
}
}

View File

@@ -1,26 +0,0 @@
/// <reference types="leaflet" />
declare namespace L {
namespace Control {
interface SidebarOptions {
position: string;
}
class Sidebar extends Control {
constructor(id: string, options?: SidebarOptions);
options: Control.ControlOptions;
addTo(map: L.Map): this;
remove(map: L.Map): this;
open(id: string): this;
close(): this;
}
}
namespace control {
function sidebar(id: string, options?: Control.SidebarOptions): L.Control.Sidebar;
}
}

View File

@@ -1,102 +0,0 @@
.leaflet-sidebar {
position: absolute;
height: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 10px;
z-index: 2000; }
.leaflet-sidebar.left {
left: -500px;
transition: left 0.5s, width 0.5s;
padding-right: 0; }
.leaflet-sidebar.left.visible {
left: 0; }
.leaflet-sidebar.right {
right: -500px;
transition: right 0.5s, width 0.5s;
padding-left: 0; }
.leaflet-sidebar.right.visible {
right: 0; }
.leaflet-sidebar > .leaflet-control {
height: 100%;
width: 100%;
overflow: auto;
-webkit-overflow-scrolling: touch;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 8px 24px;
font-size: 1.1em;
background: white;
box-shadow: 0 1px 7px rgba(0, 0, 0, 0.65);
-webkit-border-radius: 4px;
border-radius: 4px; }
.leaflet-touch .leaflet-sidebar > .leaflet-control {
box-shadow: none;
border: 2px solid rgba(0, 0, 0, 0.2);
background-clip: padding-box; }
@media (max-width: 767px) {
.leaflet-sidebar {
width: 100%;
padding: 0; }
.leaflet-sidebar.left.visible ~ .leaflet-left {
left: 100%; }
.leaflet-sidebar.right.visible ~ .leaflet-right {
right: 100%; }
.leaflet-sidebar.left {
left: -100%; }
.leaflet-sidebar.left.visible {
left: 0; }
.leaflet-sidebar.right {
right: -100%; }
.leaflet-sidebar.right.visible {
right: 0; }
.leaflet-sidebar > .leaflet-control {
box-shadow: none;
-webkit-border-radius: 0;
border-radius: 0; }
.leaflet-touch .leaflet-sidebar > .leaflet-control {
border: 0; } }
@media (min-width: 768px) and (max-width: 991px) {
.leaflet-sidebar {
width: 305px; }
.leaflet-sidebar.left.visible ~ .leaflet-left {
left: 305px; }
.leaflet-sidebar.right.visible ~ .leaflet-right {
right: 305px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.leaflet-sidebar {
width: 390px; }
.leaflet-sidebar.left.visible ~ .leaflet-left {
left: 390px; }
.leaflet-sidebar.right.visible ~ .leaflet-right {
right: 390px; } }
@media (min-width: 1200px) {
.leaflet-sidebar {
width: 460px; }
.leaflet-sidebar.left.visible ~ .leaflet-left {
left: 460px; }
.leaflet-sidebar.right.visible ~ .leaflet-right {
right: 460px; } }
.leaflet-sidebar .close {
position: absolute;
right: 20px;
top: 20px;
width: 31px;
height: 31px;
color: #333;
font-size: 25px;
line-height: 1em;
text-align: center;
background: white;
-webkit-border-radius: 16px;
border-radius: 16px;
cursor: pointer;
z-index: 1000; }
.leaflet-left {
transition: left 0.5s; }
.leaflet-right {
transition: right 0.5s; }

View File

@@ -1,202 +0,0 @@
L.Control.Sidebar = L.Control.extend({
includes: L.Evented.prototype || L.Mixin.Events,
options: {
closeButton: true,
position: 'left',
autoPan: true,
},
initialize: function (placeholder, options) {
L.setOptions(this, options);
// Find content container
var content = this._contentContainer = L.DomUtil.get(placeholder);
// Remove the content container from its original parent
if(content.parentNode != undefined){
content.parentNode.removeChild(content);
}
var l = 'leaflet-';
// Create sidebar container
var container = this._container =
L.DomUtil.create('div', l + 'sidebar ' + this.options.position);
// Style and attach content container
L.DomUtil.addClass(content, l + 'control');
container.appendChild(content);
// Create close button and attach it if configured
if (this.options.closeButton) {
var close = this._closeButton =
L.DomUtil.create('a', 'close', container);
close.innerHTML = '&times;';
}
},
addTo: function (map) {
var container = this._container;
var content = this._contentContainer;
// Attach event to close button
if (this.options.closeButton) {
var close = this._closeButton;
L.DomEvent.on(close, 'click', this.hide, this);
}
L.DomEvent
.on(container, 'transitionend',
this._handleTransitionEvent, this)
.on(container, 'webkitTransitionEnd',
this._handleTransitionEvent, this);
// Attach sidebar container to controls container
var controlContainer = map._controlContainer;
controlContainer.insertBefore(container, controlContainer.firstChild);
this._map = map;
// Make sure we don't drag the map when we interact with the content
var stop = L.DomEvent.stopPropagation;
var fakeStop = L.DomEvent._fakeStop || stop;
L.DomEvent
.on(content, 'contextmenu', stop)
.on(content, 'click', fakeStop)
.on(content, 'mousedown', stop)
.on(content, 'touchstart', stop)
.on(content, 'dblclick', fakeStop)
.on(content, 'mousewheel', stop)
.on(content, 'wheel', stop)
.on(content, 'scroll', stop)
.on(content, 'MozMousePixelScroll', stop);
return this;
},
removeFrom: function (map) {
//if the control is visible, hide it before removing it.
this.hide();
var container = this._container;
var content = this._contentContainer;
// Remove sidebar container from controls container
var controlContainer = map._controlContainer;
controlContainer.removeChild(container);
//disassociate the map object
this._map = null;
// Unregister events to prevent memory leak
var stop = L.DomEvent.stopPropagation;
var fakeStop = L.DomEvent._fakeStop || stop;
L.DomEvent
.off(content, 'contextmenu', stop)
.off(content, 'click', fakeStop)
.off(content, 'mousedown', stop)
.off(content, 'touchstart', stop)
.off(content, 'dblclick', fakeStop)
.off(content, 'mousewheel', stop)
.off(content, 'wheel', stop)
.off(content, 'scroll', stop)
.off(content, 'MozMousePixelScroll', stop);
L.DomEvent
.off(container, 'transitionend',
this._handleTransitionEvent, this)
.off(container, 'webkitTransitionEnd',
this._handleTransitionEvent, this);
if (this._closeButton && this._close) {
var close = this._closeButton;
L.DomEvent.off(close, 'click', this.hide, this);
}
return this;
},
isVisible: function () {
return L.DomUtil.hasClass(this._container, 'visible');
},
show: function () {
if (!this.isVisible()) {
L.DomUtil.addClass(this._container, 'visible');
if (this.options.autoPan) {
this._map.panBy([-this.getOffset() / 2, 0], {
duration: 0.5
});
}
this.fire('show');
}
},
hide: function (e) {
if (this.isVisible()) {
L.DomUtil.removeClass(this._container, 'visible');
if (this.options.autoPan) {
this._map.panBy([this.getOffset() / 2, 0], {
duration: 0.5
});
}
this.fire('hide');
}
if(e) {
L.DomEvent.stopPropagation(e);
}
},
toggle: function () {
if (this.isVisible()) {
this.hide();
} else {
this.show();
}
},
getContainer: function () {
return this._contentContainer;
},
getCloseButton: function () {
return this._closeButton;
},
setContent: function (content) {
var container = this.getContainer();
if (typeof content === 'string') {
container.innerHTML = content;
} else {
// clean current content
while (container.firstChild) {
container.removeChild(container.firstChild);
}
container.appendChild(content);
}
return this;
},
getOffset: function () {
if (this.options.position === 'right') {
return -this._container.offsetWidth;
} else {
return this._container.offsetWidth;
}
},
_handleTransitionEvent: function (e) {
if (e.propertyName == 'left' || e.propertyName == 'right')
this.fire(this.isVisible() ? 'shown' : 'hidden');
}
});
L.control.sidebar = function (placeholder, options) {
return new L.Control.Sidebar(placeholder, options);
};

View File

@@ -1,200 +0,0 @@
.sidebar {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
overflow: hidden;
z-index: 2000; }
.sidebar.collapsed {
width: 40px; }
@media (min-width: 768px) {
.sidebar {
top: 10px;
bottom: 10px;
transition: width 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar {
width: 305px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar {
width: 390px; } }
@media (min-width: 1200px) {
.sidebar {
width: 460px; } }
.sidebar-left {
left: 0; }
@media (min-width: 768px) {
.sidebar-left {
left: 10px; } }
.sidebar-right {
right: 0; }
@media (min-width: 768px) {
.sidebar-right {
right: 10px; } }
.sidebar-tabs {
top: 0;
bottom: 0;
height: 100%;
background-color: #fff; }
.sidebar-left .sidebar-tabs {
left: 0; }
.sidebar-right .sidebar-tabs {
right: 0; }
.sidebar-tabs, .sidebar-tabs > ul {
position: absolute;
width: 40px;
margin: 0;
padding: 0;
list-style-type: none; }
.sidebar-tabs > li, .sidebar-tabs > ul > li {
width: 100%;
height: 40px;
color: #333;
font-size: 12pt;
overflow: hidden;
transition: all 80ms; }
.sidebar-tabs > li:hover, .sidebar-tabs > ul > li:hover {
color: #000;
background-color: #eee; }
.sidebar-tabs > li.active, .sidebar-tabs > ul > li.active {
color: #fff;
background-color: #0074d9; }
.sidebar-tabs > li.disabled, .sidebar-tabs > ul > li.disabled {
color: rgba(51, 51, 51, 0.4); }
.sidebar-tabs > li.disabled:hover, .sidebar-tabs > ul > li.disabled:hover {
background: transparent; }
.sidebar-tabs > li.disabled > a, .sidebar-tabs > ul > li.disabled > a {
cursor: default; }
.sidebar-tabs > li > a, .sidebar-tabs > ul > li > a {
display: block;
width: 100%;
height: 100%;
line-height: 40px;
color: inherit;
text-decoration: none;
text-align: center; }
.sidebar-tabs > ul + ul {
bottom: 0; }
.sidebar-content {
position: absolute;
top: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.95);
overflow-x: hidden;
overflow-y: auto; }
.sidebar-left .sidebar-content {
left: 40px;
right: 0; }
.sidebar-right .sidebar-content {
left: 0;
right: 40px; }
.sidebar.collapsed > .sidebar-content {
overflow-y: hidden; }
.sidebar-pane {
display: none;
left: 0;
right: 0;
box-sizing: border-box;
padding: 10px 20px; }
.sidebar-pane.active {
display: block; }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-pane {
min-width: 265px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-pane {
min-width: 350px; } }
@media (min-width: 1200px) {
.sidebar-pane {
min-width: 420px; } }
.sidebar-header {
margin: -10px -20px 0;
height: 40px;
padding: 0 20px;
line-height: 40px;
font-size: 14.4pt;
color: #fff;
background-color: #0074d9; }
.sidebar-right .sidebar-header {
padding-left: 40px; }
.sidebar-close {
position: absolute;
top: 0;
width: 40px;
height: 40px;
text-align: center;
cursor: pointer; }
.sidebar-left .sidebar-close {
right: 0; }
.sidebar-right .sidebar-close {
left: 0; }
.sidebar-left ~ .sidebar-map {
margin-left: 40px; }
@media (min-width: 768px) {
.sidebar-left ~ .sidebar-map {
margin-left: 0; } }
.sidebar-right ~ .sidebar-map {
margin-right: 40px; }
@media (min-width: 768px) {
.sidebar-right ~ .sidebar-map {
margin-right: 0; } }
.sidebar {
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); }
.sidebar.leaflet-touch {
box-shadow: none;
border-right: 2px solid rgba(0, 0, 0, 0.2); }
@media (min-width: 768px) {
.sidebar {
border-radius: 4px; }
.sidebar.leaflet-touch {
border: 2px solid rgba(0, 0, 0, 0.2); } }
@media (min-width: 768px) {
.sidebar-left ~ .sidebar-map .leaflet-left {
transition: left 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-left ~ .sidebar-map .leaflet-left {
left: 315px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-left ~ .sidebar-map .leaflet-left {
left: 400px; } }
@media (min-width: 1200px) {
.sidebar-left ~ .sidebar-map .leaflet-left {
left: 470px; } }
@media (min-width: 768px) {
.sidebar-left.collapsed ~ .sidebar-map .leaflet-left {
left: 50px; } }
@media (min-width: 768px) {
.sidebar-right ~ .sidebar-map .leaflet-right {
transition: right 500ms; } }
@media (min-width: 768px) and (max-width: 991px) {
.sidebar-right ~ .sidebar-map .leaflet-right {
right: 315px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.sidebar-right ~ .sidebar-map .leaflet-right {
right: 400px; } }
@media (min-width: 1200px) {
.sidebar-right ~ .sidebar-map .leaflet-right {
right: 470px; } }
@media (min-width: 768px) {
.sidebar-right.collapsed ~ .sidebar-map .leaflet-right {
right: 50px; } }

View File

@@ -1,216 +0,0 @@
/* global L */
/**
* @name Sidebar
* @class L.Control.Sidebar
* @extends L.Control
* @param {string} id - The id of the sidebar element (without the # character)
* @param {Object} [options] - Optional options object
* @param {string} [options.position=left] - Position of the sidebar: 'left' or 'right'
* @see L.control.sidebar
*/
L.Control.Sidebar = L.Control.extend(/** @lends L.Control.Sidebar.prototype */ {
includes: (L.Evented.prototype || L.Mixin.Events),
options: {
position: 'left'
},
initialize: function (id, options) {
var i, child;
L.setOptions(this, options);
// Find sidebar HTMLElement
this._sidebar = L.DomUtil.get(id);
// Attach .sidebar-left/right class
L.DomUtil.addClass(this._sidebar, 'sidebar-' + this.options.position);
// Attach touch styling if necessary
if (L.Browser.touch)
L.DomUtil.addClass(this._sidebar, 'leaflet-touch');
// Find sidebar > div.sidebar-content
for (i = this._sidebar.children.length - 1; i >= 0; i--) {
child = this._sidebar.children[i];
if (child.tagName == 'DIV' &&
L.DomUtil.hasClass(child, 'sidebar-content'))
this._container = child;
}
// Find sidebar ul.sidebar-tabs > li, sidebar .sidebar-tabs > ul > li
this._tabitems = this._sidebar.querySelectorAll('ul.sidebar-tabs > li, .sidebar-tabs > ul > li');
for (i = this._tabitems.length - 1; i >= 0; i--) {
this._tabitems[i]._sidebar = this;
}
// Find sidebar > div.sidebar-content > div.sidebar-pane
this._panes = [];
this._closeButtons = [];
for (i = this._container.children.length - 1; i >= 0; i--) {
child = this._container.children[i];
if (child.tagName == 'DIV' &&
L.DomUtil.hasClass(child, 'sidebar-pane')) {
this._panes.push(child);
var closeButtons = child.querySelectorAll('.sidebar-close');
for (var j = 0, len = closeButtons.length; j < len; j++)
this._closeButtons.push(closeButtons[j]);
}
}
},
/**
* Add this sidebar to the specified map.
*
* @param {L.Map} map
* @returns {Sidebar}
*/
addTo: function (map) {
var i, child;
this._map = map;
for (i = this._tabitems.length - 1; i >= 0; i--) {
child = this._tabitems[i];
var sub = child.querySelector('a');
if (sub.hasAttribute('href') && sub.getAttribute('href').slice(0,1) == '#') {
L.DomEvent
.on(sub, 'click', L.DomEvent.preventDefault )
.on(sub, 'click', this._onClick, child);
}
}
for (i = this._closeButtons.length - 1; i >= 0; i--) {
child = this._closeButtons[i];
L.DomEvent.on(child, 'click', this._onCloseClick, this);
}
return this;
},
/**
* @deprecated - Please use remove() instead of removeFrom(), as of Leaflet 0.8-dev, the removeFrom() has been replaced with remove()
* Removes this sidebar from the map.
* @param {L.Map} map
* @returns {Sidebar}
*/
removeFrom: function(map) {
console.log('removeFrom() has been deprecated, please use remove() instead as support for this function will be ending soon.');
this.remove(map);
},
/**
* Remove this sidebar from the map.
*
* @param {L.Map} map
* @returns {Sidebar}
*/
remove: function (map) {
var i, child;
this._map = null;
for (i = this._tabitems.length - 1; i >= 0; i--) {
child = this._tabitems[i];
L.DomEvent.off(child.querySelector('a'), 'click', this._onClick);
}
for (i = this._closeButtons.length - 1; i >= 0; i--) {
child = this._closeButtons[i];
L.DomEvent.off(child, 'click', this._onCloseClick, this);
}
return this;
},
/**
* Open sidebar (if necessary) and show the specified tab.
*
* @param {string} id - The id of the tab to show (without the # character)
*/
open: function(id) {
var i, child;
// hide old active contents and show new content
for (i = this._panes.length - 1; i >= 0; i--) {
child = this._panes[i];
if (child.id == id)
L.DomUtil.addClass(child, 'active');
else if (L.DomUtil.hasClass(child, 'active'))
L.DomUtil.removeClass(child, 'active');
}
// remove old active highlights and set new highlight
for (i = this._tabitems.length - 1; i >= 0; i--) {
child = this._tabitems[i];
if (child.querySelector('a').hash == '#' + id)
L.DomUtil.addClass(child, 'active');
else if (L.DomUtil.hasClass(child, 'active'))
L.DomUtil.removeClass(child, 'active');
}
this.fire('content', { id: id });
// open sidebar (if necessary)
if (L.DomUtil.hasClass(this._sidebar, 'collapsed')) {
this.fire('opening');
L.DomUtil.removeClass(this._sidebar, 'collapsed');
}
return this;
},
/**
* Close the sidebar (if necessary).
*/
close: function() {
// remove old active highlights
for (var i = this._tabitems.length - 1; i >= 0; i--) {
var child = this._tabitems[i];
if (L.DomUtil.hasClass(child, 'active'))
L.DomUtil.removeClass(child, 'active');
}
// close sidebar
if (!L.DomUtil.hasClass(this._sidebar, 'collapsed')) {
this.fire('closing');
L.DomUtil.addClass(this._sidebar, 'collapsed');
}
return this;
},
/**
* @private
*/
_onClick: function() {
if (L.DomUtil.hasClass(this, 'active'))
this._sidebar.close();
else if (!L.DomUtil.hasClass(this, 'disabled'))
this._sidebar.open(this.querySelector('a').hash.slice(1));
},
/**
* @private
*/
_onCloseClick: function () {
this.close();
}
});
/**
* Creates a new sidebar.
*
* @example
* var sidebar = L.control.sidebar('sidebar').addTo(map);
*
* @param {string} id - The id of the sidebar element (without the # character)
* @param {Object} [options] - Optional options object
* @param {string} [options.position=left] - Position of the sidebar: 'left' or 'right'
* @returns {Sidebar} A new sidebar instance
*/
L.control.sidebar = function (id, options) {
return new L.Control.Sidebar(id, options);
};

281
public/admin.css Normal file
View File

@@ -0,0 +1,281 @@
/* =====================================================================
Moderation Page Styles
===================================================================== */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #f4f5f7;
color: #1a1a2e;
font-size: 15px;
}
/* -----------------------------------------------------------------
Header
----------------------------------------------------------------- */
.admin-header {
background: var(--color-primary);
color: white;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-header h1 { font-size: 1.2rem; }
.admin-header a {
color: white;
text-decoration: none;
opacity: 0.8;
font-size: 0.85rem;
}
.admin-header a:hover { opacity: 1; }
.admin-nav {
display: flex;
gap: 16px;
align-items: center;
}
/* -----------------------------------------------------------------
Container
----------------------------------------------------------------- */
.admin-container {
max-width: 900px;
margin: 24px auto;
padding: 0 16px;
}
/* -----------------------------------------------------------------
Statistics Cards
----------------------------------------------------------------- */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
margin-bottom: 32px;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 16px;
text-align: center;
border: 1px solid #e0e0e0;
}
.stat-card .stat-number {
font-size: 1.8rem;
font-weight: 700;
color: var(--color-primary);
}
.stat-card .stat-label {
font-size: 0.8rem;
color: #5a5a7a;
margin-top: 4px;
}
/* -----------------------------------------------------------------
Section Headers
----------------------------------------------------------------- */
h2 {
font-size: 1.1rem;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid var(--color-primary);
}
.section { margin-bottom: 40px; }
/* -----------------------------------------------------------------
Contribution Rows
----------------------------------------------------------------- */
.contribution-row {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.contribution-info { flex: 1; }
.contribution-info .title {
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 4px;
}
.contribution-info .meta {
font-size: 0.8rem;
color: #5a5a7a;
margin-bottom: 4px;
}
.contribution-info .description {
font-size: 0.85rem;
color: #5a5a7a;
line-height: 1.4;
}
/* -----------------------------------------------------------------
Badges
----------------------------------------------------------------- */
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-pending { background: #fff3cd; color: #856404; }
.badge-approved { background: #d4edda; color: #155724; }
.badge-rejected { background: #f8d7da; color: #721c24; }
.badge-point { background: #e3f2fd; color: #1565c0; }
.badge-line { background: #f3e5f5; color: #6a1b9a; }
.badge-polygon { background: #e8f5e9; color: #2e7d32; }
/* -----------------------------------------------------------------
Action Buttons
----------------------------------------------------------------- */
.action-buttons {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
}
.btn-approve { background: #2e7d32; color: white; }
.btn-approve:hover { background: #1b5e20; }
.btn-reject { background: #c62828; color: white; }
.btn-reject:hover { background: #b71c1c; }
/* -----------------------------------------------------------------
Empty State
----------------------------------------------------------------- */
.empty-state {
text-align: center;
padding: 40px;
color: #999;
font-size: 0.9rem;
}
/* -----------------------------------------------------------------
Login Page
----------------------------------------------------------------- */
.login-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.login-box {
background: white;
border-radius: 12px;
padding: 32px;
max-width: 380px;
width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
text-align: center;
}
.login-box h1 {
font-size: 1.3rem;
margin-bottom: 8px;
}
.login-box p {
font-size: 0.85rem;
color: #5a5a7a;
margin-bottom: 20px;
}
.login-box input[type="password"] {
width: 100%;
padding: 10px 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 0.9rem;
margin-bottom: 12px;
font-family: 'Segoe UI', system-ui, sans-serif;
}
.login-box input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(0, 55, 109, 0.1);
}
.login-box button {
width: 100%;
padding: 10px;
background: var(--color-primary);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
font-family: 'Segoe UI', system-ui, sans-serif;
}
.login-box button:hover { filter: brightness(1.15); }
.error {
color: #c62828;
font-size: 0.85rem;
margin-bottom: 12px;
}
.back-link {
margin-top: 16px;
font-size: 0.8rem;
}
.back-link a { color: #5a5a7a; }
/* -----------------------------------------------------------------
Mobile Responsive
----------------------------------------------------------------- */
@media (max-width: 768px) {
.contribution-row {
flex-direction: column;
}
.action-buttons {
width: 100%;
}
.action-buttons form {
flex: 1;
}
.action-buttons .btn {
width: 100%;
justify-content: center;
}
}

254
public/admin.php Normal file
View File

@@ -0,0 +1,254 @@
<?php
// =====================================================================
// Moderation Page
// Lists pending Contributions for Review. Moderators can approve
// or reject Contributions.
// ToDo: Extend with News Management, User Management, Analytics.
// =====================================================================
require_once __DIR__ . '/api/db.php';
require_once __DIR__ . '/api/auth.php';
// -----------------------------------------------------------------
// Routing: Login, Logout, or Main Page
// -----------------------------------------------------------------
$page = $_GET['page'] ?? 'main';
// Handle Login Form Submission
if ($page === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$password = $_POST['password'] ?? '';
if (admin_login($password)) {
header('Location: admin.php');
exit;
} else {
$login_error = 'Falsches Passwort.';
}
}
// Handle Logout
if ($page === 'logout') {
admin_logout();
header('Location: admin.php?page=login');
exit;
}
// -----------------------------------------------------------------
// Load Municipality for Theming (needed for both Login and Main Page)
// -----------------------------------------------------------------
$pdo = get_db();
$stmt = $pdo->prepare("SELECT * FROM municipalities WHERE slug = :slug");
$stmt->execute([':slug' => 'lohne']);
$municipality = $stmt->fetch();
// Show Login Page if not authenticated
if ($page === 'login' || !is_admin()) {
show_login_page($municipality, $login_error ?? null);
exit;
}
// -----------------------------------------------------------------
// Handle Moderation Actions (Approve / Reject)
// -----------------------------------------------------------------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mod_action'])) {
$contribution_id = $_POST['contribution_id'] ?? '';
$mod_action = $_POST['mod_action'];
if ($contribution_id && in_array($mod_action, ['approved', 'rejected'])) {
$stmt = $pdo->prepare("UPDATE contributions SET status = :status WHERE contribution_id = :id");
$stmt->execute([':status' => $mod_action, ':id' => $contribution_id]);
}
// Redirects to prevent Form Resubmission on Refresh
header('Location: admin.php');
exit;
}
// -----------------------------------------------------------------
// Load Contributions Data
// -----------------------------------------------------------------
// Pending Contributions
$stmt = $pdo->prepare("
SELECT contribution_id, title, category, description, author_name, geom_type, status, created_at
FROM contributions
WHERE municipality_id = :mid AND status = 'pending'
ORDER BY created_at DESC
");
$stmt->execute([':mid' => $municipality['municipality_id']]);
$pending = $stmt->fetchAll();
// Recently moderated Contributions
$stmt = $pdo->prepare("
SELECT contribution_id, title, category, description, author_name, geom_type, status, created_at, updated_at
FROM contributions
WHERE municipality_id = :mid AND status IN ('approved', 'rejected')
ORDER BY updated_at DESC
LIMIT 20
");
$stmt->execute([':mid' => $municipality['municipality_id']]);
$moderated = $stmt->fetchAll();
// Statistics
$stmt = $pdo->prepare("
SELECT status, COUNT(*) as count
FROM contributions
WHERE municipality_id = :mid
GROUP BY status
");
$stmt->execute([':mid' => $municipality['municipality_id']]);
$stats_rows = $stmt->fetchAll();
$stats = [];
foreach ($stats_rows as $row) {
$stats[$row['status']] = $row['count'];
}
// -----------------------------------------------------------------
// Render Main Page
// -----------------------------------------------------------------
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Moderation — <?= htmlspecialchars($municipality['name']) ?></title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="admin.css">
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>
</head>
<body>
<div class="admin-header">
<h1><i class="fa-solid fa-shield-halved"></i> Moderation — <?= htmlspecialchars($municipality['name']) ?></h1>
<div class="admin-nav">
<a href="index.php"><i class="fa-solid fa-map"></i> Zur Karte</a>
<a href="admin.php?page=logout"><i class="fa-solid fa-right-from-bracket"></i> Abmelden</a>
</div>
</div>
<div class="admin-container">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number"><?= ($stats['pending'] ?? 0) ?></div>
<div class="stat-label">Ausstehend</div>
</div>
<div class="stat-card">
<div class="stat-number"><?= ($stats['approved'] ?? 0) ?></div>
<div class="stat-label">Freigegeben</div>
</div>
<div class="stat-card">
<div class="stat-number"><?= ($stats['rejected'] ?? 0) ?></div>
<div class="stat-label">Abgelehnt</div>
</div>
<div class="stat-card">
<div class="stat-number"><?= array_sum(array_column($stats_rows, 'count')) ?></div>
<div class="stat-label">Gesamt</div>
</div>
</div>
<div class="section">
<h2><i class="fa-solid fa-clock"></i> Ausstehende Beiträge (<?= count($pending) ?>)</h2>
<?php if (empty($pending)): ?>
<div class="empty-state">
<i class="fa-solid fa-check-circle" style="font-size:2rem;margin-bottom:8px;display:block;"></i>
Keine ausstehenden Beiträge.
</div>
<?php else: ?>
<?php foreach ($pending as $item): ?>
<div class="contribution-row">
<div class="contribution-info">
<div class="title"><?= htmlspecialchars($item['title']) ?></div>
<div class="meta">
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
<span class="badge badge-pending">ausstehend</span>
· <?= htmlspecialchars($item['category']) ?>
· <?= htmlspecialchars($item['author_name']) ?>
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
</div>
<?php if ($item['description']): ?>
<div class="description"><?= htmlspecialchars($item['description']) ?></div>
<?php endif; ?>
</div>
<div class="action-buttons">
<form method="POST">
<input type="hidden" name="contribution_id" value="<?= $item['contribution_id'] ?>">
<input type="hidden" name="mod_action" value="approved">
<button type="submit" class="btn btn-approve"><i class="fa-solid fa-check"></i> Freigeben</button>
</form>
<form method="POST">
<input type="hidden" name="contribution_id" value="<?= $item['contribution_id'] ?>">
<input type="hidden" name="mod_action" value="rejected">
<button type="submit" class="btn btn-reject"><i class="fa-solid fa-xmark"></i> Ablehnen</button>
</form>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<div class="section">
<h2><i class="fa-solid fa-history"></i> Kürzlich moderiert</h2>
<?php if (empty($moderated)): ?>
<div class="empty-state">Noch keine moderierten Beiträge.</div>
<?php else: ?>
<?php foreach ($moderated as $item): ?>
<div class="contribution-row">
<div class="contribution-info">
<div class="title"><?= htmlspecialchars($item['title']) ?></div>
<div class="meta">
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
<span class="badge badge-<?= $item['status'] ?>"><?= $item['status'] === 'approved' ? 'freigegeben' : 'abgelehnt' ?></span>
· <?= htmlspecialchars($item['category']) ?>
· <?= htmlspecialchars($item['author_name']) ?>
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</body>
</html>
<?php
// -----------------------------------------------------------------
// Login Page
// -----------------------------------------------------------------
function show_login_page($municipality, $error = null) {
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Moderation — Anmeldung</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="admin.css">
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>
</head>
<body>
<div class="login-wrapper">
<div class="login-box">
<h1><i class="fa-solid fa-shield-halved"></i> Moderation</h1>
<p>Bitte geben Sie das Moderationspasswort ein.</p>
<?php if ($error): ?>
<div class="error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST" action="admin.php?page=login">
<input type="password" name="password" placeholder="Passwort" autofocus>
<button type="submit"><i class="fa-solid fa-right-to-bracket"></i> Anmelden</button>
</form>
<div class="back-link"><a href="index.php">← Zurück zur Karte</a></div>
</div>
</div>
</body>
</html>
<?php
}
?>

41
public/api/auth.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
// =====================================================================
// Admin Authentication Helper
// Provides simple Password-based Session Authentication for the
// Moderation Page. Uses ADMIN_PASSWORD from .env File.
// ToDo: Replace with full User Authentication in Phase 3-3.
// =====================================================================
// Reads Admin Password from Environment
function get_admin_password() {
return getenv('ADMIN_PASSWORD');
}
// Checks if current Session is authenticated as Admin
function is_admin() {
return isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
}
// Authenticates with Password, returns true on Success
function admin_login($password) {
$correct = get_admin_password();
if ($correct && $password === $correct) {
$_SESSION['is_admin'] = true;
return true;
}
return false;
}
// Logs out Admin Session
function admin_logout() {
$_SESSION['is_admin'] = false;
session_destroy();
}
// Redirects to Login if not authenticated
function require_admin() {
if (!is_admin()) {
header('Location: admin.php?page=login');
exit;
}
}

View File

@@ -0,0 +1,360 @@
<?php
// =====================================================================
// Contributions API Endpoint
// Handles CRUD Operations for Contributions (Points, Lines, Polygons)
// and Voting. Actions are determined by the 'action' Parameter in
// the Request.
//
// Supported Actions:
// read — Load approved Contributions
// create — Insert Contributions
// update — Update Contributions
// delete — Delete Contributions
// vote — Like or Dislike Contributions
// =====================================================================
require_once __DIR__ . '/db.php';
// ---------------------------------------------------------------------
// Read Action Parameter and Route to correct Handler
// ---------------------------------------------------------------------
$input = get_input();
$action = $input['action'] ?? '';
switch ($action) {
case 'read':
handle_read($input);
break;
case 'create':
handle_create($input);
break;
case 'update':
handle_update($input);
break;
case 'delete':
handle_delete($input);
break;
case 'vote':
handle_vote($input);
break;
default:
error_response('Unknown Action. Supported Actions are read, create, update, delete, vote.');
}
// =====================================================================
// Action Handlers
// =====================================================================
// ---------------------------------------------------------------------
// READ: Loads approved Contributions as GeoJSON FeatureCollection
// Required: municipality_id
// Optional: category
// ---------------------------------------------------------------------
function handle_read($input) {
$pdo = get_db();
// Validate Input
$missing = validate_required($input, ['municipality_id']);
if (!empty($missing)) {
error_response('Missing Fields: ' . implode(', ', $missing));
}
$municipality_id = $input['municipality_id'];
// Builds SQL Query with Placeholders for prepared Statement
$sql = "SELECT *, ST_AsGeoJSON(geom) AS geojson
FROM contributions
WHERE municipality_id = :mid";
$params = [':mid' => $municipality_id];
// Optional: Filters by Status (Default: only approved)
$status = $input['status'] ?? 'approved';
if ($status !== 'all') {
$sql .= " AND status = :status";
$params[':status'] = $status;
}
// Optional: Filters by Category
if (!empty($input['category'])) {
$sql .= " AND category = :cat";
$params[':cat'] = $input['category'];
}
$sql .= " ORDER BY created_at DESC";
try {
// Prepared Statement to prevent SQL Injection
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
// Fetches Results as PHP-Array
$rows = $stmt->fetchAll();
} catch (PDOException $e) {
error_response('Database Error: ' . $e->getMessage(), 500);
}
// Builds GeoJSON FeatureCollection
$features = [];
foreach ($rows as $row) {
$geometry = json_decode($row['geojson']);
// Removes raw Geometry Columns from Properties
unset($row['geom']);
unset($row['geojson']);
$features[] = [
'type' => 'Feature',
'geometry' => $geometry,
'properties' => $row
];
}
$featureCollection = [
'type' => 'FeatureCollection',
'features' => $features
];
json_response($featureCollection);
}
// ---------------------------------------------------------------------
// CREATE: Inserts new Contributions
// Required: municipality_id, geom, geom_type, category, title, author_name
// Optional: description
// ---------------------------------------------------------------------
function handle_create($input) {
$pdo = get_db();
// Validates Input
$missing = validate_required($input, [
'municipality_id', 'geom', 'geom_type', 'category', 'title', 'author_name'
]);
if (!empty($missing)) {
error_response('Missing Fields: ' . implode(', ', $missing));
}
// Validates Geometry Type
$valid_geom_types = ['point', 'line', 'polygon'];
if (!in_array($input['geom_type'], $valid_geom_types)) {
error_response('Invalid Geometry Type. Must be: ' . implode(', ', $valid_geom_types));
}
// Validates GeoJSON
$geojson = json_decode($input['geom']);
if (!$geojson || !isset($geojson->type)) {
error_response('Invalid GeoJSON in Geometry Field.');
}
// Prepared SQL Statement
try {
$stmt = $pdo->prepare("
INSERT INTO contributions
(municipality_id, geom, geom_type, category, title, description, author_name)
VALUES
(:mid, ST_SetSRID(ST_GeomFromGeoJSON(:geom), 4326), :geom_type,
:category, :title, :description, :author_name)
");
$stmt->execute([
':mid' => $input['municipality_id'],
':geom' => $input['geom'],
':geom_type' => $input['geom_type'],
':category' => $input['category'],
':title' => $input['title'],
':description' => $input['description'] ?? '',
':author_name' => $input['author_name']
]);
json_response([
'message' => 'Contribution created successfully.',
'contribution_id' => (int) $pdo->lastInsertId()
], 201);
} catch (PDOException $e) {
error_response('Database Error: ' . $e->getMessage(), 500);
}
}
// ---------------------------------------------------------------------
// UPDATE: Updates existing Contributions
// Required: contribution_id
// Optional: category, title, description, status
// Provided Fields are updated. Others remain unchanged.
// ---------------------------------------------------------------------
function handle_update($input) {
$pdo = get_db();
// Validates Input
$missing = validate_required($input, ['contribution_id']);
if (!empty($missing)) {
error_response('Missing Fields: ' . implode(', ', $missing));
}
$contribution_id = $input['contribution_id'];
// Checks if Contribution exists
$stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id");
$stmt->execute([':id' => $contribution_id]);
if (!$stmt->fetch()) {
error_response('Contribution not found.', 404);
}
// Builds dynamic SQL Query to only update sent Fields
$updatable_fields = ['category', 'title', 'description', 'status'];
$set_clauses = [];
$params = [':id' => $contribution_id];
foreach ($updatable_fields as $field) {
if (isset($input[$field]) && $input[$field] !== '') {
$set_clauses[] = "$field = :$field";
$params[":$field"] = $input[$field];
}
}
if (empty($set_clauses)) {
error_response('No Fields to update. Provide at least one of: ' . implode(', ', $updatable_fields));
}
// Validates Status
if (isset($params[':status'])) {
$valid_statuses = ['pending', 'approved', 'rejected', 'in_progress', 'done'];
if (!in_array($params[':status'], $valid_statuses)) {
error_response('Invalid Status. Must be: ' . implode(', ', $valid_statuses));
}
}
// Builds SQL Statement
$sql = "UPDATE contributions SET " . implode(', ', $set_clauses) . " WHERE contribution_id = :id";
// Prepared SQL Statement
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
json_response(['message' => 'Contribution updated successfully.']);
} catch (PDOException $e) {
error_response('Database Error: ' . $e->getMessage(), 500);
}
}
// ---------------------------------------------------------------------
// DELETE: Deletes existing Contributions
// Required: contribution_id
// Associated Votes are deleted automatically
// ---------------------------------------------------------------------
function handle_delete($input) {
$pdo = get_db();
// Validates Input
$missing = validate_required($input, ['contribution_id']);
if (!empty($missing)) {
error_response('Missing Fields: ' . implode(', ', $missing));
}
$contribution_id = $input['contribution_id'];
// Checks if Contribution exists
$stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id");
$stmt->execute([':id' => $contribution_id]);
if (!$stmt->fetch()) {
error_response('Contribution not found.', 404);
}
// Prepared SQL Statement
try {
$stmt = $pdo->prepare("DELETE FROM contributions WHERE contribution_id = :id");
$stmt->execute([':id' => $contribution_id]);
json_response(['message' => 'Contribution deleted successfully.']);
} catch (PDOException $e) {
error_response('Database Error: ' . $e->getMessage(), 500);
}
}
// ---------------------------------------------------------------------
// VOTE: Likes or Dislikes a Contribution
// Required: contribution_id, voter_name, vote_type
// Database Trigger automatically updates Likes and Dislikes Count
// UNIQUE Constraint prevents duplicate Votes per Voter.
// ---------------------------------------------------------------------
function handle_vote($input) {
$pdo = get_db();
// Validates Input
$missing = validate_required($input, ['contribution_id', 'voter_name', 'vote_type']);
if (!empty($missing)) {
error_response('Missing Fields: ' . implode(', ', $missing));
}
// Validates Vote Type
$valid_vote_types = ['like', 'dislike'];
if (!in_array($input['vote_type'], $valid_vote_types)) {
error_response('Invalid vote_type. Must be: ' . implode(', ', $valid_vote_types));
}
// Checks if Contribution exists
$stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id");
$stmt->execute([':id' => $input['contribution_id']]);
if (!$stmt->fetch()) {
error_response('Contribution not found.', 404);
}
// Prepared SQL Statement
try {
// Checks if Voter already voted on this Contribution
$stmt = $pdo->prepare("
SELECT vote_id, vote_type FROM votes
WHERE contribution_id = :cid AND voter_name = :voter
");
$stmt->execute([':cid' => $input['contribution_id'], ':voter' => $input['voter_name']]);
$existing = $stmt->fetch();
if ($existing) {
if ($existing['vote_type'] === $input['vote_type']) {
// Same Vote Type — Removes Vote
$stmt = $pdo->prepare("DELETE FROM votes WHERE vote_id = :vid");
$stmt->execute([':vid' => $existing['vote_id']]);
json_response(['message' => 'Vote removed.', 'action' => 'removed']);
} else {
// Different Vote Type — Switches Vote
$stmt = $pdo->prepare("DELETE FROM votes WHERE vote_id = :vid");
$stmt->execute([':vid' => $existing['vote_id']]);
$stmt = $pdo->prepare("
INSERT INTO votes (contribution_id, voter_name, vote_type)
VALUES (:cid, :voter, :vtype)
");
$stmt->execute([
':cid' => $input['contribution_id'],
':voter' => $input['voter_name'],
':vtype' => $input['vote_type']
]);
json_response(['message' => 'Vote changed.', 'action' => 'changed'], 200);
}
} else {
// No existing Vote — Inserts Vote
$stmt = $pdo->prepare("
INSERT INTO votes (contribution_id, voter_name, vote_type)
VALUES (:cid, :voter, :vtype)
");
$stmt->execute([
':cid' => $input['contribution_id'],
':voter' => $input['voter_name'],
':vtype' => $input['vote_type']
]);
json_response(['message' => 'Vote recorded.', 'action' => 'created'], 201);
}
} catch (PDOException $e) {
error_response('Database Error: ' . $e->getMessage(), 500);
}
}

94
public/api/db.php Normal file
View File

@@ -0,0 +1,94 @@
<?php
// =====================================================================
// Database Helper
// Provides PDO Connection to Database and shared miscellaneous
// Functions for all API Endpoints.
// =====================================================================
require_once __DIR__ . '/init.php';
// ---------------------------------------------------------------------
// JSON Response
// Creates JSON Response including HTTP Status Code and HTTP Header
// for every API Endpoint and terminates the Script.
// ---------------------------------------------------------------------
function json_response($data, $status_code = 200) {
// Defines HTTP Status Code and HTTP Header
// 1XX Informational, 2XX Successful, 3XX Redirection,
// 4XX Client Error, 5XX Server Error
http_response_code($status_code);
header('Content-Type: application/json; charset=utf-8');
// Converts PHP-Array to JSON-String
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// ---------------------------------------------------------------------
// Error Response
// Creates standardized Error Responses with Error Message and HTTP Status
// Code. Uses json_response() for consistent Formatting.
// ---------------------------------------------------------------------
function error_response($message, $status_code = 400) {
json_response(['error' => $message], $status_code);
}
// ---------------------------------------------------------------------
// Validate Required Fields
// Checks if specified Fields exist in the given Data Array and are
// non-empty. Returns an Array of missing Field Names, or an empty
// Array if all Fields are present.
// ---------------------------------------------------------------------
function validate_required($data, $fields) {
$missing = [];
foreach ($fields as $field) {
// Checks if Fields exists in Data Array and are not empty
if (!isset($data[$field]) || trim($data[$field]) === '') {
$missing[] = $field;
}
}
// Returns Array of missing Fields or emty Array
return $missing;
}
// ---------------------------------------------------------------------
// Get POST Input
// Reads POST Parameters. Returns an associative Array.
// Fallback to JSON Request Body if no POST Data is present.
// ---------------------------------------------------------------------
function get_input() {
// Checks for standard POST Requests
if (!empty($_POST)) {
return array_map('trim', $_POST);
}
// Fall back for JSON POST Requests
$json = file_get_contents('php://input');
$data = json_decode($json, true);
if (is_array($data)) {
return array_map('trim', $data);
}
return [];
}
// ---------------------------------------------------------------------
// Get PDO Connection
// Returns PDO Instance wrapped in a Function to prevent global
// Variable Dependencies in Endpoint Files.
// ---------------------------------------------------------------------
function get_db() {
global $pdo;
if (!$pdo) {
error_response('Database Connection failed.', 500);
}
return $pdo;
}

51
public/api/init.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
// =====================================================================
// Database Connection
// =====================================================================
// Reads Environment Configfile
$envFile = __DIR__ . '/../../.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
list($key, $value) = array_map('trim', explode('=', $line, 2));
putenv("$key=$value");
}
}
// Defines Environment Variables
$host = getenv('POSTGRES_HOSTNAME');
$port = getenv('POSTGRES_PORT');
$db = getenv('POSTGRES_DB');
$user = getenv('POSTGRES_USER');
$pass = getenv('POSTGRES_PASSWORD');
// Output Buffering and Session Start
ob_start();
session_start();
// Initializes Database Connection
try {
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
];
$dsn = "pgsql:host=$host;dbname=$db;port=$port";
$pdo = new PDO($dsn, $user, $pass, $opt);
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
// Creates Error Message
} catch(PDOException $e) {
echo "Error: ".$e->getMessage();
}
?>

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

350
public/index.php Normal file
View File

@@ -0,0 +1,350 @@
<?php
// =====================================================================
// WebGIS Citizen Participation Portal — Main Page
// Loads Municipality Configuration from the Database.
// Renders Leaflet Map Interface including Leaflet Plugins
// =====================================================================
require_once __DIR__ . '/api/db.php';
// -----------------------------------------------------------------
// Loads Municipality Configuration
// ToDo's: Dynamic Loading via URL Slug once multi-tenant Routing
// is implemented. Hardcoded Slug for now.
// -----------------------------------------------------------------
$pdo = get_db();
$stmt = $pdo->prepare("SELECT * FROM municipalities WHERE slug = :slug");
$stmt->execute([':slug' => 'lohne']);
$municipality = $stmt->fetch();
if (!$municipality) {
http_response_code(404);
echo "<!DOCTYPE html><html><body><h1>404 — Municipality not listed in Database.</h1></body></html>";
exit;
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bürgerbeteiligungsportal <?= htmlspecialchars($municipality['name']) ?></title>
<link rel="icon" href="assets/icon-municipality.png" type="image/png">
<meta name="description" content="Bürgerbeteiligungsportal. Hinweise und Vorschläge auf der Karte eintragen.">
<!-- ============================================================= -->
<!-- Loads CSS Dependencies -->
<!-- ============================================================= -->
<!-- Leaflet -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css">
<!-- Geoman Drawing Tools -->
<link rel="stylesheet" href="https://unpkg.com/@geoman-io/leaflet-geoman-free@2.17.0/dist/leaflet-geoman.css">
<!-- Leaflet Sidebar -->
<link rel="stylesheet" href="https://unpkg.com/leaflet-sidebar-v2@3.2.3/css/leaflet-sidebar.min.css">
<!-- Leaflet Fullscreen -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.fullscreen/3.0.2/Control.FullScreen.css">
<!-- Leaflet Geocoder for Address Search -->
<link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder@2.4.0/dist/Control.Geocoder.css">
<!-- Leaflet Polyline Measurement Tool -->
<link rel="stylesheet" href="https://ppete2.github.io/Leaflet.PolylineMeasure/Leaflet.PolylineMeasure.css">
<!-- Font Awesome 6 for Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Application Styles -->
<link rel="stylesheet" href="styles.css">
<!-- ============================================================= -->
<!-- Municipality Theme loaded from Database -->
<!-- ============================================================= -->
<style>
:root {
--color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>;
--color-primary-light: <?= htmlspecialchars($municipality['primary_color']) ?>22;
}
</style>
</head>
<body>
<!-- ============================================================= -->
<!-- Header -->
<!-- ============================================================= -->
<header id="app-header">
<div class="header-left">
<img src="assets/logo-municipality.png" alt="<?= htmlspecialchars($municipality['name']) ?>" class="header-logo" onerror="this.style.display='none'">
<h1 class="header-title">Bürgerbeteiligung <?= htmlspecialchars($municipality['name']) ?></h1>
</div>
<nav class="header-nav">
<button class="nav-btn" onclick="showInfoModal()">
<i class="fa-solid fa-circle-info"></i>
<span class="nav-label">Informationen</span>
</button>
<button class="nav-btn" onclick="showPrivacyModal()">
<i class="fa-solid fa-shield-halved"></i>
<span class="nav-label">Datenschutz</span>
</button>
<button class="nav-btn" onclick="showImprintModal()">
<i class="fa-solid fa-scale-balanced"></i>
<span class="nav-label">Impressum</span>
</button>
</nav>
<!-- Admin Login Button -->
<a href="admin.php" class="nav-btn nav-btn-admin" title="Moderationsbereich">
<i class="fa-solid fa-lock"></i>
</a>
<!-- Mobile Hamburger Menu -->
<button class="header-menu-toggle" onclick="toggleMobileNav()">
</header>
<!-- ============================================================= -->
<!-- Map Container with Sidebar -->
<!-- ============================================================= -->
<main id="app-main">
<!-- Leaflet Sidebar -->
<div id="sidebar" class="leaflet-sidebar collapsed">
<!-- Sidebar Tab Icons -->
<div class="leaflet-sidebar-tabs">
<ul role="tablist">
<li><a href="#tab-home" role="tab"><i class="fa-solid fa-house"></i></a></li>
<li><a href="#tab-help" role="tab"><i class="fa-solid fa-circle-question"></i></a></li>
<li><a href="#tab-list" role="tab"><i class="fa-solid fa-list"></i></a></li>
<li><a href="#tab-news" role="tab"><i class="fa-solid fa-newspaper"></i></a></li>
</ul>
</div>
<!-- Sidebar Tab Content -->
<div class="leaflet-sidebar-content">
<!-- Home Tab -->
<div class="leaflet-sidebar-pane" id="tab-home">
<h2 class="leaflet-sidebar-header">
Start
<span class="leaflet-sidebar-close"><i class="fa-solid fa-xmark"></i></span>
</h2>
<div class="sidebar-body">
<p>Willkommen beim Bürgerbeteiligungsportal <strong><?= htmlspecialchars($municipality['name']) ?></strong>.</p>
<p>Verwenden Sie die Karte, um Hinweise und Aufgaben für die Stadtverwaltung hinzuzufügen oder bestehende Beiträge der Bürgerschaft zu betrachten.</p>
<h3>Kategorien</h3>
<div id="category-filter">
<!-- Category Filter Checkboxes — populated by app.js -->
</div>
<h3>Statistik</h3>
<div id="stats-container">
<!-- Contribution Statistics — populated by app.js -->
</div>
</div>
</div>
<!-- List Tab -->
<div class="leaflet-sidebar-pane" id="tab-list">
<h2 class="leaflet-sidebar-header">
Beiträge
<span class="leaflet-sidebar-close"><i class="fa-solid fa-xmark"></i></span>
</h2>
<div class="sidebar-body">
<div class="list-search">
<input type="text" id="list-search-input" placeholder="Beiträge durchsuchen..." class="form-input">
</div>
<div id="contributions-list">
<!-- Contribution Cards — populated by app.js -->
</div>
</div>
</div>
<!-- Help Tab -->
<div class="leaflet-sidebar-pane" id="tab-help">
<h2 class="leaflet-sidebar-header">
Hilfe
<span class="leaflet-sidebar-close"><i class="fa-solid fa-xmark"></i></span>
</h2>
<div class="sidebar-body">
<h3><i class="fa-solid fa-map-location-dot"></i> Karte bedienen</h3>
<p>Verschieben Sie die Karte per Mausklick und Ziehen. Zoomen Sie mit dem Mausrad oder den Zoom-Buttons.</p>
<h3><i class="fa-solid fa-plus"></i> Beitrag erstellen</h3>
<p>Verwenden Sie die Zeichenwerkzeuge rechts, um Beiträge als Punkte, Linien oder Flächen zu zeichnen. Anschließend können Sie Kategorie und Beschreibung hinzufügen.</p>
<h3><i class="fa-solid fa-thumbs-up"></i> Abstimmen</h3>
<p>Klicken Sie auf bestehende Beiträge und nutzen Sie die Like/Dislike Funktion, um Ihre Meinung kundzugeben.</p>
<h3><i class="fa-solid fa-magnifying-glass"></i> Suchen</h3>
<p>Verwenden Sie die Adresssuche rechts, um bestimmte Orte auf der Karte zu finden.</p>
</div>
</div>
<!-- News Tab -->
<div class="leaflet-sidebar-pane" id="tab-news">
<h2 class="leaflet-sidebar-header">
Neuigkeiten
<span class="leaflet-sidebar-close"><i class="fa-solid fa-xmark"></i></span>
</h2>
<div class="sidebar-body">
<div class="news-item">
<span class="news-date">April 2026</span>
<h3>Portal gestartet</h3>
<p>Das Bürgerbeteiligungsportal für <?= htmlspecialchars($municipality['name']) ?> ist online. Wir freuen uns auf Ihre Hinweise und Vorschläge!</p>
</div>
<!-- News Items can be added or loaded from Database here -->
</div>
</div>
</div>
</div>
<!-- Leaflet Map -->
<div id="map"></div>
</main>
<!-- ============================================================= -->
<!-- Footer -->
<!-- ============================================================= -->
<footer id="app-footer">
<div class="footer-content">
<!-- <img src="assets/logo-company.png" alt="Company Logo" class="footer-logo" onerror="this.style.display='none'"> -->
<span class="footer-text">© <a href="https://endex-geodaten.de" target="_blank" style="color:inherit;">endex GmbH</a></span>
</div>
</footer>
<!-- ============================================================= -->
<!-- Welcome Modal shown on first Visit -->
<!-- ============================================================= -->
<div id="welcome-modal" class="modal-overlay" style="display:none;">
<div class="modal-content">
<h2><i class="fa-solid fa-hand-wave"></i> Willkommen!</h2>
<p>Herzlich willkommen beim Bürgerbeteiligungsportal <strong><?= htmlspecialchars($municipality['name']) ?></strong>.</p>
<p>Hier können Sie:</p>
<ul>
<li>Hinweise und Verbesserungsvorschläge für die Stadtverwaltung hinzufügen</li>
<li>Bestehende Beiträge der Bürgerschaft betrachten und bewerten</li>
</ul>
<p style="background:#fff3cd;padding:10px;border-radius:6px;border:1px solid #ffc107;font-size:0.85rem;color:#856404;">
<i class="fa-solid fa-triangle-exclamation"></i> <strong>Hinweis:</strong> Dieses Bürgerbeteiligungsportal befindet sich noch in der Entwicklung und wurde nicht offiziell beauftragt.
</p>
<p>Zum Hinzufügen von Beiträgen geben Sie bitte zunächst Ihren Namen ein.</p> <div class="modal-actions">
<button class="btn btn-primary" onclick="closeWelcomeAndShowLogin()">Loslegen</button>
</div>
</div>
</div>
<!-- ============================================================= -->
<!-- Login Modal for Identification -->
<!-- ToDo's: User Authentification and Administration -->
<!-- ============================================================= -->
<div id="login-modal" class="modal-overlay" style="display:none;">
<div class="modal-content modal-small">
<h2><i class="fa-solid fa-user"></i> Anmelden</h2>
<p>Bitte geben Sie Ihren Namen ein, um Beiträge hinzufügen und abstimmen zu können.</p>
<div class="form-group">
<label for="user-name-input">Ihr Name</label>
<input type="text" id="user-name-input" class="form-input" placeholder="Vor- und Nachname">
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="skipLogin()">Gastuser</button>
<button class="btn btn-primary" onclick="submitLogin()">Anmelden</button>
</div>
</div>
</div>
<!-- ============================================================= -->
<!-- Create Contribution Modal -->
<!-- ============================================================= -->
<div id="create-modal" class="modal-overlay" style="display:none;">
<div class="modal-content">
<h2><i class="fa-solid fa-plus-circle"></i> Beitrag</h2>
<div class="form-group">
<label for="create-category">Kategorie</label>
<select id="create-category" class="form-input">
<option value="">— Bitte wählen —</option>
<!-- Categories populated dynamically -->
</select>
</div>
<div class="form-group">
<label for="create-title">Titel</label>
<input type="text" id="create-title" class="form-input" placeholder="Kurze Beschreibung des Anliegens">
</div>
<div class="form-group">
<label for="create-description">Beschreibung</label>
<textarea id="create-description" class="form-input" rows="4" placeholder="Detaillierte Beschreibung (optional)"></textarea>
</div>
<input type="hidden" id="create-geom">
<input type="hidden" id="create-geom-type">
<div class="modal-actions">
<button class="btn btn-secondary" onclick="cancelCreate()">Abbrechen</button>
<button class="btn btn-primary" onclick="submitCreate()">Beitrag einreichen</button>
</div>
</div>
</div>
<!-- ============================================================= -->
<!-- Loads JavaScript Dependencies -->
<!-- ============================================================= -->
<!-- Leaflet 1.9.4 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<!-- Geoman Drawing Tools -->
<script src="https://unpkg.com/@geoman-io/leaflet-geoman-free@2.17.0/dist/leaflet-geoman.min.js"></script>
<!-- Leaflet Sidebar v2 -->
<script src="https://unpkg.com/leaflet-sidebar-v2@3.2.3/js/leaflet-sidebar.min.js"></script>
<!-- Leaflet Fullscreen -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.fullscreen/3.0.2/Control.FullScreen.min.js"></script>
<!-- Leaflet Geocoder (Address Search) -->
<script src="https://unpkg.com/leaflet-control-geocoder@2.4.0/dist/Control.Geocoder.min.js"></script>
<!-- Leaflet PolylineMeasure -->
<script src="https://ppete2.github.io/Leaflet.PolylineMeasure/Leaflet.PolylineMeasure.js"></script>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.14.0/dist/sweetalert2.all.min.js"></script>
<!-- ============================================================= -->
<!-- Municipality Configuration (passed to JavaScript) -->
<!-- ============================================================= -->
<script>
// Municipality Configuration from Database — used by app.js
var MUNICIPALITY = {
id: <?= $municipality['municipality_id'] ?>,
name: "<?= htmlspecialchars($municipality['name'], ENT_QUOTES) ?>",
slug: "<?= htmlspecialchars($municipality['slug'], ENT_QUOTES) ?>",
center: [<?= $municipality['center_lat'] ?>, <?= $municipality['center_lng'] ?>],
zoom: <?= $municipality['default_zoom'] ?>,
primaryColor: "<?= htmlspecialchars($municipality['primary_color'], ENT_QUOTES) ?>"
};
</script>
<!-- Application Logic -->
<script src="js/app.js"></script>
</body>
</html>

948
public/js/app.js Normal file
View File

@@ -0,0 +1,948 @@
// =====================================================================
// WebGIS Citizen Participation Portal — Application Logic
// Initializes Leaflet Map, loads Contributions from the API,
// handles CRUD Workflow, and manages all UI Interactions.
//
// Depends on: MUNICIPALITY Object set in Main Page, Leaflet, Geoman,
// Sidebar, Geocoder, PolylineMeasure, Fullscreen,
// and SweetAlert2 Plugins.
// =====================================================================
// =====================================================================
// Block 1: Configuration and Application State
// =====================================================================
// API Endpoint as relative Path
var API_URL = 'api/contributions.php';
// Current User Name, set via Login Modal, stored in sessionStorage
var currentUser = sessionStorage.getItem('webgis_user') || '';
// Category Definitions with Labels, Icons, and Colors
var CATEGORIES = {
mobility: { label: 'Mobilität', icon: '🚲', color: '#1565C0', faIcon: 'fa-bicycle' },
building: { label: 'Bauen', icon: '🏗️', color: '#E65100', faIcon: 'fa-helmet-safety' },
energy: { label: 'Energie', icon: '⚡', color: '#F9A825', faIcon: 'fa-bolt' },
environment: { label: 'Umwelt', icon: '🌳', color: '#2E7D32', faIcon: 'fa-tree' },
industry: { label: 'Industrie', icon: '🏭', color: '#6A1B9A', faIcon: 'fa-industry' },
consumption: { label: 'Konsum', icon: '🛒', color: '#AD1457', faIcon: 'fa-cart-shopping' },
other: { label: 'Sonstiges', icon: '📌', color: '#546E7A', faIcon: 'fa-map-pin' }
};
// Application State
var map; // Leaflet Map Instance
var sidebar; // Sidebar Instance
var contributionsLayer; // GeoJSON Layer holding all Contributions
var contributionsData = []; // Raw Contribution Data Array
var activeFilters = Object.keys(CATEGORIES); // Active Category Filters
var drawnGeometry = null; // Temporary Storage for Geometry drawn with Geoman
var drawnGeomType = null; // Temporary Storage for Geometry Type
var userVotes = {}; // Tracks User Votes
// =====================================================================
// Block 2: Map Initialization
// =====================================================================
map = L.map('map', {
center: MUNICIPALITY.center,
zoom: MUNICIPALITY.zoom,
minZoom: 10,
maxBounds: [
[MUNICIPALITY.center[0] - 0.25, MUNICIPALITY.center[1] - 0.25],
[MUNICIPALITY.center[0] + 0.25, MUNICIPALITY.center[1] + 0.25]
],
maxBoundsViscosity: 0.8,
zoomControl: false,
attributionControl: true
});
// =====================================================================
// Block 3: Basemaps and Layer Control
// =====================================================================
// Basemap Tile Layers
var basemapOSM = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 20
});
var basemapCartoDB = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '© <a href="https://carto.com/">CARTO</a>',
maxZoom: 20
});
var basemapSatellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© <a href="https://www.esri.com/">Esri</a>',
maxZoom: 20
});
// Set Default Basemap
basemapCartoDB.addTo(map);
// Layer Control
var basemaps = {
'OpenStreetMap': basemapOSM,
'CartoDB (hell)': basemapCartoDB,
'Satellit (Esri)': basemapSatellite,
};
var overlays = {}; // Populated later with Contribution Layers
var layerControl = L.control.layers(basemaps, overlays, {
position: 'topright',
collapsed: true
}).addTo(map);
// =====================================================================
// Block 4: Map Controls
// =====================================================================
// Zoom Control
L.control.zoom({
position: 'topright'
}).addTo(map);
// Scale Bar
L.control.scale({
position: 'bottomright',
maxWidth: 200,
imperial: false
}).addTo(map);
// Fullscreen Button
L.control.fullscreen({
position: 'topright',
title: 'Vollbild',
titleCancel: 'Vollbild deaktivieren'
}).addTo(map);
// Geocoder Address Search
L.Control.geocoder({
position: 'topright',
placeholder: 'Adresse suchen...',
defaultMarkGeocode: true,
geocoder: L.Control.Geocoder.nominatim({
geocodingQueryParams: {
countrycodes: 'de',
viewbox: (MUNICIPALITY.center[1] - 0.3) + ',' + (MUNICIPALITY.center[0] - 0.2) + ',' +
(MUNICIPALITY.center[1] + 0.3) + ',' + (MUNICIPALITY.center[0] + 0.2),
bounded: 0,
}
})
}).addTo(map);
// Polyline Measure Tool
L.control.polylineMeasure({
position: 'topright',
unit: 'metres',
showBearings: false,
clearMeasurementsOnStop: false,
showClearControl: true
}).addTo(map);
// Mouse Position Display
var MousePositionControl = L.Control.extend({
options: { position: 'bottomright' },
onAdd: function () {
var container = L.DomUtil.create('div', 'mouse-position-display');
container.innerHTML = 'Lat: , Lng: ';
map.on('mousemove', function (e) {
container.innerHTML = 'Lat: ' + e.latlng.lat.toFixed(5) + ', Lng: ' + e.latlng.lng.toFixed(5);
});
return container;
}
});
new MousePositionControl().addTo(map);
// GPS Location Button
var GpsControl = L.Control.extend({
options: { position: 'topright' },
onAdd: function () {
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
var button = L.DomUtil.create('a', 'gps-control-button', container);
button.href = '#';
button.title = 'Mein Standort';
button.innerHTML = '<i class="fa-solid fa-location-crosshairs"></i>';
L.DomEvent.on(button, 'click', function (e) {
L.DomEvent.preventDefault(e);
map.locate({ setView: true, maxZoom: 17 });
});
return container;
}
});
new GpsControl().addTo(map);
// GPS Location Found Handler
var gpsMarker = null;
map.on('locationfound', function (e) {
if (gpsMarker) {
map.removeLayer(gpsMarker);
}
gpsMarker = L.circleMarker(e.latlng, {
radius: 8,
color: '#1565C0',
fillColor: '#42A5F5',
fillOpacity: 0.8,
weight: 2
}).addTo(map).bindPopup('Ihr Standort').openPopup();
});
map.on('locationerror', function () {
Swal.fire('Standort nicht gefunden', 'Bitte gestatten Sie den Standortzugriff in Ihrem Browser.', 'warning');
});
// =====================================================================
// Block 5: Sidebar Initialization
// =====================================================================
sidebar = L.control.sidebar({
autopan: true,
closeButton: true,
container: 'sidebar',
position: 'left'
}).addTo(map);
// =====================================================================
// Block 6: Geoman Drawing Tools and CRUD Trigger
// =====================================================================
map.pm.addControls({
position: 'topright',
drawMarker: true,
drawPolyline: true,
drawPolygon: true,
drawCircleMarker: false,
drawCircle: false,
drawText: false,
drawRectangle: false,
editMode: false,
dragMode: false,
cutPolygon: false,
removalMode: false,
rotateMode: false
});
map.pm.setLang('de');
// Captures drawn Geometry and opens the Create Modal
map.on('pm:create', function (e) {
var geojson = e.layer.toGeoJSON().geometry;
// Determines drawn Geometry Type and normalizes to simple Types
if (e.shape === 'Marker') {
drawnGeometry = { type: 'Point', coordinates: geojson.coordinates };
drawnGeomType = 'point';
} else if (e.shape === 'Line') {
drawnGeometry = { type: 'LineString', coordinates: geojson.coordinates };
drawnGeomType = 'line';
} else if (e.shape === 'Polygon') {
drawnGeometry = { type: 'Polygon', coordinates: geojson.coordinates };
drawnGeomType = 'polygon';
} else {
// removes unsupported Objects
map.removeLayer(e.layer);
return;
}
// Removes the drawn Layer, which will be re-added after Moderation Confirmation
map.removeLayer(e.layer);
// Checks if User is logged in
if (!currentUser) {
showLoginModal();
return;
}
// Populates hidden Fields and opens Create Modal
document.getElementById('create-geom').value = JSON.stringify(drawnGeometry);
document.getElementById('create-geom-type').value = drawnGeomType;
document.getElementById('create-modal').style.display = 'flex';
});
// =====================================================================
// Block 7: API Communication
// =====================================================================
// Generic API Call Function
function apiCall(data, callback) {
var formData = new FormData();
for (var key in data) {
formData.append(key, data[key]);
}
fetch(API_URL, { method: 'POST', body: formData })
.then(function (response) {
return response.json().then(function (json) {
json._status = response.status;
return json;
});
})
.then(function (json) {
callback(json);
})
.catch(function (error) {
console.error('API Error:', error);
Swal.fire('Verbindungsfehler', 'Verbindung zum Server fehlgeschlagen.', 'error');
});
}
// Loads all Contributions from API and displays Contributions on Map
function loadContributions() {
apiCall({ action: 'read', municipality_id: MUNICIPALITY.id }, function (data) {
if (data.error) {
console.error('Load Error:', data.error);
return;
}
contributionsData = data.features || [];
// Removes existing Layer if present
if (contributionsLayer) {
map.removeLayer(contributionsLayer);
layerControl.removeLayer(contributionsLayer);
}
// Creates new GeoJSON Layer
contributionsLayer = L.geoJSON(data, {
pointToLayer: stylePoint,
style: styleLinePolygon,
onEachFeature: bindFeaturePopup
}).addTo(map);
layerControl.addOverlay(contributionsLayer, 'Beiträge');
// Update Sidebar List and Statistics
updateContributionsList();
updateStatistics();
});
}
// =====================================================================
// Block 8: Feature Styling by Category
// =====================================================================
// Style for Point Features (CircleMarkers)
function stylePoint(feature, latlng) {
var cat = CATEGORIES[feature.properties.category] || CATEGORIES.other;
return L.circleMarker(latlng, {
radius: 8,
color: '#ffffff',
weight: 2,
fillColor: cat.color,
fillOpacity: 0.9
});
}
// Style for Line and Polygon Features
function styleLinePolygon(feature) {
var cat = CATEGORIES[feature.properties.category] || CATEGORIES.other;
return {
color: cat.color,
weight: 3,
opacity: 0.8,
fillColor: cat.color,
fillOpacity: 0.25
};
}
// =====================================================================
// Block 9: Feature Popups for Read, Edit, Delete and Vote
// =====================================================================
function bindFeaturePopup(feature, layer) {
var props = feature.properties;
var cat = CATEGORIES[props.category] || CATEGORIES.other;
// Formats Date
var date = new Date(props.created_at);
var dateStr = date.toLocaleDateString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric'
});
// Builds Popup on Click
var html = '' +
'<div class="popup-detail">' +
'<span class="popup-detail-category">' + cat.icon + ' ' + cat.label + '</span>' +
'<div class="popup-detail-title">' + escapeHtml(props.title) + '</div>' +
(props.description ? '<div class="popup-detail-description">' + escapeHtml(props.description) + '</div>' : '') +
'<div class="popup-detail-meta">' +
'<i class="fa-solid fa-user"></i> ' + escapeHtml(props.author_name) +
' &middot; <i class="fa-solid fa-calendar"></i> ' + dateStr +
'</div>' +
'<div class="popup-detail-votes">' +
'<button class="popup-vote-btn' + (userVotes[props.contribution_id] === 'like' ? ' liked' : '') + '" id="vote-like-' + props.contribution_id + '" onclick="voteContribution(' + props.contribution_id + ', \'like\')" title="Gefällt mir">' +
'<i class="fa-solid fa-thumbs-up"></i> <span id="likes-' + props.contribution_id + '">' + props.likes_count + '</span>' +
'</button>' +
'<button class="popup-vote-btn' + (userVotes[props.contribution_id] === 'dislike' ? ' disliked' : '') + '" id="vote-dislike-' + props.contribution_id + '" onclick="voteContribution(' + props.contribution_id + ', \'dislike\')" title="Gefällt mir nicht">' +
'<i class="fa-solid fa-thumbs-down"></i> <span id="dislikes-' + props.contribution_id + '">' + props.dislikes_count + '</span>' +
'</button>' +
'</div>' +
(currentUser === props.author_name ?
'<div class="popup-detail-actions">' +
'<button class="btn btn-primary" onclick="editContribution(' + props.contribution_id + ')"><i class="fa-solid fa-pen"></i> Bearbeiten</button>' +
'<button class="btn btn-danger" onclick="deleteContribution(' + props.contribution_id + ')"><i class="fa-solid fa-trash"></i> Löschen</button>' +
'</div>' : '') +
'</div>';
layer.bindPopup(html, { maxWidth: 320, minWidth: 240 });
// Builds Tooltip on Hover
layer.bindTooltip(cat.icon + ' ' + escapeHtml(props.title), {
direction: 'top',
offset: [0, -10]
});
}
// =====================================================================
// Block 10: CRUD Operations
// =====================================================================
// CREATE: Submits new Contributions from Modal
function submitCreate() {
var category = document.getElementById('create-category').value;
var title = document.getElementById('create-title').value.trim();
var description = document.getElementById('create-description').value.trim();
var geom = document.getElementById('create-geom').value;
var geomType = document.getElementById('create-geom-type').value;
// Validates
if (!category) {
Swal.fire('Kategorie fehlt', 'Bitte wählen Sie eine Kategorie aus.', 'warning');
return;
}
if (!title) {
Swal.fire('Titel fehlt', 'Bitte geben Sie einen Titel ein.', 'warning');
return;
}
if (!geom) {
Swal.fire('Geometrie fehlt', 'Bitte zeichnen Sie zuerst ein Objekt auf der Karte.', 'warning');
return;
}
apiCall({
action: 'create',
municipality_id: MUNICIPALITY.id,
category: category,
title: title,
description: description,
geom: geom,
geom_type: geomType,
author_name: currentUser
}, function (response) {
if (response.error) {
Swal.fire('Fehler', response.error, 'error');
return;
}
Swal.fire('Eingereicht!', 'Ihr Beitrag wurde erfolgreich eingereicht und wird nach Prüfung durch das Moderationsteam veröffentlicht.', 'success');
closeCreateModal();
loadContributions();
});
}
// Cancels Create, closes Modal and clears Form
function cancelCreate() {
closeCreateModal();
}
function closeCreateModal() {
document.getElementById('create-modal').style.display = 'none';
document.getElementById('create-category').value = '';
document.getElementById('create-title').value = '';
document.getElementById('create-description').value = '';
document.getElementById('create-geom').value = '';
document.getElementById('create-geom-type').value = '';
drawnGeometry = null;
drawnGeomType = null;
}
// UPDATE: Edits existing Contributions
function editContribution(contributionId) {
// Finds Contribution in local Data
var contribution = contributionsData.find(function (f) {
return f.properties.contribution_id === contributionId;
});
if (!contribution) return;
var props = contribution.properties;
Swal.fire({
title: 'Beitrag bearbeiten',
html:
'<div style="text-align:left;">' +
'<label style="font-weight:600;font-size:0.85rem;">Titel</label>' +
'<input id="swal-title" class="swal2-input" value="' + escapeHtml(props.title) + '">' +
'<label style="font-weight:600;font-size:0.85rem;">Beschreibung</label>' +
'<textarea id="swal-description" class="swal2-textarea">' + escapeHtml(props.description || '') + '</textarea>' +
'</div>',
showCancelButton: true,
confirmButtonText: 'Speichern',
cancelButtonText: 'Abbrechen',
confirmButtonColor: MUNICIPALITY.primaryColor,
preConfirm: function () {
return {
title: document.getElementById('swal-title').value.trim(),
description: document.getElementById('swal-description').value.trim()
};
}
}).then(function (result) {
if (!result.isConfirmed) return;
apiCall({
action: 'update',
contribution_id: contributionId,
title: result.value.title,
description: result.value.description
}, function (response) {
if (response.error) {
Swal.fire('Fehler', response.error, 'error');
return;
}
Swal.fire('Gespeichert!', 'Der Beitrag wurde aktualisiert.', 'success');
loadContributions();
});
});
}
// DELETE: Deletes existing Contributions
function deleteContribution(contributionId) {
Swal.fire({
title: 'Beitrag löschen?',
text: 'Aktion kann nicht rückgängig gemacht werden.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Löschen',
cancelButtonText: 'Abbrechen',
confirmButtonColor: '#c62828'
}).then(function (result) {
if (!result.isConfirmed) return;
apiCall({
action: 'delete',
contribution_id: contributionId
}, function (response) {
if (response.error) {
Swal.fire('Fehler', response.error, 'error');
return;
}
Swal.fire('Gelöscht!', 'Der Beitrag wurde entfernt.', 'success');
map.closePopup();
loadContributions();
});
});
}
// VOTE: Like or Dislike existing Contributions
function voteContribution(contributionId, voteType) {
if (!currentUser) {
showLoginModal();
return;
}
apiCall({
action: 'vote',
contribution_id: contributionId,
voter_name: currentUser,
vote_type: voteType
}, function (response) {
if (response.error) {
return;
}
// Updates local Vote State
var likeBtn = document.getElementById('vote-like-' + contributionId);
var dislikeBtn = document.getElementById('vote-dislike-' + contributionId);
var likesSpan = document.getElementById('likes-' + contributionId);
var dislikesSpan = document.getElementById('dislikes-' + contributionId);
if (response.action === 'created') {
// New Vote — Highlights Button and updates Count
userVotes[contributionId] = voteType;
if (voteType === 'like') {
likeBtn.classList.add('liked');
likesSpan.textContent = parseInt(likesSpan.textContent) + 1;
} else {
dislikeBtn.classList.add('disliked');
dislikesSpan.textContent = parseInt(dislikesSpan.textContent) + 1;
}
} else if (response.action === 'removed') {
// Vote removed — Removes Button Highlight and updates Count
delete userVotes[contributionId];
if (voteType === 'like') {
likeBtn.classList.remove('liked');
likesSpan.textContent = Math.max(0, parseInt(likesSpan.textContent) - 1);
} else {
dislikeBtn.classList.remove('disliked');
dislikesSpan.textContent = Math.max(0, parseInt(dislikesSpan.textContent) - 1);
}
} else if (response.action === 'changed') {
// Vote changed — Switches Highlights and updates both Counts
userVotes[contributionId] = voteType;
if (voteType === 'like') {
likeBtn.classList.add('liked');
dislikeBtn.classList.remove('disliked');
likesSpan.textContent = parseInt(likesSpan.textContent) + 1;
dislikesSpan.textContent = Math.max(0, parseInt(dislikesSpan.textContent) - 1);
} else {
dislikeBtn.classList.add('disliked');
likeBtn.classList.remove('liked');
dislikesSpan.textContent = parseInt(dislikesSpan.textContent) + 1;
likesSpan.textContent = Math.max(0, parseInt(likesSpan.textContent) - 1);
}
}
});
}
// =====================================================================
// Block 11: Sidebar Contributions List
// =====================================================================
function updateContributionsList() {
var container = document.getElementById('contributions-list');
var searchInput = document.getElementById('list-search-input');
var searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
// Filters by Categories and Search Term
var filtered = contributionsData.filter(function (f) {
var props = f.properties;
var matchesCategory = activeFilters.indexOf(props.category) !== -1;
var cat = CATEGORIES[props.category] || CATEGORIES.other;
var matchesSearch = !searchTerm ||
props.title.toLowerCase().indexOf(searchTerm) !== -1 ||
(props.description && props.description.toLowerCase().indexOf(searchTerm) !== -1) ||
props.author_name.toLowerCase().indexOf(searchTerm) !== -1 ||
cat.label.toLowerCase().indexOf(searchTerm) !== -1;
return matchesCategory && matchesSearch;
});
// Sorts by Date (newest first)
filtered.sort(function (a, b) {
return new Date(b.properties.created_at) - new Date(a.properties.created_at);
});
// Builds HTML
if (filtered.length === 0) {
container.innerHTML = '<p style="text-align:center;color:#999;padding:20px;">Keine Beiträge gefunden.</p>';
return;
}
var html = '';
filtered.forEach(function (f) {
var props = f.properties;
var cat = CATEGORIES[props.category] || CATEGORIES.other;
var date = new Date(props.created_at).toLocaleDateString('de-DE');
html += '' +
'<div class="contribution-card" onclick="flyToContribution(' + props.contribution_id + ')">' +
'<div class="contribution-card-header">' +
'<span class="contribution-card-category">' + cat.icon + ' ' + cat.label + '</span>' +
'</div>' +
'<div class="contribution-card-title">' + escapeHtml(props.title) + '</div>' +
'<div class="contribution-card-meta">' +
'<span>' + escapeHtml(props.author_name) + ' · ' + date + '</span>' +
'<span class="contribution-card-votes">' +
'<span title="Likes"><i class="fa-solid fa-thumbs-up"></i> ' + props.likes_count + '</span>' +
'<span title="Dislikes"><i class="fa-solid fa-thumbs-down"></i> ' + props.dislikes_count + '</span>' +
'</span>' +
'</div>' +
'</div>';
});
container.innerHTML = html;
}
// Flies to a Contribution on the Map and open Popup
function flyToContribution(contributionId) {
if (!contributionsLayer) return;
contributionsLayer.eachLayer(function (layer) {
if (layer.feature && layer.feature.properties.contribution_id === contributionId) {
// Zooms to Feature
if (layer.getLatLng) {
// Point Feature
map.flyTo(layer.getLatLng(), 17);
} else if (layer.getBounds) {
// Line or Polygon Feature
map.flyToBounds(layer.getBounds(), { maxZoom: 17 });
}
// Opens Popup
layer.openPopup();
// Closes Sidebar on Mobile
if (window.innerWidth < 769) {
sidebar.close();
}
}
});
}
// Search Input Event Listener
document.getElementById('list-search-input').addEventListener('input', function () {
updateContributionsList();
});
// =====================================================================
// Block 12: Sidebar Category Filter and Statistics
// =====================================================================
// Builds Category Filter Checkboxes
function buildCategoryFilter() {
var container = document.getElementById('category-filter');
var html = '';
for (var key in CATEGORIES) {
var cat = CATEGORIES[key];
var checked = activeFilters.indexOf(key) !== -1 ? 'checked' : '';
html += '' +
'<label style="display:flex;align-items:center;gap:8px;margin-bottom:6px;cursor:pointer;">' +
'<input type="checkbox" value="' + key + '" ' + checked + ' onchange="toggleCategoryFilter(this)">' +
'<span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:' + cat.color + ';"></span>' +
'<span>' + cat.icon + ' ' + cat.label + '</span>' +
'</label>';
}
container.innerHTML = html;
}
// Toggles a Category Filter on or off
function toggleCategoryFilter(checkbox) {
var category = checkbox.value;
if (checkbox.checked) {
if (activeFilters.indexOf(category) === -1) {
activeFilters.push(category);
}
} else {
activeFilters = activeFilters.filter(function (c) { return c !== category; });
}
// Refilters Map Layer
if (contributionsLayer) {
contributionsLayer.eachLayer(function (layer) {
if (layer.feature) {
var cat = layer.feature.properties.category;
if (activeFilters.indexOf(cat) !== -1) {
layer.setStyle({ opacity: 1, fillOpacity: layer.feature.geometry.type === 'Point' ? 0.9 : 0.25 });
if (layer.setRadius) layer.setRadius(8);
layer.options.interactive = true;
} else {
layer.setStyle({ opacity: 0, fillOpacity: 0 });
if (layer.setRadius) layer.setRadius(0);
layer.options.interactive = false;
layer.closePopup();
layer.closeTooltip();
}
}
});
}
// Updates List
updateContributionsList();
}
// Updates Statistics in Home Tab
function updateStatistics() {
var container = document.getElementById('stats-container');
var total = contributionsData.length;
// Counts per Category
var counts = {};
contributionsData.forEach(function (f) {
var cat = f.properties.category;
counts[cat] = (counts[cat] || 0) + 1;
});
var html = '<p style="font-size:0.9rem;"><strong>' + total + '</strong> Beiträge insgesamt</p>';
for (var key in CATEGORIES) {
var cat = CATEGORIES[key];
var count = counts[key] || 0;
if (count > 0) {
html += '<div style="display:flex;align-items:center;gap:8px;margin:4px 0;font-size:0.85rem;">' +
'<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + cat.color + ';"></span>' +
cat.label + ': ' + count +
'</div>';
}
}
container.innerHTML = html;
}
// =====================================================================
// Block 13: Modals — Welcome, Login, Info, Privacy, Imprint
// =====================================================================
// Welcome Modal shows on new Visits
function checkWelcomeModal() {
var hasVisited = localStorage.getItem('webgis_welcomed');
if (!hasVisited) {
document.getElementById('welcome-modal').style.display = 'flex';
}
}
function closeWelcomeAndShowLogin() {
localStorage.setItem('webgis_welcomed', 'true');
document.getElementById('welcome-modal').style.display = 'none';
showLoginModal();
}
// Login Modal shows new Session
function showLoginModal() {
document.getElementById('login-modal').style.display = 'flex';
document.getElementById('user-name-input').value = currentUser;
document.getElementById('user-name-input').focus();
}
function submitLogin() {
var name = document.getElementById('user-name-input').value.trim();
if (!name) {
Swal.fire('Name eingeben', 'Bitte geben Sie Ihren Namen ein.', 'warning');
return;
}
currentUser = name;
sessionStorage.setItem('webgis_user', currentUser);
document.getElementById('login-modal').style.display = 'none';
// Open Create Modal if Geometry is pending
if (drawnGeometry) {
document.getElementById('create-geom').value = JSON.stringify(drawnGeometry);
document.getElementById('create-geom-type').value = drawnGeomType;
document.getElementById('create-modal').style.display = 'flex';
}
}
function skipLogin() {
document.getElementById('login-modal').style.display = 'none';
}
// Info Modal
function showInfoModal() {
Swal.fire({
title: 'Informationen',
html: '<p style="text-align:left;line-height:1.6;">Das Bürgerbeteiligungsportal gestattet ' +
'Bürgerinnen und Bürgern sowie der Stadtverwaltung, gemeinsam an der Gestaltung von ' +
'<strong>' + MUNICIPALITY.name + '</strong> mitzuwirken.</p>' +
'<p style="text-align:left;line-height:1.6;">Bitte tragen Sie Hinweise, Anregungen und Vorschläge ' +
'mithilfe der Zeichenwerkzeuge auf der Karte ein.</p>',
confirmButtonColor: MUNICIPALITY.primaryColor
});
}
// Privacy Modal
function showPrivacyModal() {
Swal.fire({
title: 'Datenschutz',
html: '<p style="text-align:left;line-height:1.6;">Das Bürgerbeteiligungsportal speichert die von Ihnen ' +
'hinterlegten Daten zur Durchführung der Bürgerbeteiligung.</p>' +
'<p style="text-align:left;line-height:1.6;">Ihre Daten werden nicht an Dritte weitergegeben. ' +
'Details entnehmen Sie bitte der vollständigen Datenschutzerklärung von ' +
MUNICIPALITY.name + '.</p>',
confirmButtonColor: MUNICIPALITY.primaryColor
});
}
// Imprint Modal
function showImprintModal() {
Swal.fire({
title: 'Impressum',
html: '<p style="text-align:left;line-height:1.6;">Stadt ' + MUNICIPALITY.name + '</p>' +
'<p style="text-align:left;line-height:1.6;color:#777;">Die vollständigen Angaben ' +
'werden hier hinzugefügt, sobald das Portal in den Produktivbetrieb geht.</p>',
confirmButtonColor: MUNICIPALITY.primaryColor
});
}
// =====================================================================
// Block 14: Mobile Navigation
// =====================================================================
function toggleMobileNav() {
var nav = document.querySelector('.header-nav');
nav.classList.toggle('open');
}
// Closes Mobile Nav when clicking outside
document.addEventListener('click', function (e) {
var nav = document.querySelector('.header-nav');
var toggle = document.querySelector('.header-menu-toggle');
if (nav.classList.contains('open') && !nav.contains(e.target) && !toggle.contains(e.target)) {
nav.classList.remove('open');
}
});
// Closes Modals on Escape Key
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
document.getElementById('welcome-modal').style.display = 'none';
document.getElementById('login-modal').style.display = 'none';
document.getElementById('create-modal').style.display = 'none';
}
});
// =====================================================================
// Block 15: Utility Functions
// =====================================================================
// Escapes HTML to prevent Cross-Site Scripting (XSS) in Popups and Lists
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
// =====================================================================
// Block 16: Application Startup
// =====================================================================
// Populates Category Dropdown in Create Modal from Categories Object
function buildCategoryDropdown() {
var select = document.getElementById('create-category');
for (var key in CATEGORIES) {
var cat = CATEGORIES[key];
var option = document.createElement('option');
option.value = key;
option.textContent = cat.icon + ' ' + cat.label;
select.appendChild(option);
}
}
// Populates Category Dropdown
buildCategoryDropdown();
// Initializes Category Filter in Sidebar
buildCategoryFilter();
// Loads Contributions from API
loadContributions();
// Shows Welcome Modal on first Visit
checkWelcomeModal();

678
public/styles.css Normal file
View File

@@ -0,0 +1,678 @@
/* =====================================================================
WebGIS Citizen Participation Portal — Styles
Mobile-First Layout with CSS Custom Properties for Municipality Theme
===================================================================== */
/* -----------------------------------------------------------------
CSS Custom Properties Defaults — overridden per Municipality
----------------------------------------------------------------- */
:root {
/* Municipality Colors */
--color-primary: #00376D;
--color-primary-light: #00376D22;
/* Neutral Colors */
--color-bg: #f4f5f7;
--color-surface: #ffffff;
--color-text: #1a1a2e;
--color-text-secondary: #5a5a7a;
--color-border: #e0e0e0;
/* Feedback Colors */
--color-success: #2e7d32;
--color-error: #c62828;
--color-warning: #f57f17;
/* Spacing */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
/* Layout */
--header-height: 56px;
--footer-height: 40px;
--map-side-padding: 0px;
/* Typography */
--font-body: 'Segoe UI', system-ui, -apple-system, sans-serif;
--font-heading: 'Segoe UI', system-ui, -apple-system, sans-serif;
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
}
/* -----------------------------------------------------------------
Reset and Base
----------------------------------------------------------------- */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
overflow: hidden;
font-family: var(--font-body);
font-size: 15px;
color: var(--color-text);
background: var(--color-bg);
}
/* -----------------------------------------------------------------
Header
----------------------------------------------------------------- */
#app-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
height: var(--header-height);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--space-md);
background: var(--color-primary);
color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.header-left {
display: flex;
align-items: center;
gap: var(--space-sm);
min-width: 0;
}
.header-logo {
height: 36px;
width: auto;
object-fit: contain;
flex-shrink: 0;
}
.header-title {
font-family: var(--font-heading);
font-size: 1.1rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-nav {
display: flex;
gap: var(--space-xs);
}
.nav-btn {
display: flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-sm) var(--space-md);
border: none;
background: rgba(255, 255, 255, 0.1);
color: white;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: background var(--transition-fast);
}
.nav-btn:hover {
background: rgba(255, 255, 255, 0.25);
}
.nav-btn-admin {
text-decoration: none;
margin-left: var(--space-sm);
background: rgba(255, 255, 255, 0.05);
opacity: 0.6;
font-size: 0.8rem;
}
.nav-btn-admin:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.2);
}
.header-menu-toggle {
display: none;
border: none;
background: none;
color: white;
font-size: 1.4rem;
cursor: pointer;
padding: var(--space-sm);
}
/* -----------------------------------------------------------------
Main and Map Container
----------------------------------------------------------------- */
#app-main {
position: fixed;
top: var(--header-height);
bottom: var(--footer-height);
left: var(--map-side-padding);
right: var(--map-side-padding);
}
#map {
width: 100%;
height: 100%;
}
/* -----------------------------------------------------------------
Footer
----------------------------------------------------------------- */
#app-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
height: var(--footer-height);
display: flex;
align-items: center;
justify-content: center;
padding: 0 var(--space-md);
background: var(--color-surface);
border-top: 1px solid var(--color-border);
font-size: 0.8rem;
color: var(--color-text-secondary);
}
.footer-content {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.footer-logo {
height: 22px;
width: auto;
object-fit: contain;
}
/* -----------------------------------------------------------------
Mouse Position Display
----------------------------------------------------------------- */
.mouse-position-display {
background: rgba(255, 255, 255, 0.85);
padding: 2px 8px;
font-size: 0.75rem;
border-radius: 4px;
}
@media (max-width: 768px) {
.mouse-position-display {
display: none;
}
}
/* -----------------------------------------------------------------
GPS Control Button
----------------------------------------------------------------- */
.gps-control-button {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
font-size: 16px;
}
/* -----------------------------------------------------------------
Sidebar Overrides
----------------------------------------------------------------- */
.leaflet-sidebar {
z-index: 999;
top: var(--header-height);
bottom: var(--footer-height);
}
.leaflet-sidebar-header {
background: var(--color-primary);
color: white;
}
.leaflet-sidebar-close {
color: white;
}
.leaflet-sidebar-tabs > ul > li > a {
color: var(--color-text-secondary);
transition: color var(--transition-fast);
}
.leaflet-sidebar-tabs > ul > li.active > a {
color: var(--color-primary);
border-color: var(--color-primary);
}
.sidebar-body {
padding: var(--space-md);
}
.sidebar-body h3 {
font-size: 0.95rem;
font-weight: 600;
margin: var(--space-lg) 0 var(--space-sm) 0;
color: var(--color-primary);
}
.sidebar-body h3:first-child {
margin-top: 0;
}
.sidebar-body p {
margin-bottom: var(--space-sm);
line-height: 1.5;
color: var(--color-text-secondary);
}
/* -----------------------------------------------------------------
Contributions List (Sidebar Tab)
----------------------------------------------------------------- */
.list-search {
margin-bottom: var(--space-md);
}
.contribution-card {
padding: var(--space-md);
margin-bottom: var(--space-sm);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
cursor: pointer;
transition: box-shadow var(--transition-fast), border-color var(--transition-fast);
}
.contribution-card:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.contribution-card-header {
display: flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: var(--space-xs);
}
.contribution-card-category {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-primary);
}
.contribution-card-title {
font-weight: 600;
font-size: 0.95rem;
margin-bottom: var(--space-xs);
}
.contribution-card-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--color-text-secondary);
}
.contribution-card-votes {
display: flex;
gap: var(--space-sm);
}
/* -----------------------------------------------------------------
News Items (Sidebar Tab)
----------------------------------------------------------------- */
.news-item {
padding: var(--space-md);
margin-bottom: var(--space-sm);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
}
.news-date {
font-size: 0.75rem;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.news-item h3 {
font-size: 0.95rem;
margin: var(--space-xs) 0;
color: var(--color-text);
}
/* -----------------------------------------------------------------
Modals (Welcome, Login, Create Contribution)
----------------------------------------------------------------- */
.modal-overlay {
position: fixed;
inset: 0;
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.modal-content {
background: var(--color-surface);
border-radius: 12px;
padding: var(--space-xl);
max-width: 480px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
}
.modal-small {
max-width: 380px;
}
.modal-content h2 {
font-family: var(--font-heading);
font-size: 1.3rem;
margin-bottom: var(--space-md);
color: var(--color-primary);
display: flex;
align-items: center;
gap: var(--space-sm);
}
.modal-content p {
line-height: 1.6;
margin-bottom: var(--space-sm);
color: var(--color-text-secondary);
}
.modal-content ul {
margin: var(--space-sm) 0 var(--space-md) var(--space-lg);
line-height: 1.8;
color: var(--color-text-secondary);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-sm);
margin-top: var(--space-lg);
}
/* -----------------------------------------------------------------
Form Elements
----------------------------------------------------------------- */
.form-group {
margin-bottom: var(--space-md);
}
.form-group label {
display: block;
font-size: 0.85rem;
font-weight: 600;
margin-bottom: var(--space-xs);
color: var(--color-text);
}
.form-input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
font-family: var(--font-body);
font-size: 0.9rem;
color: var(--color-text);
background: var(--color-surface);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.form-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-primary-light);
}
textarea.form-input {
resize: vertical;
}
select.form-input {
cursor: pointer;
}
/* -----------------------------------------------------------------
Buttons
----------------------------------------------------------------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-xs);
padding: 10px 20px;
border: none;
border-radius: 6px;
font-family: var(--font-body);
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background var(--transition-fast), transform var(--transition-fast);
min-height: 44px;
min-width: 44px;
}
.btn:active {
transform: scale(0.97);
}
.btn-primary {
background: var(--color-primary);
color: white;
}
.btn-primary:hover {
filter: brightness(1.15);
}
.btn-secondary {
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
}
.btn-secondary:hover {
background: var(--color-border);
}
.btn-success {
background: var(--color-success);
color: white;
}
.btn-danger {
background: var(--color-error);
color: white;
}
/* -----------------------------------------------------------------
Map Popup Overrides (Contribution Detail View)
----------------------------------------------------------------- */
.popup-detail {
min-width: 220px;
max-width: 300px;
}
.popup-detail-title {
font-weight: 600;
font-size: 1rem;
margin-bottom: var(--space-xs);
}
.popup-detail-category {
display: inline-block;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 2px 8px;
border-radius: 4px;
background: var(--color-primary-light);
color: var(--color-primary);
margin-bottom: var(--space-sm);
}
.popup-detail-description {
font-size: 0.85rem;
line-height: 1.5;
color: var(--color-text-secondary);
margin-bottom: var(--space-sm);
}
.popup-detail-meta {
font-size: 0.75rem;
color: var(--color-text-secondary);
margin-bottom: var(--space-sm);
padding-top: var(--space-sm);
border-top: 1px solid var(--color-border);
}
.popup-detail-votes {
display: flex;
gap: var(--space-sm);
margin-top: var(--space-sm);
}
.popup-vote-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border: 1px solid var(--color-border);
border-radius: 20px;
background: var(--color-surface);
cursor: pointer;
font-size: 0.85rem;
transition: all var(--transition-fast);
}
.popup-vote-btn:hover {
border-color: var(--color-primary);
background: var(--color-primary-light);
}
.popup-vote-btn.liked {
border-color: var(--color-success);
background: #e8f5e9;
color: var(--color-success);
}
.popup-vote-btn.disliked {
border-color: var(--color-error);
background: #ffebee;
color: var(--color-error);
}
.popup-detail-actions {
display: flex;
gap: var(--space-sm);
margin-top: var(--space-sm);
}
.popup-detail-actions .btn {
flex: 1;
padding: 6px 12px;
font-size: 0.8rem;
min-height: 36px;
}
/* -----------------------------------------------------------------
SweetAlert Font Override
----------------------------------------------------------------- */
.swal2-input,
.swal2-textarea {
font-family: var(--font-body) !important;
}
/* -----------------------------------------------------------------
Mobile Responsive Overrides
----------------------------------------------------------------- */
@media (max-width: 768px) {
:root {
--header-height: 48px;
--footer-height: 32px;
--map-side-padding: 0px;
}
.header-title {
font-size: 0.9rem;
}
.header-nav {
display: none;
position: absolute;
top: var(--header-height);
right: 0;
background: var(--color-primary);
flex-direction: column;
padding: var(--space-sm);
border-radius: 0 0 0 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.header-nav.open {
display: flex;
}
.header-menu-toggle {
display: block;
}
.nav-label {
display: inline;
}
.modal-content {
padding: var(--space-lg);
border-radius: 8px;
}
}
/* -----------------------------------------------------------------
Desktop Overrides
----------------------------------------------------------------- */
@media (min-width: 769px) {
:root {
--map-side-padding: 8px;
}
}

129
scripts/backup.sh Normal file
View File

@@ -0,0 +1,129 @@
#!/usr/bin/env bash
# =====================================================================
# WebGIS Database Backup Script
# Location: /opt/webgis-lohne/scripts/backup.sh (on Server)
# Purpose: Creates compressed pg_dump Backups with daily/weekly/monthly
# Rotation. Intended to be run via Cron.
# =====================================================================
# Safety Switches
set -euo pipefail
# Logs Error Messages
trap 'echo "[$(date)] ERROR: Script failed at Line ${LINENO} with Exit Code $?."' ERR
# ---------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------
DB_HOST="localhost"
DB_PORT="5432"
DB_NAME="webgis-db"
DB_USER="webgis-db-admin"
BACKUP_ROOT="/var/backups/webgis"
BACKUP_DIR_DAILY="${BACKUP_ROOT}/daily"
BACKUP_DIR_WEEKLY="${BACKUP_ROOT}/weekly"
BACKUP_DIR_MONTHLY="${BACKUP_ROOT}/monthly"
# Retention Periods in Days
KEEP_DAILY=7
KEEP_WEEKLY=28
KEEP_MONTHLY=365
# Minimum acceptable Backup File Size in Bytes
# Valid Dumps of even empty Databases are several KBs
MIN_BACKUP_SIZE=10000
# Password is read from protected File
# pg_dump honors the PGPASSFILE Environment Variable.
export PGPASSFILE="/root/.pgpass_webgis"
# ---------------------------------------------------------------------
# Preflight Checks
# ---------------------------------------------------------------------
# Check pg_dump Availability
if ! command -v pg_dump &> /dev/null; then
echo "[$(date)] ERROR: pg_dump not found. Install postgresql-client."
exit 1
fi
# Check Password File Existence and Permissions
if [[ ! -f "${PGPASSFILE}" ]]; then
echo "[$(date)] ERROR: Password File ${PGPASSFILE} not found."
exit 1
fi
PGPASS_PERMS=$(stat -c "%a" "${PGPASSFILE}")
if [[ "${PGPASS_PERMS}" != "600" ]]; then
echo "[$(date)] ERROR: ${PGPASSFILE} has Permissions ${PGPASS_PERMS}, expected 600."
exit 1
fi
# ---------------------------------------------------------------------
# Preparation
# ---------------------------------------------------------------------
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
DAY_OF_WEEK=$(date +"%u") # 1=Monday ... 7=Sunday
DAY_OF_MONTH=$(date +"%d")
mkdir -p "${BACKUP_DIR_DAILY}" "${BACKUP_DIR_WEEKLY}" "${BACKUP_DIR_MONTHLY}"
# ---------------------------------------------------------------------
# Create Daily Backup in compressed Custom Format
# ---------------------------------------------------------------------
DAILY_FILE="${BACKUP_DIR_DAILY}/webgis_${TIMESTAMP}.dump"
echo "[$(date)] Starting daily Backup -> ${DAILY_FILE}"
pg_dump \
--host="${DB_HOST}" \
--port="${DB_PORT}" \
--username="${DB_USER}" \
--format=custom \
--compress=9 \
--file="${DAILY_FILE}" \
"${DB_NAME}"
# Verify Backup File Size
BACKUP_SIZE=$(stat -c "%s" "${DAILY_FILE}")
if [[ "${BACKUP_SIZE}" -lt "${MIN_BACKUP_SIZE}" ]]; then
echo "[$(date)] ERROR: Backup File is only ${BACKUP_SIZE} Bytes (Minimum: ${MIN_BACKUP_SIZE}). Dump probably corrupt."
exit 1
fi
echo "[$(date)] Daily Backup complete (${BACKUP_SIZE} Bytes)."
# ---------------------------------------------------------------------
# Promote to Weekly Backup on Sundays
# ---------------------------------------------------------------------
if [[ "${DAY_OF_WEEK}" == "7" ]]; then
cp "${DAILY_FILE}" "${BACKUP_DIR_WEEKLY}/webgis_${TIMESTAMP}.dump"
echo "[$(date)] Promoted to weekly Backup."
fi
# ---------------------------------------------------------------------
# Promote to Monthly Backup on the First of the Month
# ---------------------------------------------------------------------
if [[ "${DAY_OF_MONTH}" == "01" ]]; then
cp "${DAILY_FILE}" "${BACKUP_DIR_MONTHLY}/webgis_${TIMESTAMP}.dump"
echo "[$(date)] Promoted to monthly Backup."
fi
# ---------------------------------------------------------------------
# Rotation: Delete Backups older than Retention Period
# ---------------------------------------------------------------------
find "${BACKUP_DIR_DAILY}" -name "*.dump" -mtime +${KEEP_DAILY} -delete
find "${BACKUP_DIR_WEEKLY}" -name "*.dump" -mtime +${KEEP_WEEKLY} -delete
find "${BACKUP_DIR_MONTHLY}" -name "*.dump" -mtime +${KEEP_MONTHLY} -delete
echo "[$(date)] Backup Rotation complete."

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,41 +0,0 @@
#mapdiv {
height: 100vh;
}
.popup-container {
width: 80vw; /* 80% of the viewport width */
max-width: 300px; /* Maximum width */
height: 60vh; /* 60% of the viewport height */
max-height: 350px; /* Maximum height */
padding: 10px; /* Add some padding */
box-sizing: border-box; /* Ensure padding is included in width/height */
}
.popup-form-group {
display: flex; /* popup-label und popup-input nebeneinander statt untereinander */
align-items: center;
margin-bottom: 15px;
}
.popup-label {
flex: 1;
margin-right: 10px;
}
.popup-input {
flex: 2;
}
.popup-button-group {
display: flex;
justify-content: space-between;
}
.popup-button {
flex: 1;
margin-right: 10px;
}
.popup-button:last-child {
margin-right: 0;
}

21
README.md Normal file
View File

@@ -0,0 +1,21 @@
# WebGIS Citizen Participation Portal
Citizen Participation Portal for Lohne (Oldenburg).
## Project Structure
- `migrations/` — versioned SQL Schema Migrations
- `api/` — Backend (PHP)
- `public/` — Frontend (HTML, CSS, JS)
- `scripts/` — Maintenance Scripts (backup, deployment)
- `legacy/` — Reference Code from Prototype
## Local Setup
1. Copy `.env.example` to `.env` and fill in Database Credentials.
2. Run the SQL Migration in pgAdmin and execute in the target database.
3. Serve `public/` with a PHP-capable Web Server.
## SSH tunnel to database server
1. Create SSH Tunnel to Database Server.