Short description

A new concept for accesskey support in Moin 1.8

Having accesskeys is Moin is an often requested feature: Accesskeys speed up working with the wiki and are especially very helpful for disabled persons. However one big problem is, that you can't hardcode accesskeys into Moin code since every language has its own shortcuts for different actions. But there are also known conflicts between assistive technologies and browser/webpage shortcuts. That's why Moin has never implemented accesskeys for standard wiki operations.

The solution to the above mentioned problem is, that Moin ships with a basic accesskey support funtionality instead of built-in, working accesskeys. This means: it is finally up to the user and/or wikiadmin, to define the accesskeys he/she wants. Moin therefore ships without accesskeys but with the ability to define accesskeys as needed.

This is done as follows:

The Accesskey-Augmentation-Javascript-Function

First we have to modify common.js and provide there two new functions:

function addAccessKeys(){
    // Add 'accesskey=".."' attribute to the elments specified in var shortcut_list 
    for (i=0; i < shortcut_list.length; i++){
        // Split up the values which are stored like this:
        // Variant 1: (elementtype[name/id])#(elementname)=(accesskey)
        // Variant 2: (elementtype[name/id])#(elementname)=(accesskey)!(descriptiontext)
        var short1 = shortcut_list[i].split("#");
        var short2 = short1[1].split("=");
        var short3 = short2[1].split("!");
        var type = short1[0];
        var name = short2[0];
        var key = short3[0];
        if (short3.length > 1) {
            var desc = short3[1];
        }
        else {
            var desc = ""
        }
        //alert("type: " + type + " name: " + name + " key: " + key + " desc:" + desc);

        // Add keys
        if (type == "id") {
            var element = document.getElementById(name);
            if (element != null) {
                //alert("OK!!!!!!!!!! name:" + name + " element:" + element)
                var ak = document.createAttribute("accesskey");
                ak.nodeValue = key;
                element.setAttributeNode(ak);
                addAccessKeyDescription(document.getElementById('accesskeys'), key, name, desc);
            }
            else {
                //alert("ERROR!!!!!! type:" + type + " name:" + name +" not found")
            }
        }
        else {
            var elements = document.getElementsByName(name);
            for (j=0; j < elements.length; j++) {
                if (elements[j] != null) {
                    //alert("OK!!!!! name:" + name + " elements[" + j + "]:" + elements[j])
                    var ak = document.createAttribute("accesskey");
                    ak.nodeValue = key;
                    elements[j].setAttributeNode(ak);
                    addAccessKeyDescription(document.getElementById('accesskeys'), key, name, desc);
                }
                else {
                    //alert("ERROR!!!!!!!! type:" + type + " name:" + name +" not found")
                }
            }
        }
    }
}

function addAccessKeyDescription(div, key, name, desc){
    // If there is a 'div id="accesskeys"' in the page, add a list of available shortcuts so users get a clue
    // which keys to hit for a certain action
    if (div != null) {
        //alert("div is there");
        key_list = document.getElementById('accesskey_list');
        if (key_list == null) {
            key_list = document.createElement("ul");
            div.appendChild(key_list);
            var id = document.createAttribute("id");
            id.nodeValue = "accesskey_list";
            key_list.setAttributeNode(id);
            //alert("Created ul keylist");
        }
        if (desc == ""){
            var text = document.createTextNode(key + " = " + name)
        }
        else {
            var text = document.createTextNode(key + " = " + desc)
        }
        key_list.appendChild(document.createElement("li")).appendChild(text);
    }
    else {
        //alert("No div");
    }
}

The function addAccessKeys() is called by the load-Function when loading/displaying a page ("onload event")

function load() {
    // Do not name this "onload", it does not work with IE :-)
    // TODO: create separate onload for each type of view and set the
    // correct function name in the html. 
    // e.g <body onlod='editor_onload()'>
    
    // Page view stuff
    update_edit_links();
    add_gui_editor_links();
    
    // Editor stuff
    show_switch2gui();

    addAccessKeys();
}

The addAccessKeys()-Function does look for an javascript array "var shortcut_list" which holds the definition of the shortcut-keys, analyses/searches the page for matches and adds the respective accesskey attribute to the node. After that, the function calls the function "addAccessKeyDescription". This is not necessary to get accesskeys really work. It is mainly done for usablility sake's and to help users. The addAccessKeyDescription-Function looks for a 'div id="accesskeys"' and adds to that div a list of available accesskeys on the current page. Normally you would put that div in the footer of a page. Simply by looking there you can now see, which accesskeys you can use at the moment on the current page to trigger some actions.

Putting the "var shortcut_list" into the page header

The javascript array "var shortcut_list" is an array of strings with a special format:

"name#nav=1!Navigation Menu", "id#searchinput=2!Search", "name#texteditlink=E!Edit Page"

or more general

"[name/id]#[element]=[shortcut key]![description text]"

[name/id] specifies whether the node in the DOM tree has a name or id. [element] is the name/id of the element. [shortcut key] defines the accesskey for the element. [description text] is the description text which is displayed by the addAccessKeyDescription-Function in the footer of a page.

This string array is put into the page header by a modified headerscript-Function in theme/_init_.py:

   1     def headscript(self, d):
   2         """ Return html head script with common functions
   3 
   4         Changed: Added support for customizable accesskeys
   5 
   6         @param d: parameter dictionary
   7         @rtype: unicode
   8         @return: script for html head
   9         """
  10         # Don't add script for print view
  11         if self.request.action == 'print':
  12             return u''
  13 
  14         _ = self.request.getText
  15         script = u"""
  16 <script type="text/javascript">
  17 <!--
  18 var search_hint = "%(search_hint)s";
  19 //-->
  20 </script>
  21 """ % {
  22     'search_hint': _('Search'),
  23     }
  24         
  25         # Accesskey customization
  26         user = self.request.user
  27         content = ''
  28         if user.valid and user.name: 
  29             homewiki, homepage = wikiutil.getInterwikiHomePage(self.request)
  30             # We don't support interwiki homepages at the moment.
  31             # In the long run better move accesskey customization to the userprefs menu instead
  32             # of using an attached file 'shortcuts.js" to the user's homepage.
  33             # This will solve most security, perfomance and interwiki homepage problems
  34             if homewiki == 'Self':
  35                 from MoinMoin.action import AttachFile
  36                 import os
  37                 pagename, filename = AttachFile.absoluteName('shortcuts.js', homepage)
  38                 fname = wikiutil.taintfilename(filename)
  39                 fpath = AttachFile.getFilename(self.request, pagename, fname)
  40                 base, ext = os.path.splitext(filename)
  41                 try:
  42                     # Try to get the user's shortcut list
  43                     content = file(fpath, 'r').read()
  44                     content = wikiutil.decodeUnknownInput(content)
  45                     # Escape malicious code
  46 ## Turned off due to i18n problems with regex checking
  47 ##                    paras = []
  48 ##                    try:
  49 ##                        paras = content.split(',')
  50 ##                    except:
  51 ##                        paras[0] = content
  52 ##                    import re
  53 ##                    pattern1 = re.compile('^"(name|id)\#[-_a-zA-Z0-9]+\=[a-zA-Z0-9]+"$')
  54 ##                    pattern2 = re.compile('^"(name|id)\#[-_a-zA-Z0-9]+\=[a-zA-Z0-9]+\![ -_a-zA-Z0-9]+"$')
  55 ##                    for para in paras:
  56 ##                        fail1 = fail2 = False
  57 ##                        if re.search(pattern1, para.strip()) == None:
  58 ##                            fail1 = True
  59 ##                        if re.search(pattern2, para.strip()) == None:
  60 ##                            fail2 = True
  61 ##                        if fail1 and fail2:
  62 ##                            content = ''
  63 ##                            break
  64                     
  65                     content = content.replace(')', '')
  66                     content = content.replace(';', '')
  67 
  68                 except:
  69                     # User hasn't specified a shortcut list
  70                     pass
  71 
  72         if not content: 
  73             # If there is no user shortcut list: Do we have some global shortcut lists
  74             # set in wikiconfig.py?
  75             lang_keydefaults = 'accesskey_defaults_%s' % self.request.lang
  76             # Check whether there is a shortcut list fitting to the request.lang object
  77             if hasattr(self.request.cfg, lang_keydefaults):
  78                 content = getattr(self.request.cfg, lang_keydefaults)
  79             # Otherwise check if 'accesskey_defaults' is set
  80             elif hasattr(self.request.cfg, 'accesskey_defaults'):
  81                 content = self.request.cfg.accesskey_defaults
  82                 
  83         script += u"""
  84 <script type="text/javascript">
  85 <!--
  86 var shortcut_list = new Array (%(shortcut_list)s);
  87 //-->
  88 </script>
  89 """ % { 'shortcut_list': content,}
  90 
  91         return script

The function looks first whether we have a kown user which has provided a "shortcuts.js" file as an attachment to his wikihomepage. The shortcut.js file defines the acceesskeys. If not, the function tries to retrieve from the wikiconfig.py the accesskeys the wikiadmin has set for a specific language, e.g. for de

        accesskey_defaults_de = u'"name#nav=1!Navigation MenĂ¼", "id#searchinput=2!Volltextsuche", "name#texteditlink=E!Seite editieren"'

If there is no language specific shortcut definition, the function tries to get the general, language independent definition

        accesskey_defaults = u'"name#nav=1!Navigation Menu", "id#searchinput=2!Search", "name#texteditlink=E!Edit Page"'

If there is no general shortcut definition, there are no accesskeys added to the page.

Adding the 'div id="accesskeys"' to the page

To get the little accesskey cheat-sheet work, one needs to add the div somewhere in the page, e.g. somewhere in the footer:

   1     def footer(self, d, **keywords):
   2         """ Assemble wiki footer
   3 
   4         @param d: parameter dictionary
   5         @keyword ...:...
   6         @rtype: unicode
   7         @return: page footer html
   8         """
   9         page = d['page']
  10         html = [
  11             # End of page
  12             self.pageinfo(page),
  13             self.endPage(),
  14 
  15             # Pre footer custom html (not recommended!)
  16             self.emit_custom_html(self.cfg.page_footer1),
  17 
  18             # Footer
  19             u'<div id="footer">',
  20             self.editbar(d),
  21             self.credits(d),
  22             self.showversion(d, **keywords),
  23             self.showaccesskeys(d),
  24             u'</div>',
  25 
  26             # Post footer custom html
  27             self.emit_custom_html(self.cfg.page_footer2),
  28             ]
  29         return u'\n'.join(html)

with self.showaccesskeys(d):

   1     def showaccesskeys(self, d):
   2         """ Create accesskey html
   3 
   4         New helper function.
   5 
   6         If you put a <div id="accesskeys></div> somewhere in the page (e.g. by setting in wikiconfig.py
   7         "page_footer2 = u'<div id="accesskeys"></div>'"), the javascript function responsible for
   8         accesskeys augmentation of the page will add here a list of available shortcuts for the current
   9         page. 
  10         
  11         However since we want to have also some translated text before this list, we have to do it here so as to be
  12         able to use the built-in getText.
  13 
  14         @param d: parameter dictionary
  15         @rtype: unicode
  16         @return: accesskey list html
  17         """
  18         _ = self.request.getText
  19         html = ''
  20         show_keys = False
  21         if hasattr(self.request.cfg, 'show_accesskeys'):
  22             show_keys = self.request.cfg.show_accesskeys
  23         if show_keys:
  24             html = u'<div id="accesskeys"><h1 class="screenreader_info">%s</h1>%s</div>' % (_('Accesskeys'),_('Available accesskeys on this page:'))
  25         return html

so you can turn of cheat-sheet in wikiconfig.py as follows

        show_accesskeys = True

Accesskey description is added by a Javascript function (see also above):

function addAccessKeyDescription(div, key, name, desc){
    // If there is a 'div id="accesskeys"' in the page, add a list of available shortcuts so users get a clue
    // which keys to hit for a certain action
    if (div != null) {
        //alert("div is there");
        key_list = document.getElementById('accesskey_list');
        if (key_list == null) {
            key_list = document.createElement("ul");
            div.appendChild(key_list);
            var id = document.createAttribute("id");
            id.nodeValue = "accesskey_list";
            key_list.setAttributeNode(id);
            //alert("Created ul keylist");
        }
        if (desc == ""){
            var text = document.createTextNode(key + " = " + name)
        }
        else {
            var text = document.createTextNode(key + " = " + desc)
        }
        key_list.appendChild(document.createElement("li")).appendChild(text);
    }
    else {
        //alert("No div");
    }
}

That's the core of the new accesskey support in Moin.

What else to do?

The get this accesskey support really work in Moin, Moin development needs to add "name"/"id" attribs to various html-elements.

Add name-attribs to theme/_init_.py / common.js

Problem: A lot of "id" were removed from the theme/_init_.py because and id must occur only once in a page otherwise this will trigger an invalid html-error by validation services. Therefore "id" was changed into "css-class". For our accesskey-support we need the "name" attrib (not css class) to get it work. Therefore we have to add it at the appropriate place (for gui-edit-link also in common.js). E.g. it would be nice to add for each element of the page trail a name with a number (name=trail01...). So blind users can easily set accesskeys on this an navigate within the wiki (see further down for a suggestion where to add name-elements).

Disable/Remove accesskey

The new possibility in Moin to set accesskeys with a link definition (since Moin 1.6.1 with [[target|label|accesskey=1]] would interfere with our new approach. It should thus be removed/disabled IMHO.

Add help pages

We need a help page summarizing important id/name and elment-names to set accesskeys e.g. like this:

name#page_content : for the beginning of the page content (including pagetitle and potential message boxes) 
id#top : for the top of the page 
id#bottom : for the bottom of the page 
name#edit : for the edit and actions menu (name#texteditlink, name#guieditlink, name#info, name#attachments, name#action for the "More actions" box) 
name#nav : for the navigation menu (name#PageName for the pages there, name#navbar_current_page for the current page to quickly return to the page after e.g. viewing a diff)
name#user : for the users menu (login/logoff, Homepage, user preferences)
name#trail : for the page trail (and name#trail00, name#trail01... for the pages in the trail) 
id#searchinput : for the searchinput field 
name#save_button : for the edit save 
name#preview_button : for the page preview 
name#button_spellcheck : for the spell check 
name#button_cancel : for cancel 
id#editor-textarea : for the large input field to quickly set focus back on the edit box and return to editing 

We need also a list on the help page telling the user how to activate accesskeys in his browser:

Alt + accesskey (followed by Enter to execute a link) 
Internet Explorer for Windows 

Alt + accesskey 
Firefox for Windows <=1.5 
Mozilla for Windows 
Netscape 6+ for Windows 

Alt + Shift + accesskey 
Firefox for Windows 2 

Shift + Esc + accesskey 
Opera for Windows or Mac 

Ctrl + accesskey 
Internet Explorer 5.2 for Mac 
Safari 1.2 (Mac only) 
Firefox for Mac (including 2!) 
Mozilla for Mac 
Netscape 6+ for Mac 

Not supported 
Camino (Mac only) 
IE<4, Netscape<6 

Maybe it is also a good idea to provide a sample file/template for "shortcuts.js" to ease customization for new users.

That's it! By using a html-function to augment a wikipage with accesskeys you can do lot and have a quite flexible approach. Even macro/parser developper (Third-Party) can provide id/names for certain actions, tell them to the user and the user can set accesskeys on this (e.g. for slideshow, Arnica/Gallery2)! This kind of accesskey support is already fully implemented in http://www.moinmo.in/ThemeMarket/SimpleMente. You can try it there. However this theme is hard to maintain, since a lot of functions of ThemeBase were overridden (e.g. simply by changing css-class in name). Therefore it would be better to move that in ThemeBase. I would appreciate it very much, if Moin core developpment could discuss this approach for feasibility and - provided it is ok - to implement it in some way in the standard distribution. This would be a great accessibility feature and would set Moin apart from other wiki engines.

Related Pages

MoinMoinExtensions/HotKeys ThemeMarket/SimpleMente

Discussion


CategoryFeatureRequest

MoinMoin: FeatureRequests/BasicAccesskeySupportInsteadOfBuiltInAccesskeys (last edited 2008-09-04 09:15:32 by OliverSiemoneit)