You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by br...@apache.org on 2019/06/15 16:27:04 UTC

[jspwiki] branch master updated: 2.11.0-M5-git-04: [JSPWIKI-1097] Refactored %%collapse and %%collapsbox

This is an automated email from the ASF dual-hosted git repository.

brushed pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/jspwiki.git


The following commit(s) were added to refs/heads/master by this push:
     new fa07f54  2.11.0-M5-git-04: [JSPWIKI-1097] Refactored %%collapse and %%collapsbox
fa07f54 is described below

commit fa07f5449356f5f06685af3580e9d490d7c8e419
Author: brushed <di...@gmail.com>
AuthorDate: Sat Jun 15 18:13:53 2019 +0200

    2.11.0-M5-git-04: [JSPWIKI-1097] Refactored %%collapse and %%collapsbox
---
 ChangeLog                                          |   8 +
 .../src/main/java/org/apache/wiki/Release.java     |   2 +-
 jspwiki-war/src/main/config/wro/wro-haddock.xml    |   1 -
 .../src/main/scripts/behaviors/Collapsible.js      | 473 +++++++++------------
 .../src/main/scripts/behaviors/CommentBox.js       |   2 +-
 .../src/main/scripts/lib/mootools-core-1.6.0.js    |  77 ----
 .../src/main/scripts/moo-extend/Cookie.Flags.js    |  86 ----
 jspwiki-war/src/main/scripts/util/cookies.js       |   2 +-
 .../src/main/scripts/wiki/Wiki.Behaviors.js        |  43 +-
 jspwiki-war/src/main/scripts/wiki/Wiki.js          |   4 +-
 .../main/styles/haddock/default/Collapsible.less   | 237 +++++++----
 .../src/main/styles/haddock/default/Columns.less   |  11 +-
 .../src/main/styles/haddock/default/TOCPlugin.less |  30 +-
 .../main/styles/haddock/default/Template.Edit.less |   5 +-
 .../src/main/styles/haddock/default/tables.less    |   4 +-
 .../src/main/styles/haddock/default/type.less      |  22 +-
 .../src/main/styles/haddock/default/variables.less |   2 +
 .../main/styles/haddock/default/wiki-wysiwyg.less  |   6 +-
 jspwiki-war/src/main/webapp/Comment.jsp            |   1 +
 .../src/main/webapp/favicons/site.webmanifest      |   8 +-
 .../src/main/webapp/templates/default/Nav.jsp      |   2 +-
 21 files changed, 431 insertions(+), 595 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 8b5b21f..efa01b1 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,11 @@
+2019-06-15  Dirk Frederickx (brushed AT apache DOT org)
+
+       * 2.11.0-M5-git-04
+
+       * [JSPWIKI-1097] Refactored %%collapse and %%collapsbox.
+         Added keyboard support to expand/collapse lists and boxes. Bugfixes on cookie handling.
+
+
 2019-05-28  Dirk Frederickx (brushed AT apache DOT org)
 
        * 2.11.0-M5-git-03
diff --git a/jspwiki-main/src/main/java/org/apache/wiki/Release.java b/jspwiki-main/src/main/java/org/apache/wiki/Release.java
index f20e5f3..8205c47 100644
--- a/jspwiki-main/src/main/java/org/apache/wiki/Release.java
+++ b/jspwiki-main/src/main/java/org/apache/wiki/Release.java
@@ -72,7 +72,7 @@ public final class Release {
      *  <p>
      *  If the build identifier is empty, it is not added.
      */
-    public static final String     BUILD         = "03";
+    public static final String     BUILD         = "04";
 
     /**
      *  This is the generic version string you should use when printing out the version.  It is of
diff --git a/jspwiki-war/src/main/config/wro/wro-haddock.xml b/jspwiki-war/src/main/config/wro/wro-haddock.xml
index 240751d..05b6b64 100644
--- a/jspwiki-war/src/main/config/wro/wro-haddock.xml
+++ b/jspwiki-war/src/main/config/wro/wro-haddock.xml
@@ -35,7 +35,6 @@
     <js>/scripts/moo-extend/Behavior.js</js>
     <js>/scripts/moo-extend/String.Extend.js</js>
     <!--<js>/scripts/moo-extend/Date.Extend.js</js>-->
-    <js>/scripts/moo-extend/Cookie.Flags.js</js>
     <js>/scripts/moo-extend/Array.Extend.js</js>
     <js>/scripts/moo-extend/Array.NaturalSort.js</js>
     <js>/scripts/moo-extend/Element.Extend.js</js>
diff --git a/jspwiki-war/src/main/scripts/behaviors/Collapsible.js b/jspwiki-war/src/main/scripts/behaviors/Collapsible.js
index 1fcacdf..1c01480 100644
--- a/jspwiki-war/src/main/scripts/behaviors/Collapsible.js
+++ b/jspwiki-war/src/main/scripts/behaviors/Collapsible.js
@@ -18,360 +18,283 @@
     specific language governing permissions and limitations
     under the License.
 */
+/*eslint-env browser */
+/*global $ */
+
 /*
 Class: Collapsible
+    Implement collapsible lists and collapsible boxes.
+    The state is stored in a cookie.
+    Keyboard navigation is supported: press the spacebar or enter-key to toggle  a section.
+    Expanding or collapsing sections can be animated. (with css support)
 
 Options:
-    options - (object, optional)
-
-    bullet - (string) css selector to create collapsible bullets, default is "b.bullet", //"b.bullet[html=&bull;]"
-    open - (string) css class of expanded "bullet" and "target" elements
-    close - (string) css class of collapsed "bullet" and "target" elements
-    hint - (object) hint titles for the open en closed bullet, will be localized
+    elements -
+    cookie - (optional) store the state of all collapsibles to a next page load
 
-    nested - (optional) css selector of nested container elements, example "li",
-    target - (optional) css selector of the target element which will expand/collapse, eg "ul,ol"
-        The target is a descendent of the main element, default target is the main element itself.
+Usage:
+> new Collapsible( $$(".page div[class^=collapse]"),{  cookie:{name:...} })
+*/
 
-    collapsed - (optional) css selector to match element which should be collapsed at initialization (eg "ol")
-        The initial state will be overruled by the Cookie.Flags, if any.
-    cookie - (optional) Cookie.Flags instance, persistent store of the collapsible state of the targets
+!function () {
 
-    fx - (optional, default = "height") Fx animation parameter - the css style to be animated
-    fxy - (optional, default = "y") Fx animation parameter
-    fxReset - (optional, default = "auto") Fx animation parameter - end value of Fx animated style.
-        At the end of the animation, "fx" is reset to this "fxReset" value. ("auto" or fixed value)
+    var _UID = 0,
+        _CollapseButton = "button.collapse-btn",
+        _CollapseBodyClass = "collapse-body",
+        _ClosedStateClass = "closed",
+        _AriaExpanded = "aria-expanded";
 
-Depends on:
-    String.Extend: xsubs()
-    Element.Extend: ifClass()
+    /*
+    Collapsible list
 
-DOM structure:
+    DOM structure BEFORE:
     (start code)
-    div.collapsible
-        ul
-            li
-                b.bullet.xpand|xpand[onclick="..."]
-                Toggle-text
-                ul.xpand|xpand
-                    li .. collapsible content ..
+        div.collapse
+            ul
+                li
+                    List-item-text
+                    ul
+                        li ...
     (end)
 
-Example:
+    DOM structure AFTER:
     (start code)
-    ...
+        div.collapse
+            ul
+                li
+                  button.collapse-btn#UID List-item-text
+                  ul.collapse-body
+                    li ...
     (end)
-*/
-!(function(){
-
-var TCollapsible = this.Collapsible = new Class({
-
-    Implements: Options,
-
-    options: {
-        bullet: "b.bullet", //clickable bullet
-        hint: { open:"collapse", close:"expand" },
-        open: "xpand",
-        close: "clpse",
-
-        //cookie: null,    //cookie-parameters persist the state of the targets
-        //target: "ul,ol", //the elements which will expand/collapse
-        //nested: "li",    //(optional) css selector of nested container elements
-        expand: "ul", //css selector to check if default state is expanded or collapse (collapsed == OL)
-
-        fx: "height",    //style attribute to animate on collapse
-        fxy: "y",        //scroll direction to animate on collapse,
-        fxReset: "auto"    //end value after animation is complete on expanded element: "auto" or fixed width
-    },
-
-    initialize: function(element, options){
+    */
+    function buildCollapsibleList(li) {
 
-        var self = this;
+        var collapseBody = li.getElement("> ul,> ol");
 
-        self.element = element = document.getElement(element);
-        //note: setOptions() makes a copy of all objects, so first copy the cookie!
-        self.cookie = options && options.cookie;
-        self.nodes = [];
-        self.states = ( (self.cookie && $.cookie(self.cookie)) || "").split("");
-
-        //console.log(self.cookie, self.nodes,self.states.join(''));
-
-        options = self.setOptions(options).options;
-
-        if( options.nested ){
-
-            element.getElements( options.nested ).each( self.build, self );
-
-        } else {
-
-            self.build( element );
+        if (collapseBody && collapseBody.firstChild) {
 
+            li.ifClass(collapseBody.matches("ol"), _ClosedStateClass);
+            collapseBody.addClass(_CollapseBodyClass);
         }
+        return li;
+    }
 
-        element.addEvent(
-            //EG: "click:relay(b.bullet.xpand,b.bullet.clpse)"
-            "click:relay({0}.{1},{0}.{2})".xsubs(options.bullet,options.open,options.close),
-            function(event){ event.stop(); self.toggle(this); }
-        );
-
-    },
+    /*
+    Collapsible box
 
-    build: function( element ){
+    DOM structure BEFORE:
+    (start code)
+        div.collapsebox
+          h4 title
+          ... body ...
+    (end)
 
-        var self = this,
-            options = self.options,
-            bullet = options.bullet,
-            target;
+    DOM structure AFTER:
+    (start code)
+        div.collapsebox
+          button.collapse-btn#UID
+          h4 title
+          div.collapse-body
+            ... body ...
+    (end)
+    */
+    function buildCollapsibleBox(el) {
 
-        if( !self.skip(element) ){
+        var header = el.firstElementChild,
+            next,
+            collapseBody;
 
-            bullet = element.getElement(bullet) || bullet.slick().inject(element,"top");
-            target = element.getElement(options.target);
+        if (header && header.nextSibling) {
 
-            if( target && (target.textContent.trim()!="") ){
+            collapseBody = ("div." + _CollapseBodyClass).slick();
+            while ((next = header.nextSibling)) { collapseBody.appendChild(next); }
 
-                //console.log("FX tween",bullet,target,self.initState(element,target));
-                if( options.fx ){
-                    target.set("tween",{
-                        property: options.fx,
-                        onComplete: function(){ self.fxReset( this.element ); }
-                    });
-                }
+            el.appendChild(collapseBody); //append after the header
 
-                self.update(bullet, target, self.initState(element,target), true);
-            }
+            if (el.className.test(/-closed\b/)) { el.addClass(_ClosedStateClass); }
+            //if( el.hasClass("closed"){ ... }
         }
-    },
-
-    //dummy skip function, can be overwritten by descendent classes
-    skip: function( /*element*/ ){
-        return false;
-    },
-
-    //function initState: returns true:expanded; false:collapsed
-    //state from cookie
-    initState:function( element, target ){
+        return el;
+    }
 
-        var self = this,
-            expand = target.matches(self.options.expand),
-            nodes = self.nodes,
-            states = self.states,
-            offset = nodes.length;
+    /*
+    A11Y
+    */
+    function setAriaExpanded(el, state) {
 
-        if( offset < states.length ){
-            expand = (states[offset] == 'T');
-        }
-        self.nodes[offset] = element;
-        states[offset] = expand ? "T":"F";
+        el.setAttribute(_AriaExpanded, state);
+    }
 
-        return expand;
-    },
+    /*
+    Function: addCollapseToggle(el, index)
+        Add a collapse BUTTON to the dom and set the proper event handlers
 
-    //function getState: returns true:expanded, false:collapsed
-    getState: function( target ){
+    DOM structure AFTER:
+    (start code)
+        <ELEMENT el>
+          label#UID.collapse-label   #text-content
+          <ELEMENT>.collapse-body
+            ...
+    */
+    function addCollapseToggle(el, index) {
 
-        return target.hasClass(this.options.open);
+        var id = this.UID + index,
+            isCollapsed = el.hasClass(_ClosedStateClass),
+            collapseBody = el.getElement("> ." + _CollapseBodyClass),
+            button;
 
-    },
+        if (this.flags[index]) { isCollapsed = (this.flags[index] == 'T'); }
+        this.flags[index] = isCollapsed ? "T" : "F";
 
-    toggle: function(bullet){
+        //put the label with open/close triangle
+        //$.create( _CollapseButton, {id:id, disabled:!collapseBody, start:el});
+        button = _CollapseButton.slick({ id: id });
+        button.disabled = !collapseBody;
+        el.insertBefore(button, el.firstChild);
 
-        var self = this,
-            cookie = self.cookie,
-            options = self.options,
-            nested = options.nested,
-            element = nested ? bullet.getParent(nested) : self.element,
-            target, state, offset;
+        if (collapseBody) {
 
-        if( element ){
-            target = element.getElement(options.target);
+            button.addEvent("click", toggle.bind(this));
+            button.addEvent("keydown", keyToggle);
 
-            if( target ){
-                state = !self.getState(target); //toggle state
-                self.update( bullet, target, state );
+            if (isCollapsed) { collapseBody.style.height = 0; }
+            setAriaExpanded(collapseBody, !isCollapsed);
+            setAriaExpanded(button, !isCollapsed);
 
-                if( cookie ){
-                    offset = self.nodes.indexOf(element);
-                    if( offset >= 0 ){
-                        self.states[offset] = (state ? "T" : "F");
-                        console.log("write",cookie,self.states.join(""))
-                        $.cookie( cookie, self.states.join("") ); //write cookie
-                    }
-                }
-            }
+            collapseBody.addEvent("transitionend", animationEnd);
         }
-    },
-
-    update: function( bullet, target, expand, force ){
-
-        var options = this.options, open=options.open, close=options.close;
-
-        if( bullet ){
+    }
 
-            bullet.ifClass(expand, open, close)
-                  .set( "title", options.hint[expand ? "open" : "close"].localize() );
+    /*
+    EventHandler: keyToggle
+        Collapse/Expand the body by pressing the spacebar or return-key on a collapse button with :focus
+    */
+    function keyToggle(ev) {
 
-        }
-        if( target ){
+        var code = ev.keyCode;
 
-            this.animate( target.ifClass(expand, open, close), expand, force );
+        if (code === 32 || code === 13) {
 
+            ev.preventDefault();
+            ev.target.click();  //trigger button
         }
+    }
 
-    },
-
-    animate: function( element, expand, force ){
-
-        var fx = element.get("tween"),
-            fxReset = this.options.fxReset,
-            max = (fxReset!="auto") ? fxReset : element.getScrollSize()[this.options.fxy];
-
-        if( this.options.fx ){
-
-            if( force ){
-                fx.set( expand ? fxReset : 0);
-            } else {
-                fx.start( expand ? max : [max,0] );
-            }
+    /*
+    EventHandler: animationEnd
+        Runs after completing the "height" animation on the collapseBody.
 
-        }
+        - if state=collapsed,  (height =  0px)
+            aria-expanded is still true,
+            now set the aria-expanded to false, which sets "display:none" (in the css)
+            to make sure the nested buttons, links etc. are not reachable anymore via te keyboard
 
-    },
+        - if state = expanded,  (height = n px)
+            aria-expanded is already true
+            now remove "height" from the inline style to return back to 'auto' height
+    */
+    function animationEnd() {
 
-    fxReset: function(element){
+        if (this.style.height == "0px") {
 
-        var options = this.options;
+            setAriaExpanded(this, false); //finalize the collapsed state of the body
 
-        if( options.fx && this.getState(element) ){
+        } else {
 
-            element.setStyle(options.fx, options.fxReset);
+            this.style.height = null;
 
         }
-
     }
 
-});
-
-
-/*
-Class: Collapsible.List
-    Converts ul/ol lists into collapsible trees.
-    Converts every nested ul/ol into a collasible item.
-    By default, OL elements are collapsed.
-
-DOM Structure:
-    (start code)
-    div.collapsible
-        ul
-            li
-                b.bullet.xpand|xpand[onclick="..."]
-                Toggle-text
-                ul.xpand|xpand
-                    li ... collapsible content ...
-    (end)
-*/
-TCollapsible/*this.Collapsible*/.List = new Class({
+    /*
+    EventHandler: toggle(event)
+        Store the new state in a cookie; and make sure all animations work.
+    */
+    function toggle(ev) {
 
-    Extends:TCollapsible,
+        var button = ev.target,
+            collapseBody = button.getElement("~ ." + _CollapseBodyClass), //get next-child .collapse-body
+            collapseBodyTransition = collapseBody.style.transition,
+            isExpanded = !collapseBody.style.height; //height = null(expanded) || 0px (collapsed)
 
-    initialize: function(element,options){
 
-        this.parent( element, Object.merge({
-            target:   "> ul, > ol",
-            nested:   "li",
-            collapsed:"ol"
-        },options));
+        function animateHeight(animate2steps, collapseMe){
 
-    },
+            requestAnimationFrame(function () {
 
-    // SKIP empty LI elements  (so, do not insert collapse-bullets)
-    // LI element is not-empty when is has
-    // - a child-node different from ul/ol
-    // - a non-empty #text-nodes
-    // Otherwise, it is considered
-    skip: function(element){
+                collapseBody.style.height = collapseMe ? 0 : collapseBody.scrollHeight + "px";
 
-        var n = element.firstChild,isTextNode, re=/ul|ol/i;
+                if (animate2steps) {
 
-        while( n ){
-
-            isTextNode = (n.nodeType==3);
-
-            if( ( !isTextNode && ( !re.test(n.tagName) ) )
-             || (  isTextNode && ( n.nodeValue.trim()!="") ) ){
-
-                     return false;
-            }
-            n=n.nextSibling;
+                    collapseBody.style.transition = collapseBodyTransition;
+                    animateHeight(false, true);
+                }
+            });
         }
 
-        return true; //skip this element
+        if (isExpanded) {
 
-    }
-
-});
-
-/*
-Class: Collapsible.Box
-    Makes a collapsible box.
-    - the first element becomes the visible title, which gets a bullet inserted
-    - all other child elements are wrapped into a collapsible element
-
-Options:
+            // *** transition from expanded to collapsed ***
 
+            // first temporarily disable css transitions
+            collapseBody.style.transition = "";
 
-DOM Structure:
-    (start code)
-    div.collapsebox.panel.panel-default
-        div.panel-heading
-            b.bullet.xpand|clpse[onclick="..."]
-            h4.panel-title title
-        div.panel-body.xpand|clpse
-            .. collapsible content ..
-    (end)
-
-*/
-TCollapsible/*this.Collapsible*/.Box = new Class({
+            // on the next frame, explicitly set the height to its current pixel height, removing the 'auto' height
+            // then put height=0 to collapse the boddy
+            // finally, at the end of the transition ( see animationEnd() ):
+            // set the aria-expanded to false, which sets "display:none" (in the css)
+            // to make sure the nested [tabindex=0] are not reachable anymore
+            animateHeight(true);
 
-    Extends:TCollapsible,
-
-    initialize:function(element,options){
+        } else {
 
-        //FFS: how to protect against empty boxes..
-        //if( element.getChildren().length >= 2 ){      //we don"t do empty boxes
+            // *** transition from collapsed to expanded ***
+            //first set the ariaExpanded=true,  which removes the "display:none" style (in the css)
+            setAriaExpanded(collapseBody, true);
 
-            options.collapsed = options.collapsed ? "div":""; // T/F converted to matching css selector
-            options.target = options.target || "!^"; //or  "> :last-child" or "> .panel-body"
+            // on the next frame, set the height (which is 0) to the real height
+            // finally, at the end of the transition ( see animationEnd() )
+            // remove the "height" from the inline style to return it back to 'auto' height
+            animateHeight();
 
-            this.parent( element, options );
-        //}
-    },
+        }
 
-    build: function( element ){
+        setAriaExpanded(button, !isExpanded);
+        this.flags[ button.id.split("-")[1] ] = isExpanded ? "T" : "F";  //new state: collapsed=="T"
 
-        var options = this.options, heading, body, next
-            panelCSS = "panel".fetchContext(element);
+        if (this.pims) {
+            $.cookie(this.pims, this.flags.join(""));
+        }
+    }
 
-        //we don"t do double invocations
-        if( !element.getElement( options.bullet ) ){
+    /*
+    Collapsible
+        Creates a new instance,
+        adding collapsible behaviour to a set of elements, and keeping their states in a cookie.
+    */
+    this.Collapsible = function (elements, options) {
 
-            //build bootstrap panel layout
-            element.className += " "+panelCSS;
+        var self = this, els = [];
 
-            heading = ["div.panel-heading",[options.bullet]].slick().wraps(
-                element.getFirst().addClass("panel-title")
-            );
+        self.UID = "C0llapse" + _UID++ + "-";
+        self.pims = options.cookie;
+        self.flags = ((self.pims && $.cookie(self.pims)) || "").split("");
 
-            body = "div.panel-body".slick();
-                while(next = heading.nextSibling) body.appendChild( next );
+        elements.forEach(function (el) {
 
-            //if( body.get("text").trim()!="" ) this-is-and-empty-box !!
+            if (el.matches(".collapse")) {
 
-            this.parent( element.grab( "div".slick().grab(body) ) );
+                el.getElements("li").forEach(function (el) {
+                    els.push(buildCollapsibleList(el));
+                });
+            }
+            else /*if( el.matches(".collapsebox") )*/ {
 
-        }
+                els.push(buildCollapsibleBox(el));
+            }
+        });
+        els.forEach(addCollapseToggle, /*bind to:*/ self);
+        //console.log(self.flags.join(""));
     }
 
-});
-
-})();
+}();
diff --git a/jspwiki-war/src/main/scripts/behaviors/CommentBox.js b/jspwiki-war/src/main/scripts/behaviors/CommentBox.js
index 8093b74..a9ebf88 100644
--- a/jspwiki-war/src/main/scripts/behaviors/CommentBox.js
+++ b/jspwiki-war/src/main/scripts/behaviors/CommentBox.js
@@ -50,7 +50,7 @@ function CommentBox(element, options){
 
         caption = "h4".slick({ text:caption.deCamelize() });
 
-    } else if( header && header.match("h2,h3,h4") ) {
+    } else if( header && header.matches("h2,h3,h4") ) {
 
         caption = header;
     }
diff --git a/jspwiki-war/src/main/scripts/lib/mootools-core-1.6.0.js b/jspwiki-war/src/main/scripts/lib/mootools-core-1.6.0.js
index 58ed08d..9c0f284 100644
--- a/jspwiki-war/src/main/scripts/lib/mootools-core-1.6.0.js
+++ b/jspwiki-war/src/main/scripts/lib/mootools-core-1.6.0.js
@@ -6073,83 +6073,6 @@ Element.implement({
 /*
 ---
 
-name: Cookie
-
-description: Class for creating, reading, and deleting browser Cookies.
-
-license: MIT-style license.
-
-credits:
-  - Based on the functions by Peter-Paul Koch (http://quirksmode.org).
-
-requires: [Options, Browser]
-
-provides: Cookie
-
-...
-*/
-
-var Cookie = new Class({
-
-	Implements: Options,
-
-	options: {
-		path: '/',
-		domain: false,
-		duration: false,
-		secure: false,
-		document: document,
-		encode: true,
-		httpOnly: false
-	},
-
-	initialize: function(key, options){
-		this.key = key;
-		this.setOptions(options);
-	},
-
-	write: function(value){
-		if (this.options.encode) value = encodeURIComponent(value);
-		if (this.options.domain) value += '; domain=' + this.options.domain;
-		if (this.options.path) value += '; path=' + this.options.path;
-		if (this.options.duration){
-			var date = new Date();
-			date.setTime(date.getTime() + this.options.duration * 24 * 60 * 60 * 1000);
-			value += '; expires=' + date.toGMTString();
-		}
-		if (this.options.secure) value += '; secure';
-		if (this.options.httpOnly) value += '; HttpOnly';
-		this.options.document.cookie = this.key + '=' + value;
-		return this;
-	},
-
-	read: function(){
-		var value = this.options.document.cookie.match('(?:^|;)\\s*' + this.key.escapeRegExp() + '=([^;]*)');
-		return (value) ? decodeURIComponent(value[1]) : null;
-	},
-
-	dispose: function(){
-		new Cookie(this.key, Object.merge({}, this.options, {duration: -1})).write('');
-		return this;
-	}
-
-});
-
-Cookie.write = function(key, value, options){
-	return new Cookie(key, options).write(value);
-};
-
-Cookie.read = function(key){
-	return new Cookie(key).read();
-};
-
-Cookie.dispose = function(key, options){
-	return new Cookie(key, options).dispose();
-};
-
-/*
----
-
 name: DOMReady
 
 description: Contains the custom event domready.
diff --git a/jspwiki-war/src/main/scripts/moo-extend/Cookie.Flags.js b/jspwiki-war/src/main/scripts/moo-extend/Cookie.Flags.js
deleted file mode 100644
index 0d16633..0000000
--- a/jspwiki-war/src/main/scripts/moo-extend/Cookie.Flags.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
-    JSPWiki - a JSP-based WikiWiki clone.
-
-    Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); fyou may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.
-*/
-/*
-Class: Cookie.Flags
-    Descendent of the Mootools Cookie class.
-    Stores the True/False state of a set of dom-elements in a cookie.
-    Encoding: per element in order of appearance, a character 'T' of 'F' is stored.
-
-    Side-effect: you always FIRST need to get all elements in sequence, to build
-    the internal elements[] array.
-    Then you can change the status flags.
-
-Example:
-(start code)
-    var cookie = new Cookie.Flags( 'mycookie_name', {duration:20});
-
-    default_state = false;
-    state = cookie.get(element_1, default_state); //add item to the cookie-repo ?? checkme...
-    .. repeat this for all dom-elements..
-
-    cookie.write(element_x, true);
-(end)
-*/
-Cookie.Flags = new Class({
-    Extends: Cookie,
-
-    initialize: function(key,options){
-
-        var self = this;
-        self.flags = ''; //sequence of state-flags, eg 'TTFTFFT'
-        self.elements = []; //array of elements, mapping one on one to the flags[]
-        self.parent(key,options);
-        self.pims = self.read();
-        //console.log(this.key, 'pims:',this.pims);
-
-    },
-
-    get: function(element, bool){
-
-        var self = this,
-            cookie = self.pims,
-            index = self.flags.length;
-
-        if( cookie && (index < cookie.length) ){
-            bool = (cookie.charAt(index)=='T');
-        }
-        self.flags += (bool?'T':'F');
-        self.elements.push(element);
-        //console.log("Cookie.Flags.get", cookie, index, this.flags)
-        return bool;
-
-    },
-
-    write: function(element, bool){
-
-        var self = this,
-            flags = self.flags,
-            index = self.elements.indexOf(element);
-
-        if( index >= 0 ){
-            flags = flags.slice(0,index) + (bool?'T':'F') + flags.slice(index+1);
-            self.parent(flags); //write cookie
-        }
-        //console.log("Cookie.Flags.write", flags, index, bool);
-
-    }
-
-});
diff --git a/jspwiki-war/src/main/scripts/util/cookies.js b/jspwiki-war/src/main/scripts/util/cookies.js
index 563061d..1dc9f82 100644
--- a/jspwiki-war/src/main/scripts/util/cookies.js
+++ b/jspwiki-war/src/main/scripts/util/cookies.js
@@ -39,7 +39,7 @@ $.cookie = function (name, value, options) {
         name = options.name;
     }
 
-    if (!/%/.test(name)) { name = encodeURIComponent(name); } //fixme
+    if (!/%/.test(name)) { name = encodeURIComponent(name); } //avoided double encoding..
 
     if (value == undefined) {
         // read cookie
diff --git a/jspwiki-war/src/main/scripts/wiki/Wiki.Behaviors.js b/jspwiki-war/src/main/scripts/wiki/Wiki.Behaviors.js
index b2ea887..162d25b 100644
--- a/jspwiki-war/src/main/scripts/wiki/Wiki.Behaviors.js
+++ b/jspwiki-war/src/main/scripts/wiki/Wiki.Behaviors.js
@@ -144,9 +144,6 @@ Behavior:%%graphBar .. /%
     .add("*[class^=graphBars]", GraphBar )
 
 
-    //FIXME -- OBSOLETE ?? top level TAB of the page
-    //.add(".page > .tabmenu a:not([href])", Tab )
-
 /*
 Behavior:tabs & pills
 >   %%tabbedSection .. /%
@@ -221,14 +218,6 @@ Behavior: Quote (based on Bootstrap)
     })
 
 
-    //FIXME  under construction
-    .add(".typography", function(element){
-
-        element.mapTextNodes( function(s){ return s.replace( /---/g, "&mdash;" );  });
-
-
-    })
-
 /*
 Behavior: Viewer
 >     %%viewer [link to youtube, vimeo, some-wiki-page, http://some-external-site ..] /%
@@ -405,33 +394,21 @@ Depends on:
 */
 
 //helper function
-function collapseFn(element, cookie){
-
-    var TCollapsible = Collapsible,
-        clazz = element.className,
-        list = "collapse",
-        box = list + "box";
-
-    cookie = cookie || wiki.PageName;
+function collapseFn(elements, pagename){
 
-    cookie = new Cookie.Flags("JSPWiki.Collapse." + cookie, {path: wiki.BaseUrl, duration: 20});
-
-    if( clazz == list ){
-
-        new TCollapsible.List(element, { cookie: cookie });
-
-    } else if( clazz.indexOf(box) == 0 ){
+    new Collapsible( elements, {
+        cookie: {
+            name: "JSPWiki.Collapse." + (pagename || wiki.PageName),
+            path: wiki.BaseUrl,
+            duration: 20
+        }
+    });
 
-        new TCollapsible.Box(element, {
-            cookie: cookie,
-            collapsed: clazz.indexOf(box + "-closed") == 0
-        });
-    }
 }
 
 wiki
-    .add(".page div[class^=collapse]", collapseFn )
-    .add(".sidebar div[class^=collapse]", collapseFn, "Sidebar")
+    .once(".page div[class^=collapse]", collapseFn )
+    .once(".sidebar div[class^=collapse]", collapseFn, "Sidebar")
 
 /*
 Behavior:Comment Box
diff --git a/jspwiki-war/src/main/scripts/wiki/Wiki.js b/jspwiki-war/src/main/scripts/wiki/Wiki.js
index bf0bd27..ef2c1b7 100644
--- a/jspwiki-war/src/main/scripts/wiki/Wiki.js
+++ b/jspwiki-war/src/main/scripts/wiki/Wiki.js
@@ -365,9 +365,7 @@ var Wiki = {
                     target.fireEvent(popstate);
 
                 }
-
                 target = target.getParent();
-
             }
         }
     },
@@ -858,7 +856,7 @@ var Wiki = {
         }
 
         if( element.matches(".modal") ){
-            element.openModal( ); // open the modal dialog
+            element.openModal( function(){} ); // open the modal dialog
         }
     }
 
diff --git a/jspwiki-war/src/main/styles/haddock/default/Collapsible.less b/jspwiki-war/src/main/styles/haddock/default/Collapsible.less
index 7577a72..5cf2d78 100644
--- a/jspwiki-war/src/main/styles/haddock/default/Collapsible.less
+++ b/jspwiki-war/src/main/styles/haddock/default/Collapsible.less
@@ -18,122 +18,185 @@
     specific language governing permissions and limitations
     under the License.
 */
+
 /*
 Style: Collapsible
-    Generic support for Collapsible.List, Collapsible.Box
-
-DOM structure:
-(start code)
-    li
-        b.bullet
-        .. content ..
-    li
-        b.bullet(.clpse)(.xpand)
-        ul
-            li .. content ..
-            li .. content ..
-(end)
+    Generic support for Collapsible Lists and Boxes.
+    (see behavior/Collapsible.js)
 */
-.bullet {
-    display:inline-block;
-    outline:0;
-    float:left;
-    line-height:1.4; //heuristic, for vertical
-
-    // triangular bullets
-    width:0;
-    height:0;
-    border:.4em solid transparent;
-    border-radius: @border-radius-base; //smooth edges
-    border-left-color: @text-color;
-    border-right: none;
-    margin: .3em .9em .3em .7em;
-
-    &.clpse {
-        cursor: pointer;
-        border-left-color: @link-color;
-        &:hover { border-left-color: @link-hover-color; }
-    }
-    &.xpand {
-        cursor: pointer;
-        border: .4em solid transparent; //reset
-        border-top-color: @link-color;
-        border-bottom: none;
-        margin: .5em .6em .5em .6em;
-        &:hover { border-top-color: @link-hover-color; }
-    }
-
-}
-.xpand, .clpse { overflow:hidden; }
+//fixme: need to overrule [class^=tab-] *:first-child{margin-top:0;}
 
 
 /*
-TODO: Experimental - css3 animation iso mootools tween.
-div.xpand { .transition(all .5s ease); height:auto; }
-div.clpse { .transition(all .5s ease); height:0; }
+Collapsible toggle button, and body scroll animation
 */
+//list buttons are rendered as little triangles, which turn 90deg when clicked
+.collapse-btn {
+	position: relative;
+    color: @gray-light;  //default color
+	border: 0;
+	background: transparent;
+	padding:0;
+
+    &:focus {
+        outline: 0;
+    }
+	&:not([disabled]) {
+	    color:@link-color;
+
+    	&:hover, &:focus {
+        	color: @wiki-mark;
+    	}
+	}
+    &::before {
+        content: '';
+        width: 0;
+        height: 0;
+        border-style: solid;
+        border-color: transparent currentColor;
+        border-width: @collapse-btn-base/2 0 @collapse-btn-base/2 @collapse-btn-base/2;
+        position: absolute;
+        display: block;
+        top: -@collapse-btn-base*(1 - 1/8);
+        left: -@collapse-btn-base*(1 + 1/4);
+        transition: transform .25s ease-in-out;
+    }
+	&[aria-expanded="false"]::before {
+    	transform: rotate(45deg) translate(@collapse-btn-base/4);
+	}
+}
+.collapse-body {
+    //height will be toggled by javascript to make the animation work
+    transition: height .4s ease-in-out;
+    height: auto;
+    overflow: hidden;
+
+	&[aria-expanded="false"] {
+		display:none;   //make sure nested buttons are not reachable anymore -- hidden content is not focusable
+	}
+}
 
 
 /*
-Style: Collapsible.List
+Style: Collapsible Lists
 >   %%collapse
 
-DOM structure:
+DOM structure BEFORE:
 (start code)
-div.collapse
-    ul
-        li
-            b.bullet
-            .. li-text ..
-        li
-            b.bullet(.xpand)(.clpse)
-            .. li-text ..
-            ul.xpand|clpse
-                li ..collapsible content ..
-                li ..collapsible content ..
+    div.collapsible
+        ul
+            li
+                List-item-text
+                ul
+                    li ...
 (end)
+
+DOM structure AFTER:
+(start code)
+    div.collapsible
+        ul
+            li
+                button.collapse-btn#UID List-item-text
+                ul.collapsse-body
+                    li ...
 */
 .collapse {
+    ul, ol {
+        list-style:none;
+    }
 
-    > ul, > ol { margin-left:-2em; }  // first ul/ol, shift left to create space for the bullets
-    ul, ol { list-style:none;  }
+	//additional bootstrap list styles
+    .list-nostyle,
+    .list-unstyled,
+    .list-group {
+    	.collapse-btn[disabled] { color:transparent; } //hide the button
+
+    }
 
-    //li:not(.xpand), li:not(.clpse) { list-style:auto; .bullet {display:none;} }
-    li { white-space:nowrap; overflow:hidden; }  //assume li-items fit on one line
+	//unstyled list: a lign all list-items on the left margin
+    & > ul.list-unstyled, & > ol.list-unstyled {
+    	padding-left: 1em;
+    }
+    & li > ul.list-unstyled, & li > ol.list-unstyled {
+    	padding-left:1em;
+    	margin-left:-1em;
+    }
 
+    //list group: draw nested boxes around groups of list items
+    &.list-group {
+    	.collapse-btn {
+    		margin-right: @collapse-btn-base;
+    		&::before {
+				top: -@collapse-btn-base;
+        		left: 0;
+	    	}
+    	}
+    }
 }
 
 
 /*
-Style: Collapsible-Box
+Collapsible Boxes
 >   %%collapsebox
+>   %%collapsebox.closed
 >   %%collapsebox-closed
+>   %%collapsebox.info , etc..
 
-DOM structure:
+DOM structure BEFORE:
 (start code)
-    //before
     div.collapsebox
-        b.bullet.xpand|clpse'[click='...']
-        h1-4  title
-        .. collapsible content ..
-
-    //after
-    div.panel.panel-default
-        div.panel-heading
-            b.bullet.xpand|clpse[onclick="..."]
-            h4.panel-title title
-        div.panel-body.xpand|clpse
-            .. collapsible content ..
+      h4 title
+      ... body ...
 (end)
-*/
-//re-use bootstrap/panels.less
-div[class^=collapsebox] {
 
-    > .panel-heading { padding-left:0; } //remove some left space next to the bullet
+DOM structure AFTER:
+(start code)
+    div.collapsebox
+      button.collapse-btn#UID
+      h4 title
+      div.collapse-body
+        ... body ...
+(end)
 
-    //bullets get color ot text
-    &:not(.panel-default) .bullet {
-        &.xpand, &.xpand:hover { border-top-color: currentColor; }
-        &.clpse, &.clpse:hover { border-left-color: currentColor; }
-    }
+*/
+div[class^=collapsebox] {
+    margin-bottom: @line-height-computed;  //copied from contextual boxes from bootstrap
+
+	//collapse boxes have animated +/- toggle buttons
+	//inspired by https://codepen.io/El11/pen/gXxxYz
+    .collapse-btn {
+        margin: @line-height-computed/2 0;  //adopt margin from header  -- see bootstrap style.less
+        margin-right: @collapse-btn-base/2;
+        height: @collapse-btn-base;
+        width: @collapse-btn-base;
+        float: left;
+
+        & + h2,& + h3,& + h4 {
+            margin: 0;
+        }
+        &::before,
+        &::after {
+        	background: @link-color;
+        	position: absolute;
+        	content: '';
+        	border: 0; //overwrite the triangle borders
+        	height: @collapse-btn-base/4;
+        	width: @collapse-btn-base;
+        	left: 0;
+        	top: 0;
+        }
+        &::before {
+        	transform-origin: center;   //vertical leg of plus sign
+        }
+		&:not([disabled]) {
+	    	&:hover, &:focus {
+	    		&::before, &::after {
+        			background: @wiki-mark;
+	    		}
+    		}
+		}
+        &[aria-expanded="false"]::before {
+        	transform: rotate(-90deg);
+        }
+	}
 }
diff --git a/jspwiki-war/src/main/styles/haddock/default/Columns.less b/jspwiki-war/src/main/styles/haddock/default/Columns.less
index a2179ba..ebb25b1 100644
--- a/jspwiki-war/src/main/styles/haddock/default/Columns.less
+++ b/jspwiki-war/src/main/styles/haddock/default/Columns.less
@@ -54,13 +54,22 @@ DOM structure:
 
     .col > [class*="bg-"],
     .col > .default,
+    .col > .collapsebox,
     .col > .info, .col > .information,
     .col > .success,
     .col > .warning,
     .col > .danger, .col > .error {
-        margin:  -@padding-base-vertical  -@padding-base-horizontal ;
+        margin: -@padding-base-vertical -@padding-base-horizontal ;
         min-height: 100%;
     }
+
+    .col > pre:first-child:last-child {
+        margin: -@padding-base-vertical -@padding-base-horizontal ;
+    }
+    &.border > .col > pre:first-child:last-child {
+    	border: none;
+    }
+
     .col > pre:only-child {
     	margin:-@padding-base-vertical 0;
     }
diff --git a/jspwiki-war/src/main/styles/haddock/default/TOCPlugin.less b/jspwiki-war/src/main/styles/haddock/default/TOCPlugin.less
index e2e8649..dc2c3ec 100644
--- a/jspwiki-war/src/main/styles/haddock/default/TOCPlugin.less
+++ b/jspwiki-war/src/main/styles/haddock/default/TOCPlugin.less
@@ -33,33 +33,33 @@ Style: TableOfContentsPlugin
 DOM structure:
     (begin)
     div.toc
-        div.collapsebox.panel.panel-default
-            div.panel-head
-                b.bullet xpand|clpse
-                h4#section-TOC Table Of Contents
-            div.xpand|clpse
-                div.panel-body
-                    ul
-                        li.toclevel-1
-                        li.toclevel-2
-                        li.toclevel-3
+        div.collapsebox
+            h4#section-TOC Table Of Contents
+            div.collapse-body
+                ul
+                    li.toclevel-1
+                    li.toclevel-2
+                    li.toclevel-3
     (end)
 */
 .toc {
     width: @wiki-commentbox-width-inverse;
     min-width: @wiki-small-viewport-dialog-width;
 
-    //when collapsed the header bottom border doubles with the bottom panel border
-    .panel-heading { border-bottom:none; }   //ugh -- for now just hide the border
+    .collapsebox {
+		padding: ((@line-height-computed - 1) / 2);   //re-use same padding as a PRE block
+		border-radius: @alert-border-radius;
+    	border: 1px solid @panel-default-border;
+    }
 
     ul {
         .list-unstyled;
         li:hover { background:@dropdown-link-hover-bg; }
         margin-bottom:0;
     }
-    .toclevel-1 { padding-left:1.2em }
-    .toclevel-2 { padding-left:2.4em; }
-    .toclevel-3 { padding-left:3.6em; }
+    .toclevel-1 { padding-left:1em }
+    .toclevel-2 { padding-left:2em; }
+    .toclevel-3 { padding-left:3em; }
 }
 //in case a TOC is added to the sidebar;  it will show the TOC of the main page
 .sidebar .toc {
diff --git a/jspwiki-war/src/main/styles/haddock/default/Template.Edit.less b/jspwiki-war/src/main/styles/haddock/default/Template.Edit.less
index 282bfe0..9c5a5e9 100644
--- a/jspwiki-war/src/main/styles/haddock/default/Template.Edit.less
+++ b/jspwiki-war/src/main/styles/haddock/default/Template.Edit.less
@@ -159,8 +159,11 @@ VERTICAL VIEW
 }
 
 .comment-page {
-    height: 150px;
+    height: 30vh;
     overflow-y: scroll;
+    border: 3px double @silver;
+    padding: @padding-base-vertical @padding-base-horizontal;
+
 }
 .comment-page + [data-resize] {
 	cursor:row-resize;
diff --git a/jspwiki-war/src/main/styles/haddock/default/tables.less b/jspwiki-war/src/main/styles/haddock/default/tables.less
index 136ba77..e41ba56 100644
--- a/jspwiki-war/src/main/styles/haddock/default/tables.less
+++ b/jspwiki-war/src/main/styles/haddock/default/tables.less
@@ -64,8 +64,8 @@ table[border="1"] td { border:0; }
         }
 }
 
-.table-noborder table.wikitable ,
-.table-borderless table.wikitable {
+.wikitable.table-noborder ,
+.wikitable.table-borderless {
 	td, tr:first-child th {
 	    border: 0;
 	}
diff --git a/jspwiki-war/src/main/styles/haddock/default/type.less b/jspwiki-war/src/main/styles/haddock/default/type.less
index 8340ff8..78c5be7 100644
--- a/jspwiki-war/src/main/styles/haddock/default/type.less
+++ b/jspwiki-war/src/main/styles/haddock/default/type.less
@@ -58,8 +58,16 @@ DOM structure:
 
     .btn; .btn-default; .btn-xs;
     margin-right: .25em;
-    .transition(all 1s ease);
-    &:hover { .opacity(1); border-color:transparent; }
+    .transition(all .5s ease);
+    &:hover, &:focus {
+    	.opacity(1);
+    	border-color:transparent;
+    }
+    &:focus {
+    	outline:none;
+    	border-color: @link-color;
+    	box-shadow: 0 0 .25em @link-color;
+    }
 	user-select:none;
 }
 
@@ -438,6 +446,10 @@ a:hover {
     //text-decoration: underline;
     -webkit-text-decoration-skip: ink;
 }
+a:focus {
+	outline:none;
+	box-shadow: 0 0 .25em @link-color;
+}
 
 //mimic :hover, for old ie cases which may not yet support :hover
 .hover { background-color:@wiki-hover; }
@@ -662,7 +674,11 @@ div.dropcaps {
 
 //%%quote .. /%
 //.quote is replaced by <blockquote>, to reuse bootstraps styling -- see Wiki.Behaviour.js
-blockquote:last-child { margin-bottom:0; }
+blockquote {
+	border-left: @line-height-computed/2 solid @blockquote-border-color;
+
+    &:last-child { margin-bottom:0; }
+}
 
 //when showing the quote style in a dialog dropdown, use .quote-item -- see Wiki.Snips.js
 .dialog .quote-item {
diff --git a/jspwiki-war/src/main/styles/haddock/default/variables.less b/jspwiki-war/src/main/styles/haddock/default/variables.less
index e224f30..d8f4534 100644
--- a/jspwiki-war/src/main/styles/haddock/default/variables.less
+++ b/jspwiki-war/src/main/styles/haddock/default/variables.less
@@ -156,6 +156,8 @@ images/feather-small.png   wxh  162x286
 @wiki-captcha-width:   125px;
 
 
+@collapse-btn-base: .8em;
+
 @pre-scrollable-max-height: 240px;  //bootstrap default is 340px
 
 @dropcaps-color:       @text-color;
diff --git a/jspwiki-war/src/main/styles/haddock/default/wiki-wysiwyg.less b/jspwiki-war/src/main/styles/haddock/default/wiki-wysiwyg.less
index e29bf64..559debd 100644
--- a/jspwiki-war/src/main/styles/haddock/default/wiki-wysiwyg.less
+++ b/jspwiki-war/src/main/styles/haddock/default/wiki-wysiwyg.less
@@ -142,9 +142,9 @@ FIXME: tidy up the css
 			background: none;
 		}
 
-		&.bold-item .button-icon::before { content: "B"; font-weight: bold; }
-		&.italic-item .button-icon::before { content: "I"; font-family: serif; }
-		&.strikethrough-item .button-icon::before { content: "S"; text-decoration: line-through; }
+		&.bold-item .button-icon:before { content: "B"; font-weight: bold; }
+		&.italic-item .button-icon:before { content: "I"; font-family: serif; }
+		&.strikethrough-item .button-icon:before { content: "S"; text-decoration: line-through; }
 		&.justifyleft-item .button-icon:before { content: "\f036"; }
 		&.justifyright-item .button-icon:before { content: "\f038"; }
 		&.justifycenter-item .button-icon:before { content: "\f037"; }
diff --git a/jspwiki-war/src/main/webapp/Comment.jsp b/jspwiki-war/src/main/webapp/Comment.jsp
index fee11bc..f08b743 100644
--- a/jspwiki-war/src/main/webapp/Comment.jsp
+++ b/jspwiki-war/src/main/webapp/Comment.jsp
@@ -121,6 +121,7 @@
     {
         link = HttpUtil.retrieveCookieValue( request, "link" );
         if( link == null ) link = "";
+        link = TextUtil.urlDecodeUTF8(link);
     }
 
     session.setAttribute( "link", link );
diff --git a/jspwiki-war/src/main/webapp/favicons/site.webmanifest b/jspwiki-war/src/main/webapp/favicons/site.webmanifest
index 9690474..25b3ce6 100644
--- a/jspwiki-war/src/main/webapp/favicons/site.webmanifest
+++ b/jspwiki-war/src/main/webapp/favicons/site.webmanifest
@@ -1,10 +1,10 @@
 {
-    "name": "JSPWiki",
+    "name": "jspĪ‰iki",
     "short_name": "JSPWiki",
-    "start_url":".",
+    "start_url": "..",
     "display": "standalone",
-    "background_color": "#4B8ADD",
-    "theme_color": "#4B8ADD",
+    "background_color": "#4b8Add",
+    "theme_color": "#4b8Add",
     "description": "JSPWiki is a simple (well, not any more) WikiWiki clone, written in Java and JSP. JSPWiki supports all the traditional wiki features, as well as very detailed access control and security integration using JAAS.  For more information see https://jspwiki-wiki.apache.org/",
     "icons": [
         {
diff --git a/jspwiki-war/src/main/webapp/templates/default/Nav.jsp b/jspwiki-war/src/main/webapp/templates/default/Nav.jsp
index c04b7bb..545b33b 100644
--- a/jspwiki-war/src/main/webapp/templates/default/Nav.jsp
+++ b/jspwiki-war/src/main/webapp/templates/default/Nav.jsp
@@ -114,7 +114,7 @@
   <%-- info --%>
   <wiki:CheckRequestContext context='view|info|upload|rename|edit|comment|conflict'>
   <wiki:PageExists>
-  <li id="info" tabindex="0">
+  <li id="info" tabindex="0" role="contentinfo">
       <a href="#" accessKey="i">
         <span class="icon-info-menu"></span>
         <span><fmt:message key='info.tab'/></span>