adds dynamic fragments to LightPIT request handling framework + basic language recognition code

Tue, 26 Dec 2017 17:36:47 +0100

author
Mike Becker <universe@uap-core.de>
date
Tue, 26 Dec 2017 17:36:47 +0100
changeset 13
f4608ad6c947
parent 12
005d27918b57
child 14
2b270c714678

adds dynamic fragments to LightPIT request handling framework + basic language recognition code

src/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/Constants.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/Functions.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/modules/LanguageModule.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/resources/localization/language.properties file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/resources/localization/language_de.properties file | annotate | diff | comparison | revisions
src/java/logging.properties file | annotate | diff | comparison | revisions
web/META-INF/context.xml file | annotate | diff | comparison | revisions
web/WEB-INF/dynamic_fragments/language.jsp file | annotate | diff | comparison | revisions
web/WEB-INF/jsp/html_full.jsp file | annotate | diff | comparison | revisions
web/WEB-INF/web.xml file | annotate | diff | comparison | revisions
web/language.css file | annotate | diff | comparison | revisions
--- a/src/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Sat Dec 23 17:28:19 2017 +0100
+++ b/src/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Tue Dec 26 17:36:47 2017 +0100
@@ -31,13 +31,17 @@
 import java.io.IOException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -49,6 +53,8 @@
     
     private static final Logger LOG = LoggerFactory.getLogger(AbstractLightPITServlet.class);
     
+    private static final String HTML_FULL_DISPATCHER = Functions.jspPath("html_full");
+    
     /**
      * Store a reference to the annotation for quicker access.
      */
@@ -164,29 +170,43 @@
         LOG.trace("{} destroyed", getServletName());
     }
     
-    
     /**
-     * Sets several requests attributes, that can be used by the JSP.
+     * Sets the name of the dynamic fragment.
+     * 
+     * It is sufficient to specify the name without any extension. The extension
+     * is added automatically if not specified.
+     * 
+     * The fragment must be located in the dynamic fragments folder.
      * 
      * @param req the servlet request object
-     * @see Constants#REQ_ATTR_PATH
-     * @see Constants#REQ_ATTR_MODULE_CLASSNAME
-     * @see Constants#REQ_ATTR_MODULE_INFO
+     * @param fragmentName the name of the fragment
+     * @see Constants#DYN_FRAGMENT_PATH_PREFIX
      */
-    private void setGenericRequestAttributes(HttpServletRequest req) {
-        req.setAttribute(Constants.REQ_ATTR_PATH, Functions.fullPath(req));
-
-        req.setAttribute(Constants.REQ_ATTR_MODULE_CLASSNAME, this.getClass().getName());
-
-        moduleInfoELProxy.ifPresent((proxy) -> req.setAttribute(Constants.REQ_ATTR_MODULE_INFO, proxy));
+    public void setDynamicFragment(HttpServletRequest req, String fragmentName) {
+        req.setAttribute(Constants.REQ_ATTR_FRAGMENT, Functions.dynFragmentPath(fragmentName));
+    }
+    
+    /**
+     * Specifies the name of an additional stylesheet used by the module.
+     * 
+     * Setting an additional stylesheet is optional, but quite common for HTML
+     * output.
+     * 
+     * It is sufficient to specify the name without any extension. The extension
+     * is added automatically if not specified.
+     * 
+     * @param req the servlet request object
+     * @param stylesheet the name of the stylesheet
+     */
+    public void setStylesheet(HttpServletRequest req, String stylesheet) {
+        req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Functions.enforceExt(stylesheet, ".css"));
     }
     
     private void forwardToFullView(HttpServletRequest req, HttpServletResponse resp)
             throws IOException, ServletException {
         
-        setGenericRequestAttributes(req);
         req.setAttribute(Constants.REQ_ATTR_MENU, getModuleManager().getMainMenu());
-        req.getRequestDispatcher(Functions.jspPath("full.jsp")).forward(req, resp);
+        req.getRequestDispatcher(HTML_FULL_DISPATCHER).forward(req, resp);
     }
     
     private Optional<HandlerMethod> findMapping(HttpMethod method, HttpServletRequest req) {
@@ -212,6 +232,22 @@
     
     private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp)
             throws ServletException, IOException {
+        
+        HttpSession session = req.getSession();
+        
+        // choose the requested language as session language (if available) or fall back to english, otherwise
+        if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) {
+            Optional<List<String>> availableLanguages = Functions.availableLanguages(getServletContext()).map(Arrays::asList);
+            Optional<Locale> reqLocale = Optional.of(req.getLocale());
+            Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH);
+            session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale);
+            LOG.debug("Settng language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage());
+        }
+        
+        req.setAttribute(Constants.REQ_ATTR_PATH, Functions.fullPath(req));
+        req.setAttribute(Constants.REQ_ATTR_MODULE_CLASSNAME, this.getClass().getName());
+        moduleInfoELProxy.ifPresent((proxy) -> req.setAttribute(Constants.REQ_ATTR_MODULE_INFO, proxy));
+        
         Optional<HandlerMethod> mapping = findMapping(method, req);
         if (mapping.isPresent()) {
             forwardAsSepcified(mapping.get().apply(req, resp), req, resp);
--- a/src/java/de/uapcore/lightpit/Constants.java	Sat Dec 23 17:28:19 2017 +0100
+++ b/src/java/de/uapcore/lightpit/Constants.java	Tue Dec 26 17:36:47 2017 +0100
@@ -31,11 +31,23 @@
 import static de.uapcore.lightpit.Functions.fqn;
 
 /**
- * Contains all constants used by the this application.
+ * Contains all non-local scope constants used by the this application.
+ * 
+ * Constants with (class) local scope are defined in their respective classes.
  */
 public final class Constants {
     public static final String JSP_PATH_PREFIX = "/WEB-INF/jsp/";
     
+    public static final String JSPF_PATH_PREFIX = "/WEB-INF/jspf/";
+    
+    public static final String DYN_FRAGMENT_PATH_PREFIX = "/WEB-INF/dynamic_fragments/";
+    
+    
+    /**
+     * Name for the context parameter specifying the available languages.
+     */
+    public static final String CTX_ATTR_LANGUAGES = "available-languages";
+    
     /**
      * Key for the request attribute containing the class name of the currently dispatching module.
      */
@@ -55,6 +67,22 @@
      * Key for the request attribute containing the full path information (servlet path + path info).
      */
     public static final String REQ_ATTR_PATH = fqn(AbstractLightPITServlet.class, "path");
+
+    /**
+     * Key for the name of the fragment which should be rendered.
+     */    
+    public static final String REQ_ATTR_FRAGMENT = fqn(AbstractLightPITServlet.class, "fragment");
+    
+    /**
+     * Key for the name of the additional stylesheet used by a module.
+     */    
+    public static final String REQ_ATTR_STYLESHEET = fqn(AbstractLightPITServlet.class, "extraCss");
+    
+    
+    /**
+     * Key for the current language selection within the session.
+     */
+    public static final String SESSION_ATTR_LANGUAGE = fqn(AbstractLightPITServlet.class, "language");
     
     /**
      * This class is not instantiatable.
--- a/src/java/de/uapcore/lightpit/Functions.java	Sat Dec 23 17:28:19 2017 +0100
+++ b/src/java/de/uapcore/lightpit/Functions.java	Tue Dec 26 17:36:47 2017 +0100
@@ -29,6 +29,7 @@
 package de.uapcore.lightpit;
 
 import java.util.Optional;
+import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServletRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -40,8 +41,24 @@
     
     private static final Logger LOG = LoggerFactory.getLogger(Functions.class);
 
+    public static Optional<String[]> availableLanguages(ServletContext ctx) {
+        return Optional.ofNullable(ctx.getInitParameter(Constants.CTX_ATTR_LANGUAGES)).map((x) -> x.split("\\s*,\\s*"));
+    }
+    
+    public static String enforceExt(String filename, String ext) {
+        return filename.endsWith(ext) ? filename : filename + ext;
+    }
+
     public static String jspPath(String filename) {
-        return Constants.JSP_PATH_PREFIX + filename;
+        return enforceExt(Constants.JSP_PATH_PREFIX + filename, ".jsp");
+    }
+    
+    public static String jspfPath(String filename) {
+        return enforceExt(Constants.JSPF_PATH_PREFIX + filename, ".jspf");
+    }
+    
+    public static String dynFragmentPath(String filename) {
+        return enforceExt(Constants.DYN_FRAGMENT_PATH_PREFIX + filename, ".jsp");
     }
     
     public static String fqn(String base, String name) {
--- a/src/java/de/uapcore/lightpit/modules/LanguageModule.java	Sat Dec 23 17:28:19 2017 +0100
+++ b/src/java/de/uapcore/lightpit/modules/LanguageModule.java	Tue Dec 26 17:36:47 2017 +0100
@@ -30,12 +30,22 @@
 
 import de.uapcore.lightpit.LightPITModule;
 import de.uapcore.lightpit.AbstractLightPITServlet;
+import de.uapcore.lightpit.Constants;
+import de.uapcore.lightpit.Functions;
 import de.uapcore.lightpit.HttpMethod;
 import javax.servlet.annotation.WebServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import de.uapcore.lightpit.RequestMapping;
 import de.uapcore.lightpit.ResponseType;
+import java.util.ArrayList;
+import java.util.IllformedLocaleException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import javax.servlet.ServletException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 
 @LightPITModule(
@@ -48,9 +58,42 @@
 )
 public final class LanguageModule extends AbstractLightPITServlet {
     
+    private static final Logger LOG = LoggerFactory.getLogger(LanguageModule.class);
+    
+    private List<Locale> languages = new ArrayList<>();
+
+    @Override
+    public void init() throws ServletException {
+        super.init();
+        
+        Optional<String[]> langs = Functions.availableLanguages(getServletContext());
+        if (langs.isPresent()) {
+            for (String lang : langs.get()) {
+                try {
+                    Locale locale = Locale.forLanguageTag(lang);
+                    if (locale.getLanguage().isEmpty()) {
+                        throw new IllformedLocaleException();
+                    }
+                    languages.add(locale);
+                } catch (IllformedLocaleException ex) {
+                    LOG.warn("Specified lanaguge {} in context parameter cannot be mapped to an existing locale - skipping.", lang);
+                }
+            }
+            
+        } else {
+            languages.add(Locale.ENGLISH);
+            LOG.warn("Context parameter 'available-languges' not found. Only english will be available.");
+        }
+    }
+    
     @RequestMapping(method = HttpMethod.GET)
     public ResponseType handle(HttpServletRequest req, HttpServletResponse resp) {
+
+        req.setAttribute("languages", languages);
+        req.setAttribute("browserLanguage", req.getLocale());
         
+        setStylesheet(req, "language");
+        setDynamicFragment(req, "language");
         return ResponseType.HTML_FULL;
     }
 }
--- a/src/java/de/uapcore/lightpit/resources/localization/language.properties	Sat Dec 23 17:28:19 2017 +0100
+++ b/src/java/de/uapcore/lightpit/resources/localization/language.properties	Tue Dec 26 17:36:47 2017 +0100
@@ -22,3 +22,5 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 
 menuLabel = Languages
+submit = Switch language
+browserLanguage = Browser language
--- a/src/java/de/uapcore/lightpit/resources/localization/language_de.properties	Sat Dec 23 17:28:19 2017 +0100
+++ b/src/java/de/uapcore/lightpit/resources/localization/language_de.properties	Tue Dec 26 17:36:47 2017 +0100
@@ -22,3 +22,5 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 
 menuLabel = Sprache
+submit = Sprache ausw\u00e4hlen
+browserLanguage = Browsersprache
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java/logging.properties	Tue Dec 26 17:36:47 2017 +0100
@@ -0,0 +1,27 @@
+# Copyright 2017 Mike Becker. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
+
+
+handlers= java.util.logging.ConsoleHandler
+
+.level = DEBUG
--- a/web/META-INF/context.xml	Sat Dec 23 17:28:19 2017 +0100
+++ b/web/META-INF/context.xml	Tue Dec 26 17:36:47 2017 +0100
@@ -1,2 +1,2 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<Context path="/lightpit"/>
+<Context path="/lightpit" />
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/WEB-INF/dynamic_fragments/language.jsp	Tue Dec 26 17:36:47 2017 +0100
@@ -0,0 +1,46 @@
+<%-- 
+DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+
+Copyright 2017 Mike Becker. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
+--%>
+<%@page pageEncoding="UTF-8" session="true" %>
+<%@page import="de.uapcore.lightpit.Constants" %>
+<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+
+<c:set scope="page" var="currentLanguage" value="${sessionScope[Constants.SESSION_ATTR_LANGUAGE]}" />
+
+<fmt:bundle basename="${requestScope[Constants.REQ_ATTR_MODULE_INFO].bundleBaseName}">
+<form method="POST"id="lang-selector">
+    <c:forEach items="${languages}" var="l">
+        <label>
+            <input type="radio" name="language" value="${l.language}"
+                   <c:if test="${l.language eq currentLanguage.language}">checked</c:if>/>
+            ${l.displayLanguage}
+            (${l.getDisplayLanguage(currentLanguage)}<c:if test="${not empty browserLanguage and l.language eq browserLanguage.language}">&nbsp;-&nbsp;<fmt:message key="browserLanguage"/></c:if>)
+        </label>
+    </c:forEach>
+    <input type="submit" value="<fmt:message key="submit" />"/>
+</form>
+</fmt:bundle>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/WEB-INF/jsp/html_full.jsp	Tue Dec 26 17:36:47 2017 +0100
@@ -0,0 +1,85 @@
+<%-- 
+DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+
+Copyright 2017 Mike Becker. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
+--%>
+<%@page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" session="true" %>
+<%@page import="de.uapcore.lightpit.Constants" %>
+<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+
+<%-- Define an alias for the main menu --%>
+<c:set scope="page" var="mainMenu" value="${requestScope[Constants.REQ_ATTR_MENU]}"/>
+
+<%-- Define an alias for the fragment name --%>
+<c:set scope="page" var="fragment" value="${requestScope[Constants.REQ_ATTR_FRAGMENT]}"/>
+
+<%-- Define an alias for the additional stylesheet --%>
+<c:set scope="page" var="extraCss" value="${requestScope[Constants.REQ_ATTR_STYLESHEET]}"/>
+
+<%-- Define an alias for the module info --%>
+<c:set scope="page" var="moduleInfo" value="${requestScope[Constants.REQ_ATTR_MODULE_INFO]}"/>
+
+<!DOCTYPE html>
+<html>
+    <head>
+        <base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/">
+        <title>LightPIT -
+            <fmt:bundle basename="${moduleInfo.bundleBaseName}">
+                <fmt:message key="${moduleInfo.titleKey}" />
+            </fmt:bundle>
+        </title>
+        <meta charset="UTF-8">
+        <link rel="stylesheet" href="lightpit.css" type="text/css">
+        <c:if test="${not empty extraCss}">
+        <link rel="stylesheet" href="${extraCss}" type="text/css">
+        </c:if>
+    </head>
+    <body>
+        <div id="mainMenu">
+            <c:forEach var="menu" items="${mainMenu}">
+                <div class="menuEntry"
+                     <c:if test="${requestScope[Constants.REQ_ATTR_MODULE_CLASSNAME] eq menu.moduleClassName}">
+                         data-active
+                     </c:if>
+                >
+                    <a href="${menu.pathName}">
+                    <fmt:bundle basename="${menu.resourceKey.bundle}">
+                        <fmt:message key="${menu.resourceKey.key}" />
+                    </fmt:bundle>
+                    </a>
+                </div>
+            </c:forEach>
+        </div>
+        <div id="subMenu">
+            
+        </div>
+        <div id="content-area">
+            <c:if test="${not empty fragment}">
+            <c:import url="${fragment}" />
+            </c:if>
+        </div>
+    </body>
+</html>
--- a/web/WEB-INF/web.xml	Sat Dec 23 17:28:19 2017 +0100
+++ b/web/WEB-INF/web.xml	Tue Dec 26 17:36:47 2017 +0100
@@ -5,4 +5,8 @@
             30
         </session-timeout>
     </session-config>
+    <context-param>
+        <param-name>available-languages</param-name>
+        <param-value>en,de</param-value>
+    </context-param>
 </web-app>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/language.css	Tue Dec 26 17:36:47 2017 +0100
@@ -0,0 +1,39 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ * 
+ * Copyright 2017 Mike Becker. All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * 
+ */
+
+#lang-selector {
+    max-width: 30%;
+    display: flex;
+    flex-basis: content;
+    flex-direction: column;
+}
+
+input {
+    margin: .5em;
+}

mercurial