summaryrefslogtreecommitdiff
path: root/program
diff options
context:
space:
mode:
Diffstat (limited to 'program')
-rw-r--r--program/include/rcmail.php9
-rw-r--r--program/include/rcmail_output_html.php33
-rw-r--r--program/js/app.js595
-rw-r--r--program/js/common.js24
-rw-r--r--program/js/list.js171
-rw-r--r--program/js/treelist.js114
-rw-r--r--program/lib/Roundcube/html.php21
-rw-r--r--program/localization/en_US/labels.inc57
-rw-r--r--program/steps/addressbook/edit.inc3
-rw-r--r--program/steps/addressbook/func.inc14
-rw-r--r--program/steps/addressbook/show.inc1
-rw-r--r--program/steps/mail/compose.inc5
-rw-r--r--program/steps/mail/func.inc31
-rw-r--r--program/steps/mail/list_contacts.inc2
-rw-r--r--program/steps/mail/search_contacts.inc2
-rw-r--r--program/steps/mail/show.inc7
-rw-r--r--program/steps/settings/edit_folder.inc3
-rw-r--r--program/steps/settings/func.inc2
-rw-r--r--program/steps/settings/responses.inc2
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+'">&nbsp;</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+'">&nbsp;</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+'">&nbsp;</span>';
+ html = '<span class="'+flags.attachmentClass+'" title="'+label+'"></span>';
else if (/application\/|multipart\/(m|signed)/.test(flags.ctype))
- html = '<span class="attachment">&nbsp;</span>';
+ html = '<span class="attachment" title="'+label+'"></span>';
else if (/multipart\/report/.test(flags.ctype))
- html = '<span class="report">&nbsp;</span>';
- else
+ html = '<span class="report"></span>';
+ else
html = '&nbsp;';
}
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+'">&nbsp;</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+'">&nbsp;</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 = '&nbsp;';
}
@@ -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, '&lt;').replace(/>/g, '&gt;').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);
+ };
+
- container.show().data('callback', callback);
+ /***********************************************/
+ /********* 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;
+
+ 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') + '">&nbsp;</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') + '">&nbsp;</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='')
), '&raquo;');
}
else
- $val = '&nbsp;';
+ $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', '&nbsp;');
+ $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, '&nbsp;');
+ $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 ? '&nbsp;' . 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 ? '&nbsp;' . 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),