diff options
| author | Thomas Bruederli <thomas@roundcube.net> | 2014-06-05 09:18:07 +0200 | 
|---|---|---|
| committer | Thomas Bruederli <thomas@roundcube.net> | 2014-06-05 09:18:07 +0200 | 
| commit | 99cdca46b7bcc46fe6affd9e9f9f60a546b2e5b8 (patch) | |
| tree | e3d0bec8e981825e98681fb4d5ec1ec73ee65c40 /program | |
| parent | 17a76c3fd7665e92d2160f2178e31b3821a98e1e (diff) | |
| parent | 3412e50b54e3daac8745234e21ab6e72be0ed165 (diff) | |
Merge branch 'dev-accessibility'
Conflicts:
	program/include/rcmail_output_html.php
	program/js/app.js
	program/js/treelist.js
	program/lib/Roundcube/html.php
	skins/larry/styles.css
	skins/larry/templates/compose.html
Diffstat (limited to 'program')
| -rw-r--r-- | program/include/rcmail.php | 9 | ||||
| -rw-r--r-- | program/include/rcmail_output_html.php | 33 | ||||
| -rw-r--r-- | program/js/app.js | 595 | ||||
| -rw-r--r-- | program/js/common.js | 24 | ||||
| -rw-r--r-- | program/js/list.js | 171 | ||||
| -rw-r--r-- | program/js/treelist.js | 114 | ||||
| -rw-r--r-- | program/lib/Roundcube/html.php | 21 | ||||
| -rw-r--r-- | program/localization/en_US/labels.inc | 57 | ||||
| -rw-r--r-- | program/steps/addressbook/edit.inc | 3 | ||||
| -rw-r--r-- | program/steps/addressbook/func.inc | 14 | ||||
| -rw-r--r-- | program/steps/addressbook/show.inc | 1 | ||||
| -rw-r--r-- | program/steps/mail/compose.inc | 5 | ||||
| -rw-r--r-- | program/steps/mail/func.inc | 31 | ||||
| -rw-r--r-- | program/steps/mail/list_contacts.inc | 2 | ||||
| -rw-r--r-- | program/steps/mail/search_contacts.inc | 2 | ||||
| -rw-r--r-- | program/steps/mail/show.inc | 7 | ||||
| -rw-r--r-- | program/steps/settings/edit_folder.inc | 3 | ||||
| -rw-r--r-- | program/steps/settings/func.inc | 2 | ||||
| -rw-r--r-- | program/steps/settings/responses.inc | 2 | 
19 files changed, 879 insertions, 217 deletions
| diff --git a/program/include/rcmail.php b/program/include/rcmail.php index a9e717b86..1a227927e 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -1077,14 +1077,17 @@ class rcmail extends rcube          }          else {              foreach ($table_data as $row_data) { -                $class = !empty($row_data['class']) ? $row_data['class'] : ''; +                $class = !empty($row_data['class']) ? $row_data['class'] : null; +                if (!empty($attrib['rowclass'])) +                    $class = trim($class . ' ' . $attrib['rowclass']);                  $rowid = 'rcmrow' . rcube_utils::html_identifier($row_data[$id_col]);                  $table->add_row(array('id' => $rowid, 'class' => $class));                  // format each col                  foreach ($a_show_cols as $col) { -                    $table->add($col, $this->Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col])); +                    $val = is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]; +                    $table->add($col, empty($attrib['ishtml']) ? $this->Q($val) : $val);                  }              }          } @@ -1490,7 +1493,7 @@ class rcmail extends rcube              $html_name = $this->Q($foldername) . ($unread ? html::span('unreadcount', sprintf($attrib['unreadwrap'], $unread)) : '');              $link_attrib = $folder['virtual'] ? array() : array(                  'href' => $this->url(array('_mbox' => $folder['id'])), -                'onclick' => sprintf("return %s.command('list','%s',this)", rcmail_output::JS_OBJECT_NAME, $js_name), +                'onclick' => sprintf("return %s.command('list','%s',this,event)", rcmail_output::JS_OBJECT_NAME, $js_name),                  'rel' => $folder['id'],                  'title' => $title,              ); diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php index 43d73a6b4..6594209f6 100644 --- a/program/include/rcmail_output_html.php +++ b/program/include/rcmail_output_html.php @@ -893,6 +893,14 @@ EOF;              return '';          } +        // localize title and summary attributes +        if ($command != 'button' && !empty($attrib['title']) && $this->app->text_exists($attrib['title'])) { +            $attrib['title'] = $this->app->gettext($attrib['title']); +        } +        if ($command != 'button' && !empty($attrib['summary']) && $this->app->text_exists($attrib['summary'])) { +            $attrib['summary'] = $this->app->gettext($attrib['summary']); +        } +          // execute command          switch ($command) {              // return a button @@ -1165,6 +1173,17 @@ EOF;              $attrib['alt'] = html::quote($this->app->gettext($attrib['alt'], $attrib['domain']));          } +        // set accessibility attributes +        if (!$attrib['role']) { +            $attrib['role'] = 'button'; +        } +        if (!empty($attrib['class']) && !empty($attrib['classact']) || !empty($attrib['imagepas']) && !empty($attrib['imageact'])) { +            if (array_key_exists('tabindex', $attrib)) +                $attrib['data-tabindex'] = $attrib['tabindex']; +            $attrib['tabindex'] = '-1';  // disable button by default +            $attrib['aria-disabled'] = 'true'; +        } +          // set title to alt attribute for IE browsers          if ($this->browser->ie && !$attrib['title'] && $attrib['alt']) {              $attrib['title'] = $attrib['alt']; @@ -1353,6 +1372,20 @@ EOF;              $is_empty = true;          } +        // set default page title +        if (empty($this->pagetitle)) { +            $this->pagetitle = 'Roundcube Mail'; +        } + +        // declare page language +        if (!empty($_SESSION['language'])) { +            $lang = substr($_SESSION['language'], 0, 2); +            $output = preg_replace('/<html/', '<html lang="' . html::quote($lang) . '"', $output, 1); +            if (!headers_sent()) { +                header('Content-Language: ' . $lang); +            } +        } +          // replace specialchars in content          $page_title  = html::quote($this->pagetitle);          $page_header = ''; diff --git a/program/js/app.js b/program/js/app.js index 11204ffb2..d12dd81ca 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -46,6 +46,7 @@ function rcube_webmail()    this.messages = {};    this.group2expand = {};    this.http_request_jobs = {}; +  this.menu_stack = new Array();    // webmail client settings    this.dblclick_time = 500; @@ -197,7 +198,10 @@ function rcube_webmail()      // enable general commands      this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref', -      'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-save', true); +      'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-close', 'menu-save', true); + +    // set active task button +    this.set_button(this.task, 'sel');      if (this.env.permaurl)        this.enable_command('permaurl', 'extwin', true); @@ -232,8 +236,7 @@ function rcube_webmail()              return ref.command('sort', $(this).attr('rel'), this);            }); -          document.onmouseup = function(e){ return ref.doc_mouse_up(e); }; -          this.gui_objects.messagelist.parentNode.onmousedown = function(e){ return ref.click_on_list(e); }; +          this.gui_objects.messagelist.parentNode.onclick = function(e){ return ref.click_on_list(e || window.event); };            this.enable_command('toggle_status', 'toggle_flag', 'sort', true);            this.enable_command('set-listmode', this.env.threads && !this.is_multifolder_listing()); @@ -276,7 +279,7 @@ function rcube_webmail()            this.env.address_group_stack = [];            this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel',              'toggle-editor', 'list-adresses', 'pushgroup', 'search', 'reset-search', 'extwin', -            'insert-response', 'save-response']; +            'insert-response', 'save-response', 'menu-open', 'menu-close'];            if (this.env.drafts_mailbox)              this.env.compose_commands.push('savedraft') @@ -302,10 +305,12 @@ function rcube_webmail()              $('a.insertresponse', this.gui_objects.responseslist)                .attr('unselectable', 'on')                .mousedown(function(e){ return rcube_event.cancel(e); }) -              .mouseup(function(e){ -                ref.command('insert-response', $(this).attr('rel')); -                $(document.body).trigger('mouseup');  // hides the menu -                return rcube_event.cancel(e); +              .bind('mouseup keypress', function(e){ +                if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) { +                  ref.command('insert-response', $(this).attr('rel')); +                  $(document.body).trigger('mouseup');  // hides the menu +                  return rcube_event.cancel(e); +                }                });              // avoid textarea loosing focus when hitting the save-response button/link @@ -314,8 +319,6 @@ function rcube_webmail()              }            } -          document.onmouseup = function(e){ return ref.doc_mouse_up(e); }; -            // init message compose form            this.init_messageform();          } @@ -339,11 +342,21 @@ function rcube_webmail()          // init address book widget          if (this.gui_objects.contactslist) {            this.contact_list = new rcube_list_widget(this.gui_objects.contactslist, -            { multiselect:true, draggable:false, keyboard:false }); +            { multiselect:true, draggable:false, keyboard:true });            this.contact_list              .addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); })              .addEventListener('select', function(o) { ref.compose_recipient_select(o); })              .addEventListener('dblclick', function(o) { ref.compose_add_recipient('to'); }) +            .addEventListener('keypress', function(o) { +              if (o.key_pressed == o.ENTER_KEY) { +                if (!ref.compose_add_recipient('to')) { +                  // execute link action on <enter> if not a recipient entry +                  if (o.last_selected && String(o.last_selected).charAt(0) == 'G') { +                    $(o.rows[o.last_selected].obj).find('a').first().click(); +                  } +                } +              } +            })              .init();          } @@ -394,7 +407,6 @@ function rcube_webmail()              this.contact_list.highlight_row(this.env.cid);            this.gui_objects.contactslist.parentNode.onmousedown = function(e){ return ref.click_on_list(e); }; -          document.onmouseup = function(e){ return ref.doc_mouse_up(e); };            $(this.gui_objects.qsearchbox).focusin(function() { ref.contact_list.blur(); }); @@ -449,9 +461,14 @@ function rcube_webmail()          if (this.gui_objects.identitieslist) {            this.identity_list = new rcube_list_widget(this.gui_objects.identitieslist, -            {multiselect:false, draggable:false, keyboard:false}); +            {multiselect:false, draggable:false, keyboard:true});            this.identity_list              .addEventListener('select', function(o) { ref.identity_select(o); }) +            .addEventListener('keypress', function(o) { +              if (o.key_pressed == o.ENTER_KEY) { +                ref.identity_select(o); +              } +            })              .init()              .focus(); @@ -459,9 +476,10 @@ function rcube_webmail()              this.identity_list.highlight_row(this.env.iid);          }          else if (this.gui_objects.sectionslist) { -          this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:false}); +          this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:true});            this.sections_list              .addEventListener('select', function(o) { ref.section_select(o); }) +            .addEventListener('keypress', function(o) { if (o.key_pressed == o.ENTER_KEY) ref.section_select(o); })              .init()              .focus();          } @@ -469,7 +487,7 @@ function rcube_webmail()            this.init_subscription_list();          }          else if (this.gui_objects.responseslist) { -          this.responses_list = new rcube_list_widget(this.gui_objects.responseslist, {multiselect:false, draggable:false, keyboard:false}); +          this.responses_list = new rcube_list_widget(this.gui_objects.responseslist, {multiselect:false, draggable:false, keyboard:true});            this.responses_list              .addEventListener('select', function(list) {                var win, id = list.get_single_selection(); @@ -559,6 +577,18 @@ function rcube_webmail()          .get(0).addEventListener('drop', function(e){ return ref.file_dropped(e); }, false);      } +    // catch document (and iframe) mouse clicks +    var body_mouseup = function(e){ return ref.doc_mouse_up(e); }; +    $(document.body) +      .bind('mouseup', body_mouseup) +      .bind('keydown', function(e){ return ref.doc_keypress(e); }); + +    $('iframe').load(function(e) { +        try { $(this.contentDocument || this.contentWindow).on('mouseup', body_mouseup);  } +        catch (e) {/* catch possible "Permission denied" error in IE */ } +      }) +      .contents().on('mouseup', body_mouseup); +      // trigger init event hook      this.triggerEvent('init', { task:this.task, action:this.env.action }); @@ -591,7 +621,7 @@ function rcube_webmail()    {      var ret, uid, cid, url, flag, aborted = false; -    if (obj && obj.blur) +    if (obj && obj.blur && !(event || rcube_event.is_keyboard(event)))        obj.blur();      // do nothing if interface is locked by other command (with exception for searching reset) @@ -634,8 +664,8 @@ function rcube_webmail()      }      // trigger plugin hooks -    this.triggerEvent('actionbefore', {props:props, action:command}); -    ret = this.triggerEvent('before'+command, props); +    this.triggerEvent('actionbefore', {props:props, action:command, originalEvent:event}); +    ret = this.triggerEvent('before'+command, props || event);      if (ret !== undefined) {        // abort if one of the handlers returned false        if (ret === false) @@ -707,9 +737,15 @@ function rcube_webmail()            var mimetype = this.env.attachments[props.id];            this.enable_command('open-attachment', mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0);          } +        this.show_menu(props, props.show || undefined, event); +        break; + +      case 'menu-close': +        this.hide_menu(props, event); +        break;        case 'menu-save': -        this.triggerEvent(command, {props:props}); +        this.triggerEvent(command, {props:props, originalEvent:event});          return false;        case 'open': @@ -887,14 +923,14 @@ function rcube_webmail()        case 'move':        case 'moveto': // deprecated          if (this.task == 'mail') -          this.move_messages(props, obj); +          this.move_messages(props, event);          else if (this.task == 'addressbook')            this.move_contacts(props);          break;        case 'copy':          if (this.task == 'mail') -          this.copy_messages(props, obj); +          this.copy_messages(props, event);          else if (this.task == 'addressbook')            this.copy_contacts(props);          break; @@ -1450,7 +1486,8 @@ function rcube_webmail()      if (menu && modkey == SHIFT_KEY && this.commands['copy']) {        var pos = rcube_event.get_mouse_pos(e);        this.env.drag_target = target; -      $(menu).css({top: (pos.y-10)+'px', left: (pos.x-10)+'px'}).show(); +      this.show_menu(this.gui_objects.dragmenu.id, true, e); +      $(menu).css({top: (pos.y-10)+'px', left: (pos.x-10)+'px'});        return true;      } @@ -1566,17 +1603,22 @@ function rcube_webmail()      }    }; +  // global mouse-click handler to cleanup some UI elements    this.doc_mouse_up = function(e)    { -    var list, id; +    var list, id, target = rcube_event.get_target(e);      // ignore event if jquery UI dialog is open -    if ($(rcube_event.get_target(e)).closest('.ui-dialog, .ui-widget-overlay').length) +    if ($(target).closest('.ui-dialog, .ui-widget-overlay').length)        return; -    list = this.message_list || this.contact_list; -    if (list && !rcube_mouse_is_over(e, list.list.parentNode)) -      list.blur(); +    // remove focus from list widgets +    if (window.rcube_list_widget && rcube_list_widget._instances.length) { +      $.each(rcube_list_widget._instances, function(i,list){ +        if (list && !rcube_mouse_is_over(e, list.list.parentNode)) +          list.blur(); +      }); +    }      // reset 'pressed' buttons      if (this.buttons_sel) { @@ -1585,17 +1627,87 @@ function rcube_webmail()            this.button_out(this.buttons_sel[id], id);        this.buttons_sel = {};      } + +    // reset popup menus; delayed to have updated menu_stack data +    window.setTimeout(function(e){ +      var obj, skip, config, id, i, parents = $(target).parents(); +      for (i = ref.menu_stack.length - 1; i >= 0; i--) { +        id = ref.menu_stack[i]; +        obj = $('#' + id); + +        if (obj.is(':visible') +          && target != obj.data('opener') +          && target != obj.get(0)  // check if scroll bar was clicked (#1489832) +          && !parents.is(obj.data('opener')) +          && id != skip +          && (obj.attr('data-editable') != 'true' || !$(target).parents('#' + id).length) +          && (obj.attr('data-sticky') != 'true' || !rcube_mouse_is_over(e, obj.get(0))) +        ) { +          ref.hide_menu(id, e); +        } +        skip = obj.data('parent'); +      } +    }, 10);    }; +  // global keypress event handler +  this.doc_keypress = function(e) +  { +    // Helper method to move focus to the next/prev active menu item +    var focus_menu_item = function(dir) { +      var obj, item, mod = dir < 0 ? 'prevAll' : 'nextAll', limit = dir < 0 ? 'last' : 'first'; +      if (ref.focused_menu && (obj = $('#'+ref.focused_menu))) { +        item = obj.find(':focus').closest('li')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit](); +        if (!item.length) +          item = obj.find(':focus').closest('ul')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit](); +        return item.focus().length; +      } + +      return 0; +    }; + +    var target = e.target || {}, +      keyCode = rcube_event.get_keycode(e); + +    if (e.keyCode != 27 && (!this.menu_keyboard_active || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')) { +      return true; +    } + +    switch (keyCode) { +      case 38: +      case 40: +      case 63232: // "up", in safari keypress +      case 63233: // "down", in safari keypress +        focus_menu_item(mod = keyCode == 38 || keyCode == 63232 ? -1 : 1); +        break; + +      case 9:   // tab +        if (this.focused_menu) { +          var mod = rcube_event.get_modifier(e); +          if (!focus_menu_item(mod == SHIFT_KEY ? -1 : 1)) { +            this.hide_menu(this.focused_menu, e); +          } +        } +        return rcube_event.cancel(e); + +      case 27:  // esc +        if (this.menu_stack.length) +          this.hide_menu(this.menu_stack[this.menu_stack.length-1], e); +        break; +    } + +    return true; +  } +    this.click_on_list = function(e)    {      if (this.gui_objects.qsearchbox)        this.gui_objects.qsearchbox.blur();      if (this.message_list) -      this.message_list.focus(); +      this.message_list.focus(e);      else if (this.contact_list) -      this.contact_list.focus(); +      this.contact_list.focus(e);      return true;    }; @@ -1880,7 +1992,7 @@ function rcube_webmail()        flags: flags.extra_flags      }); -    var c, n, col, html, css_class, +    var c, n, col, html, css_class, label, status_class = '', status_label = '',        tree = '', expando = '',        list = this.message_list,        rows = list.rows, @@ -1897,17 +2009,26 @@ function rcube_webmail()      css_class = 'msgicon';      if (this.env.status_col === null) {        css_class += ' status'; -      if (flags.deleted) -        css_class += ' deleted'; -      else if (!flags.seen) -        css_class += ' unread'; -      else if (flags.unread_children > 0) -        css_class += ' unreadchildren'; +      if (flags.deleted) { +        status_class += ' deleted'; +        status_label += this.get_label('deleted') + ' '; +      } +      else if (!flags.seen) { +        status_class += ' unread'; +        status_label += this.get_label('unread') + ' '; +      } +      else if (flags.unread_children > 0) { +        status_class += ' unreadchildren'; +      } +    } +    if (flags.answered) { +      status_class += ' replied'; +      status_label += this.get_label('replied') + ' '; +    } +    if (flags.forwarded) { +      status_class += ' forwarded'; +      status_label += this.get_label('replied') + ' ';      } -    if (flags.answered) -      css_class += ' replied'; -    if (flags.forwarded) -      css_class += ' forwarded';      // update selection      if (message.selected && !list.in_selection(uid)) @@ -1944,15 +2065,17 @@ function rcube_webmail()          row_class += ' unroot';      } -    tree += '<span id="msgicn'+row.id+'" class="'+css_class+'"> </span>'; +    tree += '<span id="msgicn'+row.id+'" class="'+css_class+status_class+'" title="'+status_label+'"></span>';      row.className = row_class;      // build subject link      if (cols.subject) { -      var action = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show'; -      var uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid'; -      cols.subject = '<a href="./?_task=mail&_action='+action+'&_mbox='+urlencode(flags.mbox)+'&'+uid_param+'='+urlencode(uid)+'"'+ -        ' onclick="return rcube_event.cancel(event)" onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')"><span>'+cols.subject+'</span></a>'; +      var action  = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show', +        uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid', +        query = { _mbox: flags.mbox }; +      query[uid_param] = uid; +      cols.subject = '<a href="' + this.url(action, query) + '" onclick="return rcube_event.keyboard_only(event)"' + +        ' onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')" tabindex="-1"><span>'+cols.subject+'</span></a>';      }      // add each submitted col @@ -1966,28 +2089,36 @@ function rcube_webmail()        if (c == 'flag') {          css_class = (flags.flagged ? 'flagged' : 'unflagged'); -        html = '<span id="flagicn'+row.id+'" class="'+css_class+'"> </span>'; +        label = this.get_label(css_class); +        html = '<span id="flagicn'+row.id+'" class="'+css_class+'" title="'+label+'"></span>';        }        else if (c == 'attachment') { +        label = this.get_label('withattachment');          if (flags.attachmentClass) -          html = '<span class="'+flags.attachmentClass+'"> </span>'; +          html = '<span class="'+flags.attachmentClass+'" title="'+label+'"></span>';          else if (/application\/|multipart\/(m|signed)/.test(flags.ctype)) -          html = '<span class="attachment"> </span>'; +          html = '<span class="attachment" title="'+label+'"></span>';          else if (/multipart\/report/.test(flags.ctype)) -          html = '<span class="report"> </span>'; -        else +          html = '<span class="report"></span>'; +          else            html = ' ';        }        else if (c == 'status') { -        if (flags.deleted) +        label = ''; +        if (flags.deleted) {            css_class = 'deleted'; -        else if (!flags.seen) +          label = this.get_label('deleted'); +        } +        else if (!flags.seen) {            css_class = 'unread'; -        else if (flags.unread_children > 0) +          label = this.get_label('unread'); +        } +        else if (flags.unread_children > 0) {            css_class = 'unreadchildren'; +        }          else            css_class = 'msgicon'; -        html = '<span id="statusicn'+row.id+'" class="'+css_class+'"> </span>'; +        html = '<span id="statusicn'+row.id+'" class="'+css_class+status_class+'" title="'+label+'"></span>';        }        else if (c == 'threads')          html = expando; @@ -1997,8 +2128,10 @@ function rcube_webmail()          html = tree + cols[c];        }        else if (c == 'priority') { -        if (flags.prio > 0 && flags.prio < 6) -          html = '<span class="prio'+flags.prio+'"> </span>'; +        if (flags.prio > 0 && flags.prio < 6) { +          label = this.get_label('priority') + ' ' + flags.prio; +          html = '<span class="prio'+flags.prio+'" title="'+label+'"></span>'; +        }          else            html = ' ';        } @@ -2308,7 +2441,6 @@ function rcube_webmail()    this.clear_message_list = function()    {      this.env.messages = {}; -    this.last_selected = 0;      this.show_contentframe(false);      if (this.message_list) @@ -2327,6 +2459,7 @@ function rcube_webmail()        url._page = page;      this.http_request('list', url, lock); +    this.update_state({ _mbox: mbox, _page: (page && page > 1 ? page : null) });    };    // removes messages that doesn't exists from list selection array @@ -2600,7 +2733,7 @@ function rcube_webmail()    // set message icon    this.set_message_icon = function(uid)    { -    var css_class, +    var css_class, label = '',        row = this.message_list.rows[uid];      if (!row) @@ -2608,38 +2741,55 @@ function rcube_webmail()      if (row.icon) {        css_class = 'msgicon'; -      if (row.deleted) +      if (row.deleted) {          css_class += ' deleted'; -      else if (row.unread) +        label += this.get_label('deleted') + ' '; +      } +      else if (row.unread) {          css_class += ' unread'; +        label += this.get_label('unread') + ' '; +      }        else if (row.unread_children)          css_class += ' unreadchildren';        if (row.msgicon == row.icon) { -        if (row.replied) +        if (row.replied) {            css_class += ' replied'; -        if (row.forwarded) +          label += this.get_label('replied') + ' '; +        } +        if (row.forwarded) {            css_class += ' forwarded'; +          label += this.get_label('forwarded') + ' '; +        }          css_class += ' status';        } -      row.icon.className = css_class; +      $(row.icon).attr('class', css_class).attr('title', label);      }      if (row.msgicon && row.msgicon != row.icon) { +      label = '';        css_class = 'msgicon'; -      if (!row.unread && row.unread_children) +      if (!row.unread && row.unread_children) {          css_class += ' unreadchildren'; -      if (row.replied) +      } +      if (row.replied) {          css_class += ' replied'; -      if (row.forwarded) +        label += this.get_label('replied') + ' '; +      } +      if (row.forwarded) {          css_class += ' forwarded'; +        label += this.get_label('forwarded') + ' '; +      } -      row.msgicon.className = css_class; +      $(row.msgicon).attr('class', css_class).attr('title', label);      }      if (row.flagicon) {        css_class = (row.flagged ? 'flagged' : 'unflagged'); -      row.flagicon.className = css_class; +      label = this.get_label(css_class); +      $(row.flagicon).attr('class', css_class) +        .attr('aria-label', label) +        .attr('title', label);      }    }; @@ -2693,12 +2843,12 @@ function rcube_webmail()    };    // copy selected messages to the specified mailbox -  this.copy_messages = function(mbox, obj) +  this.copy_messages = function(mbox, event)    {      if (mbox && typeof mbox === 'object')        mbox = mbox.id;      else if (!mbox) -      return this.folder_selector(obj, function(folder) { ref.command('copy', folder); }); +      return this.folder_selector(event, function(folder) { ref.command('copy', folder); });      // exit if current or no mailbox specified      if (!mbox || mbox == this.env.mailbox) @@ -2715,12 +2865,12 @@ function rcube_webmail()    };    // move selected messages to the specified mailbox -  this.move_messages = function(mbox, obj) +  this.move_messages = function(mbox, event)    {      if (mbox && typeof mbox === 'object')        mbox = mbox.id;      else if (!mbox) -      return this.folder_selector(obj, function(folder) { ref.command('move', folder); }); +      return this.folder_selector(event, function(folder) { ref.command('move', folder); });      // exit if current or no mailbox specified      if (!mbox || (mbox == this.env.mailbox && !this.is_multifolder_listing())) @@ -3286,7 +3436,7 @@ function rcube_webmail()      this.env.recipients_delimiter = this.env.recipients_separator + ' ';      obj.keydown(function(e) { return ref.ksearch_keydown(e, this, props); }) -      .attr('autocomplete', 'off'); +      .attr({ 'autocomplete': 'off', 'aria-autocomplete': 'list', 'aria-expanded': 'false', 'role': 'combobox' });    };    this.submit_messageform = function(draft) @@ -3357,6 +3507,8 @@ function rcube_webmail()        input.val(oldval + recipients.join(delim + ' ') + delim + ' ');        this.triggerEvent('add-recipient', { field:field, recipients:recipients });      } + +    return recipients.length;    };    // checks the input fields before sending a message @@ -3507,15 +3659,18 @@ function rcube_webmail()        $('<a>').addClass('insertresponse active')          .attr('href', '#')          .attr('rel', key) +        .attr('tabindex', '0')          .html(this.quote_html(response.name))          .appendTo(li)          .mousedown(function(e){            return rcube_event.cancel(e);          }) -        .mouseup(function(e){ -          ref.command('insert-response', key); -          $(document.body).trigger('mouseup');  // hides the menu -          return rcube_event.cancel(e); +        .bind('mouseup keypress', function(e){ +          if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) { +            ref.command('insert-response', $(this).attr('rel')); +            $(document.body).trigger('mouseup');  // hides the menu +            return rcube_event.cancel(e); +          }          });      }    }; @@ -3787,7 +3942,7 @@ function rcube_webmail()        // cleanup        rx = new RegExp(rx_delim + '\\s*' + rx_delim, 'g'); -      input_val = input_val.replace(rx, delim); +      input_val = String(input_val).replace(rx, delim);        rx = new RegExp('^[\\s' + rx_delim + ']+');        input_val = input_val.replace(rx, ''); @@ -4155,7 +4310,7 @@ function rcube_webmail()            return;          var dir = key == 38 ? 1 : 0, -          highlight = document.getElementById('rcmksearchSelected'); +          highlight = document.getElementById('rcmkSearchItem' + this.ksearch_selected);          if (!highlight)            highlight = this.ksearch_pane.__ul.firstChild; @@ -4204,14 +4359,14 @@ function rcube_webmail()    this.ksearch_select = function(node)    { -    var current = $('#rcmksearchSelected'); -    if (current[0] && node) { -      current.removeAttr('id').removeClass('selected'); +    if (this.ksearch_pane && node) { +      this.ksearch_pane.find('li.selected').removeClass('selected').removeAttr('aria-selected');      }      if (node) { -      $(node).attr('id', 'rcmksearchSelected').addClass('selected'); +      $(node).addClass('selected').attr('aria-selected', 'true');        this.ksearch_selected = node._rcm_id; +      $(this.ksearch_input).attr('aria-activedescendant', 'rcmkSearchItem' + this.ksearch_selected);      }    }; @@ -4340,16 +4495,20 @@ function rcube_webmail()        return;      // display search results -    var i, len, ul, li, text, type, init, +    var i, id, len, ul, text, type, init,        value = this.ksearch_value,        maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15;      // create results pane if not present      if (!this.ksearch_pane) {        ul = $('<ul>'); -      this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane') +      this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane').attr('role', 'listbox')          .css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);        this.ksearch_pane.__ul = ul[0]; + +      // register (delegate) event handlers +      ul.on('mouseover', 'li', function(e){ ref.ksearch_select(e.target); }) +        .on('onmouseup', 'li', function(e){ ref.ksearch_click(e.target); })      }      ul = this.ksearch_pane.__ul; @@ -4374,23 +4533,29 @@ function rcube_webmail()        for (i=0; i < len && maxlen > 0; i++) {          text = typeof results[i] === 'object' ? results[i].name : results[i];          type = typeof results[i] === 'object' ? results[i].type : ''; -        li = document.createElement('LI'); -        li.innerHTML = text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%').replace(/</g, '<').replace(/>/g, '>').replace(/##([^%]+)%%/g, '<b>$1</b>'); -        li.onmouseover = function(){ ref.ksearch_select(this); }; -        li.onmouseup = function(){ ref.ksearch_click(this) }; -        li._rcm_id = this.env.contacts.length + i; -        if (type) li.className = type; -        ul.appendChild(li); +        id = i + this.env.contacts.length; +        $('<li>').attr('id', 'rcmkSearchItem' + id) +          .attr('role', 'option') +          .html(this.quote_html(text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '<b>$1</b>')) +          .addClass(type || '') +          .appendTo(ul) +          .get(0)._rcm_id = id;          maxlen -= 1;        }      }      if (ul.childNodes.length) { +      // set the right aria-* attributes to the input field +      $(this.ksearch_input) +        .attr('aria-haspopup', 'true') +        .attr('aria-expanded', 'true') +        .attr('aria-owns', 'rcmKSearchpane'); +        this.ksearch_pane.show(); +        // select the first        if (!this.env.contacts.length) { -        $('li:first', ul).attr('id', 'rcmksearchSelected').addClass('selected'); -        this.ksearch_selected = 0; +        this.ksearch_select($('li:first', ul).get(0));        }      } @@ -4427,6 +4592,12 @@ function rcube_webmail()      if (this.ksearch_pane)        this.ksearch_pane.hide(); +    $(this.ksearch_input) +      .attr('aria-haspopup', 'false') +      .attr('aria-expanded', 'false') +      .removeAttr('aria-activedescendant') +      .removeAttr('aria-owns'); +      this.ksearch_destroy();    }; @@ -4631,6 +4802,7 @@ function rcube_webmail()        // add link to pop back to parent group        if (this.env.address_group_stack.length > 1) {          $('<a href="#list">...</a>') +          .attr('title', this.gettext('uponelevel'))            .addClass('poplink')            .appendTo(boxtitle)            .click(function(e){ return ref.command('popgroup','',this); }); @@ -5174,6 +5346,7 @@ function rcube_webmail()        if (appendcontainer.length && appendcontainer.get(0).nodeName == 'FIELDSET') {          var input, colprop = this.env.coltypes[col], +          input_id = 'ff_' + col + (colprop.count || 0),            row = $('<div>').addClass('row'),            cell = $('<div>').addClass('contactfieldcontent data'),            label = $('<div>').addClass('contactfieldlabel label'); @@ -5181,14 +5354,14 @@ function rcube_webmail()          if (colprop.subtypes_select)            label.html(colprop.subtypes_select);          else -          label.html(colprop.label); +          label.html('<label for="' + input_id + '">' + colprop.label + '</label>');          var name_suffix = colprop.limit != 1 ? '[]' : '';          if (colprop.type == 'text' || colprop.type == 'date') {            input = $('<input>')              .addClass('ff_'+col) -            .attr({type: 'text', name: '_'+col+name_suffix, size: colprop.size}) +            .attr({type: 'text', name: '_'+col+name_suffix, size: colprop.size, id: input_id})              .appendTo(cell);            this.init_edit_field(col, input); @@ -5199,7 +5372,7 @@ function rcube_webmail()          else if (colprop.type == 'textarea') {            input = $('<textarea>')              .addClass('ff_'+col) -            .attr({ name: '_'+col+name_suffix, cols:colprop.size, rows:colprop.rows }) +            .attr({ name: '_'+col+name_suffix, cols:colprop.size, rows:colprop.rows, id: input_id })              .appendTo(cell);            this.init_edit_field(col, input); @@ -5235,7 +5408,7 @@ function rcube_webmail()          else if (colprop.type == 'select') {            input = $('<select>')              .addClass('ff_'+col) -            .attr('name', '_'+col+name_suffix) +            .attr({ 'name': '_'+col+name_suffix, id: input_id })              .appendTo(cell);            var options = input.attr('options'); @@ -5542,7 +5715,7 @@ function rcube_webmail()      this.last_sub_rx = RegExp('['+delim+']?[^'+delim+']+$');      this.subscription_list = new rcube_list_widget(this.gui_objects.subscriptionlist, -      {multiselect:false, draggable:true, keyboard:false, toggleselect:true}); +      {multiselect:false, draggable:true, keyboard:true, toggleselect:true});      this.subscription_list        .addEventListener('select', function(o){ ref.subscription_select(o); })        .addEventListener('dragstart', function(o){ ref.drag_active = true; }) @@ -5551,7 +5724,8 @@ function rcube_webmail()          row.obj.onmouseover = function() { ref.focus_subscription(row.id); };          row.obj.onmouseout = function() { ref.unfocus_subscription(row.id); };        }) -      .init(); +      .init() +      .focus();      $('#mailboxroot')        .mouseover(function(){ ref.focus_subscription(this.id); }) @@ -5976,15 +6150,12 @@ function rcube_webmail()          init_button(cmd, this.buttons[cmd][i]);        }      } - -    // set active task button -    this.set_button(this.task, 'sel');    };    // set button to a specific state    this.set_button = function(command, state)    { -    var n, button, obj, a_buttons = this.buttons[command], +    var n, button, obj, $obj, a_buttons = this.buttons[command],        len = a_buttons ? a_buttons.length : 0;      for (n=0; n<len; n++) { @@ -6019,8 +6190,15 @@ function rcube_webmail()          obj.disabled = state == 'pas';        }        else if (button.type == 'uibutton') { +        button.status = state;          $(obj).button('option', 'disabled', state == 'pas');        } +      else { +        $obj = $(obj); +        $obj +          .attr('tabindex', state == 'pas' || state == 'sel' ? '-1' : ($obj.attr('data-tabindex') || '0')) +          .attr('aria-disabled', state == 'pas' || state == 'sel' ? 'true' : 'false'); +      }      }    }; @@ -6144,7 +6322,8 @@ function rcube_webmail()        this.messages[key].labels = [{'id': id, 'msg': msg}];      }      else { -      obj.click(function() { return ref.hide_message(obj); }); +      obj.click(function() { return ref.hide_message(obj); }) +        .attr('role', 'alert');      }      this.triggerEvent('message', { message:msg, type:type, timeout:timeout, object:obj }); @@ -6274,10 +6453,8 @@ function rcube_webmail()        this.treelist.select(name);      }      else if (this.gui_objects.folderlist) { -      $('li.selected', this.gui_objects.folderlist) -        .removeClass('selected').addClass('unfocused'); -      $(this.get_folder_li(name, prefix, encode)) -        .removeClass('unfocused').addClass('selected'); +      $('li.selected', this.gui_objects.folderlist).removeClass('selected'); +      $(this.get_folder_li(name, prefix, encode)).addClass('selected');        // trigger event hook        this.triggerEvent('selectfolder', { folder:name, prefix:prefix }); @@ -6327,7 +6504,7 @@ function rcube_webmail()          tr = document.createElement('tr');          for (c=0, len=repl.length; c < len; c++) { -          cell = document.createElement('td'); +          cell = document.createElement('th');            cell.innerHTML = repl[c].html || '';            if (repl[c].id) cell.id = repl[c].id;            if (repl[c].className) cell.className = repl[c].className; @@ -6511,17 +6688,15 @@ function rcube_webmail()    };    // create folder selector popup, position and display it -  this.folder_selector = function(obj, callback) +  this.folder_selector = function(event, callback)    {      var container = this.folder_selector_element;      if (!container) {        var rows = [],          delim = this.env.delimiter, -        ul = $('<ul class="toolbarmenu iconized">'), -        li = document.createElement('li'), -        link = document.createElement('a'), -        span = document.createElement('span'); +        ul = $('<ul class="toolbarmenu">'), +        link = document.createElement('a');        container = $('<div id="folder-selector" class="popupmenu"></div>');        link.href = '#'; @@ -6529,33 +6704,30 @@ function rcube_webmail()        // loop over sorted folders list        $.each(this.env.mailboxes_list, function() { -        var tmp, n = 0, s = 0, +        var n = 0, s = 0,            folder = ref.env.mailboxes[this],            id = folder.id, -          a = link.cloneNode(false), row = li.cloneNode(false); +          a = $(link.cloneNode(false)), +          row = $('<li>');          if (folder.virtual) -          a.className += ' virtual'; -        else { -          a.className += ' active'; -          a.onclick = function() { container.hide().data('callback')(folder.id); }; -        } +          a.addClass('virtual').attr('aria-disabled', 'true').attr('tabindex', '-1'); +        else +          a.addClass('active').data('id', folder.id);          if (folder['class']) -          a.className += ' ' + folder['class']; +          a.addClass(folder['class']);          // calculate/set indentation level          while ((s = id.indexOf(delim, s)) >= 0) {            n++; s++;          } -        a.style.paddingLeft =  n ? (n * 16) + 'px' : 0; +        a.css('padding-left', n ? (n * 16) + 'px' : 0);          // add folder name element -        tmp = span.cloneNode(false); -        $(tmp).text(folder.name); -        a.appendChild(tmp); +        a.append($('<span>').text(folder.name)); -        row.appendChild(a); +        row.append(a);          rows.push(row);        }); @@ -6567,23 +6739,157 @@ function rcube_webmail()        // set max-height if the list is long        if (rows.length > 10) -        container.css('max-height', $('li', container)[0].offsetHeight * 10 + 9) +        container.css('max-height', $('li', container)[0].offsetHeight * 10 + 9); -      // hide selector on click out of selector element -      var fn = function(e) { if (e.target != container.get(0)) container.hide(); }; -      $(document.body).on('mouseup', fn); -      $('iframe').contents().on('mouseup', fn) -        .load(function(e) { try { $(this).contents().on('mouseup', fn); } catch(e) {}; }); +      // register delegate event handler for folder item clicks +      container.on('click', 'a.active', function(e){ +        container.data('callback')($(this).data('id')); +        return false; +      });        this.folder_selector_element = container;      } +    container.data('callback', callback); +      // position menu on the screen -    this.element_position(container, obj); +    this.show_menu('folder-selector', true, event); +  }; + + +  /***********************************************/ +  /*********    popup menu functions     *********/ +  /***********************************************/ + +  // Show/hide a specific popup menu +  this.show_menu = function(prop, show, event) +  { +    var name = typeof prop == 'object' ? prop.menu : prop, +      obj = $('#'+name), +      ref = event && event.target ? $(event.target) : $(obj.attr('rel') || '#'+name+'link'), +      keyboard = rcube_event.is_keyboard(event), +      align = obj.attr('data-align') || '', +      stack = false; + +    // find "real" button element +    if (ref.get(0).tagName != 'A' && ref.closest('a').length) +      ref = ref.closest('a'); + +    if (typeof prop == 'string') +      prop = { menu:name }; + +    // let plugins or skins provide the menu element +    if (!obj.length) { +      obj = this.triggerEvent('menu-get', { name:name, props:prop, originalEvent:event }); +    } + +    if (!obj || !obj.length) { +      // just delegate the action to subscribers +      return this.triggerEvent(show === false ? 'menu-close' : 'menu-open', { name:name, props:prop, originalEvent:event }); +    } + +    // move element to top for proper absolute positioning +    obj.appendTo(document.body); + +    if (typeof show == 'undefined') +      show = obj.is(':visible') ? false : true; + +    if (show && ref.length) { +      var win = $(window), +        pos = ref.offset(), +        above = align.indexOf('bottom') >= 0; + +      stack = ref.attr('role') == 'menuitem' || ref.closest('[role=menuitem]').length > 0; -    container.show().data('callback', callback); +      ref.offsetWidth = ref.outerWidth(); +      ref.offsetHeight = ref.outerHeight(); +      if (!above && pos.top + ref.offsetHeight + obj.height() > win.height()) { +        above = true; +      } +      if (align.indexOf('right') >= 0) { +        pos.left = pos.left + ref.outerWidth() - obj.width(); +      } +      else if (stack) { +        pos.left = pos.left + ref.offsetWidth - 5; +        pos.top -= ref.offsetHeight; +      } +      if (pos.left + obj.width() > win.width()) { +        pos.left = win.width() - obj.width() - 12; +      } +      pos.top = Math.max(0, pos.top + (above ? -obj.height() : ref.offsetHeight)); +      obj.css({ left:pos.left+'px', top:pos.top+'px' }); +    } + +    // add menu to stack +    if (show) { +      // truncate stack down to the one containing the ref link +      for (var i = this.menu_stack.length - 1; stack && i >= 0; i--) { +        if (!$(ref).parents('#'+this.menu_stack[i]).length) +          this.hide_menu(this.menu_stack[i]); +      } +      if (stack && this.menu_stack.length) { +        obj.data('parent', $.last(this.menu_stack)); +        obj.css('z-index', ($('#'+$.last(this.menu_stack)).css('z-index') || 0) + 1); +      } +      else if (!stack && this.menu_stack.length) { +        this.hide_menu(this.menu_stack[0], event); +      } + +      obj.show().attr('aria-hidden', 'false').data('opener', ref.attr('aria-expanded', 'true').get(0)); +      this.triggerEvent('menu-open', { name:name, obj:obj, props:prop, originalEvent:event }); +      this.menu_stack.push(name); + +      this.menu_keyboard_active = show && keyboard; +      if (this.menu_keyboard_active) { +        this.focused_menu = name; +        obj.find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus(); +      } +    } +    else {  // close menu +      this.hide_menu(name, event); +    } + +    return show;    }; +  // hide the given popup menu (and it's childs) +  this.hide_menu = function(name, event) +  { +    if (!this.menu_stack.length) { +      // delegate to subscribers +      this.triggerEvent('menu-close', { name:name, props:{ menu:name }, originalEvent:event }); +      return; +    } + +    var obj, keyboard = rcube_event.is_keyboard(event); +    for (var j=this.menu_stack.length-1; j >= 0; j--) { +      obj = $('#' + this.menu_stack[j]).hide().attr('aria-hidden', 'true').data('parent', false); +      this.triggerEvent('menu-close', { name:this.menu_stack[j], obj:obj, props:{ menu:this.menu_stack[j] }, originalEvent:event }); +      if (this.menu_stack[j] == name) { +        j = -1;  // stop loop +        if (obj.data('opener')) { +          $(obj.data('opener')).attr('aria-expanded', 'false'); +          if (keyboard) +            obj.data('opener').focus(); +        } +      } +      this.menu_stack.pop(); +    } + +    // focus previous menu in stack +    if (this.menu_stack.length && keyboard) { +      this.menu_keyboard_active = true; +      this.focused_menu = $.last(this.menu_stack); +      if (!obj || !obj.data('opener')) +        $('#'+this.focused_menu).find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus(); +    } +    else { +      this.focused_menu = null; +      this.menu_keyboard_active = false; +    } +  } + +    // position a menu element on the screen in relation to other object    this.element_position = function(element, obj)    { @@ -6753,6 +7059,13 @@ function rcube_webmail()      this.start_keepalive();    }; +  // update browser location to remember current view +  this.update_state = function(query) +  { +    if (window.history.replaceState) +      window.history.replaceState({}, document.title, rcmail.url('', query)); +  }; +    // send a http request to the server    this.http_request = function(action, query, lock)    { @@ -6938,6 +7251,8 @@ function rcube_webmail()            if ((response.action == 'list' || response.action == 'search') && this.message_list) {              this.enable_command('set-listmode', this.env.threads && !is_multifolder); +            if (this.message_list.rowcount > 0) +              this.message_list.focus();              this.msglist_select(this.message_list);              this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount });            } @@ -6949,10 +7264,18 @@ function rcube_webmail()              this.enable_command('search-create', this.env.source == '');              this.enable_command('search-delete', this.env.search_id);              this.update_group_commands(); +            if (this.contact_list.rowcount > 0) +              this.contact_list.focus();              this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });            }          }          break; + +      case 'list-contacts': +      case 'search-contacts': +        if (this.contact_list && this.contact_list.rowcount > 0) +          this.contact_list.focus(); +        break;      }      if (response.unlock) diff --git a/program/js/common.js b/program/js/common.js index 48e85558f..5ac4febce 100644 --- a/program/js/common.js +++ b/program/js/common.js @@ -281,6 +281,25 @@ cancel: function(evt)    return false;  }, +/** + * Determine whether the given event was trigered from keyboard + */ +is_keyboard: function(e) +{ +  return e && ( +      (e.mozInputSource && e.mozInputSource == e.MOZ_SOURCE_KEYBOARD) || +      (!e.pageX && (e.pageY || 0) <= 0 && !e.clientX && (e.clientY || 0) <= 0) +    ); +}, + +/** + * Accept event if triggered from keyboard action (e.g. <Enter>) + */ +keyboard_only: function(e) +{ +  return rcube_event.is_keyboard(e) ? true : rcube_event.cancel(e); +}, +  touchevent: function(e)  {    return { pageX:e.pageX, pageY:e.pageY, offsetX:e.pageX - e.target.offsetLeft, offsetY:e.pageY - e.target.offsetTop, target:e.target, istouch:true }; @@ -593,6 +612,11 @@ if (!String.prototype.startsWith) {    };  } +// array utility function +jQuery.last = function(arr) { +  return arr && arr.length ? arr[arr.length-1] : undefined; +} +  // jQuery plugin to emulate HTML5 placeholder attributes on input elements  jQuery.fn.placeholder = function(text) {    return this.each(function() { diff --git a/program/js/list.js b/program/js/list.js index 04aec1c99..5492c0ad4 100644 --- a/program/js/list.js +++ b/program/js/list.js @@ -51,7 +51,7 @@ function rcube_list_widget(list, p)    this.rowcount = 0;    this.colcount = 0; -  this.subject_col = -1; +  this.subject_col = 0;    this.modkey = 0;    this.multiselect = false;    this.multiexpand = false; @@ -60,6 +60,7 @@ function rcube_list_widget(list, p)    this.column_movable = false;    this.keyboard = false;    this.toggleselect = false; +  this.aria_listbox = false;    this.drag_active = false;    this.col_drag_active = false; @@ -75,6 +76,9 @@ function rcube_list_widget(list, p)    if (p && typeof p === 'object')      for (var n in p)        this[n] = p[n]; + +  // register this instance +  rcube_list_widget._instances.push(this);  }; @@ -94,11 +98,17 @@ init: function()      this.tbody = this.list;    } +  if ($(this.list).attr('role') == 'listbox') { +    this.aria_listbox = true; +    if (this.multiselect) +      $(this.list).attr('aria-multiselectable', 'true'); +  } +    if (this.tbody) {      this.rows = {};      this.rowcount = 0; -    var r, len, rows = this.tbody.childNodes; +    var r, len, rows = this.tbody.childNodes, me = this;      for (r=0, len=rows.length; r<len; r++) {        this.rowcount += this.init_row(rows[r]) ? 1 : 0; @@ -108,8 +118,13 @@ init: function()      this.frame = this.list.parentNode;      // set body events -    if (this.keyboard) +    if (this.keyboard) {        rcube_event.add_listener({event:'keydown', object:this, method:'key_press'}); + +      // allow the table element to receive focus. +      $(this.list).attr('tabindex', '0') +        .on('focus', function(e){ me.focus(e); }); +    }    }    return this; @@ -154,6 +169,15 @@ init_row: function(row)        }, false);      } +    // label the list row with the subject col as descriptive label +    if (this.aria_listbox) { +      var lbl_id = 'l:' + row.id; +      $(row) +        .attr('role', 'option') +        .attr('aria-labelledby', lbl_id) +        .find(this.col_tagname()).eq(this.subject_col).attr('id', lbl_id); +    } +      if (document.all)        row.onselectstart = function() { return false; }; @@ -175,7 +199,7 @@ init_header: function()      if (this.fixed_header) {  // copy (modified) fixed header back to the actual table        $(this.list.tHead).replaceWith($(this.fixed_header).find('thead').clone()); -      $(this.list.tHead).find('tr td').attr('style', '');  // remove fixed widths +      $(this.list.tHead).find('th,td').attr('style', '').find('a').attr('tabindex', '-1');  // remove fixed widths      }      else if (!bw.touch && this.list.className.indexOf('fixedheader') >= 0) {        this.init_fixed_header(); @@ -202,6 +226,7 @@ init_fixed_header: function()    if (!this.fixed_header) {      this.fixed_header = $('<table>')        .attr('class', this.list.className + ' fixedcopy') +      .attr('role', 'presentation')        .css({ position:'fixed' })        .append(clone)        .append('<tbody></tbody>'); @@ -220,6 +245,12 @@ init_fixed_header: function()      $(this.fixed_header).find('thead').replaceWith(clone);    } +  // avoid scrolling header links being focused +  $(this.list.tHead).find('a.sortcol').attr('tabindex', '-1'); + +  // set tabindex to fixed header sort links +  clone.find('a.sortcol').attr('tabindex', '0'); +    this.thead = clone.get(0);    this.resize();  }, @@ -262,6 +293,7 @@ clear: function(sel)    this.rows = {};    this.rowcount = 0; +  this.last_selected = 0;    if (sel)      this.clear_selection(); @@ -370,41 +402,68 @@ update_row: function(id, cols, newid, select)   */  focus: function(e)  { -  var n, id; +  if (this.focused) +    return; +    this.focused = true; -  for (n in this.selection) { -    id = this.selection[n]; -    if (this.rows[id] && this.rows[id].obj) { -      $(this.rows[id].obj).addClass('selected').removeClass('unfocused'); -    } +  if (e) +    rcube_event.cancel(e); + +  var focus_elem = null; + +  if (this.last_selected && this.rows[this.last_selected]) { +    focus_elem = $(this.rows[this.last_selected].obj).find(this.col_tagname()).eq(this.subject_col).attr('tabindex', '0');    }    // Un-focus already focused elements (#1487123, #1487316, #1488600, #1488620) -  // It looks that window.focus() does the job for all browsers, but not Firefox (#1489058) -  $('iframe,:focus:not(body)').blur(); -  window.focus(); +  if (focus_elem && focus_elem.length) { +    // We now fix this by explicitly assigning focus to a dedicated link element +    this.focus_noscroll(focus_elem); +  } +  else { +    // It looks that window.focus() does the job for all browsers, but not Firefox (#1489058) +    $('iframe,:focus:not(body)').blur(); +    window.focus(); +  } -  if (e || (e = window.event)) -    rcube_event.cancel(e); +  $(this.list).addClass('focus').removeAttr('tabindex'); + +  // set internal focus pointer to first row +  if (!this.last_selected) +    this.select_first(CONTROL_KEY);  },  /**   * remove focus from the list   */ -blur: function() +blur: function(e)  { -  var n, id;    this.focused = false; -  for (n in this.selection) { -    id = this.selection[n]; -    if (this.rows[id] && this.rows[id].obj) { -      $(this.rows[id].obj).removeClass('selected focused').addClass('unfocused'); -    } + +  // avoid the table getting focus right again +  var me = this; +  setTimeout(function(){ +    $(me.list).removeClass('focus').attr('tabindex', '0'); +  }, 20); + +  if (this.last_selected && this.rows[this.last_selected]) { +    $(this.rows[this.last_selected].obj) +      .find(this.col_tagname()).eq(this.subject_col).removeAttr('tabindex');    }  }, +/** + * Focus the given element without scrolling the list container + */ +focus_noscroll: function(elem) +{ +  var y = this.frame.scrollTop || this.frame.scrollY; +  elem.focus(); +  this.frame.scrollTop = y; +}, +  /**   * Set/unset the given column as hidden @@ -522,6 +581,8 @@ click_row: function(e, id)    }    this.rows[id].clicked = now; +  this.focus(); +    return false;  }, @@ -794,9 +855,9 @@ get_prev_row: function()  get_first_row: function()  {    if (this.rowcount) { -    var i, len, uid, rows = this.tbody.childNodes; +    var i, uid, rows = this.tbody.childNodes; -    for (i=0, len=rows.length-1; i<len; i++) +    for (i=0; i<rows.length; i++)        if (rows[i].id && (uid = this.get_row_uid(rows[i])))          return uid;    } @@ -839,9 +900,10 @@ get_cell: function(row, index)   */  select_row: function(id, mod_key, with_mouse)  { -  var select_before = this.selection.join(','); +  var select_before = this.selection.join(','), +    in_selection_before = this.in_selection(id); -  if (!this.multiselect) +  if (!this.multiselect && with_mouse)      mod_key = 0;    if (!this.shift_start) @@ -877,20 +939,26 @@ select_row: function(id, mod_key, with_mouse)      this.multi_selecting = true;    } -  // trigger event if selection changed -  if (this.selection.join(',') != select_before) -    this.triggerEvent('select'); - -  if (this.last_selected != 0 && this.rows[this.last_selected]) -    $(this.rows[this.last_selected].obj).removeClass('focused'); +  if (this.last_selected != 0 && this.rows[this.last_selected]) { +    $(this.rows[this.last_selected].obj).removeClass('focused') +      .find(this.col_tagname()).eq(this.subject_col).removeAttr('tabindex'); +  }    // unselect if toggleselect is active and the same row was clicked again -  if (this.toggleselect && this.last_selected == id) { +  if (this.toggleselect && in_selection_before) {      this.clear_selection(); -    id = null;    } -  else +  // trigger event if selection changed +  else if (this.selection.join(',') != select_before) { +    this.triggerEvent('select'); +  } + +  if (this.rows[id]) {      $(this.rows[id].obj).addClass('focused'); +    // set cursor focus to link inside selected row +    if (this.focused) +      this.focus_noscroll($(this.rows[id].obj).find(this.col_tagname()).eq(this.subject_col).attr('tabindex', '0')); +  }    if (!this.selection.length)      this.shift_start = null; @@ -1038,7 +1106,7 @@ select_all: function(filter)        this.highlight_row(n, true, true);      }      else { -      $(this.rows[n].obj).removeClass('selected').removeClass('unfocused'); +      $(this.rows[n].obj).removeClass('selected').removeAttr('aria-selected');      }    } @@ -1095,14 +1163,16 @@ clear_selection: function(id, no_event)    else {      for (n in this.selection)        if (this.rows[this.selection[n]]) { -        $(this.rows[this.selection[n]].obj).removeClass('selected').removeClass('unfocused'); +        $(this.rows[this.selection[n]].obj).removeClass('selected').removeAttr('aria-selected');        }      this.selection = [];    } -  if (num_select && !this.selection.length && !no_event) +  if (num_select && !this.selection.length && !no_event) {      this.triggerEvent('select'); +    this.last_selected = 0; +  }  }, @@ -1156,13 +1226,13 @@ highlight_row: function(id, multiple, norecur)      if (this.selection.length > 1 || !this.in_selection(id)) {        this.clear_selection(null, true);        this.selection[0] = id; -      $(this.rows[id].obj).addClass('selected'); +      $(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');      }    }    else {      if (!this.in_selection(id)) { // select row        this.selection.push(id); -      $(this.rows[id].obj).addClass('selected'); +      $(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');        if (!norecur && !this.rows[id].expanded)          this.highlight_children(id, true);      } @@ -1172,7 +1242,7 @@ highlight_row: function(id, multiple, norecur)          a_post = this.selection.slice(p+1, this.selection.length);        this.selection = a_pre.concat(a_post); -      $(this.rows[id].obj).removeClass('selected').removeClass('unfocused'); +      $(this.rows[id].obj).removeClass('selected').removeAttr('aria-selected');        if (!norecur && !this.rows[id].expanded)          this.highlight_children(id, false);      } @@ -1252,6 +1322,14 @@ key_press: function(e)        return rcube_event.cancel(e); +    case 9: // Tab +      this.blur(); +      break; + +    case 13: // Enter +      if (!this.selection.length) +        this.select_row(this.last_selected, mod_key, false); +      default:        this.key_pressed = keyCode;        this.modkey = mod_key; @@ -1311,9 +1389,17 @@ use_arrow_key: function(keyCode, mod_key)    }    if (new_row) { +    // simulate ctr-key if no rows are selected +    if (!mod_key && !this.selection.length) +      mod_key = CONTROL_KEY; +      this.select_row(new_row.uid, mod_key, false);      this.scrollto(new_row.uid);    } +  else if (!new_row && !selected_row) { +    // select the first row if none selected yet +    this.select_first(CONTROL_KEY); +  }    return false;  }, @@ -1708,3 +1794,6 @@ column_replace: function(from, to)  rcube_list_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;  rcube_list_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;  rcube_list_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent; + +// static +rcube_list_widget._instances = []; diff --git a/program/js/treelist.js b/program/js/treelist.js index 90eeeeae7..65f5fd4f4 100644 --- a/program/js/treelist.js +++ b/program/js/treelist.js @@ -55,6 +55,7 @@ function rcube_treelist_widget(node, p)      drag_active = false,      search_active = false,      last_search = '', +    has_focus = false,      box_coords = {},      item_coords = [],      autoexpand_timer, @@ -151,6 +152,19 @@ function rcube_treelist_widget(node, p)      })    } +  container.on('focusin', function(e){ +      // TODO: only accept focus on virtual nodes from keyboard events +      has_focus = true; +    }) +    .on('focusout', function(e){ +      has_focus = false; +    }); + +  container.attr('role', 'tree'); + +  $(document.body) +    .bind('keydown', keypress); +    /////// private methods @@ -201,13 +215,13 @@ function rcube_treelist_widget(node, p)    function select(id)    {      if (selection) { -      id2dom(selection).removeClass('selected'); +      id2dom(selection).removeClass('selected').removeAttr('aria-selected');        selection = null;      }      var li = id2dom(id);      if (li.length) { -      li.addClass('selected'); +      li.addClass('selected').attr('aria-selected', 'true');        selection = id;        // TODO: expand all parent nodes if collapsed        scroll_to_node(li); @@ -262,6 +276,7 @@ function rcube_treelist_widget(node, p)      // insert as child of an existing node      if (parent_node) { +      node.level = parent_node.level + 1;        if (!parent_node.children)          parent_node.children = []; @@ -296,6 +311,7 @@ function rcube_treelist_widget(node, p)      }      // insert at top level      else { +      node.level = 0;        data.push(node);        li = render_node(node, container);      } @@ -392,7 +408,7 @@ function rcube_treelist_widget(node, p)     */    function update_data()    { -    data = walk_list(container); +    data = walk_list(container, 0);    }    /** @@ -401,6 +417,7 @@ function rcube_treelist_widget(node, p)    function update_dom(node)    {      var li = id2dom(node.id); +    li.attr('aria-expanded', node.collapsed ? 'false' : 'true');      li.children('ul').first()[(node.collapsed ? 'hide' : 'show')]();      li.children('div.treetoggle').removeClass('collapsed expanded').addClass(node.collapsed ? 'collapsed' : 'expanded');      me.triggerEvent('toggle', node); @@ -507,6 +524,7 @@ function rcube_treelist_widget(node, p)      // render child nodes      for (var i=0; i < data.length; i++) { +      data[i].level = 0;        render_node(data[i], container);      } @@ -523,6 +541,7 @@ function rcube_treelist_widget(node, p)      var li = $('<li>')        .attr('id', p.id_prefix + (p.id_encode ? p.id_encode(node.id) : node.id)) +      .attr('role', 'treeitem')        .addClass((node.classes || []).join(' '))        .data('id', node.id); @@ -546,12 +565,14 @@ function rcube_treelist_widget(node, p)      // add child list and toggle icon      if (node.children && node.children.length) { +      li.attr('aria-expanded', node.collapsed ? 'false' : 'true');        $('<div class="treetoggle '+(node.collapsed ? 'collapsed' : 'expanded') + '"> </div>').appendTo(li); -      var ul = $('<ul>').appendTo(li).attr('class', node.childlistclass); +      var ul = $('<ul>').appendTo(li).attr('class', node.childlistclass).attr('role', 'group');        if (node.collapsed)          ul.hide();        for (var i=0; i < node.children.length; i++) { +        node.children[i].level = node.level + 1;          render_node(node.children[i], ul);        }      } @@ -563,7 +584,7 @@ function rcube_treelist_widget(node, p)     * Recursively walk the DOM tree and build an internal data structure     * representing the skeleton of this tree list.     */ -  function walk_list(ul) +  function walk_list(ul, level)    {      var result = [];      ul.children('li').each(function(i,e){ @@ -572,9 +593,10 @@ function rcube_treelist_widget(node, p)          id: dom2id(li),          classes: String(li.attr('class')).split(' '),          virtual: li.hasClass('virtual'), +        level: level,          html: li.children().first().get(0).outerHTML,          text: li.children().first().text(), -        children: walk_list(sublist) +        children: walk_list(sublist, level+1)        }        if (sublist.length) { @@ -592,15 +614,29 @@ function rcube_treelist_widget(node, p)          if (!li.children('div.treetoggle').length)            $('<div class="treetoggle '+(node.collapsed ? 'collapsed' : 'expanded') + '"> </div>').appendTo(li); + +        li.attr('aria-expanded', node.collapsed ? 'false' : 'true');        }        if (li.hasClass('selected')) { +        li.attr('aria-selected', 'true');          selection = node.id;        }        li.data('id', node.id); + +      // declare list item as treeitem +      li.attr('role', 'treeitem').attr('aria-level', node.level+1); + +      // allow virtual nodes to receive focus +      if (node.virtual) { +        li.children('a:first').attr('tabindex', '0'); +      } +        result.push(node);        indexbyid[node.id] = node; -    }) +    }); + +    ul.attr('role', level == 0 ? 'tree' : 'group');      return result;    } @@ -683,6 +719,70 @@ function rcube_treelist_widget(node, p)      return undefined;    } +  /** +   * Handler for keyboard events on treelist +   */ +  function keypress(e) +  { +    var target = e.target || {}, +      keyCode = rcube_event.get_keycode(e); + +    if (!has_focus || target.nodeName == 'INPUT' || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT') +      return true; + +    switch (keyCode) { +      case 38: +      case 40: +      case 63232: // 'up', in safari keypress +      case 63233: // 'down', in safari keypress +        var li = container.find(':focus').closest('li'); +        if (li.length) { +          focus_next(li, (mod = keyCode == 38 || keyCode == 63232 ? -1 : 1)); +        } +        break; + +      case 37: // Left arrow key +      case 39: // Right arrow key +        var id, node, li = container.find(':focus').closest('li'); +        if (li.length) { +          id = dom2id(li); +          node = indexbyid[id]; +          if (node && node.children.length) +            toggle(id, rcube_event.get_modifier(e) == SHIFT_KEY);  // toggle subtree +        } +        return false; +    } + +    return true; +  } + +  function focus_next(li, dir, from_child) +  { +    var mod = dir < 0 ? 'prev' : 'next', +      next = li[mod](), limit, parent; + +    if (dir > 0 && !from_child && li.children('ul[role=group]:visible').length) { +      li.children('ul').children('li:first').children('a:first').focus(); +    } +    else if (dir < 0 && !from_child && next.children('ul[role=group]:visible').length) { +      next.children('ul').children('li:last').children('a:last').focus(); +    } +    else if (next.length && next.children('a:first')) { +      next.children('a:first').focus(); +    } +    else { +      parent = li.parent().closest('li[role=treeitem]'); +      if (parent.length) +        if (dir < 0) { +          parent.children('a:first').focus(); +        } +        else { +          focus_next(parent, dir, true); +        } +    } +>>>>>>> dev-accessibility +  } +    ///// drag & drop support diff --git a/program/lib/Roundcube/html.php b/program/lib/Roundcube/html.php index a88570d75..0209d1bf2 100644 --- a/program/lib/Roundcube/html.php +++ b/program/lib/Roundcube/html.php @@ -32,7 +32,7 @@ class html      public static $doctype = 'xhtml';      public static $lc_tags = true; -    public static $common_attrib = array('id','class','style','title','align','unselectable'); +    public static $common_attrib = array('id','class','style','title','align','unselectable','tabindex','role');      public static $containers = array('iframe','div','span','p','h1','h2','h3','ul','form','textarea','table','thead','tbody','tr','th','td','style','script'); @@ -285,7 +285,9 @@ class html              // ignore not allowed attributes, except data-*              if (!empty($allowed)) { -                if (!isset($allowed_f[$key]) && @substr_compare($key, 'data-', 0, 5) !== 0) { +                $is_data_attr = @substr_compare($key, 'data-', 0, 5) === 0; +                $is_aria_attr = @substr_compare($key, 'aria-', 0, 5) === 0; +                if (!$is_aria_attr && !isset($allowed_f[$key]) && (!$is_data_attr || !isset($allowed_f['data-*']))) {                      continue;                  }              } @@ -835,7 +837,7 @@ class html_table extends html          if (!empty($this->header)) {              $rowcontent = '';              foreach ($this->header as $c => $col) { -                $rowcontent .= self::tag($this->_col_tagname(), $col->attrib, $col->content); +                $rowcontent .= self::tag($this->_head_tagname(), $col->attrib, $col->content);              }              $thead = $this->tagname == 'table' ? self::tag('thead', null, self::tag('tr', null, $rowcontent, parent::$common_attrib)) :                  self::tag($this->_row_tagname(), array('class' => 'thead'), $rowcontent, parent::$common_attrib); @@ -888,7 +890,16 @@ class html_table extends html      private function _row_tagname()      {          static $row_tagnames = array('table' => 'tr', 'ul' => 'li', '*' => 'div'); -        return $row_tagnames[$this->tagname] ? $row_tagnames[$this->tagname] : $row_tagnames['*']; +        return $row_tagnames[$this->tagname] ?: $row_tagnames['*']; +    } + +    /** +     * Getter for the corresponding tag name for table row elements +     */ +    private function _head_tagname() +    { +        static $head_tagnames = array('table' => 'th', '*' => 'span'); +        return $head_tagnames[$this->tagname] ?: $head_tagnames['*'];      }      /** @@ -897,7 +908,7 @@ class html_table extends html      private function _col_tagname()      {          static $col_tagnames = array('table' => 'td', '*' => 'span'); -        return $col_tagnames[$this->tagname] ? $col_tagnames[$this->tagname] : $col_tagnames['*']; +        return $col_tagnames[$this->tagname] ?: $col_tagnames['*'];      }  } diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index 2da99105f..e7d385fe6 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -157,16 +157,24 @@ $labels['more']             = 'More';  $labels['back']             = 'Back';  $labels['options']          = 'Options'; +$labels['first'] = 'First'; +$labels['last'] = 'Last'; +$labels['previous'] = 'Previous'; +$labels['next'] = 'Next';  $labels['select'] = 'Select';  $labels['all'] = 'All';  $labels['none'] = 'None';  $labels['currpage'] = 'Current page'; +$labels['isread'] = 'Read';  $labels['unread'] = 'Unread';  $labels['flagged'] = 'Flagged'; +$labels['unflagged'] = 'Not Flagged';  $labels['unanswered'] = 'Unanswered';  $labels['withattachment'] = 'With attachment';  $labels['deleted'] = 'Deleted';  $labels['undeleted'] = 'Not deleted'; +$labels['replied'] = 'Replied'; +$labels['forwarded'] = 'Forwarded';  $labels['invert'] = 'Invert';  $labels['filter'] = 'Filter';  $labels['list'] = 'List'; @@ -259,6 +267,7 @@ $labels['upload'] = 'Upload';  $labels['uploadprogress'] = '$percent ($current from $total)';  $labels['close']  = 'Close';  $labels['messageoptions']  = 'Message options...'; +$labels['togglecomposeoptions'] = 'Toggle composition options';  $labels['low']     = 'Low';  $labels['lowest']  = 'Lowest'; @@ -348,7 +357,9 @@ $labels['addcontact'] = 'Add new contact';  $labels['editcontact'] = 'Edit contact';  $labels['contacts'] = 'Contacts';  $labels['contactproperties'] = 'Contact properties'; +$labels['contactnameandorg'] = 'Name and Organization';  $labels['personalinfo'] = 'Personal information'; +$labels['contactphoto'] = 'Contact photo';  $labels['edit']   = 'Edit';  $labels['cancel'] = 'Cancel'; @@ -372,6 +383,7 @@ $labels['newcontactgroup'] = 'Create new contact group';  $labels['grouprename']    = 'Rename group';  $labels['groupdelete']    = 'Delete group';  $labels['groupremoveselected'] = 'Remove selected contacts from group'; +$labels['uponelevel']     = 'Up one level';  $labels['previouspage']   = 'Show previous page';  $labels['firstpage']      = 'Show first page'; @@ -468,6 +480,7 @@ $labels['miscfolding'] = 'RFC 2047/2231 (MS Outlook)';  $labels['2047folding'] = 'Full RFC 2047 (other)';  $labels['force7bit'] = 'Use MIME encoding for 8-bit characters';  $labels['advancedoptions'] = 'Advanced options'; +$labels['toggleadvancedoptions'] = 'Toggle advanced options';  $labels['focusonnewmessage'] = 'Focus browser window on new message';  $labels['checkallfolders'] = 'Check all folders for new messages';  $labels['displaynext'] = 'After message delete/move display the next message'; @@ -569,4 +582,48 @@ $labels['japanese'] = 'Japanese';  $labels['korean'] = 'Korean';  $labels['chinese'] = 'Chinese'; +// accessibility (voice-only) headings and descriptions +$labels['arialabeltopnav'] = 'Window control'; +$labels['arialabeltasknav'] = 'Application tasks'; +$labels['arialabeltoolbar'] = 'Application toolbar'; +$labels['arialabelmessagessearchfilter'] = 'Email listing filter'; +$labels['arialabelmailsearchform'] = 'Email message search form'; +$labels['arialabelcontactsearchform'] = 'Contacts search form'; +$labels['arialabelmailquicksearchbox'] = 'Email search input'; +$labels['arialabelquicksearchbox'] = 'Search input'; +$labels['arialabelfolderlist'] = 'Email folder selection'; +$labels['arialabelmessagelist'] = 'Email Messages Listing'; +$labels['arialabelmailpreviewframe'] = 'Message preview'; +$labels['arialabelmailboxmenu'] = 'Folder actions menu'; +$labels['arialabellistselectmenu'] = 'List selection menu'; +$labels['arialabelthreadselectmenu'] = 'Threads listing menu'; +$labels['arialabelmessagelistoptions'] = 'Message list display and sorting options'; +$labels['arialabelmailimportdialog'] = 'Message import dialog'; +$labels['arialabelmessagenav'] = 'Message navigation'; +$labels['arialabelmessagebody'] = 'Message Body'; +$labels['arialabelmessageactions'] = 'Message actions'; +$labels['arialabelcontactquicksearch'] = 'Contacts search form'; +$labels['arialabelcontactsearchbox'] = 'Contact search input'; +$labels['arialabelmessageheaders'] = 'Message headers'; +$labels['arialabelcomposeoptions'] = 'Composition options'; +$labels['arialabelresponsesmenu'] = 'Canned responses menu'; +$labels['arialabelattachmentuploadform'] = 'Attachment upload form'; +$labels['arialabelattachmentpreview'] = 'Attachment preview'; +$labels['ariasummarycomposecontacts'] = 'List of contacts and groups to select as recipients'; +$labels['arialabelcontactexportoptions'] = 'Contact export options'; +$labels['arialabelabookgroupoptions'] = 'Addressbook/group options'; +$labels['arialabelpreferencesform'] = 'Preferences form'; +$labels['arialabelidentityeditfrom'] = 'Identity edit form'; +$labels['arialabelresonseeditfrom'] = 'Response edit form'; + +$labels['helplistnavigation'] = 'List keyboard navigation'; +$labels['helplistkeyboardnavigation'] = "Arrows up/down: Move row focus/selection. +Space: Select focused row. +Shift + up/down: Select additional row above/below. +Ctrl + Space: Add focused row to selection/remove from selection."; +$labels['helplistkeyboardnavmessages'] = "Arrows right/left: expand/collapse message thread (in threads mode only). +Enter: Open the selected/focused message. +Delete: Move selected messages to Trash."; +$labels['helplistkeyboardnavcontacts'] = "Enter: Open the selected/focused contact."; +  ?> diff --git a/program/steps/addressbook/edit.inc b/program/steps/addressbook/edit.inc index 3bbbfccdf..0fc753056 100644 --- a/program/steps/addressbook/edit.inc +++ b/program/steps/addressbook/edit.inc @@ -98,12 +98,15 @@ function rcmail_get_edit_record()  function rcmail_contact_edithead($attrib)  { +    global $RCMAIL; +      // check if we have a valid result      $record = rcmail_get_edit_record();      $i_size = !empty($attrib['size']) ? $attrib['size'] : 20;      $form = array(          'head' => array( +            'name' => $RCMAIL->gettext('contactnameandorg'),              'content' => array(                  'prefix' => array('size' => $i_size),                  'firstname' => array('size' => $i_size, 'visible' => true), diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index be0dd2a33..8955488bd 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -395,7 +395,7 @@ function rcmail_js_contacts_list($result, $prefix='')                          ), '»');                      }                      else -                        $val = ' '; +                        $val = '';                      break;                  default: @@ -422,7 +422,7 @@ function rcmail_contacts_list_title($attrib)      unset($attrib['name']);      $OUTPUT->add_gui_object('addresslist_title', $attrib['id']); -    $OUTPUT->add_label('contacts'); +    $OUTPUT->add_label('contacts','uponelevel');      return html::tag($attrib['tag'], $attrib, $RCMAIL->gettext($attrib['label']), html::$common_attrib);  } @@ -518,7 +518,7 @@ function rcmail_contact_form($form, $record, $attrib = null)      foreach ($coltypes as $col => $prop) {          if ($prop['subtypes']) {              $subtype_names = array_map('rcmail_get_type_label', $prop['subtypes']); -            $select_subtype = new html_select(array('name' => '_subtype_'.$col.'[]', 'class' => 'contactselectsubtype')); +            $select_subtype = new html_select(array('name' => '_subtype_'.$col.'[]', 'class' => 'contactselectsubtype', 'title' => $prop['label'] . ' ' . $RCMAIL->gettext('type')));              $select_subtype->add($subtype_names, $prop['subtypes']);              $coltypes[$col]['subtypes_select'] = $select_subtype->show();          } @@ -607,7 +607,7 @@ function rcmail_contact_form($form, $record, $attrib = null)                  // prepare subtype selector in edit mode                  if ($edit_mode && is_array($colprop['subtypes'])) {                      $subtype_names = array_map('rcmail_get_type_label', $colprop['subtypes']); -                    $select_subtype = new html_select(array('name' => '_subtype_'.$col.'[]', 'class' => 'contactselectsubtype')); +                    $select_subtype = new html_select(array('name' => '_subtype_'.$col.'[]', 'class' => 'contactselectsubtype', 'title' => $colprop['label'] . ' ' . $RCMAIL->gettext('type')));                      $select_subtype->add($subtype_names, $colprop['subtypes']);                  }                  else @@ -649,6 +649,8 @@ function rcmail_contact_form($form, $record, $attrib = null)                      if ($subtypes[$i])                          $subtype = $subtypes[$i]; +                    $colprop['id'] = 'ff_' . $col . intval($coltypes[$field]['count']); +                      // render composite field                      if ($colprop['type'] == 'composite') {                          $composite = array(); $j = 0; @@ -714,7 +716,7 @@ function rcmail_contact_form($form, $record, $attrib = null)                      // display row with label                      if ($label) {                          $rows .= html::div('row', -                            html::div('contactfieldlabel label', $select_subtype ? $select_subtype->show($subtype) : rcube::Q($label)) . +                            html::div('contactfieldlabel label', $select_subtype ? $select_subtype->show($subtype) : html::label($colprop['id'], rcube::Q($label))) .                              html::div('contactfieldcontent '.$colprop['type'], $val));                      }                      else   // row without label @@ -803,7 +805,7 @@ function rcmail_contact_photo($attrib)      else          $ff_value = '-del-'; // will disable delete-photo action -    $img = html::img(array('src' => $photo_img, 'border' => 1, 'alt' => '')); +    $img = html::img(array('src' => $photo_img, 'border' => 1, 'alt' => $RCMAIL->gettext('contactphoto')));      $content = html::div($attrib, $img);      if ($CONTACT_COLTYPES['photo'] && ($RCMAIL->action == 'edit' || $RCMAIL->action == 'add')) { diff --git a/program/steps/addressbook/show.inc b/program/steps/addressbook/show.inc index f4224a3e2..4471ea658 100644 --- a/program/steps/addressbook/show.inc +++ b/program/steps/addressbook/show.inc @@ -60,6 +60,7 @@ function rcmail_contact_head($attrib)      $form = array(          'head' => array(  // section 'head' is magic! +            'name' => $RCMAIL->gettext('contactnameandorg'),              'content' => array(                  'prefix' => array('type' => 'text'),                  'firstname' => array('type' => 'text'), diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc index 6baf6e79a..0257f3187 100644 --- a/program/steps/mail/compose.inc +++ b/program/steps/mail/compose.inc @@ -969,7 +969,7 @@ function rcmail_compose_body($attrib)          $OUTPUT->set_env('spellcheck_langs', join(',', $editor_lang_set));      } -    $out .= "\n".'<iframe name="savetarget" src="program/resources/blank.gif" style="width:0;height:0;border:none;visibility:hidden;"></iframe>'; +    $out .= "\n".'<iframe name="savetarget" src="program/resources/blank.gif" style="width:0;height:0;border:none;visibility:hidden;" aria-hidden="true"></iframe>';      return $out;  } @@ -1859,9 +1859,10 @@ function rcmail_compose_responses_list($attrib)      foreach ($RCMAIL->get_compose_responses(true) as $response) {          $key = $response['key'];          $item = html::a(array( -            'href '=> '#'.urlencode($response['name']), +            'href' => '#'.urlencode($response['name']),              'class' => rtrim('insertresponse ' . $attrib['itemclass']),              'unselectable' => 'on', +            'tabindex' => '0',              'rel' => $key,          ), rcube::Q($response['name'])); diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc index 50b1e8292..0dba3c125 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -111,7 +111,9 @@ if (empty($RCMAIL->action) || $RCMAIL->action == 'list') {      if (!$OUTPUT->ajax_call) {          $OUTPUT->add_label('checkingmail', 'deletemessage', 'movemessagetotrash',              'movingmessage', 'copyingmessage', 'deletingmessage', 'markingmessage', -            'copy', 'move', 'quota', 'replyall', 'replylist', 'stillsearching'); +            'copy', 'move', 'quota', 'replyall', 'replylist', 'stillsearching', +            'flagged', 'unflagged', 'unread', 'deleted', 'replied', 'forwarded', +            'priority', 'withattachment');      }      $pagetitle = $RCMAIL->localize_foldername($mbox_name, true); @@ -531,14 +533,19 @@ function rcmail_message_list_head($attrib, $a_show_cols)          $a_sort_cols = array('subject', 'date', 'from', 'to', 'fromto', 'size', 'cc');      if (!empty($attrib['optionsmenuicon'])) { -        $onclick = 'return ' . rcmail_output::JS_OBJECT_NAME . ".command('menu-open', 'messagelistmenu')"; -        if ($attrib['optionsmenuicon'] === true || $attrib['optionsmenuicon'] == 'true') -            $list_menu = html::div(array('onclick' => $onclick, 'class' => 'listmenu', -                'id' => 'listmenulink', 'title' => $RCMAIL->gettext('listoptions'))); -        else -            $list_menu = html::a(array('href' => '#', 'onclick' => $onclick), -                html::img(array('src' => $skin_path . $attrib['optionsmenuicon'], -                    'id' => 'listmenulink', 'title' => $RCMAIL->gettext('listoptions')))); +        $onclick = 'return ' . rcmail_output::JS_OBJECT_NAME . ".command('menu-open', 'messagelistmenu', this, event)"; +        $inner   = $RCMAIL->gettext('listoptions'); +        if (is_string($attrib['optionsmenuicon']) && $attrib['optionsmenuicon'] != 'true') { +            $inner = html::img(array('src' => $skin_path . $attrib['optionsmenuicon'], 'alt' => $RCMAIL->gettext('listoptions'))); +        } +        $list_menu = html::a(array( +            'href' => '#list-options', +            'onclick' => $onclick, +            'class' => 'listmenu', +            'id' => 'listmenulink', +            'title' => $RCMAIL->gettext('listoptions'), +            'tabindex' => '0', +        ), $inner);      }      else {          $list_menu = ''; @@ -558,12 +565,14 @@ function rcmail_message_list_head($attrib, $a_show_cols)          // get column name          switch ($col) {          case 'flag': -            $col_name = html::span('flagged', ' '); +            $col_name = html::span('flagged', $RCMAIL->gettext('flagged'));              break;          case 'attachment':          case 'priority': +            $col_name = html::span($col, $RCMAIL->gettext($col)); +            break;          case 'status': -            $col_name = html::span($col, ' '); +            $col_name = html::span($col, $RCMAIL->gettext('readstatus'));              break;          case 'threads':              $col_name = $list_menu; diff --git a/program/steps/mail/list_contacts.inc b/program/steps/mail/list_contacts.inc index 0ee81135b..4f17beffd 100644 --- a/program/steps/mail/list_contacts.inc +++ b/program/steps/mail/list_contacts.inc @@ -110,7 +110,7 @@ else if (!empty($result) && $result->count > 0) {              $keyname = $row['_type'] == 'group' ? 'contactgroup' : 'contact';              $OUTPUT->command('add_contact_row', $row_id, array( -                $keyname => html::span(array('title' => $email), rcube::Q($name ? $name : $email) . +                $keyname => html::a(array('title' => $email), rcube::Q($name ? $name : $email) .                      ($name && count($emails) > 1 ? ' ' . html::span('email', rcube::Q($email)) : '')                  )), $classname);          } diff --git a/program/steps/mail/search_contacts.inc b/program/steps/mail/search_contacts.inc index d56581695..ccef32dd2 100644 --- a/program/steps/mail/search_contacts.inc +++ b/program/steps/mail/search_contacts.inc @@ -87,7 +87,7 @@ if (!empty($result) && $result->count > 0) {              $row_id = $row['ID'].'-'.$i;              $jsresult[$row_id] = format_email_recipient($email, $name);              $OUTPUT->command('add_contact_row', $row_id, array( -                'contact' => html::span(array('title' => $email), rcube::Q($name ? $name : $email) . +                'contact' => html::a(array('title' => $email), rcube::Q($name ? $name : $email) .                      ($name && count($emails) > 1 ? ' ' . html::span('email', rcube::Q($email)) : '')                  )), 'person');          } diff --git a/program/steps/mail/show.inc b/program/steps/mail/show.inc index f1c10da3d..0ebdd6277 100644 --- a/program/steps/mail/show.inc +++ b/program/steps/mail/show.inc @@ -199,6 +199,7 @@ function rcmail_message_attachments($attrib)      if (sizeof($MESSAGE->attachments)) {          foreach ($MESSAGE->attachments as $attach_prop) {              $filename = rcmail_attachment_name($attach_prop, true); +            $size = '';              if ($PRINT_MODE) {                  $size = $RCMAIL->message_part_size($attach_prop); @@ -213,6 +214,10 @@ function rcmail_message_attachments($attrib)                      $title = '';                  } +                if ($attach_prop->size) { +                    $size = ' ' . html::span('attachment-size', '(' . $RCMAIL->show_bytes($attach_prop->size) . ')'); +                } +                  $mimetype = rcmail_fix_mimetype($attach_prop->mimetype);                  $class    = rcube_utils::file2class($mimetype, $filename);                  $id       = 'attach' . $attach_prop->mime_id; @@ -222,7 +227,7 @@ function rcmail_message_attachments($attrib)                          rcmail_output::JS_OBJECT_NAME, $attach_prop->mime_id),                      'onmouseover' => $title ? '' : 'rcube_webmail.long_subject_title_ex(this, 0)',                      'title'       => rcube::Q($title), -                    ), rcube::Q($filename)); +                    ), rcube::Q($filename) . $size);                  $ol .= html::tag('li', array('class' => $class, 'id' => $id), $link); diff --git a/program/steps/settings/edit_folder.inc b/program/steps/settings/edit_folder.inc index 6b7bd08d2..c61ac6da9 100644 --- a/program/steps/settings/edit_folder.inc +++ b/program/steps/settings/edit_folder.inc @@ -132,6 +132,7 @@ function rcmail_folder_form($attrib)          }          $select = $RCMAIL->folder_selector(array( +            'id'          => '_parent',              'name'        => '_parent',              'noselection' => '---',              'realnames'   => false, @@ -155,7 +156,7 @@ function rcmail_folder_form($attrib)      // Settings: threading      if ($threading_supported && ($mbox_imap == 'INBOX' || (!$options['noselect'] && !$options['is_root']))) { -        $select = new html_select(array('name' => '_viewmode', 'id' => '_listmode')); +        $select = new html_select(array('name' => '_viewmode', 'id' => '_viewmode'));          $select->add($RCMAIL->gettext('list'), 0);          $select->add($RCMAIL->gettext('threads'), 1); diff --git a/program/steps/settings/func.inc b/program/steps/settings/func.inc index 4b4575f10..8f9cf090f 100644 --- a/program/steps/settings/func.inc +++ b/program/steps/settings/func.inc @@ -343,7 +343,7 @@ function rcmail_user_prefs($current = null)                          if (is_array($meta) && $meta['name']) {                              $skinname     = $meta['name'];                              $author_link  = $meta['url'] ? html::a(array('href' => $meta['url'], 'target' => '_blank'), rcube::Q($meta['author'])) : rcube::Q($meta['author']); -                            $license_link = $meta['license-url'] ? html::a(array('href' => $meta['license-url'], 'target' => '_blank'), rcube::Q($meta['license'])) : rcube::Q($meta['license']); +                            $license_link = $meta['license-url'] ? html::a(array('href' => $meta['license-url'], 'target' => '_blank', 'tabindex' => '-1'), rcube::Q($meta['license'])) : rcube::Q($meta['license']);                          }                          $skinnames[] = mb_strtolower($skinname); diff --git a/program/steps/settings/responses.inc b/program/steps/settings/responses.inc index 06093b3b8..ddd1924fe 100644 --- a/program/steps/settings/responses.inc +++ b/program/steps/settings/responses.inc @@ -95,7 +95,7 @@ function rcmail_responses_list($attrib)  {      global $RCMAIL, $OUTPUT; -    $attrib += array('id' => 'rcmresponseslist', 'tagname' => 'table', 'cols' => 1); +    $attrib += array('id' => 'rcmresponseslist', 'tagname' => 'table');      $plugin = $RCMAIL->plugins->exec_hook('responses_list', array(          'list' => $RCMAIL->get_compose_responses(true), | 
