ulrich@undisclosed
2020-05-08 002c445416c96ec3af57df02e86ac8d5aaecf38b
ui2 begonnen
16 files added
1127 ■■■■■ changed files
web/ui2/data/menu/datei.json 27 ●●●●● patch | view | raw | blame | history
web/ui2/data/menu/edit.json 27 ●●●●● patch | view | raw | blame | history
web/ui2/data/menu/hauptmenue.json 27 ●●●●● patch | view | raw | blame | history
web/ui2/data/menu/nutzer.json 27 ●●●●● patch | view | raw | blame | history
web/ui2/data/menu/untermenue-1.json 27 ●●●●● patch | view | raw | blame | history
web/ui2/data/menu/untermenue-2.json 27 ●●●●● patch | view | raw | blame | history
web/ui2/data/tpl/app-menu.tpl 20 ●●●●● patch | view | raw | blame | history
web/ui2/data/tpl/dlg-info.tpl 8 ●●●●● patch | view | raw | blame | history
web/ui2/data/tpl/inhalt.tpl 3 ●●●●● patch | view | raw | blame | history
web/ui2/font/pikto.ttf patch | view | raw | blame | history
web/ui2/index.html 75 ●●●●● patch | view | raw | blame | history
web/ui2/js/app-menu.js 137 ●●●●● patch | view | raw | blame | history
web/ui2/js/app.js 186 ●●●●● patch | view | raw | blame | history
web/ui2/js/data.js 148 ●●●●● patch | view | raw | blame | history
web/ui2/js/vorlagen.js 66 ●●●●● patch | view | raw | blame | history
web/ui2/stile.css 322 ●●●●● patch | view | raw | blame | history
web/ui2/data/menu/datei.json
New file
@@ -0,0 +1,27 @@
{
  "menue": {
    "menuetitel": "Datei",
    "wurzel": false,
    "vorgaenger": {
      "vtitel": "Hauptmenü",
      "vverweis": "hauptmenue.json"
    },
    "inhalt": [
      {
        "titel": "Neuer Text",
        "umenue": false,
        "funktion": "app.datei_neuer_text"
      },
      {
        "titel": "noch mehr",
        "umenue": true,
        "verweis": "untermenue-2.json"
      },
      {
        "titel": "Benachrichtigung 2",
        "umenue": false,
        "funktion": "app.message_2"
      }
    ]
  }
}
web/ui2/data/menu/edit.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"
      }
    ]
  }
}
web/ui2/data/menu/hauptmenue.json
New file
@@ -0,0 +1,27 @@
{
  "menue": {
    "menuetitel": "Hauptmenü",
    "wurzel": true,
    "vorgaenger": {
      "vtitel": "",
      "vverweis": ""
    },
    "inhalt":  [
      {
        "titel": "Datei",
        "umenue": true,
        "verweis": "datei.json"
      },
      {
        "titel": "Bearbeiten",
        "umenue": true,
        "verweis": "edit.json"
      },
      {
        "titel": "Nutzer",
        "umenue": true,
        "verweis": "nutzer.json"
      }
    ]
  }
}
web/ui2/data/menu/nutzer.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": "Abmelden",
        "umenue": false,
        "funktion": "app.fm_logout"
      }
    ]
  }
}
web/ui2/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"
      }
    ]
  }
}
web/ui2/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')"
      }
    ]
  }
}
web/ui2/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}}
web/ui2/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>
web/ui2/data/tpl/inhalt.tpl
New file
@@ -0,0 +1,3 @@
<div>
</div>
web/ui2/font/pikto.ttf
Binary files differ
web/ui2/index.html
New file
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html>
  <head>
    <title>Nutzerverwaltung</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="stile.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">Nutzerverwaltung</span>
      </div>
    </div>
    <div class="inhalt">
      <!-- westliche Seitenleiste -->
      <div class="west">
        westliche Seitenleiste
      </div>
      <div class="zentrum-behaelter">
        <!-- Einblendbereich -->
        <div class="dialog"></div>
        <!-- Breadcrumb -->
        <div class="breadcrumb">
          <a class="bc-link">Ordner 1</a> /
          <a class="bc-link">Ordner 2</a>
        </div>
        <!-- zentraler Inhaltsbereich -->
        <div class="zentrum">
          <div class="zentraler-inhalt">
            <div class="zentrum-liste" id="nutzer">
              <figure>
                <i class="demo-icon icon-folder">&#xe800;</i>
                <figcaption class="i-name">icon-folder</figcaption>
              </figure>
            </div>
          </div>
        </div>
      </div>
      <!-- oestliche Seitenleiste -->
      <div class="ost ost-open">
        &ouml;stliche Seitenleiste
      </div>
    </div>
    <!-- Fusszeile -->
    <div class="sued sued-open">
      Fußzeile
    </div>
    <!-- Skripte -->
    <script src="/jslib/mustache/mustache.min.js"></script>
    <script src="/jslib/moment/moment-with-locales.min.js"></script>
    <script src="/jslib/numeral/numeral.min.js"></script>
    <script src="js/app-menu.js"></script>
    <script src="js/vorlagen.js"></script>
    <script src="js/data.js"></script>
    <script src="js/app.js"></script>
    <script>
      var app;
      document.addEventListener('DOMContentLoaded', function () {
        app = new AppVorlage();
        app.init();
      });
    </script>
  </body>
</html>
web/ui2/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);
    }
  };
}
web/ui2/js/app.js
New file
@@ -0,0 +1,186 @@
function AppVorlage() {
  var self = this;
  var appMenu;
  var vorlagen;
  var api;
  var userid;
  var pfad = '';
  var loc;
  var modus = 'kacheln';
  var PERS_DIR = "Persoenlich";
  var PUB_DIR = "Oeffentlich";
  var DAV_DIR = "Austausch";
  var BASE_DIR = "$basis";
  var DATA_DIR = "$daten";
  var WWW_DIR = "www";
  this.datei_neuer_text = function () {
    self.meldung_mit_timeout("Neuer Text", 1500);
  };
  /* Funktionen aus App-Vorlage */
  this.init = function () {
    self.vorlagen = new Vorlagen();
    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();
    });
    self.um_get_login();
    self.um_get_user_list();
    self.loc = window.location.protocol + '//' + window.location.host;
  };
  this.login_zeigen = function() {
    self.meldung_mit_timeout("Benutzer: " + self.userid, 1500);
  };
  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.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.vorlagen.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);
            });
  };
  this.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 = '';
  };
  /* API functions */
  this.um_get_user_list = function() {
    var m = 'getUserNameList';
    var u = '../svc/' + m;
    self.fm_get(u, "json", function (antwort) {
      var elem = document.getElementById('nutzer');
      elem.textContent = antwort;
    });
  }
  /* -------- An- und Abmelden ------------- */
  this.um_get_login = function() {
    var m = '?c=de.uhilger.um.pub.SessionManager&m=getSessionUser';
    var u = '../pub' + m;
    self.fm_get(u, "text", function (resp) {
      self.userid = resp;
      self.login_zeigen();
      //document.querySelector("#userMenu").textContent = resp;
    });
  };
  this.um_logout = function() {
    var m = '?c=de.uhilger.um.pub.SessionManager&m=expireSession';
    var u = '../pub' + m;
    self.fm_get(u, "text", function (resp) {
      //$('#userMenu').text('nicht angemeldet');
      window.location.href = '../logout.html';
    });
  };
  /* -------- ajax helper functions ----------- */
  this.fm_get = function (u, dtype, scallback) {
    var xmlhttp = new XMLHttpRequest();
    var url = u;
    xmlhttp.onreadystatechange = function() {
      if (this.readyState == 4 && this.status == 200) {
        scallback(this.responseText);
      }
    };
    xmlhttp.open("GET", url, true);
    xmlhttp.send();
  };
}
web/ui2/js/data.js
New file
@@ -0,0 +1,148 @@
/*
 Dateiverwaltung - File management in your browser
 Copyright (C) 2017 Ulrich Hilger, http://uhilger.de
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
 published by the Free Software Foundation, either version 3 of the
 License, or (at your option) any later version.
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU Affero General Public License for more details.
 You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/*
 * In data.js finden sich die Objekte vom Browser-Client
 * der Dateiverwaltung
 */
/* ----- Objekte ----- */
function IssueList(il) {
  this.issues = il;
}
function CompilerIssue(sn, ms, ki, ln) {
  var self = this;
  this.sourceName = sn;
  this.message = ms;
  this.kind = ki;
  this.lineNumber = ln;
}
function FileList(fl) {
  this.files = fl;
}
function FileRef(obj) {
  var self = this;
  this.fr = obj;
  this.fnx;
  this.fext = '';
  this.typeClass = function () {
    if (modus == 'kacheln') {
      if (self.fr.isDirectory) {
        return 'fa-folder ordner';
      } else {
        return 'fa-file datei';
      }
    } else {
      if (self.fr.isDirectory) {
        return 'fa-folder ordner';
      } else {
        return 'fa-file-o datei';
      }
    }
  };
  this.mini = function () {
    var miniatur = false;
    var namen = self.fr.absolutePath.split('/');
    if (namen.length > 0) {
      self.fnx = decodeURIComponent(namen[namen.length - 1]);
    } else {
      self.fnx = decodeURIComponent(self.fr.absolutePath);
    }
    if (self.fnx.indexOf('.jpg') > -1 || self.fnx.indexOf('.png') > -1 ||
            self.fnx.indexOf('.gif') > -1 || self.fnx.indexOf('.jpeg') > -1) {
      miniatur = true;
    }
    return miniatur;
  };
  this.dia = function () {
    return fm_slideshow;
  };
  this.miniurl = function () {
    // var userid = $('#userMenu').text();
    if (self.fext === '') {
      //self.fext = '';
      var dotpos = self.fnx.indexOf('.');
      if (dotpos > -1) {
        var fny = self.fnx;
        self.fnx = self.fnx.substring(0, dotpos);
        self.fext = fny.substr(dotpos);
      }
    }
    var path = fm_get_path(app.userid);
    var imgurl = loc + path + '/' + self.fnx + '_tn' + self.fext;
    return imgurl;
  };
  this.bildurl = function () {
    // var userid = $('#userMenu').text();
    if (self.fext === '') {
      //self.fext = '';
      var dotpos = self.fnx.indexOf('.');
      if (dotpos > -1) {
        var fny = self.fnx;
        self.fnx = self.fnx.substring(0, dotpos);
        self.fext = fny.substr(dotpos);
      }
    }
    var path = fm_get_path(app.userid);
    var imgurl = loc + path + '/' + self.fnx + self.fext;
    return imgurl;
  };
  this.fileName = function () {
    var namen = self.fr.absolutePath.split('/');
    if (namen.length > 0) {
      return decodeURIComponent(namen[namen.length - 1]);
    } else {
      return decodeURIComponent(self.fr.absolutePath);
    }
  };
  this.fileDate = function () {
    return moment(self.fr.lastModified).format("YYYY-MM-DD-hh-mm-ss-SSS");
  };
  this.fileSize = function () {
    return numeral(self.fr.length).format("0.00 b");
  };
}
function BcrFiles(fl) {
  this.files = fl;
}
function BcrFile(rp, n) {
  this.relPath = rp;
  this.fName = n;
}
web/ui2/js/vorlagen.js
New file
@@ -0,0 +1,66 @@
function Vorlagen() {
  var self = this;
  this.cache = {}; // mustache templates
  /*
    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();
  };
}
web/ui2/stile.css
New file
@@ -0,0 +1,322 @@
/*
    Created on : 24.01.2020, 09:08:45
    Author     : ulrich
*/
/* aus App-Vorlage */
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;
}
.breadcrumb {
  background-color: beige;
  padding: 0.2em;
  font-size: medium;
}
.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;
}
/* für app-menu */
.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;
}
/* für Hamburger Icon */
.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;
}
/* Font für Piktogramme mit Fontello aus FontAwesome erzeugt */
@font-face {
  font-family: 'pikto';
  src: url('font/pikto.ttf?37040783') format('truetype');
  font-weight: normal;
  font-style: normal;
}
 [class^="icon-"]:before, [class*=" icon-"]:before {
  font-family: "pikto";
  font-style: normal;
  font-weight: normal;
  speak: none;
  display: inline-block;
  text-decoration: inherit;
  width: 1em;
  margin-right: .2em;
  text-align: center;
  /* opacity: .8; */
  /* For safety - reset parent styles, that can break glyph codes*/
  font-variant: normal;
  text-transform: none;
  /* fix buttons height, for twitter bootstrap */
  line-height: 1em;
  /* Animation center compensation - margins should be symmetric */
  /* remove if not needed */
  margin-left: .2em;
  /* you can be more comfortable with increased icons size */
  /* font-size: 120%; */
  /* Font smoothing. That was taken from TWBS */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Uncomment for 3D effect */
  /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
.icon-folder:before { content: '\e800'; } /* '' */
.icon-folder-open:before { content: '\e801'; } /* '' */
.icon-th-large:before { content: '\e802'; } /* '' */
.icon-th-list:before { content: '\e803'; } /* '' */
.icon-doc-text:before { content: '\f0f6'; } /* '' */
.icon-folder-empty:before { content: '\f114'; } /* '' */
.icon-folder-open-empty:before { content: '\f115'; } /* '' */
.icon-doc-inv:before { content: '\f15b'; } /* '' */
.icon-doc-text-inv:before { content: '\f15c'; } /* '' */