Compare commits
120 Commits
7bcb31a8f8
...
dev/lukas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f15e92d65 | ||
|
|
942affd5e5 | ||
|
|
02ba53724e | ||
|
|
d0bba3d3f8 | ||
| f23897018c | |||
| c39667e368 | |||
| cb8994b493 | |||
| 62ba9b5345 | |||
| 360eb3744a | |||
| 601c13012c | |||
| 6200b061f2 | |||
| fa984e7391 | |||
| 125c255115 | |||
| 04e692a6dd | |||
| 25cf797294 | |||
| 62ae9f18b0 | |||
| 5cadc5c1b4 | |||
| 9ca215c36d | |||
| 04f96b7aba | |||
| ffe81cdf88 | |||
| c9040b2f4e | |||
| 9c8e641557 | |||
| 076e82213d | |||
| 6a721fde7c | |||
| 8179498333 | |||
| ec4c9fa8a9 | |||
| 8d67c0c0b9 | |||
| ade9ca2128 | |||
| 2993a443a7 | |||
|
|
025cd975f0 | ||
|
|
0b02b435ef | ||
|
|
c52dbf618e | ||
|
|
2b1f7e3a38 | ||
| 4926433c35 | |||
| aae29618b3 | |||
| a828a3878e | |||
| f107d97b87 | |||
| 7e6b55abd4 | |||
| d98d6a6713 | |||
| 3e73dee40b | |||
| adf863934e | |||
| 27d41c0847 | |||
| 9d7eb25d1f | |||
| f30a01615e | |||
| 2c02a61791 | |||
| a38cf999f2 | |||
| 78bdc22781 | |||
| f810ed520c | |||
| 2b3fcb6ebf | |||
| 5fe7522f5f | |||
| f8f0d514bb | |||
| 5e8b4745f1 | |||
| c3569d6b98 | |||
| 7dea362c89 | |||
| 11a062dd84 | |||
| aec6a9bfb6 | |||
| 94d4308d3f | |||
| a37c1ffe01 | |||
| 8151390835 | |||
| 99cf34671a | |||
| f9187a3e84 | |||
| 94100b9371 | |||
| 84ce0de870 | |||
| 391cec07c8 | |||
| d3cfcbab25 | |||
| 1eafc27c53 | |||
| dbacae3f2e | |||
|
|
de9724b820 | ||
| 556c5ea4b9 | |||
| 1dfffd93e5 | |||
| b3879d812f | |||
| f0a88b13d1 | |||
|
|
7aa0cad5fb | ||
| e459a86edb | |||
| adc2b71eb7 | |||
| b6bedc788b | |||
| 583bbcd27d | |||
| 2a24f486b5 | |||
| d29f484993 | |||
| 3f72ef3bc4 | |||
| a0cbe29f97 | |||
|
|
15705dac97 | ||
| c8f4832a95 | |||
| 1714e33fa7 | |||
|
|
5e66e73db6 | ||
| 1337b0dca3 | |||
| 765b74ceec | |||
| 871e43aef5 | |||
| bfc21d8fb6 | |||
|
|
250ca9909d | ||
| c249c8e049 | |||
| 958f15a7a4 | |||
| 855b69f95d | |||
| 77df35926d | |||
| 65ef7f07c9 | |||
| 6eca88e941 | |||
| 801131985d | |||
| 4707e73421 | |||
| 241ec75323 | |||
| d3297d2a3c | |||
| c7e9444903 | |||
| 72315b4030 | |||
| 403d81b132 | |||
| 4f35ddeafe | |||
| 19b038d4f5 | |||
| 4554ea3ff0 | |||
| 0083a05482 | |||
| 041d1603dc | |||
| b3a4ba6d4a | |||
| 04dc118598 | |||
| dec36d4053 | |||
| d2f2b577be | |||
| a640ed1b78 | |||
| 7c0c0b5048 | |||
| 50035a524d | |||
| e8ce6c6f36 | |||
| 97ab6a52ab | |||
| b8f1c32a22 | |||
| 0aeee9a168 | |||
|
|
1f8e3935bb |
8
.env.example
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Example Environment Configfile
|
||||||
|
POSTGRES_HOSTNAME=postgres_host
|
||||||
|
POSTGRES_PORT=postgres_port
|
||||||
|
POSTGRES_DB=postgres_database
|
||||||
|
POSTGRES_USER=postgres_user
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
ADMIN_PASSWORD=
|
||||||
|
MUNICIPALITY_SLUG=lohne
|
||||||
8
.gitattributes
vendored
Normal 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
|
||||||
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.env
|
||||||
|
.vscode/
|
||||||
|
*.log
|
||||||
|
scripts
|
||||||
|
|
||||||
|
public/uploads/photos/*
|
||||||
|
!public/uploads/photos/.gitkeep
|
||||||
183
EXTENSION.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# Neue Ideenkarte anlegen
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
| Variable | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| `<name>` | Name der Kommune (z.B. `lohne`) |
|
||||||
|
| `<ID>` | Eindeutige Port-ID für die Datenbank (z.B. `4` → Port `5434`) |
|
||||||
|
| `<branch-name>` | Git-Branch des Frontend-Repos |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 1 — DNS Record anlegen
|
||||||
|
|
||||||
|
Im DNS-Panel einen neuen A-Record anlegen:
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Name | `<name>` |
|
||||||
|
| Typ | `A` |
|
||||||
|
| Ziel | `195.59.32.237` |
|
||||||
|
| TTL | `600s` |
|
||||||
|
|
||||||
|
> ⚠️ DNS muss vollständig propagiert sein, bevor Certbot in Schritt 3 ausgeführt wird.
|
||||||
|
|
||||||
|
Propagation prüfen:
|
||||||
|
```bash
|
||||||
|
dig <name>.endex-geodaten.de
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 2 — Nginx `default.conf` anpassen
|
||||||
|
|
||||||
|
### 2a — Subdomain in den Port-80-Block eintragen
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server_name endex-geodaten.de www.endex-geodaten.de git.endex-geodaten.de lohne.endex-geodaten.de <name>.endex-geodaten.de localhost;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2b — Neuen HTTPS-Server-Block hinzufügen
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# WEBGIS <NAME>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 3 — SSL-Zertifikat erneuern
|
||||||
|
|
||||||
|
Da kein Wildcard-Zertifikat verwendet wird, muss das Cert neu ausgestellt werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose run --rm certbot certonly --webroot \
|
||||||
|
--webroot-path=/var/www/certbot \
|
||||||
|
-d endex-geodaten.de \
|
||||||
|
-d www.endex-geodaten.de \
|
||||||
|
-d git.endex-geodaten.de \
|
||||||
|
-d lohne.endex-geodaten.de \
|
||||||
|
-d <name>.endex-geodaten.de
|
||||||
|
```
|
||||||
|
|
||||||
|
Nginx neu laden:
|
||||||
|
```bash
|
||||||
|
docker compose exec nginx nginx -s reload
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 4 — Docker Container in `docker-compose.yml` anlegen
|
||||||
|
|
||||||
|
### PHP/UI Container
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
webgis-<name>-php:
|
||||||
|
build: php-docker/
|
||||||
|
container_name: webgis-<name>-php
|
||||||
|
volumes:
|
||||||
|
- ./webgis-<name>:/var/www/webgis-<name>
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
- webgis-<name>-nw
|
||||||
|
```
|
||||||
|
|
||||||
|
### Datenbank Container
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
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_<NAME>_DB_USER}
|
||||||
|
- POSTGRES_PASSWORD=${WEBGIS_<NAME>_DB_PW}
|
||||||
|
- POSTGRES_DB=${WEBGIS_<NAME>_DB_NAME}
|
||||||
|
volumes:
|
||||||
|
- ./webgis-<name>-data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- webgis-<name>-nw
|
||||||
|
```
|
||||||
|
|
||||||
|
### Netzwerk ergänzen
|
||||||
|
|
||||||
|
Unter dem `networks:` Block am Ende der `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
networks:
|
||||||
|
webgis-<name>-nw:
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 5 — Nginx Volume ergänzen
|
||||||
|
|
||||||
|
Beim nginx-Service in `docker-compose.yml` das neue Volume eintragen:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./webgis-<name>:/var/www/webgis-<name>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 6 — Frontend Source Code klonen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule add -b <branch-name> https://git.endex-geodaten.de/lukas.uptmoor/webgis-<name>.git
|
||||||
|
```
|
||||||
|
|
||||||
|
> Jede Kommune erhält ein eigenes Repo, da Features initial variieren können.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 7 — Container starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d webgis-<name>-php webgis-<name>-db
|
||||||
|
```
|
||||||
|
|
||||||
|
Logs prüfen:
|
||||||
|
```bash
|
||||||
|
docker compose logs -f webgis-<name>-php
|
||||||
|
docker compose logs -f webgis-<name>-db
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 8 — Datenbank vorbereiten
|
||||||
|
|
||||||
|
SSH-Tunnel öffnen:
|
||||||
|
```bash
|
||||||
|
ssh -L 5433:localhost:543<ID> root@endex-geodaten.de
|
||||||
|
```
|
||||||
|
|
||||||
|
Strukturen laden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it webgis-<name>-db psql -U $POSTGRES_USER -d $POSTGRES_DB < migrations/001_initial_schema.sql
|
||||||
|
```
|
||||||
26
init.php
@@ -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();
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
169
migrations/001_initial_schema.sql
Normal 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
|
||||||
|
-- =====================================================================
|
||||||
48
migrations/002_add_votes_index.sql
Normal 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
|
||||||
|
-- =====================================================================
|
||||||
44
migrations/003_news_table.sql
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- Migration 004: Creates News Table for Municipality Announcements
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- Block 1: Creates Table "news"
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS news (
|
||||||
|
news_id SERIAL PRIMARY KEY,
|
||||||
|
municipality_id INTEGER NOT NULL REFERENCES municipalities(municipality_id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
author_name VARCHAR(100) NOT NULL DEFAULT 'Stadtverwaltung',
|
||||||
|
published_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- Block 2: Trigger Functions
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Automatically Refresh updated_at on every UPDATE.
|
||||||
|
CREATE TRIGGER set_news_updated_at
|
||||||
|
BEFORE UPDATE ON news
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- Block 3 Indexes for fast Queries
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
CREATE INDEX idx_news_municipality ON news(municipality_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- Block 4: Seed Data — Initial News Article
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
INSERT INTO news (municipality_id, title, content)
|
||||||
|
SELECT municipality_id, 'Mitmachkarte gestartet',
|
||||||
|
'Die Mitmachkarte als Bürgerbeteiligungsportal der Stadt Lohne (Oldenburg) wird nun freigeschaltet. Wir freuen uns auf Ihre Hinweise und Vorschläge!'
|
||||||
|
FROM municipalities WHERE slug = 'lohne';
|
||||||
8
migrations/004_reverse_geocoding.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- Migration 004: Adds Address Column for Reverse Geocoding
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
ALTER TABLE contributions
|
||||||
|
ADD COLUMN address VARCHAR(255) DEFAULT NULL;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN contributions.address IS 'Reverse geocoded Address, stored automatically on Creation.';
|
||||||
27
migrations/005_browser_id.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- Migration 005: Adds Browser ID for anonymous User Identification
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
-- Adds browser_id Column to Contributions
|
||||||
|
ALTER TABLE contributions
|
||||||
|
ADD COLUMN browser_id VARCHAR(36) DEFAULT NULL;
|
||||||
|
|
||||||
|
-- Adds browser_id Column to Votes
|
||||||
|
-- Replaces voter_name for Identification
|
||||||
|
ALTER TABLE votes
|
||||||
|
ADD COLUMN browser_id VARCHAR(36) DEFAULT NULL;
|
||||||
|
|
||||||
|
-- Index for fast Vote Lookup by Browser
|
||||||
|
CREATE INDEX idx_votes_browser ON votes(browser_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- New UNIQUE Constraint: One Vote per Browser per Contribution
|
||||||
|
|
||||||
|
-- Drops old Constraint voter_name based
|
||||||
|
ALTER TABLE votes
|
||||||
|
DROP CONSTRAINT IF EXISTS votes_unique_per_voter;
|
||||||
|
|
||||||
|
-- Creates new Constraint browser_id based
|
||||||
|
ALTER TABLE votes
|
||||||
|
ADD CONSTRAINT votes_contribution_browser_unique
|
||||||
|
UNIQUE (contribution_id, browser_id);
|
||||||
36
migrations/006_comments_and_photos.sql
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- Migration 006: Comments Table and Photo Support
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- Block 1: Creates Table "comments"
|
||||||
|
-- Stores Comments on Contributions. Comments is linked to
|
||||||
|
-- Contributions and identified by browser_id.
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS comments (
|
||||||
|
comment_id SERIAL PRIMARY KEY,
|
||||||
|
contribution_id INTEGER NOT NULL REFERENCES contributions(contribution_id) ON DELETE CASCADE,
|
||||||
|
author_name VARCHAR(100) NOT NULL,
|
||||||
|
browser_id VARCHAR(36) DEFAULT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- Block 2: Indexes for fast Comment Queries
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
CREATE INDEX idx_comments_contribution ON comments(contribution_id);
|
||||||
|
CREATE INDEX idx_comments_browser ON comments(browser_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- Block 3: Adds Photo Path Column to Contributions
|
||||||
|
-- Stores relative Path to uploaded Photo File.
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
ALTER TABLE contributions
|
||||||
|
ADD COLUMN photo_path VARCHAR(255) DEFAULT NULL;
|
||||||
|
ADD COLUMN comment_count INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN contributions.photo_path IS 'Relative Path to uploaded Photo. NULL = no Photo.';
|
||||||
@@ -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]);
|
|
||||||
1
plugins/minimap/Control.MiniMap.min.css
vendored
@@ -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)}
|
|
||||||
1
plugins/minimap/Control.MiniMap.min.js
vendored
|
Before Width: | Height: | Size: 219 B |
@@ -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 |
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
*.min.js
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
github: Turbo87
|
|
||||||
custom: https://paypal.me/tobiasbieniek
|
|
||||||
@@ -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: ""
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
lib-cov
|
|
||||||
*.seed
|
|
||||||
*.log
|
|
||||||
*.csv
|
|
||||||
*.dat
|
|
||||||
*.out
|
|
||||||
*.pid
|
|
||||||
*.gz
|
|
||||||
|
|
||||||
pids
|
|
||||||
logs
|
|
||||||
results
|
|
||||||
|
|
||||||
npm-debug.log
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
|
|
||||||
.eslintcache
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: [
|
|
||||||
'stylelint-scss',
|
|
||||||
],
|
|
||||||
extends: [
|
|
||||||
'stylelint-config-recommended-scss',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'no-descending-specificity': null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
@@ -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>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## [Leaflet](http://leafletjs.com/)
|
|
||||||
|
|
||||||
 
|
|
||||||
|
|
||||||
Example code at [`examples/index.html`](examples/index.html) ([Preview](http://turbo87.github.io/sidebar-v2/examples/index.html))
|
|
||||||
|
|
||||||
|
|
||||||
## [OpenLayers 3](http://openlayers.org/)
|
|
||||||
|
|
||||||
 
|
|
||||||
|
|
||||||
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/)
|
|
||||||
|
|
||||||
 
|
|
||||||
|
|
||||||
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/)
|
|
||||||
|
|
||||||
 
|
|
||||||
|
|
||||||
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).
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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; } }
|
|
||||||
@@ -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}}
|
|
||||||
@@ -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; } }
|
|
||||||
@@ -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}}
|
|
||||||
@@ -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; } }
|
|
||||||
@@ -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}}
|
|
||||||
@@ -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; } }
|
|
||||||
@@ -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}}
|
|
||||||
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 333 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 559 KiB |
@@ -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.
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 © OpenStreetMap contributors'
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
var marker = L.marker([51.2, 7]).addTo(map);
|
|
||||||
|
|
||||||
var sidebar = L.control.sidebar('sidebar').addTo(map);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 © 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>
|
|
||||||
@@ -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'));
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -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};
|
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
@@ -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)};
|
|
||||||
@@ -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();
|
|
||||||
};
|
|
||||||
@@ -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()};
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
@@ -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 = '×';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
@@ -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; } }
|
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
891
public/admin.php
Normal file
@@ -0,0 +1,891 @@
|
|||||||
|
<?php
|
||||||
|
// =====================================================================
|
||||||
|
// Moderation Page
|
||||||
|
// Lists Contributions for Review. Moderators can approve, reject,
|
||||||
|
// edit and delete Contributions. Includes Map Preview and Filtering.
|
||||||
|
//
|
||||||
|
// ToDo's:
|
||||||
|
// - Comment Moderation Tab
|
||||||
|
// - News Management Tab
|
||||||
|
// - User Management Tab
|
||||||
|
// - Analytics Tab
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/api/db.php';
|
||||||
|
require_once __DIR__ . '/api/auth.php';
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Routing: Login, Logout, or Main Page
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
$page = $_GET['page'] ?? 'main';
|
||||||
|
|
||||||
|
// Handles Login
|
||||||
|
if ($page === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
if (admin_login($password)) {
|
||||||
|
header('Location: admin.php');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$login_error = 'Falsches Passwort.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles Logout
|
||||||
|
if ($page === 'logout') {
|
||||||
|
admin_logout();
|
||||||
|
header('Location: admin.php?page=login');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Loads Municipality Configuration for Theming
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
$pdo = get_db();
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM municipalities WHERE slug = :slug");
|
||||||
|
$stmt->execute([':slug' => getenv('MUNICIPALITY_SLUG')]);
|
||||||
|
$municipality = $stmt->fetch();
|
||||||
|
|
||||||
|
// Loads News for Moderation
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT news_id, title, content, author_name, published_at, created_at
|
||||||
|
FROM news
|
||||||
|
WHERE municipality_id = :mid
|
||||||
|
ORDER BY published_at DESC
|
||||||
|
");
|
||||||
|
$stmt->execute([':mid' => $municipality['municipality_id']]);
|
||||||
|
$news_items = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Shows Login Page if not authenticated
|
||||||
|
if ($page === 'login' || !is_admin()) {
|
||||||
|
show_login_page($municipality, $login_error ?? null);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Loads shared Category Definitions
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
$categories = get_categories();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Loads Contributions and Statistics
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
// Loads all Contributions for Municipality
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT contribution_id, title, category, description, author_name,
|
||||||
|
geom_type, status, likes_count, dislikes_count, created_at, updated_at
|
||||||
|
FROM contributions
|
||||||
|
WHERE municipality_id = :mid
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
");
|
||||||
|
$stmt->execute([':mid' => $municipality['municipality_id']]);
|
||||||
|
$all_contributions = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Counts per Status
|
||||||
|
$counts = ['pending' => 0, 'approved' => 0, 'rejected' => 0];
|
||||||
|
foreach ($all_contributions as $item) {
|
||||||
|
if (isset($counts[$item['status']])) {
|
||||||
|
$counts[$item['status']]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$counts['total'] = count($all_contributions);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Renders 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="icon" href="<?= htmlspecialchars($municipality['logo_path'] ?? 'assets/icon-municipality.png') ?>" type="image/png">
|
||||||
|
|
||||||
|
<!-- Loads CSS Dependencies -->
|
||||||
|
|
||||||
|
<!-- Font Awesome for Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Leaflet -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css">
|
||||||
|
|
||||||
|
<!-- Application Styles -->
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Loads JavaScript Dependencies -->
|
||||||
|
|
||||||
|
<!-- SweetAlert2 -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.14.0/dist/sweetalert2.all.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Loads Municipality Theme from Database -->
|
||||||
|
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
<!-- Header -->
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-inner">
|
||||||
|
<h1><i class="fa-solid fa-shield-halved"></i> Moderationsportal <?= htmlspecialchars($municipality['name']) ?></h1>
|
||||||
|
<div class="page-header-nav">
|
||||||
|
<a href="index.php"><i class="fa-solid fa-map"></i> Bürgerportal</a>
|
||||||
|
<a href="admin.php?page=logout"><i class="fa-solid fa-right-from-bracket"></i> Abmelden</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-container">
|
||||||
|
|
||||||
|
<!-- ========================================================= -->
|
||||||
|
<!-- Page Navigation Tabs -->
|
||||||
|
<!-- ========================================================= -->
|
||||||
|
<div class="page-tabs">
|
||||||
|
<button class="page-tab active" onclick="showPageTab('contributions')">
|
||||||
|
<i class="fa-solid fa-list-check"></i> Beiträge
|
||||||
|
</button>
|
||||||
|
<button class="page-tab" onclick="showPageTab('news')">
|
||||||
|
<i class="fa-solid fa-newspaper"></i> Neuigkeiten
|
||||||
|
</button>
|
||||||
|
<button class="page-tab" onclick="showPageTab('stats')">
|
||||||
|
<i class="fa-solid fa-chart-bar"></i> Statistik
|
||||||
|
</button>
|
||||||
|
<button class="page-tab" onclick="showPageTab('users')">
|
||||||
|
<i class="fa-solid fa-users"></i> Benutzer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ========================================================= -->
|
||||||
|
<!-- Contributions Tab -->
|
||||||
|
<!-- ========================================================= -->
|
||||||
|
<div id="tab-contributions" class="page-tab-content">
|
||||||
|
|
||||||
|
<!-- Statistics Cards -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number"><?= $counts['total'] ?></div>
|
||||||
|
<div class="stat-label">Alle</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number"><?= $counts['pending'] ?></div>
|
||||||
|
<div class="stat-label">Ausstehend</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number"><?= $counts['approved'] ?></div>
|
||||||
|
<div class="stat-label">Akzeptiert</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number"><?= $counts['rejected'] ?></div>
|
||||||
|
<div class="stat-label">Abgelehnt</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Filter Tabs -->
|
||||||
|
<div class="filter-tabs">
|
||||||
|
<button class="filter-tab active" onclick="filterByStatus('all', this)">
|
||||||
|
Alle <span class="tab-count"><?= $counts['total'] ?></span>
|
||||||
|
</button>
|
||||||
|
<button class="filter-tab" onclick="filterByStatus('pending', this)">
|
||||||
|
Ausstehend <span class="tab-count"><?= $counts['pending'] ?></span>
|
||||||
|
</button>
|
||||||
|
<button class="filter-tab" onclick="filterByStatus('approved', this)">
|
||||||
|
Akzeptiert <span class="tab-count"><?= $counts['approved'] ?></span>
|
||||||
|
</button>
|
||||||
|
<button class="filter-tab" onclick="filterByStatus('rejected', this)">
|
||||||
|
Abgelehnt <span class="tab-count"><?= $counts['rejected'] ?></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sort Controls -->
|
||||||
|
<div class="sort-controls">
|
||||||
|
<span id="visible-count"><?= $counts['total'] ?> Beiträge</span>
|
||||||
|
<select onchange="sortContributions(this.value)">
|
||||||
|
<option value="date-desc">Neueste zuerst</option>
|
||||||
|
<option value="date-asc">Älteste zuerst</option>
|
||||||
|
<option value="category">Nach Kategorie</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contribution List -->
|
||||||
|
<div id="contributions-container">
|
||||||
|
<?php if (empty($all_contributions)): ?>
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fa-solid fa-inbox" style="font-size:2rem;margin-bottom:8px;display:block;"></i>
|
||||||
|
Noch keine Beiträge vorhanden.
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($all_contributions as $item):
|
||||||
|
$cat = $categories[$item['category']] ?? ['label' => $item['category'], 'faIcon' => 'fa-question', 'color' => '#999'];
|
||||||
|
$status_label = ['pending' => 'Ausstehend', 'approved' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
|
||||||
|
?>
|
||||||
|
<div class="contribution-row"
|
||||||
|
data-status="<?= $item['status'] ?>"
|
||||||
|
data-category="<?= htmlspecialchars($item['category']) ?>"
|
||||||
|
data-date="<?= $item['created_at'] ?>"
|
||||||
|
data-id="<?= $item['contribution_id'] ?>">
|
||||||
|
|
||||||
|
<!-- Collapsed Header: Title + Status -->
|
||||||
|
<div class="contribution-row-header" onclick="toggleRow(this.parentElement)">
|
||||||
|
<div class="contribution-row-summary">
|
||||||
|
<span class="title"><?= htmlspecialchars($item['title']) ?></span>
|
||||||
|
<span class="badge badge-category">
|
||||||
|
<i class="fa-solid <?= $cat['faIcon'] ?>"></i>
|
||||||
|
<?= $cat['label'] ?>
|
||||||
|
</span>
|
||||||
|
<span class="badge badge-<?= $item['status'] ?>"><?= $status_label[$item['status']] ?? $item['status'] ?></span>
|
||||||
|
</div>
|
||||||
|
<i class="fa-solid fa-chevron-down collapse-icon"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expanded Detail -->
|
||||||
|
<div class="contribution-row-detail">
|
||||||
|
<div class="detail-layout">
|
||||||
|
<!-- Map Preview -->
|
||||||
|
<div class="detail-map" id="map-<?= $item['contribution_id'] ?>"
|
||||||
|
data-contribution-id="<?= $item['contribution_id'] ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="detail-content">
|
||||||
|
<?php if ($item['description']): ?>
|
||||||
|
<div class="description"><?= htmlspecialchars($item['description']) ?></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="description empty">Keine Beschreibung vorhanden.</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="detail-meta">
|
||||||
|
<span><i class="fa-solid fa-user"></i> <?= htmlspecialchars($item['author_name']) ?></span>
|
||||||
|
<span><i class="fa-solid fa-calendar"></i> <?= date('d.m.Y, H:i', strtotime($item['created_at'])) ?> Uhr</span>
|
||||||
|
<span>
|
||||||
|
<i class="fa-solid fa-thumbs-up"></i> <?= $item['likes_count'] ?>
|
||||||
|
·
|
||||||
|
<i class="fa-solid fa-thumbs-down"></i> <?= $item['dislikes_count'] ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<?php if ($item['status'] !== 'approved'): ?>
|
||||||
|
<button class="btn btn-approve" onclick="changeStatus(<?= $item['contribution_id'] ?>, 'approved')">
|
||||||
|
<i class="fa-solid fa-check"></i> Akzeptieren
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($item['status'] !== 'rejected'): ?>
|
||||||
|
<button class="btn btn-reject" onclick="changeStatus(<?= $item['contribution_id'] ?>, 'rejected')">
|
||||||
|
<i class="fa-solid fa-xmark"></i> Ablehnen
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($item['status'] !== 'pending'): ?>
|
||||||
|
<button class="btn btn-reset" onclick="changeStatus(..., 'pending')">
|
||||||
|
<i class="fa-solid fa-rotate-left"></i> Zurücksetzen
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<button class="btn btn-edit" onclick="editContribution(<?= $item['contribution_id'] ?>, '<?= htmlspecialchars(addslashes($item['title']), ENT_QUOTES) ?>', '<?= htmlspecialchars(addslashes($item['description'] ?? ''), ENT_QUOTES) ?>')">
|
||||||
|
<i class="fa-solid fa-pen"></i> Bearbeiten
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-delete" onclick="deleteContribution(<?= $item['contribution_id'] ?>)">
|
||||||
|
<i class="fa-solid fa-trash"></i> Löschen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a class="btn btn-map" href="index.php" target="_blank">
|
||||||
|
<i class="fa-solid fa-map-location-dot"></i> Karte
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ========================================================= -->
|
||||||
|
<!-- News Article Tab -->
|
||||||
|
<!-- ========================================================= -->
|
||||||
|
<div id="tab-news" class="page-tab-content" style="display:none;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
|
||||||
|
<h2 style="margin:0;border:none;padding:0;"><i class="fa-solid fa-newspaper"></i> Neuigkeiten</h2>
|
||||||
|
<button class="btn btn-approve" onclick="createNews()">
|
||||||
|
<i class="fa-solid fa-plus"></i> Nachricht hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (empty($news_items)): ?>
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fa-solid fa-newspaper" style="font-size:2rem;margin-bottom:8px;display:block;"></i>
|
||||||
|
Noch keine Neuigkeiten veröffentlicht.
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($news_items as $news): ?>
|
||||||
|
<div class="contribution-row" data-id="<?= $news['news_id'] ?>">
|
||||||
|
<div class="contribution-row-header" onclick="toggleRow(this.parentElement)">
|
||||||
|
<div class="contribution-row-summary">
|
||||||
|
<span class="title"><?= htmlspecialchars($news['title']) ?></span>
|
||||||
|
<span style="font-size:0.8rem;color:#999;">
|
||||||
|
<?= date('d.m.Y', strtotime($news['published_at'])) ?>
|
||||||
|
· <?= htmlspecialchars($news['author_name']) ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<i class="fa-solid fa-chevron-down collapse-icon"></i>
|
||||||
|
</div>
|
||||||
|
<div class="contribution-row-detail">
|
||||||
|
<div style="padding:12px 0;font-size:0.9rem;line-height:1.6;color:#5a5a7a;">
|
||||||
|
<?= nl2br(htmlspecialchars($news['content'])) ?>
|
||||||
|
</div>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn btn-edit" onclick="editNews(<?= $news['news_id'] ?>, '<?= htmlspecialchars(addslashes($news['title']), ENT_QUOTES) ?>', '<?= htmlspecialchars(addslashes($news['content']), ENT_QUOTES) ?>', '<?= htmlspecialchars(addslashes($news['author_name']), ENT_QUOTES) ?>')">
|
||||||
|
<i class="fa-solid fa-pen"></i> Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-delete" onclick="deleteNews(<?= $news['news_id'] ?>)">
|
||||||
|
<i class="fa-solid fa-trash"></i> Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ========================================================= -->
|
||||||
|
<!-- Placeholder Tabs for future Features -->
|
||||||
|
<!-- ========================================================= -->
|
||||||
|
<div id="tab-stats" class="page-tab-content" style="display:none;">
|
||||||
|
<div class="placeholder-content">
|
||||||
|
<i class="fa-solid fa-chart-bar"></i>
|
||||||
|
<p>Statistiken und Analysen - geplant in zukünftiger Version.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-users" class="page-tab-content" style="display:none;">
|
||||||
|
<div class="placeholder-content">
|
||||||
|
<i class="fa-solid fa-users"></i>
|
||||||
|
<p>Benutzerverwaltung - geplant in zukünftiger Version.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
<!-- JavaScript: Leaflet, Interactions, API Calls -->
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Municipality Configuration for Map Previews
|
||||||
|
const MUNICIPALITY_CENTER = [<?= $municipality['center_lat'] ?>, <?= $municipality['center_lng'] ?>];
|
||||||
|
const MUNICIPALITY_ID = <?= $municipality['municipality_id'] ?>;
|
||||||
|
const API_URL = 'api/contributions.php';
|
||||||
|
const PRIMARY_COLOR = '<?= htmlspecialchars($municipality['primary_color']) ?>';
|
||||||
|
|
||||||
|
// Current Status Filter
|
||||||
|
let currentFilter = 'all';
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// Page Tab Navigation
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
function showPageTab(tabName) {
|
||||||
|
// Hides all Tab Contents
|
||||||
|
document.querySelectorAll('.page-tab-content').forEach(function (el) {
|
||||||
|
el.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deactivates all Tab Buttons
|
||||||
|
document.querySelectorAll('.page-tab').forEach(function (el) {
|
||||||
|
el.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shows selected Tab and activates Button
|
||||||
|
document.getElementById('tab-' + tabName).style.display = 'block';
|
||||||
|
event.currentTarget.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// Collapsible Rows
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
function toggleRow(row) {
|
||||||
|
const wasOpen = row.classList.contains('open');
|
||||||
|
|
||||||
|
// Closes all open Rows
|
||||||
|
document.querySelectorAll('.contribution-row.open').forEach(function (el) {
|
||||||
|
el.classList.remove('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggles clicked Row
|
||||||
|
if (!wasOpen) {
|
||||||
|
row.classList.add('open');
|
||||||
|
|
||||||
|
// Loads Map Preview if not already loaded
|
||||||
|
const mapDiv = row.querySelector('.detail-map');
|
||||||
|
if (mapDiv && !mapDiv.dataset.loaded) {
|
||||||
|
loadMapPreview(mapDiv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// Map Preview (Leaflet Mini Map per Contribution)
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
function loadMapPreview(mapDiv) {
|
||||||
|
const contributionId = mapDiv.dataset.contributionId;
|
||||||
|
|
||||||
|
// Fetches all Contributions to find the Geometry
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'read');
|
||||||
|
formData.append('municipality_id', MUNICIPALITY_ID);
|
||||||
|
formData.append('status', 'all');
|
||||||
|
|
||||||
|
fetch(API_URL, { method: 'POST', body: formData })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
if (!data.features) return;
|
||||||
|
|
||||||
|
// Finds specific Contribution
|
||||||
|
const feature = data.features.find(function (f) {
|
||||||
|
return f.properties.contribution_id == contributionId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!feature) {
|
||||||
|
mapDiv.innerHTML = '<div style="padding:20px;color:#999;text-align:center;font-size:0.8rem;">Geometrie nicht gefunden.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates Leaflet Mini Map
|
||||||
|
const miniMap = L.map(mapDiv, {
|
||||||
|
zoomControl: false,
|
||||||
|
attributionControl: false,
|
||||||
|
dragging: true,
|
||||||
|
scrollWheelZoom: false
|
||||||
|
});
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||||
|
maxZoom: 20
|
||||||
|
}).addTo(miniMap);
|
||||||
|
|
||||||
|
// Adds Geometry to Mini Map
|
||||||
|
const geojsonLayer = L.geoJSON(feature, {
|
||||||
|
style: { color: PRIMARY_COLOR, weight: 3, fillOpacity: 0.2 },
|
||||||
|
pointToLayer: function (f, latlng) {
|
||||||
|
return L.circleMarker(latlng, {
|
||||||
|
radius: 8, color: '#ffffff', weight: 2,
|
||||||
|
fillColor: PRIMARY_COLOR, fillOpacity: 0.9
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).addTo(miniMap);
|
||||||
|
|
||||||
|
// Fits Map to Geometry Bounds
|
||||||
|
const bounds = geojsonLayer.getBounds();
|
||||||
|
if (bounds.isValid()) {
|
||||||
|
miniMap.fitBounds(bounds, { padding: [25, 25], maxZoom: 17 });
|
||||||
|
} else {
|
||||||
|
miniMap.setView(MUNICIPALITY_CENTER, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
mapDiv.dataset.loaded = 'true';
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
mapDiv.innerHTML = '<div style="padding:20px;color:#999;text-align:center;font-size:0.8rem;">Karte nicht verfügbar.</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// Status Filter
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
function filterByStatus(status, tabButton) {
|
||||||
|
currentFilter = status;
|
||||||
|
|
||||||
|
// Updates active Tab
|
||||||
|
document.querySelectorAll('.filter-tab').forEach(function (el) {
|
||||||
|
el.classList.remove('active');
|
||||||
|
});
|
||||||
|
tabButton.classList.add('active');
|
||||||
|
|
||||||
|
// Shows/Hides Contribution Rows
|
||||||
|
let visibleCount = 0;
|
||||||
|
document.querySelectorAll('.contribution-row').forEach(function (row) {
|
||||||
|
if (status === 'all' || row.dataset.status === status) {
|
||||||
|
row.style.display = '';
|
||||||
|
visibleCount++;
|
||||||
|
} else {
|
||||||
|
row.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Updates Count Display
|
||||||
|
document.getElementById('visible-count').textContent = visibleCount + ' Beiträge';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// Sort Contributions
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
function sortContributions(sortBy) {
|
||||||
|
const container = document.getElementById('contributions-container');
|
||||||
|
const rows = Array.from(container.querySelectorAll('.contribution-row'));
|
||||||
|
|
||||||
|
rows.sort(function (a, b) {
|
||||||
|
if (sortBy === 'date-desc') {
|
||||||
|
return new Date(b.dataset.date) - new Date(a.dataset.date);
|
||||||
|
} else if (sortBy === 'date-asc') {
|
||||||
|
return new Date(a.dataset.date) - new Date(b.dataset.date);
|
||||||
|
} else if (sortBy === 'category') {
|
||||||
|
return a.dataset.category.localeCompare(b.dataset.category);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reappends sorted Rows
|
||||||
|
rows.forEach(function (row) {
|
||||||
|
container.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// API Helper
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
function apiCall(data) {
|
||||||
|
const formData = new FormData();
|
||||||
|
for (const key in data) {
|
||||||
|
formData.append(key, data[key]);
|
||||||
|
}
|
||||||
|
return fetch(API_URL, { method: 'POST', body: formData })
|
||||||
|
.then(function (r) { return r.json(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// Change Contribution Status
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
function changeStatus(contributionId, newStatus) {
|
||||||
|
const labels = { approved: 'freigeben', rejected: 'ablehnen', pending: 'zurücksetzen' };
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Beitrag ' + labels[newStatus] + '?',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Ja',
|
||||||
|
cancelButtonText: 'Abbrechen',
|
||||||
|
confirmButtonColor: PRIMARY_COLOR
|
||||||
|
}).then(function (result) {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
|
apiCall({
|
||||||
|
action: 'update',
|
||||||
|
contribution_id: contributionId,
|
||||||
|
status: newStatus
|
||||||
|
}).then(function (response) {
|
||||||
|
if (response.error) {
|
||||||
|
Swal.fire('Fehler', response.error, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Reloads Page to reflect Changes
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// Edit Contribution (Title and Description)
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
function editContribution(contributionId, currentTitle, currentDescription) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Beitrag bearbeiten',
|
||||||
|
html:
|
||||||
|
'<div style="text-align:left;">' +
|
||||||
|
'<div style="margin-bottom:12px;">' +
|
||||||
|
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Titel</label>' +
|
||||||
|
'<input id="swal-title" class="swal2-input" style="margin:0;width:100%;" value="' + currentTitle + '">' +
|
||||||
|
'</div>' +
|
||||||
|
'<div>' +
|
||||||
|
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Beschreibung</label>' +
|
||||||
|
'<textarea id="swal-description" class="swal2-textarea" style="margin:0;width:100%;">' + currentDescription + '</textarea>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Speichern',
|
||||||
|
cancelButtonText: 'Abbrechen',
|
||||||
|
confirmButtonColor: PRIMARY_COLOR,
|
||||||
|
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
|
||||||
|
}).then(function (response) {
|
||||||
|
if (response.error) {
|
||||||
|
Swal.fire('Fehler', response.error, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Swal.fire('Gespeichert!', 'Beitrag wurde aktualisiert.', 'success')
|
||||||
|
.then(function () { location.reload(); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// Delete Contribution
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
function deleteContribution(contributionId) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Beitrag löschen?',
|
||||||
|
text: 'Diese Aktion kann nicht rückgängig gemacht werden.',
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Beitrag löschen',
|
||||||
|
cancelButtonText: 'Abbrechen',
|
||||||
|
confirmButtonColor: '#c62828'
|
||||||
|
}).then(function (result) {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
|
apiCall({
|
||||||
|
action: 'delete',
|
||||||
|
contribution_id: contributionId
|
||||||
|
}).then(function (response) {
|
||||||
|
if (response.error) {
|
||||||
|
Swal.fire('Fehler', response.error, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Swal.fire('Gelöscht!', 'Beitrag wurde gelöscht.', 'success')
|
||||||
|
.then(function () { location.reload(); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// Create News Article
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
function createNews() {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Neuigkeit hinzufügen',
|
||||||
|
html:
|
||||||
|
'<div style="text-align:left;">' +
|
||||||
|
'<div style="margin-bottom:12px;">' +
|
||||||
|
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Titel</label>' +
|
||||||
|
'<input id="swal-news-title" class="swal2-input" style="margin:0;width:100%;" placeholder="Titel der Neuigkeit">' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="margin-bottom:12px;">' +
|
||||||
|
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Inhalt</label>' +
|
||||||
|
'<textarea id="swal-news-content" class="swal2-textarea" style="margin:0;width:100%;" placeholder="Neuigkeit verfassen..."></textarea>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div>' +
|
||||||
|
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Autor</label>' +
|
||||||
|
'<input id="swal-news-author" class="swal2-input" style="margin:0;width:100%;" value="Stadtverwaltung">' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Veröffentlichen',
|
||||||
|
cancelButtonText: 'Abbrechen',
|
||||||
|
confirmButtonColor: PRIMARY_COLOR,
|
||||||
|
preConfirm: function () {
|
||||||
|
const title = document.getElementById('swal-news-title').value.trim();
|
||||||
|
const content = document.getElementById('swal-news-content').value.trim();
|
||||||
|
const author = document.getElementById('swal-news-author').value.trim() || 'Stadtverwaltung';
|
||||||
|
if (!title || !content) {
|
||||||
|
Swal.showValidationMessage('Titel und Inhalt sind Pflichtfelder.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return { title: title, content: content, author_name: author };
|
||||||
|
}
|
||||||
|
}).then(function (result) {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'create_news');
|
||||||
|
formData.append('municipality_id', MUNICIPALITY_ID);
|
||||||
|
formData.append('title', result.value.title);
|
||||||
|
formData.append('content', result.value.content);
|
||||||
|
formData.append('author_name', result.value.author_name);
|
||||||
|
|
||||||
|
fetch(API_URL, { method: 'POST', body: formData })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.error) {
|
||||||
|
Swal.fire('Fehler', response.error, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Swal.fire('Veröffentlicht!', 'Neuigkeit wurde veröffentlicht.', 'success')
|
||||||
|
.then(function () { location.reload(); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// Edit News Article
|
||||||
|
// =============================================================
|
||||||
|
function editNews(newsId, currentTitle, currentContent, currentAuthor) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Neuigkeit bearbeiten',
|
||||||
|
html:
|
||||||
|
'<div style="text-align:left;">' +
|
||||||
|
'<div style="margin-bottom:12px;">' +
|
||||||
|
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Titel</label>' +
|
||||||
|
'<input id="swal-news-title" class="swal2-input" style="margin:0;width:100%;" value="' + currentTitle + '">' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="margin-bottom:12px;">' +
|
||||||
|
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Inhalt</label>' +
|
||||||
|
'<textarea id="swal-news-content" class="swal2-textarea" style="margin:0;width:100%;">' + currentContent + '</textarea>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div>' +
|
||||||
|
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Autor</label>' +
|
||||||
|
'<input id="swal-news-author" class="swal2-input" style="margin:0;width:100%;" value="' + currentAuthor + '">' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Speichern',
|
||||||
|
cancelButtonText: 'Abbrechen',
|
||||||
|
confirmButtonColor: PRIMARY_COLOR,
|
||||||
|
preConfirm: function () {
|
||||||
|
return {
|
||||||
|
title: document.getElementById('swal-news-title').value.trim(),
|
||||||
|
content: document.getElementById('swal-news-content').value.trim(),
|
||||||
|
author_name: document.getElementById('swal-news-author').value.trim() || 'Stadtverwaltung'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}).then(function (result) {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'update_news');
|
||||||
|
formData.append('news_id', newsId);
|
||||||
|
formData.append('title', result.value.title);
|
||||||
|
formData.append('content', result.value.content);
|
||||||
|
formData.append('author_name', result.value.author_name);
|
||||||
|
|
||||||
|
fetch(API_URL, { method: 'POST', body: formData })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.error) {
|
||||||
|
Swal.fire('Fehler', response.error, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Swal.fire('Gespeichert!', 'Neuigkeit wurde aktualisiert.', 'success')
|
||||||
|
.then(function () { location.reload(); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// Create News Article
|
||||||
|
// =============================================================
|
||||||
|
function deleteNews(newsId) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Neuigkeit löschen?',
|
||||||
|
text: 'Diese 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;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'delete_news');
|
||||||
|
formData.append('news_id', newsId);
|
||||||
|
|
||||||
|
fetch(API_URL, { method: 'POST', body: formData })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.error) {
|
||||||
|
Swal.fire('Fehler', response.error, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Swal.fire('Gelöscht!', 'Neuigkeit wurde gelöscht.', 'success')
|
||||||
|
.then(function () { location.reload(); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</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="icon" href="<?= htmlspecialchars($municipality['logo_path'] ?? 'assets/icon-municipality.png') ?>" type="image/png"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="styles.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> Moderationsportal</h1>
|
||||||
|
<p>Bitte geben Sie das Moderationspasswort ein.</p>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="login-error"><i class="fa-solid fa-triangle-exclamation"></i> <?= 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"><i class="fa fa-arrow-left"></i></i> <a href="index.php">Zurück zum Bürgerportal</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
28
public/api/auth.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
// =====================================================================
|
||||||
|
// Admin Authentication Helper
|
||||||
|
// Provides simple Password-based Session Authentication for the
|
||||||
|
// Moderation Page. Reads Password from .env File.
|
||||||
|
// ToDo: Replace with full User Authentication in Phase 3-3.
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
// 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 = getenv('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();
|
||||||
|
}
|
||||||
680
public/api/contributions.php
Normal file
@@ -0,0 +1,680 @@
|
|||||||
|
<?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;
|
||||||
|
case 'create_news':
|
||||||
|
handle_create_news($input);
|
||||||
|
break;
|
||||||
|
case 'update_news':
|
||||||
|
handle_update_news($input);
|
||||||
|
break;
|
||||||
|
case 'delete_news':
|
||||||
|
handle_delete_news($input);
|
||||||
|
break;
|
||||||
|
case 'read_comments':
|
||||||
|
handle_read_comments($input);
|
||||||
|
break;
|
||||||
|
case 'create_comment':
|
||||||
|
handle_create_comment($input);
|
||||||
|
break;
|
||||||
|
case 'delete_comment':
|
||||||
|
handle_delete_comment($input);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
error_response('Unknown Action. Supported Actions are read, create, update, delete, vote.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Action Handlers for Contributions
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// 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
|
||||||
|
];
|
||||||
|
|
||||||
|
// Includes User's Votes for persistent Vote Display
|
||||||
|
// Returns which Contributions the current Browser has voted on
|
||||||
|
$browser_id = $input['browser_id'] ?? '';
|
||||||
|
if ($browser_id !== '') {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT contribution_id, vote_type
|
||||||
|
FROM votes
|
||||||
|
WHERE browser_id = :bid
|
||||||
|
");
|
||||||
|
$stmt->execute([':bid' => $browser_id]);
|
||||||
|
$user_votes = [];
|
||||||
|
foreach ($stmt->fetchAll() as $v) {
|
||||||
|
$user_votes[$v['contribution_id']] = $v['vote_type'];
|
||||||
|
}
|
||||||
|
$featureCollection['user_votes'] = $user_votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
json_response($featureCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// CREATE: Inserts new Contributions
|
||||||
|
// Required: municipality_id, geom, geom_type, category, title, author_name
|
||||||
|
// Optional: description
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// CREATE: Inserts new Contributions with optional Photo Upload
|
||||||
|
// Required: municipality_id, geom, geom_type, category, title, author_name
|
||||||
|
// Optional: description, browser_id, photo (File Upload)
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles Photo Upload
|
||||||
|
$photo_path = null;
|
||||||
|
if (isset($_FILES['photo']) && $_FILES['photo']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$photo_path = handle_photo_upload($_FILES['photo']);
|
||||||
|
if (!$photo_path) {
|
||||||
|
error_response('Photo Upload failed. JPG, PNG, GIF and WebP up to 5 MB are allowed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepared SQL Statement
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO contributions
|
||||||
|
(municipality_id, geom, geom_type, category, title, description, author_name, browser_id, photo_path)
|
||||||
|
VALUES
|
||||||
|
(:mid, ST_SetSRID(ST_GeomFromGeoJSON(:geom), 4326), :geom_type,
|
||||||
|
:category, :title, :description, :author_name, :browser_id, :photo_path)
|
||||||
|
");
|
||||||
|
|
||||||
|
$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'],
|
||||||
|
':browser_id' => $input['browser_id'] ?? null,
|
||||||
|
':photo_path' => $photo_path
|
||||||
|
]);
|
||||||
|
|
||||||
|
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', 'address'];
|
||||||
|
$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
|
||||||
|
$browser_id = $input['browser_id'] ?? '';
|
||||||
|
if (empty($browser_id)) {
|
||||||
|
error_response('Browser ID required for Voting.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT vote_id, vote_type FROM votes
|
||||||
|
WHERE contribution_id = :cid AND browser_id = :bid
|
||||||
|
");
|
||||||
|
$stmt->execute([':cid' => $input['contribution_id'], ':bid' => $browser_id]);
|
||||||
|
$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, browser_id)
|
||||||
|
VALUES (:cid, :voter, :vtype, :bid)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':cid' => $input['contribution_id'],
|
||||||
|
':voter' => $input['voter_name'],
|
||||||
|
':vtype' => $input['vote_type'],
|
||||||
|
':bid' => $browser_id
|
||||||
|
]);
|
||||||
|
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, browser_id)
|
||||||
|
VALUES (:cid, :voter, :vtype, :bid)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':cid' => $input['contribution_id'],
|
||||||
|
':voter' => $input['voter_name'],
|
||||||
|
':vtype' => $input['vote_type'],
|
||||||
|
':bid' => $browser_id
|
||||||
|
]);
|
||||||
|
json_response(['message' => 'Vote recorded.', 'action' => 'created'], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_response('Database Error: ' . $e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Action Handlers for News
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// CREATE NEWS: Inserts new News Entry
|
||||||
|
// Required: municipality_id, title, content
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
function handle_create_news($input) {
|
||||||
|
$pdo = get_db();
|
||||||
|
$missing = validate_required($input, ['municipality_id', 'title', 'content']);
|
||||||
|
if (!empty($missing)) {
|
||||||
|
error_response('Missing Fields: ' . implode(', ', $missing));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO news (municipality_id, title, content, author_name)
|
||||||
|
VALUES (:mid, :title, :content, :author)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':mid' => $input['municipality_id'],
|
||||||
|
':title' => $input['title'],
|
||||||
|
':content' => $input['content'],
|
||||||
|
':author' => $input['author_name'] ?? 'Stadtverwaltung'
|
||||||
|
]);
|
||||||
|
json_response(['message' => 'News created successfully.', 'news_id' => (int) $pdo->lastInsertId()], 201);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_response('Database Error: ' . $e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// UPDATE NEWS: Updates existing News Entry
|
||||||
|
// Required: news_id
|
||||||
|
// Optional: title, content
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
function handle_update_news($input) {
|
||||||
|
$pdo = get_db();
|
||||||
|
$missing = validate_required($input, ['news_id']);
|
||||||
|
if (!empty($missing)) {
|
||||||
|
error_response('Missing Fields: ' . implode(', ', $missing));
|
||||||
|
}
|
||||||
|
|
||||||
|
$set = [];
|
||||||
|
$params = [':id' => $input['news_id']];
|
||||||
|
|
||||||
|
foreach (['title', 'content', 'author_name'] as $field) {
|
||||||
|
if (isset($input[$field]) && $input[$field] !== '') {
|
||||||
|
$set[] = "$field = :$field";
|
||||||
|
$params[":$field"] = $input[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($set)) {
|
||||||
|
error_response('No Fields to update.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("UPDATE news SET " . implode(', ', $set) . " WHERE news_id = :id");
|
||||||
|
$stmt->execute($params);
|
||||||
|
json_response(['message' => 'News updated successfully.']);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_response('Database Error: ' . $e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// DELETE NEWS: Deletes existing News Entry
|
||||||
|
// Required: news_id
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
function handle_delete_news($input) {
|
||||||
|
$pdo = get_db();
|
||||||
|
$missing = validate_required($input, ['news_id']);
|
||||||
|
if (!empty($missing)) {
|
||||||
|
error_response('Missing Fields: ' . implode(', ', $missing));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM news WHERE news_id = :id");
|
||||||
|
$stmt->execute([':id' => $input['news_id']]);
|
||||||
|
json_response(['message' => 'News deleted successfully.']);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_response('Database Error: ' . $e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Action Handlers for Photos
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// PHOTO UPLOAD: Validates and Saves uploaded Photo Files
|
||||||
|
// Returns relative Path on Success, null on Failure.
|
||||||
|
// Allowed: JPG, PNG, GIF, WebP. with maximum Size of 5 MB.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
function handle_photo_upload($file) {
|
||||||
|
// Validates File Size
|
||||||
|
$max_size = 5 * 1024 * 1024;
|
||||||
|
if ($file['size'] > $max_size) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates MIME Type
|
||||||
|
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$mime = finfo_file($finfo, $file['tmp_name']);
|
||||||
|
finfo_close($finfo);
|
||||||
|
|
||||||
|
if (!in_array($mime, $allowed_types)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates unique Filename
|
||||||
|
$ext = [
|
||||||
|
'image/jpeg' => 'jpg',
|
||||||
|
'image/png' => 'png',
|
||||||
|
'image/gif' => 'gif',
|
||||||
|
'image/webp' => 'webp'
|
||||||
|
][$mime];
|
||||||
|
|
||||||
|
$filename = uniqid('photo_', true) . '.' . $ext;
|
||||||
|
$upload_dir = __DIR__ . '/../uploads/photos/';
|
||||||
|
$target_path = $upload_dir . $filename;
|
||||||
|
|
||||||
|
// Creates Upload Directory
|
||||||
|
if (!is_dir($upload_dir)) {
|
||||||
|
mkdir($upload_dir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moves uploaded File
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $target_path)) {
|
||||||
|
return 'uploads/photos/' . $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Action Handlers for Comments
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// READ COMMENTS: Loads Comments for a Contribution
|
||||||
|
// Returns Comments sorted by Date (newest first)
|
||||||
|
// Required: contribution_id
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
function handle_read_comments($input) {
|
||||||
|
$pdo = get_db();
|
||||||
|
|
||||||
|
$missing = validate_required($input, ['contribution_id']);
|
||||||
|
if (!empty($missing)) {
|
||||||
|
error_response('Missing Fields: ' . implode(', ', $missing));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT comment_id, contribution_id, author_name, browser_id, content, created_at
|
||||||
|
FROM comments
|
||||||
|
WHERE contribution_id = :cid
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([':cid' => $input['contribution_id']]);
|
||||||
|
$comments = $stmt->fetchAll();
|
||||||
|
|
||||||
|
json_response(['comments' => $comments, 'count' => count($comments)]);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_response('Database Error: ' . $e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// CREATE COMMENT: Adds Comments to Contributions
|
||||||
|
// Required: contribution_id, author_name, content
|
||||||
|
// Optional: browser_id
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
function handle_create_comment($input) {
|
||||||
|
$pdo = get_db();
|
||||||
|
|
||||||
|
$missing = validate_required($input, ['contribution_id', 'author_name', 'content']);
|
||||||
|
if (!empty($missing)) {
|
||||||
|
error_response('Missing Fields: ' . implode(', ', $missing));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates Content Length
|
||||||
|
if (strlen($input['content']) > 1000) {
|
||||||
|
error_response('Comment too long. Maximum 1000 Characters.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO comments (contribution_id, author_name, browser_id, content)
|
||||||
|
VALUES (:cid, :author, :bid, :content)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':cid' => $input['contribution_id'],
|
||||||
|
':author' => $input['author_name'],
|
||||||
|
':bid' => $input['browser_id'] ?? null,
|
||||||
|
':content' => $input['content']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stmt2 = $pdo->prepare("
|
||||||
|
UPDATE contributions
|
||||||
|
SET comment_count = comment_count + 1
|
||||||
|
WHERE contribution_id = :cid;
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt2->execute([':cid' => $input['contribution_id']]);
|
||||||
|
|
||||||
|
json_response([
|
||||||
|
'message' => 'Comment created successfully.',
|
||||||
|
'comment_id' => (int) $pdo->lastInsertId()
|
||||||
|
], 201);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_response('Database Error: ' . $e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// DELETE COMMENT: Removes a Comment
|
||||||
|
// Required: comment_id
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
function handle_delete_comment($input) {
|
||||||
|
$pdo = get_db();
|
||||||
|
|
||||||
|
$missing = validate_required($input, ['comment_id']);
|
||||||
|
if (!empty($missing)) {
|
||||||
|
error_response('Missing Fields: ' . implode(', ', $missing));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM comments WHERE comment_id = :id");
|
||||||
|
$stmt->execute([':id' => $input['comment_id']]);
|
||||||
|
|
||||||
|
$stmt2 = $pdo->prepare("
|
||||||
|
UPDATE contributions
|
||||||
|
SET comment_count = comment_count - 1
|
||||||
|
WHERE contribution_id = :cid;
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt2->execute([':cid' => $input['contribution_id']]);
|
||||||
|
|
||||||
|
json_response(['message' => 'Comment deleted successfully.']);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_response('Database Error: ' . $e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
public/api/db.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
// =====================================================================
|
||||||
|
// Database Helper Functions
|
||||||
|
// Provides PDO Connection, JSON Response Helpers, Category Definitions
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Category Definitions
|
||||||
|
// Returns associative Array of Category Keys to Labels, Icons,
|
||||||
|
// and Colors. Shared between Citizen Participation Portal and
|
||||||
|
// Moderation Page.
|
||||||
|
// ToDo: Move to Database Table.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
function get_categories() {
|
||||||
|
return [
|
||||||
|
'consumption' => ['label' => 'Geschäfte', 'faIcon' => 'fa-cart-shopping', 'color' => '#C00000'],
|
||||||
|
'building' => ['label' => 'Bauen', 'faIcon' => 'fa-building', 'color' => '#E65100'],
|
||||||
|
'energy' => ['label' => 'Energie', 'faIcon' => 'fa-bolt', 'color' => '#FFC000'],
|
||||||
|
'environment' => ['label' => 'Umwelt', 'faIcon' => 'fa-seedling', 'color' => '#92D050'],
|
||||||
|
'mobility' => ['label' => 'Mobilität', 'faIcon' => 'fa-bus', 'color' => '#0070C0'],
|
||||||
|
'industry' => ['label' => 'Industrie', 'faIcon' => 'fa-industry', 'color' => '#7030A0'],
|
||||||
|
'other' => ['label' => 'Sonstiges', 'faIcon' => 'fa-thumbtack', 'color' => '#7F7F7F'],
|
||||||
|
];
|
||||||
|
}
|
||||||
43
public/api/init.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?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 {
|
||||||
|
$dsn = "pgsql:host=$host;dbname=$db;port=$port";
|
||||||
|
$pdo = new PDO($dsn, $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Creates Error Message
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
BIN
public/assets/icon-municipality.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
public/assets/logo-company.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
public/assets/logo-municipality.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
39
public/imprint.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/api/db.php';
|
||||||
|
$pdo = get_db();
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM municipalities WHERE slug = :slug");
|
||||||
|
$stmt->execute([':slug' => getenv('MUNICIPALITY_SLUG')]);
|
||||||
|
$municipality = $stmt->fetch();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Impressum — <?= htmlspecialchars($municipality['name']) ?></title>
|
||||||
|
<link rel="icon" href="<?= htmlspecialchars($municipality['logo_path'] ?? 'assets/icon-municipality.png') ?>" type="image/png">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-inner">
|
||||||
|
<h1><i class="fa-solid fa-scale-balanced"></i> Impressum</h1>
|
||||||
|
<div class="page-header-nav">
|
||||||
|
<a href="index.php"><i class="fa-solid fa-arrow-left"></i> Zurück zur Karte</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="page-content-box">
|
||||||
|
<div class="dev-notice">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||||
|
Dieses Portal befindet sich in der Entwicklung und wurde nicht offiziell beauftragt. Das Impressum wird mit der offiziellen Inbetriebnahme hier hinzugefügt.
|
||||||
|
</div>
|
||||||
|
<h2>Impressum</h2>
|
||||||
|
<p>Das Impressum wird hier hinzugefügt, sobald das Portal in den Produktivbetrieb geht.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
394
public/index.php
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
<?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';
|
||||||
|
require_once __DIR__ . '/api/auth.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' => getenv('MUNICIPALITY_SLUG')]);
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads News for Sidebar
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM news WHERE municipality_id = :mid ORDER BY published_at DESC LIMIT 10");
|
||||||
|
$stmt->execute([':mid' => $municipality['municipality_id']]);
|
||||||
|
$news_items = $stmt->fetchAll();
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!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="<?= htmlspecialchars($municipality['logo_path'] ?? '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 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 class="portal-page">
|
||||||
|
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
<!-- Header -->
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
<header id="app-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<?php if (!empty($municipality['logo_path'])): ?>
|
||||||
|
<img src="<?= htmlspecialchars($municipality['logo_path']) ?>" alt="<?= htmlspecialchars($municipality['name']) ?>" class="header-logo" onerror="this.style.display='none'">
|
||||||
|
<?php endif; ?>
|
||||||
|
<h1 class="header-title">Mitmachkarte <?= 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>
|
||||||
|
<a href="privacy.php" class="nav-btn" target="_blank">
|
||||||
|
<i class="fa-solid fa-shield-halved"></i>
|
||||||
|
<span class="nav-label">Datenschutz</span>
|
||||||
|
</a>
|
||||||
|
<a href="imprint.php" class="nav-btn" target="_blank">
|
||||||
|
<i class="fa-solid fa-scale-balanced"></i>
|
||||||
|
<span class="nav-label">Impressum</span>
|
||||||
|
</a>
|
||||||
|
<a href="admin.php" class="nav-btn nav-btn-admin" title="Moderationsbereich" target="_blank">
|
||||||
|
<i class="fa-solid fa-lock"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Mobile Hamburger Menu -->
|
||||||
|
<button class="header-menu-toggle" onclick="toggleMobileNav()">
|
||||||
|
<i class="fa-solid fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
</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">
|
||||||
|
<!-- News Search -->
|
||||||
|
<div class="list-search">
|
||||||
|
<input type="text" id="news-search-input" placeholder="Neuigkeiten durchsuchen..." class="form-input" oninput="filterNews()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- News Items Container -->
|
||||||
|
<div id="news-list">
|
||||||
|
<?php if (empty($news_items)): ?>
|
||||||
|
<p style="text-align:center;color:#999;padding:20px;">Noch keine Neuigkeiten veröffentlicht.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($news_items as $news): ?>
|
||||||
|
<div class="news-item"
|
||||||
|
data-title="<?= htmlspecialchars(strtolower($news['title'])) ?>"
|
||||||
|
data-content="<?= htmlspecialchars(strtolower($news['content'])) ?>"
|
||||||
|
data-author="<?= htmlspecialchars(strtolower($news['author_name'])) ?>">
|
||||||
|
<h3><?= htmlspecialchars($news['title']) ?></h3>
|
||||||
|
<p><?= nl2br(htmlspecialchars($news['content'])) ?></p>
|
||||||
|
<span class="news-date">
|
||||||
|
<?= htmlspecialchars($news['author_name']) ?>
|
||||||
|
· <?= date('d.m.Y', strtotime($news['published_at'])) ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Leaflet Map -->
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
<!-- Footer -->
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
<footer id="app-footer">
|
||||||
|
<span class="dev-warning">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation"></i> Pilotprojekt - nicht offiziell von der Stadt Lohne (Oldenburg) beauftragt
|
||||||
|
</span>
|
||||||
|
<div class="footer-content">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Photo Upload -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="create-photo"></i> Foto</label>
|
||||||
|
<input type="file" id="create-photo" class="form-input" accept="image/jpeg,image/png,image/gif,image/webp">
|
||||||
|
<div id="photo-preview" style="margin-top:8px;display:none;">
|
||||||
|
<img id="photo-preview-img" style="max-width:100%;max-height:200px;border-radius:6px;border:1px solid var(--color-border);">
|
||||||
|
</div>
|
||||||
|
</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
|
||||||
|
const 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) ?>"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Category Definitions from Database
|
||||||
|
const CATEGORIES = <?= json_encode(get_categories(), JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
|
||||||
|
// Admin Status from PHP Session
|
||||||
|
const IS_ADMIN = <?= (function_exists('is_admin') && is_admin()) ? 'true' : 'false' ?>;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Application Logic -->
|
||||||
|
<script src="js/app.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1236
public/js/app.js
Normal file
39
public/privacy.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/api/db.php';
|
||||||
|
$pdo = get_db();
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM municipalities WHERE slug = :slug");
|
||||||
|
$stmt->execute([':slug' => getenv('MUNICIPALITY_SLUG')]);
|
||||||
|
$municipality = $stmt->fetch();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Datenschutz — <?= htmlspecialchars($municipality['name']) ?></title>
|
||||||
|
<link rel="icon" href="<?= htmlspecialchars($municipality['logo_path'] ?? 'assets/icon-municipality.png') ?>" type="image/png">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-inner">
|
||||||
|
<h1><i class="fa-solid fa-lock"></i> Datenschutz</h1>
|
||||||
|
<div class="page-header-nav">
|
||||||
|
<a href="index.php"><i class="fa-solid fa-arrow-left"></i> Zurück zur Karte</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="page-content-box">
|
||||||
|
<div class="dev-notice">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||||
|
Dieses Portal befindet sich in der Entwicklung und wurde nicht offiziell beauftragt. Die Datenschutzerklärung wird mit der offiziellen Inbetriebnahme hier hinzugefügt.
|
||||||
|
</div>
|
||||||
|
<h2>Datenschutz</h2>
|
||||||
|
<p>Die Datenschutzerklärung wird hier hinzugefügt, sobald das Portal in den Produktivbetrieb geht.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1086
public/styles.css
Normal file
7
public/uploads/.htaccess
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Prevents PHP in Upload Directory
|
||||||
|
php_flag engine off
|
||||||
|
|
||||||
|
# Allows Image Files
|
||||||
|
<FilesMatch "\.(?i:jpg|jpeg|png|gif|webp)$">
|
||||||
|
Require all granted
|
||||||
|
</FilesMatch>
|
||||||
0
public/uploads/photos/.gitkeep
Normal file
129
scripts/backup.sh
Normal 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."
|
||||||
7
source/jquery-ui.min.css
vendored
6
source/jquery-ui.min.js
vendored
41
styles.css
@@ -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
@@ -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.
|
||||||