Persoenliche Mediazentrale
ulrich
2021-04-04 cfa85894465dbf2d286e083d962babdf14641582
UI begonnen
11 files added
1 files renamed
6 files modified
837 ■■■■■ changed files
src/de/uhilger/mediaz/App.java 1 ●●●● patch | view | raw | blame | history
src/de/uhilger/mediaz/Server.java 7 ●●●●● patch | view | raw | blame | history
src/de/uhilger/mediaz/api/AblageTestHandler.java 4 ●●●● patch | view | raw | blame | history
src/de/uhilger/mediaz/api/StoreTestHandler.java 10 ●●●● patch | view | raw | blame | history
src/de/uhilger/mediaz/conf/Store.java 12 ●●●●● patch | view | raw | blame | history
src/de/uhilger/mediaz/entity/Ablageort.java 11 ●●●●● patch | view | raw | blame | history
src/mediaz_de_DE.properties 2 ●●●●● patch | view | raw | blame | history
ui/app-menu.css 36 ●●●●● patch | view | raw | blame | history
ui/app.css 119 ●●●●● patch | view | raw | blame | history
ui/data/menu/hauptmenue.json 32 ●●●●● patch | view | raw | blame | history
ui/data/menu/untermenue-1.json 27 ●●●●● patch | view | raw | blame | history
ui/data/menu/untermenue-2.json 27 ●●●●● patch | view | raw | blame | history
ui/data/tpl/app-menu.tpl 20 ●●●●● patch | view | raw | blame | history
ui/data/tpl/dlg-info.tpl 8 ●●●●● patch | view | raw | blame | history
ui/hamburger.css 99 ●●●●● patch | view | raw | blame | history
ui/index.html 79 ●●●●● patch | view | raw | blame | history
ui/js/app-menu.js 137 ●●●●● patch | view | raw | blame | history
ui/js/app.js 206 ●●●●● patch | view | raw | blame | history
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>
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());
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");
    
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}", 
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;
    }
src/de/uhilger/mediaz/entity/Ablageort.java
File was renamed from src/de/uhilger/mediaz/entity/Ablage.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;
  }
  
}
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
ui/app-menu.css
New file
@@ -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;
}
ui/app.css
New file
@@ -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;
}
ui/data/menu/hauptmenue.json
New file
@@ -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"
      }
    ]
  }
}
ui/data/menu/untermenue-1.json
New file
@@ -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"
      }
    ]
  }
}
ui/data/menu/untermenue-2.json
New file
@@ -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')"
      }
    ]
  }
}
ui/data/tpl/app-menu.tpl
New file
@@ -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}}
ui/data/tpl/dlg-info.tpl
New file
@@ -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>
ui/hamburger.css
New file
@@ -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;
}
ui/index.html
New file
@@ -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>
ui/js/app-menu.js
New file
@@ -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);
    }
  };
}
ui/js/app.js
New file
@@ -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();
  };
}