From cfa85894465dbf2d286e083d962babdf14641582 Mon Sep 17 00:00:00 2001
From: ulrich
Date: Sun, 04 Apr 2021 14:30:21 +0000
Subject: [PATCH] UI begonnen

---
 ui/js/app-menu.js                                |  137 +++++++++
 src/de/uhilger/mediaz/App.java                   |    1 
 src/de/uhilger/mediaz/api/StoreTestHandler.java  |   10 
 ui/data/menu/untermenue-2.json                   |   27 +
 ui/data/tpl/dlg-info.tpl                         |    8 
 ui/app.css                                       |  119 ++++++++
 ui/hamburger.css                                 |   99 +++++++
 ui/index.html                                    |   79 +++++
 src/de/uhilger/mediaz/api/AblageTestHandler.java |    4 
 ui/data/tpl/app-menu.tpl                         |   20 +
 src/de/uhilger/mediaz/conf/Store.java            |   12 
 src/mediaz_de_DE.properties                      |    2 
 src/de/uhilger/mediaz/Server.java                |    7 
 ui/data/menu/hauptmenue.json                     |   32 ++
 src/de/uhilger/mediaz/entity/Ablageort.java      |   11 
 ui/app-menu.css                                  |   36 ++
 ui/js/app.js                                     |  206 ++++++++++++++
 ui/data/menu/untermenue-1.json                   |   27 +
 18 files changed, 822 insertions(+), 15 deletions(-)

diff --git a/src/de/uhilger/mediaz/App.java b/src/de/uhilger/mediaz/App.java
index 63d8cbb..777c985 100644
--- a/src/de/uhilger/mediaz/App.java
+++ b/src/de/uhilger/mediaz/App.java
@@ -46,6 +46,7 @@
   public static final String RB_AP_CONF = "appParamConf";
   public static final String RB_AP_WWW_DATA = "appParamWWWData"; 
   public static final String RB_AP_CTX = "appParamCtx"; 
+  public static final String RB_AP_UI = "appParamUi"; 
 
   /**
    * <p>Start-Methode dieser Anwendung</p>
diff --git a/src/de/uhilger/mediaz/Server.java b/src/de/uhilger/mediaz/Server.java
index c31c9fb..4bc3286 100644
--- a/src/de/uhilger/mediaz/Server.java
+++ b/src/de/uhilger/mediaz/Server.java
@@ -22,6 +22,7 @@
 import de.uhilger.mediaz.api.FileHandler;
 import de.uhilger.mediaz.api.StopServerHandler;
 import de.uhilger.mediaz.api.StoreTestHandler;
+import java.io.File;
 import java.io.IOException;
 import java.util.logging.Logger;
 import java.net.InetSocketAddress;
@@ -41,6 +42,7 @@
   
   public static final String RB_SERVER_START_MSG = "msgServerStart";
   public static final String RB_WEBROOT = "webroot";
+  public static final String RB_UI_ROOT = "uiroot";
   public static final String RB_STOP_SERVER = "stopServer";
   public static final String RB_ABLAGE_TEST = "testAblage";
   public static final String RB_STORE_TEST = "testStore";
@@ -90,9 +92,14 @@
    */
   public void start() throws IOException {
     logger.log(Level.INFO, App.getRs(RB_SERVER_START_MSG), Integer.toString(port));
+    
+    String ui = App.getInitParameter(App.getRs(App.RB_AP_UI));
+    
+    File uiDir = new File(ui);
 
     HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
     server.createContext(ctx + App.getRs(RB_WEBROOT), new FileHandler(App.getInitParameter(App.getRs(App.RB_AP_WWW_DATA))));
+    server.createContext(ctx + App.getRs(RB_UI_ROOT), new FileHandler(uiDir.getAbsolutePath()));    
     server.createContext(ctx + App.getRs(RB_STOP_SERVER), new StopServerHandler());
     server.createContext(ctx + App.getRs(RB_ABLAGE_TEST), new AblageTestHandler());
     server.createContext(ctx + App.getRs(RB_STORE_TEST), new StoreTestHandler());
diff --git a/src/de/uhilger/mediaz/api/AblageTestHandler.java b/src/de/uhilger/mediaz/api/AblageTestHandler.java
index 05e18e7..e43f4c4 100644
--- a/src/de/uhilger/mediaz/api/AblageTestHandler.java
+++ b/src/de/uhilger/mediaz/api/AblageTestHandler.java
@@ -8,7 +8,7 @@
 import com.google.gson.Gson;
 import com.sun.net.httpserver.HttpExchange;
 import com.sun.net.httpserver.HttpHandler;
-import de.uhilger.mediaz.entity.Ablage;
+import de.uhilger.mediaz.entity.Ablageort;
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -21,7 +21,7 @@
 
   @Override
   public void handle(HttpExchange e) throws IOException {
-    Ablage ablage = new Ablage();
+    Ablageort ablage = new Ablageort();
     ablage.setName("Katalog");
     ablage.setOrt("/home/ulrich/Videos");
     
diff --git a/src/de/uhilger/mediaz/api/StoreTestHandler.java b/src/de/uhilger/mediaz/api/StoreTestHandler.java
index 9735b8e..8e040a0 100644
--- a/src/de/uhilger/mediaz/api/StoreTestHandler.java
+++ b/src/de/uhilger/mediaz/api/StoreTestHandler.java
@@ -8,7 +8,7 @@
 import com.sun.net.httpserver.HttpExchange;
 import com.sun.net.httpserver.HttpHandler;
 import de.uhilger.mediaz.conf.Store;
-import de.uhilger.mediaz.entity.Ablage;
+import de.uhilger.mediaz.entity.Ablageort;
 import de.uhilger.mediaz.entity.ConfigurationElement;
 import java.io.File;
 import java.io.IOException;
@@ -27,11 +27,11 @@
 
   @Override
   public void handle(HttpExchange e) throws IOException {
-    Ablage ablage = new Ablage();
-    ablage.setName("Katalog");
-    ablage.setOrt("/home/ulrich/Videos");
+    Ablageort ort = new Ablageort();
+    ort.setName("Katalog");
+    ort.setOrt("/home/ulrich/Videos");
     Store store = new Store();
-    File file = store.writeToFile(ablage);
+    File file = store.writeToFile(ort);
     try {
       ConfigurationElement elem = store.readFromFile(file);
       logger.log(Level.INFO, "Typ: {0}, Name: {1}", 
diff --git a/src/de/uhilger/mediaz/conf/Store.java b/src/de/uhilger/mediaz/conf/Store.java
index 2fd46c7..546aeee 100644
--- a/src/de/uhilger/mediaz/conf/Store.java
+++ b/src/de/uhilger/mediaz/conf/Store.java
@@ -8,16 +8,14 @@
 import com.google.gson.Gson;
 import de.uhilger.mediaz.App;
 import de.uhilger.mediaz.Server;
-import de.uhilger.mediaz.entity.Ablage;
+import de.uhilger.mediaz.entity.Ablageort;
 import de.uhilger.mediaz.entity.ConfigurationElement;
 import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.util.logging.Logger;
 
 /**
@@ -29,7 +27,7 @@
   private static final Logger logger = Logger.getLogger(Store.class.getName());
   
   
-  private static final String typeAblage = "Ablage";
+  private static final String typeAblageort = "Ablageort";
   
   /**
    * Ein Objekt als JSON in eine Datei schreiben
@@ -76,10 +74,10 @@
     String json = sb.toString();
     Gson gson = new Gson();
     switch(type) {
-      case typeAblage:
-        return gson.fromJson(json, Ablage.class);
+      case typeAblageort:
+        return gson.fromJson(json, Ablageort.class);
       default:
-        Ablage ablage = new Ablage();
+        Ablageort ablage = new Ablageort();
         ablage.setName("Test");
         return ablage;
     }
diff --git a/src/de/uhilger/mediaz/entity/Ablage.java b/src/de/uhilger/mediaz/entity/Ablageort.java
similarity index 74%
rename from src/de/uhilger/mediaz/entity/Ablage.java
rename to src/de/uhilger/mediaz/entity/Ablageort.java
index 91ee34b..2ab0c85 100644
--- a/src/de/uhilger/mediaz/entity/Ablage.java
+++ b/src/de/uhilger/mediaz/entity/Ablageort.java
@@ -9,10 +9,11 @@
  *
  * @author ulrich
  */
-public class Ablage implements ConfigurationElement {
+public class Ablageort implements ConfigurationElement {
   
   private String name;
   private String ort;
+  private String url;
 
   @Override
   public String getName() {
@@ -30,5 +31,13 @@
   public void setOrt(String ort) {
     this.ort = ort;
   }
+
+  public String getUrl() {
+    return url;
+  }
+
+  public void setUrl(String url) {
+    this.url = url;
+  }
   
 }
diff --git a/src/mediaz_de_DE.properties b/src/mediaz_de_DE.properties
index d126562..5f0d59a 100644
--- a/src/mediaz_de_DE.properties
+++ b/src/mediaz_de_DE.properties
@@ -4,9 +4,11 @@
 appParamConf=conf
 appParamWWWData=www-data
 appParamCtx=ctx
+appParamUi=ui
 
 # API-Endpunkte
 webroot=/
+uiroot=/ui
 stopServer=/server/stop
 testAblage=/test/ablage
 testStore=/test/store
diff --git a/ui/app-menu.css b/ui/app-menu.css
new file mode 100644
index 0000000..aa3e116
--- /dev/null
+++ b/ui/app-menu.css
@@ -0,0 +1,36 @@
+
+.app-menu {
+  margin: 0;
+  padding: 0;
+}
+
+.app-menu-kopf {
+  text-align: center;
+}
+
+ul.app-menu {
+	list-style: none;
+}
+
+.app-menu-item-back {
+  margin-bottom: 0.3em;
+  cursor: pointer;
+}
+
+.app-menu-item {
+  text-align: right;
+  cursor: pointer;
+}
+
+.app-menu-item-submark {
+  color: transparent;
+  cursor: pointer;
+}
+
+/*
+  Das div-Element, das das Menue aufnimmt erhaelt
+  die Klasse app-menu-content
+*/
+.app-menu-content {
+  overflow: hidden;
+}
diff --git a/ui/app.css b/ui/app.css
new file mode 100644
index 0000000..5827ce8
--- /dev/null
+++ b/ui/app.css
@@ -0,0 +1,119 @@
+html, body {
+  margin: 0;
+  padding: 0;
+  height: 100%; /* Anmerkung 2 */
+  font-size: larger;
+  font-family: 'Roboto Condensed';
+}
+body {
+  min-height: 0; /* Anmerkung 1 */
+  display: flex;
+  flex-flow: column;
+}
+.inhalt {
+  display: flex;
+  flex-flow: row;
+  height: 100%; /* Anmerkung 2 */
+  min-height: 0; /* Anmerkung 1 */
+  background-color: #ededed;
+  overflow: hidden;
+}
+.nord {
+  background-color: black;
+  display: flex;
+  flex-flow: row;
+  height: 2em;
+  align-items: center;
+}
+.sued {
+  height: 1.5em;
+  overflow: hidden;
+  transition: all 0.3s ease-in;
+  background-color: lightgray;
+}
+.west {
+  flex-grow: 0;
+  flex-shrink: 0;
+  flex-basis: 4em;
+  background-color: white;
+  transition: all 0.3s ease-in;
+  overflow: hidden;
+  white-space: nowrap;
+}
+.ost {
+  flex-grow: 0;
+  flex-shrink: 0;
+  flex-basis: 6em;
+  transition: all 0.3s ease-in;
+  background-color: antiquewhite;
+  overflow: hidden;
+}
+.zentrum-behaelter {
+  display: flex;
+  flex-flow: column;
+  /* background-color: #eaeaea; */
+  width: 100%;
+}
+
+.zentrum {
+  width: 100%;
+  height: 100%;
+  overflow-x: hidden;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+}
+
+.zentraler-inhalt {
+  padding: 0.5em;
+}
+
+/*
+  Anmerkungen:
+  1.) min.height: 0 fuer body und inhalt ist gegen einen Bug, vgl.
+      http://stackoverflow.com/questions/33859811/understanding-flexbox-and-overflowauto
+  2.) height 100% fuer html, body und inhalt sorgt dafuer, dass sich alles
+      immer ueber das gesamte Browserfenster ausdehnt.
+*/
+
+.app-titel {
+  margin-left: 0.6em;
+  color: white;
+}
+
+.pointer-cursor {
+  cursor: pointer;
+}
+
+.dialog {
+  position: relative;
+  /* height: 0.1em; */
+  transition: all 0.3s ease-in;
+}
+
+.dlg-behaelter {
+  line-height: 1.6;
+  padding: 0.4em;
+}
+
+.dlg-info {
+  background-color: #dcf2fb; // blau
+  padding: 0.4em;
+}
+
+/*
+  Close Button
+
+  <div>
+    <span class="close-btn">&#10006;</span>
+  </div>
+*/
+
+.close-btn {
+  position: absolute;
+  top: 0px;
+  right: 0.4em;
+  margin: 0;
+  padding: 0;
+  font-size: 1.3em;
+  color: #b8b8b8;
+}
diff --git a/ui/data/menu/hauptmenue.json b/ui/data/menu/hauptmenue.json
new file mode 100644
index 0000000..b0685f0
--- /dev/null
+++ b/ui/data/menu/hauptmenue.json
@@ -0,0 +1,32 @@
+{
+  "menue": {
+    "menuetitel": "Hauptmenü",
+    "wurzel": true,
+    "vorgaenger": {
+      "vtitel": "",
+      "vverweis": ""
+    },
+    "inhalt":  [
+      {
+        "titel": "Seite umschalten",
+        "umenue": false,
+        "funktion": "app.seitenleiste_umschalten"
+      },
+      {
+        "titel": "Fuss umschalten",
+        "umenue": false,
+        "funktion": "app.fusszeile_umschalten"
+      },
+      {
+        "titel": "mehr",
+        "umenue": true,
+        "verweis": "untermenue-1.json"
+      },
+      {
+        "titel": "Info",
+        "umenue": false,
+        "funktion": "app.info_dialog_zeigen"
+      }
+    ]
+  }
+}
diff --git a/ui/data/menu/untermenue-1.json b/ui/data/menu/untermenue-1.json
new file mode 100644
index 0000000..24f547c
--- /dev/null
+++ b/ui/data/menu/untermenue-1.json
@@ -0,0 +1,27 @@
+{
+  "menue": {
+    "menuetitel": "Untermenü 1",
+    "wurzel": false,
+    "vorgaenger": {
+      "vtitel": "Hauptmenü",
+      "vverweis": "hauptmenue.json"
+    },
+    "inhalt": [
+      {
+        "titel": "Benachrichtigung 1",
+        "umenue": false,
+        "funktion": "app.message_1"
+      },
+      {
+        "titel": "noch mehr",
+        "umenue": true,
+        "verweis": "untermenue-2.json"
+      },
+      {
+        "titel": "Benachrichtigung 2",
+        "umenue": false,
+        "funktion": "app.message_2"
+      }
+    ]
+  }
+}
diff --git a/ui/data/menu/untermenue-2.json b/ui/data/menu/untermenue-2.json
new file mode 100644
index 0000000..00c0268
--- /dev/null
+++ b/ui/data/menu/untermenue-2.json
@@ -0,0 +1,27 @@
+{
+  "menue": {
+    "menuetitel": "Untermenü 2",
+    "wurzel": false,
+    "vorgaenger": {
+      "vtitel": "Untermenü 1",
+      "vverweis": "untermenue-1.json"
+    },
+    "inhalt":  [
+      {
+        "titel": "Funktion U2.1",
+        "umenue": false,
+        "funktion": "app.message_3('U2.1')"
+      },
+      {
+        "titel": "Funktion U2.2",
+        "umenue": false,
+        "funktion": "app.message_3('U2.2')"
+      },
+      {
+        "titel": "Funktion U2.3",
+        "umenue": false,
+        "funktion": "app.message_3('U2.3')"
+      }
+    ]
+  }
+}
diff --git a/ui/data/tpl/app-menu.tpl b/ui/data/tpl/app-menu.tpl
new file mode 100644
index 0000000..de9b1e4
--- /dev/null
+++ b/ui/data/tpl/app-menu.tpl
@@ -0,0 +1,20 @@
+{{#menue}}
+  <p class="app-menu-kopf">
+    {{menuetitel}}
+  </p>
+  <ul class="app-menu">
+    {{^wurzel}}
+      <li class="app-menu-item-back bitem" data-verweis="{{vorgaenger.vverweis}}">
+        &#10094; {{vorgaenger.vtitel}}</i>
+      </li>
+    {{/wurzel}}                  
+    {{#inhalt}}          
+      {{#umenue}}          
+        <li class="app-menu-item smenu" data-verweis="{{verweis}}">{{titel}}&nbsp;&#10095;</li>
+      {{/umenue}}          
+      {{^umenue}}          
+        <li class="app-menu-item mitem" data-verweis="{{funktion}}" >{{titel}}&nbsp;<span class="app-menu-item-submark">&#10095;</span></i></li>
+      {{/umenue}}          
+    {{/inhalt}}
+  </ul>
+{{/menue}}
\ No newline at end of file
diff --git a/ui/data/tpl/dlg-info.tpl b/ui/data/tpl/dlg-info.tpl
new file mode 100644
index 0000000..692ace2
--- /dev/null
+++ b/ui/data/tpl/dlg-info.tpl
@@ -0,0 +1,8 @@
+<div class="dlg-info">
+  <span class="close-btn pointer-cursor">&#10006;</span>
+  <div class="dlg-behaelter">
+    <div class="dlg-info-app-titel">app-vorlage</div>
+    <div class="dlg-info-app-info">Eine Vorlage f&uuml;r Apps von <a href='https://uhilger.de'>Ulrich Hilger</a>.</div>
+    <div class="dlg-info-app-info">Weitere Infos im <a href='/gitblit/docs/web!app-vorlage.git'>Code-Repository</a>.</div>
+  </div>
+</div>
diff --git a/ui/hamburger.css b/ui/hamburger.css
new file mode 100644
index 0000000..c311070
--- /dev/null
+++ b/ui/hamburger.css
@@ -0,0 +1,99 @@
+/*!
+ * entnommen aus
+ *
+ * Hamburgers
+ * @description Tasty CSS-animated hamburgers
+ * @author Jonathan Suh @jonsuh
+ * @site https://jonsuh.com/hamburgers
+ * @link https://github.com/jonsuh/hamburgers
+ */
+
+.hamburger {
+  display: inline-block;
+  cursor: pointer;
+  transition-property: opacity, filter;
+  transition-duration: 0.15s;
+  transition-timing-function: linear;
+  font: inherit;
+  color: inherit;
+  text-transform: none;
+  background-color: transparent;
+  border: 0;
+  margin: 0;
+  overflow: visible;
+}
+
+.hamburger:hover {
+  opacity: 0.7;
+}
+
+.hamburger-box {
+  width: 40px;
+  height: 24px;
+  display: inline-block;
+  position: relative;
+}
+
+.hamburger-inner {
+  display: block;
+  top: 50%;
+  margin: 0;
+}
+
+.hamburger-inner, .hamburger-inner::before, .hamburger-inner::after {
+  width: 30px;
+  height: 4px;
+  background-color: white; /* #000; */
+  border-radius: 4px;
+  position: absolute;
+  transition-property: transform;
+  transition-duration: 0.15s;
+  transition-timing-function: ease;
+}
+
+.hamburger-inner::before, .hamburger-inner::after {
+  content: "";
+  display: block;
+}
+
+.hamburger-inner::before {
+  top: -10px;
+}
+
+.hamburger-inner::after {
+  bottom: -10px;
+}
+
+/*
+ * Elastic
+ */
+.hamburger--elastic .hamburger-inner {
+  top: 2px;
+  transition-duration: 0.275s;
+  transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
+}
+
+.hamburger--elastic .hamburger-inner::before {
+  top: 10px;
+  transition: opacity 0.125s 0.275s ease;
+}
+
+.hamburger--elastic .hamburger-inner::after {
+  top: 20px;
+  transition: transform 0.275s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+}
+
+.hamburger--elastic.is-active .hamburger-inner {
+  transform: translate3d(0, 10px, 0) rotate(135deg);
+  transition-delay: 0.075s;
+}
+
+.hamburger--elastic.is-active .hamburger-inner::before {
+  transition-delay: 0s;
+  opacity: 0;
+}
+
+.hamburger--elastic.is-active .hamburger-inner::after {
+  transform: translate3d(0, -20px, 0) rotate(-270deg);
+  transition-delay: 0.075s;
+}
diff --git a/ui/index.html b/ui/index.html
new file mode 100644
index 0000000..d206386
--- /dev/null
+++ b/ui/index.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>App-Vorlage</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="apple-mobile-web-app-capable" content="yes" />
+    <link href="https://fonts.googleapis.com/css?family=Roboto+Condensed" rel="stylesheet">
+    <link rel="stylesheet" type="text/css" href="app-menu.css">
+    <link rel="stylesheet" type="text/css" href="hamburger.css">
+    <link rel="stylesheet" type="text/css" href="app.css">
+  </head>
+  <body>
+    <!-- Kopfzeile -->
+    <div class="nord">
+      <div id="nav-menu">
+        <div id="nav-toggle" class="hamburger hamburger--elastic">
+          <div class="hamburger-box">
+            <div class="hamburger-inner"></div>
+          </div>
+        </div>
+      </div>
+      <div class="app-titel">
+        <span id="app-titel">App-Vorlage</span>
+      </div>
+    </div>
+    <div class="inhalt">
+      <!-- westliche Seitenleiste -->
+      <div class="west">
+        westliche Seitenleiste
+      </div>
+      <div class="zentrum-behaelter">
+        <!-- Einblendbereich -->
+        <div class="dialog"></div>
+        <!-- zentraler Inhaltsbereich -->
+        <div class="zentrum">
+          <div class="zentraler-inhalt">
+            <p>
+              Hier kann beliebiger Inhalt erscheinen.
+            </p>
+            <p>
+              Wenn dessen Darstellung mehr
+              Platz benötigt als das Anzeigegerät bietet wird ein
+              Rollbalken eingeblendet. Beim Rollen zu anfangs nicht sichtbaren
+              Teilen des Inhalts bleiben die den Inhaltsbereich
+              umschließenden Elemente sichtbar.
+            </p>
+            <p>
+              Ein Klick auf das Hamburger-Piktogramm oben links bzw. dessen
+              Antippen blendet ein Menü ein von dem aus weitere Funktionen
+              ausgelöst werden können.
+            </p>
+          </div>
+        </div>
+      </div>
+      <!-- oestliche Seitenleiste -->
+      <div class="ost ost-open">
+        östliche Seitenleiste
+      </div>
+    </div>
+    <!-- Fusszeile -->
+    <div class="sued sued-open">
+      Fußzeile
+    </div>
+    <!-- Skripte -->
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script>
+    <script src="js/app-menu.js"></script>
+    <!-- <script src="js/vorlagen.js"></script> -->
+    <script src="js/app.js"></script>
+    <script>
+		var app;
+		document.addEventListener('DOMContentLoaded', function () {
+			app = new AppVorlage();
+			app.init();
+		});
+    </script>
+  </body>
+</html>
+
diff --git a/ui/js/app-menu.js b/ui/js/app-menu.js
new file mode 100644
index 0000000..6a3b11e
--- /dev/null
+++ b/ui/js/app-menu.js
@@ -0,0 +1,137 @@
+function AppMenu() {
+  var self = this;
+  var _app_menu_selector;
+  var _app_menu_mbreite;
+  var _app_menu_url_prefix = "";
+  var _app_menu_template;
+
+  /*
+   * die nachfolgenden Funktionen steuern das ein- und
+   * ausblenden des menues
+   */
+  this.init = function (url_prefix, mdesc, mtpl, mselector, mbreite) {
+    self._app_menu_selector = mselector;
+    self._app_menu_mbreite = mbreite;
+    var menu = document.querySelector(self._app_menu_selector);
+    menu.style.flexBasis = '0em';
+    self._app_menu_url_prefix = url_prefix;
+    /*
+      Die Menue-Vorlage wird einmal zu Beginn geladen und
+      waehrend dem Programmlauf immer wieder neu zum Rendern
+      einer dynamisch gelandenen Menuebeschreibung verwendet
+    */
+    var request = new XMLHttpRequest();
+    request.open("GET", mtpl);
+    request.addEventListener('load', function(event) {
+       if (request.status >= 200 && request.status < 300) {
+          self._app_menu_template = request.responseText;
+          Mustache.parse(self._app_menu_template);   // optional, speeds up future uses
+          self.app_menu_laden(mdesc);
+       } else {
+          console.warn(request.statusText, request.responseText);
+       }
+    });
+    request.send();
+  };
+
+  this.app_menu_do_toggle = function(elem) {
+    self.toggle();
+  };
+
+  this.toggle = function() {
+    var menuDiv = document.querySelector(self._app_menu_selector);
+    if(menuDiv.classList.contains('app-menu-open')) {
+      menuDiv.classList.remove('app-menu-open');
+      menuDiv.style.flexBasis = '0em';
+    } else {
+      menuDiv.classList.add('app-menu-open');
+      menuDiv.style.flexBasis = self._app_menu_mbreite;
+    }
+  };
+
+  /*
+   * ab hier Steuerung des Menueinhalts
+   */
+
+
+  /*
+   * Menuebeschreibung als JSON-Datei laden
+   * mdesc: der URL einer JSON-Datei mit einer Menuebeschreibung
+   * richtung: z.Zt. unbenutzt: Animationsrichtung
+   */
+  this.app_menu_laden = function(mdesc, richtung) {
+    var xmlhttp = new XMLHttpRequest();
+    var url = self._app_menu_url_prefix + mdesc;
+    xmlhttp.onreadystatechange = function() {
+      if (this.readyState == 4 && this.status == 200) {
+        self.app_menu_bauen(JSON.parse(this.responseText), richtung);
+      }
+    };
+    xmlhttp.open("GET", url, true);
+    xmlhttp.send();
+  };
+
+  /*
+    Aus einer Menuebeschreibung im JSON-Format mit Hilfe
+    von Mustache und der zu Beginn geladenen HTML-Vorlage
+    ein div-Element zusammenbauen, das als Menue eingeblendet
+    werden kann und dem Element _app_menu_selector hinzufuegen
+  */
+  this.app_menu_bauen = function(menuejs, richtung) {
+
+    // neues Menue als div-Element zusammensetzen
+    var menuDiv = document.createElement("div");
+    menuDiv.classList.add('app-menu-content');
+    menuDiv.style.position = 'relative';
+    menuDiv.innerHTML = Mustache.render(self._app_menu_template, menuejs);
+
+    // altes Menue loeschen
+    self.app_menu_remove_event_listener_multi('.smenu', 'click', self.app_menu_klick_herunter);
+    self.app_menu_remove_event_listener_multi('.bitem', 'click', self.app_menu_klick_herauf);
+    self.app_menu_remove_event_listener_multi('.mitem', 'click', self.app_menu_ausfuehren);
+    var menu = document.querySelector(self._app_menu_selector);
+    menu.innerHTML = '';
+
+    // neues Menue hinzufuegen
+    menu.append(menuDiv);
+    self.app_menu_add_event_listener_multi('.smenu', 'click', self.app_menu_klick_herunter);
+    self.app_menu_add_event_listener_multi('.bitem', 'click', self.app_menu_klick_herauf);
+    self.app_menu_add_event_listener_multi('.mitem', 'click', self.app_menu_ausfuehren);
+
+    menuDiv = document.querySelector('.app-menu-content');
+    menuDiv.classList.add('slidein-from-right');
+  };
+
+  this.app_menu_klick_herunter = function() {
+    self.app_menu_laden(this.getAttribute('data-verweis'), 'herunter');
+  };
+
+  this.app_menu_klick_herauf = function() {
+    self.app_menu_laden(this.getAttribute('data-verweis'), 'herauf');
+  };
+
+  this.app_menu_ausfuehren = function() {
+    var functionName = this.getAttribute('data-verweis');
+    eval(functionName + "(this)");
+  };
+
+  /* --- Helferlein ---*/
+  /*
+    sel - '.smenu'
+    evt - 'click' fuer onclick
+    func - der verweis auf die funktion
+  */
+  this.app_menu_remove_event_listener_multi = function(sel, evt, func) {
+    var elem = document.querySelectorAll(sel);
+    for (var index = 0; index < elem.length; index++) {
+      elem[index].removeEventListener(evt, func);
+    }
+  };
+
+  this.app_menu_add_event_listener_multi = function(sel, evt, func) {
+    var elem = document.querySelectorAll(sel);
+    for (var index = 0; index < elem.length; index++) {
+      elem[index].addEventListener(evt, func);
+    }
+  };
+}
diff --git a/ui/js/app.js b/ui/js/app.js
new file mode 100644
index 0000000..4eaffef
--- /dev/null
+++ b/ui/js/app.js
@@ -0,0 +1,206 @@
+function AppVorlage() {
+  var self = this;
+  var appMenu;
+  // var vorlagen;
+  var cache; // mustache templates
+
+
+  this.init = function() {
+    //self.vorlagen = new Vorlagen();
+    self.cache = new Array();
+    self.appMenu = new AppMenu();
+    self.appMenu.init(
+      "data/menu/",
+      "hauptmenue.json",
+      "data/tpl/app-menu.tpl",
+      ".west",
+      "8em");
+
+	document.querySelector('.hamburger').addEventListener('click', function(e) {
+      self.menue_umschalten();
+    });
+
+  };
+
+  this.menue_umschalten = function() {
+    var ham = document.querySelector(".hamburger");
+    ham.classList.toggle("is-active"); // hamburger-icon umschalten
+    self.appMenu.toggle(); // menue oeffnen/schliessen
+  };
+
+  this.info_dialog_zeigen = function() {
+    self.dialog_laden_und_zeigen('data/tpl/dlg-info.tpl', '');
+    self.menue_umschalten();
+  };
+
+  this.seitenleiste_umschalten = function() {
+    var ostDiv = document.querySelector('.ost');
+    if(ostDiv.classList.contains('ost-open')) {
+      ostDiv.classList.remove('ost-open');
+  	  ostDiv.style.flexBasis = '0em';
+    } else {
+  	  ostDiv.classList.add('ost-open');
+  	  ostDiv.style.flexBasis = '6em';
+    }
+    self.menue_umschalten();
+  };
+
+  this.fusszeile_umschalten = function() {
+    var suedDiv = document.querySelector('.sued');
+    if(suedDiv.classList.contains('sued-open')) {
+      suedDiv.classList.remove('sued-open');
+	    suedDiv.style.height = '0';
+    } else {
+      suedDiv.classList.add('sued-open');
+	    suedDiv.style.height = '1.5em';
+    }
+    self.menue_umschalten();
+  };
+
+  this.menu_message = function(msg) {
+    self.meldung_mit_timeout(msg, 1500);
+    var suedDiv = document.querySelector('.sued');
+    if(suedDiv.classList.contains('sued-open')) {
+    } else {
+      suedDiv.classList.add('sued-open');
+	    suedDiv.style.height = '1.5em';
+    }
+    self.menue_umschalten();
+  };
+
+  this.message_1 = function() {
+    self.menu_message('Eine Mitteilung.');
+  };
+
+  this.message_2 = function() {
+    self.menu_message('Was wir schon immer sagen wollten.');
+  };
+
+  this.message_3 = function(text) {
+    self.menu_message(text);
+  };
+
+  this.meldung_mit_timeout = function(meldung, timeout) {
+    var s = document.querySelector('.sued');
+    s.textContent = meldung;
+    setTimeout(function() {
+      s.textContent = 'Bereit.';
+      setTimeout(function() {
+        var suedDiv = document.querySelector('.sued');
+        if(suedDiv.classList.contains('sued-open')) {
+    		  suedDiv.classList.remove('sued-open');
+    		  suedDiv.style.height = '0';
+        }
+      }, 500);
+    }, timeout);
+  };
+
+  /* Dialog-Funktionen */
+
+  /*
+    Einen Dialog aus Vorlagen erzeugen
+
+    vurl - URL zur Dialogvorlage
+    msgTpl - URL mit einer Vorlage eines Mitteilungstextes (optional)
+  */
+  this.dialog_laden_und_zeigen = function(vurl, msgTpl) {
+    if(msgTpl !== '') {
+      fetch(msgTpl)
+        .then(data => {
+          // Handle data
+          self.dialog_zeigen(vurl, data);
+        }).catch(error => {
+          // Handle error
+        });
+    } else {
+      self.dialog_zeigen(vurl, '');
+    }
+  };
+
+  this.dialog_zeigen = function(vurl, inhalt) {
+    var dlg = document.querySelector(".dialog");
+    self.html_erzeugen(
+      vurl,
+      inhalt,
+      function(html) {
+        //dlg.html(html);
+        dlg.style.height = '5em';
+        dlg.innerHTML = html;
+        document.querySelector('.close-btn').addEventListener('click', self.dialog_schliessen);
+        //dlg.slideDown(300);
+    });
+  };
+
+  self.dialog_schliessen = function() {
+    document.querySelector('.close-btn').removeEventListener('click', self.dialog_schliessen);
+    //$('.dialog').slideUp(300);
+    var dlg = document.querySelector('.dialog');
+    //dlg.style.display = "none";
+    dlg.style.height = '0';
+    dlg.innerHTML = '';
+  };
+
+  /* Vorlagen */
+
+  /*
+    Das HTML erzeugen, das entsteht, wenn eine Vorlage mit Inhalt
+    gefüllt wird
+
+    Das Füllen erfolgt asynchron, d.h. der Programmlauf geht nach dem
+    Aufruf weiter ohne auf das Laden und Füllen der Vorlage zu warten.
+    Das fertige HTML wird der Callback-Funktion übergeben
+    sobald die Vorlage geladen und gefüllt ist, unabhängig davon, wo der
+    Programmlauf zu diesem Zeitpunkt mittlerweile ist.
+
+    vurl - URL zur Vorlagendatei
+    inhalt - die JSON-Struktur, deren Inhalt in die
+              Vorlage gefüllt werden soll
+    cb - Callback-Funktion, die gerufen wird, wenn die Vorlage gefüllt ist.
+          Dieser Callback-Funktion wird das fertige HTML übergeben
+  */
+  this.html_erzeugen = function(vurl, inhalt, cb) {
+    var vorlage = self.cache[vurl];
+    if(vorlage === undefined) {
+      self.vorlage_laden_und_fuellen(vurl, inhalt, cb);
+    } else {
+      self.vorlage_fuellen(vurl, inhalt, cb);
+    }
+  };
+
+  this.vorlage_fuellen = function(vurl, inhalt, cb) {
+    cb(Mustache.render(self.cache[vurl], inhalt));
+  };
+
+  /*
+    Eine Vorlage vom Server in den lokalen Speicher laden
+    vurl - der URL unter dem die Vorlage zu finden ist
+    inhalt - die JSON-Struktur, deren Inhalt in die
+              Vorlage gefüllt werden soll
+    cb - callback: Diese Funktion wird gerufen, wenn die Vorlage mit dem
+            Inhalt gefüllt ist
+  */
+  this.vorlage_laden_und_fuellen = function(vurl, inhalt, cb) {
+    /*
+    $.ajax({
+      url: vurl,
+      type: "GET",
+      dataType : "text"
+    }).done(function( vorlage ) {
+      self.cache[vurl] = vorlage;
+      self.vorlage_fuellen(vurl, inhalt, cb);
+    });
+    */
+    var xmlhttp = new XMLHttpRequest();
+    xmlhttp.onreadystatechange = function() {
+      if (this.readyState == 4 && this.status == 200) {
+        self.cache[vurl] = this.responseText;
+        self.vorlage_fuellen(vurl, inhalt, cb);
+      }
+    };
+    xmlhttp.open("GET", vurl, true);
+    xmlhttp.send();
+  };
+
+
+}
+

--
Gitblit v1.9.3