Accessibility enhancements for the calendar module (#3084)

This commit is contained in:
Thomas Bruederli 2014-06-19 10:40:28 +02:00
parent 6d9d854e65
commit efecba6675
13 changed files with 315 additions and 152 deletions

View file

@ -299,7 +299,7 @@ class calendar extends rcube_plugin
$this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
$this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver'));
$this->rc->output->set_env('mscolors', jqueryui::get_color_values());
$this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list')));
$this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->gettext('roleorganizer'))));
$view = get_input_value('view', RCUBE_INPUT_GPC);
if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))

View file

@ -237,32 +237,33 @@ function rcube_calendar_ui(settings)
if (edit) {
rcmail.env.attachments[elem.id] = elem;
// delete icon
content = document.createElement('A');
content.href = '#delete';
content.title = rcmail.gettext('delete');
content.className = 'delete';
$(content).click({id: elem.id}, function(e) { remove_attachment(this, e.data.id); return false; });
content = $('<a href="#delete" />')
.attr('title', rcmail.gettext('delete'))
.attr('aria-label', rcmail.gettext('delete') + ' ' + Q(elem.name))
.addClass('delete')
.click({id: elem.id}, function(e) { remove_attachment(this, e.data.id); return false; });
if (!rcmail.env.deleteicon)
content.innerHTML = rcmail.gettext('delete');
content.html(rcmail.gettext('delete'));
else {
img = document.createElement('IMG');
img.src = rcmail.env.deleteicon;
img.alt = rcmail.gettext('delete');
content.appendChild(img);
content.append(img);
}
li.appendChild(content);
content.appendTo(li);
}
// name/link
content = document.createElement('A');
content.innerHTML = elem.name;
content.className = 'file';
content.href = '#load';
$(content).click({event: event, att: elem}, function(e) {
load_attachment(e.data.event, e.data.att); return false; });
li.appendChild(content);
content = $('<a href="#load" />')
.html(Q(elem.name))
.addClass('file')
.click({event: event, att: elem}, function(e) {
load_attachment(e.data.event, e.data.att);
return false;
})
.appendTo(li);
ul.appendChild(li);
}
@ -283,7 +284,7 @@ function rcube_calendar_ui(settings)
};
// event details dialog (show only)
var event_show_dialog = function(event)
var event_show_dialog = function(event, ev)
{
var $dialog = $("#eventshow").attr('class', 'uidialog');
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
@ -430,18 +431,28 @@ function rcube_calendar_ui(settings)
modal: false,
resizable: !bw.ie6,
closeOnEscape: (!bw.ie6 && !bw.ie7), // disable for performance reasons
title: Q(me.event_date_text(event)),
title: me.event_date_text(event),
open: function() {
$dialog.parent().find('.ui-button').first().focus();
$dialog.attr('aria-hidden', 'false');
setTimeout(function(){
$dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
}, 5);
},
close: function() {
$dialog.dialog('destroy').hide();
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
},
buttons: buttons,
minWidth: 320,
width: 420
}).show();
// remember opener element (to be focused on close)
$dialog.data('opener', ev && rcube_event.is_keyboard(ev) ? ev.target : null);
// set voice title on dialog widget
$dialog.dialog('widget').removeAttr('aria-labelledby')
.attr('aria-label', me.event_date_text(event, true) + ', ', event.title);
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 420);
/*
@ -472,8 +483,11 @@ function rcube_calendar_ui(settings)
// bring up the event dialog (jquery-ui popup)
var event_edit_dialog = function(action, event)
{
// copy opener element from show dialog
var op_elem = $("#eventshow:ui-dialog").data('opener');
// close show dialog first
$("#eventshow:ui-dialog").dialog('close');
$("#eventshow:ui-dialog").data('opener', null).dialog('close');
var $dialog = $('<div>');
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:action=='new' };
@ -675,7 +689,7 @@ function rcube_calendar_ui(settings)
$('#edit-tab-attachments')[(calendar.attachments?'show':'hide')]();
// activate the first tab
$('#eventtabs').tabs('select', 0);
$('#eventtabs').tabs('option', 'active', 0);
// hack: set task to 'calendar' to make all dialog actions work correctly
var comm_path_before = rcmail.env.comm_path;
@ -690,15 +704,18 @@ function rcube_calendar_ui(settings)
closeOnEscape: false,
title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'),
open: function() {
editform.attr('aria-hidden', 'false');
$dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
},
close: function() {
editform.hide().appendTo(document.body);
editform.hide().attr('aria-hidden', 'true').appendTo(document.body);
$dialog.dialog("destroy").remove();
rcmail.ksearch_blur();
rcmail.ksearch_destroy();
freebusy_data = {};
rcmail.env.comm_path = comm_path_before; // restore comm_path
if (op_elem)
$(op_elem).focus();
},
buttons: buttons,
minWidth: 500,
@ -843,12 +860,13 @@ function rcube_calendar_ui(settings)
closeOnEscape: (!bw.ie6 && !bw.ie7),
title: rcmail.gettext('scheduletime', 'calendar'),
open: function() {
$dialog.parent().find('.ui-dialog-buttonset .ui-button').first().focus();
$dialog.attr('aria-hidden', 'false').find('#shedule-find-next, #shedule-find-prev').not(':disabled').first().focus();
},
close: function() {
if (bw.ie6)
$("#edit-attendees-table").css('visibility','visible');
$dialog.dialog("destroy").hide();
$dialog.dialog("destroy").attr('aria-hidden', 'true').hide();
// TODO: focus opener button
},
resizeStop: function() {
render_freebusy_overlay();
@ -1316,6 +1334,9 @@ function rcube_calendar_ui(settings)
var now = new Date();
$('#shedule-find-prev').button('option', 'disabled', (event.start.getTime() < now.getTime()));
// speak new selection
rcmail.display_message(rcmail.gettext('suggestedslot', 'calendar') + ': ' + me.event_date_text(event, true), 'voice');
}
else {
alert(rcmail.gettext('noslotfound','calendar'));
@ -1407,7 +1428,7 @@ function rcube_calendar_ui(settings)
if (organizer && !readonly)
dispname = rcmail.env['identities-selector'];
var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + '>';
var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + ' aria-label="' + rcmail.gettext('role','calendar') + '">';
for (var r in opts)
select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
select += '</select>';
@ -1427,7 +1448,7 @@ function rcube_calendar_ui(settings)
var html = '<td class="role">' + select + '</td>' +
'<td class="name">' + dispname + '</td>' +
'<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" /></td>' +
'<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" alt="" /></td>' +
'<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + Q(data.status || '') + '</span></td>' +
'<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
@ -1482,10 +1503,11 @@ function rcube_calendar_ui(settings)
url: rcmail.url('freebusy-status'),
data: { email:email, start:date2servertime(clone_date(event.start, event.allDay?1:0)), end:date2servertime(clone_date(event.end, event.allDay?2:0)), _remote: 1 },
success: function(status){
icon.removeClass('loading').addClass(String(status).toLowerCase());
var avail = String(status).toLowerCase();
icon.removeClass('loading').addClass(avail).attr('alt', rcmail.gettext('avail' + avail, 'calendar'));
},
error: function(){
icon.removeClass('loading').addClass('unknown');
icon.removeClass('loading').addClass('unknown').attr('alt', rcmail.gettext('availunknown', 'calendar'));
}
});
};
@ -1522,11 +1544,14 @@ function rcube_calendar_ui(settings)
resizable: true,
closeOnEscape: true,
title: rcmail.gettext('findresources', 'calendar'),
open: function() {
$dialog.attr('aria-hidden', 'false');
},
close: function() {
$dialog.dialog('destroy').hide();
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
},
resize: function(e) {
var container = $(rcmail.gui_objects.resourceinfocalendar)
var container = $(rcmail.gui_objects.resourceinfocalendar);
container.fullCalendar('option', 'height', container.height() + 4);
},
buttons: buttons,
@ -1593,9 +1618,11 @@ function rcube_calendar_ui(settings)
titleFormat: { day: 'dddd ' + settings['date_long'] },
currentTimeIndicator: settings.time_indicator,
eventRender: function(event, element, view) {
var title = rcmail.get_label(event.status, 'calendar');
element.addClass('status-' + event.status);
element.find('.fc-event-head').hide();
element.find('.fc-event-title').text(rcmail.get_label(event.status, 'calendar'));
element.find('.fc-event-title').text(title);
element.attr('aria-label', me.event_date_text(event, true) + ': ' + title);
}
});
@ -1971,10 +1998,12 @@ function rcube_calendar_ui(settings)
title: rcmail.gettext((action == 'remove' ? 'removeeventconfirm' : 'changeeventconfirm'), 'calendar'),
buttons: buttons,
open: function() {
$dialog.parent().find('.ui-button').first().focus();
setTimeout(function(){
$dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
}, 5);
},
close: function(){
$dialog.dialog("destroy").hide();
$dialog.dialog("destroy").remove();
if (!rcmail.busy)
fc.fullCalendar('refetchEvents');
}
@ -2021,6 +2050,8 @@ function rcube_calendar_ui(settings)
if (event.status) {
element.addClass('cal-event-status-' + String(event.status).toLowerCase());
}
element.attr('aria-label', event.title + ', ' + me.event_date_text(event, true));
};
@ -2099,8 +2130,8 @@ function rcube_calendar_ui(settings)
allDayText: rcmail.gettext('all-day', 'calendar'),
currentTimeIndicator: settings.time_indicator,
eventRender: fc_event_render,
eventClick: function(event) {
event_show_dialog(event);
eventClick: function(event, ev, view) {
event_show_dialog(event, ev);
}
});
@ -2743,6 +2774,7 @@ function rcube_calendar_ui(settings)
id_prefix: 'rcmlical',
selectable: true,
save_state: true,
keyboard: false,
searchbox: '#calendarlistsearch',
search_action: 'calendar/calendar',
search_sources: [ 'folders', 'users' ],
@ -2773,6 +2805,12 @@ function rcube_calendar_ui(settings)
rcmail.http_post('calendar', { action:'subscribe', c:{ id:p.id, active:cal.active?1:0, permanent:cal.subscribed?1:0 } });
}
});
calendars_list.addEventListener('search-complete', function(data) {
if (data.length)
rcmail.display_message(rcmail.gettext('nrcalendarsfound','calendar').replace('$nr', data.length), 'voice');
else
rcmail.display_message(rcmail.gettext('nocalendarsfound','calendar'), 'info');
});
// init (delegate) event handler on calendar list checkboxes
$(rcmail.gui_objects.calendarslist).on('click', 'input[type=checkbox]', function(e){
@ -2922,9 +2960,9 @@ function rcube_calendar_ui(settings)
day_clicked_ts = now;
},
// callback when a specific event is clicked
eventClick: function(event) {
eventClick: function(event, ev, view) {
if (!event.temp)
event_show_dialog(event);
event_show_dialog(event, ev);
},
// callback when an event was dragged and finally dropped
eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc) {
@ -3054,7 +3092,7 @@ function rcube_calendar_ui(settings)
// scroll to current time
var $this = $(this);
var widget = $this.autocomplete('widget');
var menu = $this.data('autocomplete').menu;
var menu = $this.data('ui-autocomplete').menu;
var amregex = /^(.+)(a[.m]*)/i;
var pmregex = /^(.+)(a[.m]*)/i;
var val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1');
@ -3107,9 +3145,9 @@ function rcube_calendar_ui(settings)
return [ true, (active ? 'ui-datepicker-activerange ui-datepicker-active-' + view.name : ''), ''];
}
})) // set event handler for clicks on calendar week cell of the datepicker widget
.click(function(e) {
.on('click', 'td.ui-datepicker-week-col', function(e) {
var cell = $(e.target);
if (e.target.tagName == 'TD' && cell.hasClass('ui-datepicker-week-col')) {
if (e.target.tagName == 'TD' && cell.hasClass('')) {
var base_date = minical.datepicker('getDate');
if (minical.data('month'))
base_date.setMonth(minical.data('month')-1);
@ -3126,7 +3164,9 @@ function rcube_calendar_ui(settings)
fc.fullCalendar('gotoDate', date).fullCalendar('setDate', date).fullCalendar('changeView', 'agendaWeek');
minical.datepicker('setDate', date);
}
});
});
minical.find('.ui-datepicker-inline').attr('aria-labelledby', 'aria-label-minical');
if (rcmail.env.date) {
var viewdate = new Date();
@ -3136,10 +3176,11 @@ function rcube_calendar_ui(settings)
// init event dialog
$('#eventtabs').tabs({
show: function(event, ui) {
if (ui.panel.id == 'event-panel-attendees' || ui.panel.id == 'event-panel-resources') {
var tab = ui.panel.id == 'event-panel-resources' ? 'resource' : 'attendee';
$('#edit-'+tab+'-name').select();
activate: function(event, ui) {
if (ui.newPanel.selector == '#event-panel-attendees' || ui.newPanel.selector == '#event-panel-resources') {
var tab = ui.newPanel.selector == '#event-panel-resources' ? 'resource' : 'attendee';
if (!rcube_event.is_keyboard(event))
$('#edit-'+tab+'-name').select();
// update free-busy status if needed
if (freebusy_ui.needsupdate && me.selected_event)
update_freebusy_status(me.selected_event);
@ -3162,6 +3203,7 @@ function rcube_calendar_ui(settings)
.autocomplete({
delay: 100,
minLength: 1,
appendTo: '#eventedit',
source: autocomplete_times,
open: autocomplete_open,
change: event_times_changed,
@ -3173,9 +3215,9 @@ function rcube_calendar_ui(settings)
.click(function() { // show drop-down upon clicks
$(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " ");
}).each(function(){
$(this).data('autocomplete')._renderItem = function(ul, item) {
$(this).data('ui-autocomplete')._renderItem = function(ul, item) {
return $('<li>')
.data('item.autocomplete', item)
.data('ui-autocomplete-item', item)
.append('<a>' + item[0] + item[1] + '</a>')
.appendTo(ul);
};

View file

@ -303,10 +303,11 @@ class calendar_ui
$content = '';
if (!$activeonly || $prop['active']) {
$label_id = 'cl:' . $id;
$content = html::div(join(' ', $classes),
html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
html::span(array('class' => 'calname', 'id' => $label_id, 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
($prop['virtual'] ? '' :
html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id), '') .
(isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe')), ' ') : '') .
html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), '&nbsp;')
)
@ -326,7 +327,7 @@ class calendar_ui
$select_range = new html_select(array('name' => 'listrange', 'id' => 'agenda-listrange'));
$select_range->add(1 . ' ' . preg_replace('/\(.+\)/', '', $this->cal->lib->gettext('days')), $days);
foreach (array(2,5,7,14,30,60,90) as $days)
foreach (array(2,5,7,14,30,60,90,180,365) as $days)
$select_range->add($days . ' ' . preg_replace('/\(|\)/', '', $this->cal->lib->gettext('days')), $days);
$html .= html::label('agenda-listrange', $this->cal->gettext('listrange'));
@ -334,8 +335,8 @@ class calendar_ui
$select_sections = new html_select(array('name' => 'listsections', 'id' => 'agenda-listsections'));
$select_sections->add('---', '');
foreach (array('day' => 'days', 'week' => 'weeks', 'month' => 'months', 'smart' => 'smartsections') as $val => $label)
$select_sections->add(preg_replace('/\(|\)/', '', ucfirst($this->cal->gettext($label))), $val);
foreach (array('day' => 'libcalendaring.days', 'week' => 'libcalendaring.weeks', 'month' => 'libcalendaring.months', 'smart' => 'calendar.smartsections') as $val => $label)
$select_sections->add(preg_replace('/\(|\)/', '', ucfirst($this->rc->gettext($label))), $val);
$html .= html::span('spacer', '&nbsp;');
$html .= html::label('agenda-listsections', $this->cal->gettext('listsections'));

View file

@ -1,5 +1,5 @@
/*!
* FullCalendar v1.6.4-rcube-1.0
* FullCalendar v1.6.4-rcube-1.1
* Docs & License: http://arshaw.com/fullcalendar/
* (c) 2013 Adam Shaw, 2014 Kolab Systems AG
*/
@ -813,7 +813,7 @@ function Header(calendar, options) {
var prevButton;
$.each(this.split(','), function(j, buttonName) {
if (buttonName == 'title') {
e.append("<span class='fc-header-title'><h2>&nbsp;</h2></span>");
e.append("<span class='fc-header-title'><h2 aria-live='polite' aria-relevant='text' aria-atomic='true'>&nbsp;</h2></span>");
if (prevButton) {
prevButton.addClass(tm + '-corner-right');
}
@ -833,7 +833,7 @@ function Header(calendar, options) {
var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here?
var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here?
var button = $(
"<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default'>" +
"<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default' role='button' tabindex='0'>" +
(icon ?
"<span class='fc-icon-wrap'>" +
"<span class='ui-icon ui-icon-" + icon + "'/>" +
@ -869,6 +869,10 @@ function Header(calendar, options) {
.removeClass(tm + '-state-down');
}
)
.keypress(function(ev) {
if (ev.keyCode == 13)
$(ev.target).trigger('click');
})
.appendTo(e);
disableTextSelection(button);
if (!prevButton) {
@ -895,25 +899,25 @@ function Header(calendar, options) {
function activateButton(buttonName) {
element.find('span.fc-button-' + buttonName)
.addClass(tm + '-state-active');
.addClass(tm + '-state-active').attr('tabindex', '-1');
}
function deactivateButton(buttonName) {
element.find('span.fc-button-' + buttonName)
.removeClass(tm + '-state-active');
.removeClass(tm + '-state-active').attr('tabindex', '0');
}
function disableButton(buttonName) {
element.find('span.fc-button-' + buttonName)
.addClass(tm + '-state-disabled');
.addClass(tm + '-state-disabled').attr('tabindex', '-1');
}
function enableButton(buttonName) {
element.find('span.fc-button-' + buttonName)
.removeClass(tm + '-state-disabled');
.removeClass(tm + '-state-disabled').attr('tabindex', '0');
}
@ -1760,7 +1764,7 @@ function _exclEndDay(end, allDay) {
function lazySegBind(container, segs, bindHandlers) {
container.unbind('mouseover').mouseover(function(ev) {
container.unbind('mouseover focusin').bind('mouseover focusin', function(ev) {
var parent=ev.target, e,
i, seg;
while (parent != this) {
@ -4051,7 +4055,7 @@ function AgendaEventRenderer() {
"left:" + seg.left + "px;" +
skinCss +
"'" +
">" +
" tabindex='0'>" +
"<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
"<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
"<div class='fc-event-time'>" +
@ -4067,7 +4071,7 @@ function AgendaEventRenderer() {
"</div>"; // close inner
if (seg.isEnd && isEventResizable(event)) {
html +=
"<div class='ui-resizable-handle ui-resizable-s'>=</div>";
"<div class='ui-resizable-handle ui-resizable-s' role='presentation'>=</div>";
}
html +=
"</" + (url ? "a" : "div") + ">";
@ -4941,7 +4945,7 @@ function ListEventRenderer() {
}
function lazySegBind(container, seg, bindHandlers) {
container.unbind('mouseover').mouseover(function(ev) {
container.unbind('mouseover focusin').bind('mouseover focusin', function(ev) {
var parent = ev.target, e = parent, i, event;
while (parent != this) {
e = parent;
@ -5132,7 +5136,7 @@ function TableEventRenderer() {
rowClasses.push('fc-today');
}
s += "<tr class='" + rowClasses.join(' ') + "'>";
s += "<tr class='" + rowClasses.join(' ') + "' tabindex='0'>";
for (var col, c=0; c < tableCols.length; c++) {
col = tableCols[c];
if (col == 'handle') {
@ -5358,7 +5362,11 @@ function View(element, calendar, viewName) {
function(ev) {
trigger('eventMouseout', this, event, ev);
}
);
)
.keypress(function(ev) {
if (ev.keyCode == 13)
$(this).trigger('click', { pointerType:'keyboard' });
});
// TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
// TODO: same for resizing
}
@ -6008,7 +6016,7 @@ function DayEventRenderer() {
"left:" + segment.left + "px;" +
skinCss +
"'" +
">" +
" tabindex='0'>" +
"<div class='fc-event-inner'>";
if (!event.allDay && segment.isStart) {
html +=

View file

@ -53,7 +53,9 @@ $labels['location'] = 'Location';
$labels['url'] = 'URL';
$labels['date'] = 'Date';
$labels['start'] = 'Start';
$labels['starttime'] = 'Start time';
$labels['end'] = 'End';
$labels['endtime'] = 'End time';
$labels['repeat'] = 'Repeat';
$labels['selectdate'] = 'Choose date';
$labels['freebusy'] = 'Show me as';
@ -87,8 +89,11 @@ $labels['showurl'] = 'Show calendar URL';
$labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
$labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
$labels['findcalendars'] = 'Find calendars...';
$labels['searchterms'] = 'Search terms';
$labels['calsearchresults'] = 'Available Calendars';
$labels['calendarsubscribe'] = 'List permanently';
$labels['nocalendarsfound'] = 'No calendars found';
$labels['nrcalendarsfound'] = '$nr calendars found';
// agenda view
$labels['listrange'] = 'Range to display:';
@ -99,6 +104,7 @@ $labels['today'] = 'Today';
$labels['tomorrow'] = 'Tomorrow';
$labels['thisweek'] = 'This week';
$labels['nextweek'] = 'Next week';
$labels['prevweek'] = 'Previous week';
$labels['thismonth'] = 'This month';
$labels['nextmonth'] = 'Next month';
$labels['weekofyear'] = 'Week';
@ -140,6 +146,7 @@ $labels['onlyworkinghours'] = 'Find availability within my working hours';
$labels['reqallattendees'] = 'Required/all participants';
$labels['prevslot'] = 'Previous Slot';
$labels['nextslot'] = 'Next Slot';
$labels['suggestedslot'] = 'Suggested Slot';
$labels['noslotfound'] = 'Unable to find a free time slot';
$labels['invitationsubject'] = 'You\'ve been invited to "$title"';
$labels['invitationmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with all the event details which you can import to your calendar application.";
@ -232,4 +239,16 @@ $labels['birthdayscalendarsources'] = 'From these address books';
$labels['birthdayeventtitle'] = '$name\'s Birthday';
$labels['birthdayage'] = 'Age $age';
// (hidden) titles and labels for accessibility annotations
$labels['arialabelminical'] = 'Calendar date selection';
$labels['arialabelcalendarview'] = 'Calendar view';
$labels['arialabelsearchform'] = 'Event search form';
$labels['arialabelquicksearchbox'] = 'Event search input';
$labels['arialabelcalsearchform'] = 'Calendars search form';
$labels['calendaractions'] = 'Calendar actions';
$labels['arialabeleventattendees'] = 'Event participants list';
$labels['arialabeleventresources'] = 'Event resources list';
$labels['arialabelresourcesearchform'] = 'Resources search form';
$labels['arialabelresourceselection'] = 'Available resources';
?>

View file

@ -85,6 +85,14 @@ body.attachmentwin #topnav .topright {
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#d9f1fb', endColorstr='#c5e3ee', GradientType=0);
}
#datepicker .ui-datepicker-days-cell-over a.ui-state-default {
color: #fff;
border-color: #2fa0c0;
background: rgba(73,180,210,0.6);
text-shadow: 0px 1px 1px #666;
filter: none;
}
#datepicker .ui-datepicker-activerange a.ui-state-active {
color: #fff;
background: #00acd4;
@ -265,12 +273,19 @@ pre {
cursor: pointer;
}
#calendars .treelist div:hover > a.subscribed {
background-position: 1px -110px;
#calendars .treelist div > a.subscribed:focus {
border-radius: 3px;
outline: 2px solid rgba(30,150,192, 0.5);
}
#calendars .treelist div.subscribed a.subscribed {
background-position: -15px -110px;
#calendars .treelist div:hover > a.subscribed,
#calendars .treelist div > a.subscribed:focus {
background-position: 0 -110px;
}
#calendars .treelist div.subscribed a.subscribed,
#calendars .treelist div.subscribed a.subscribed:focus {
background-position: -16px -110px;
}
#calendars .treelist li input {
@ -420,7 +435,7 @@ pre {
}
body.calendarmain #quicksearchbar {
z-index: 200;
z-index: 20;
}
body.calendarmain #searchmenulink {
@ -600,7 +615,7 @@ a.miniColors-trigger {
.calendarmain .fc-view-table td.fc-list-header,
#attendees-freebusy-table h3.boxtitle,
#schedule-freebusy-times thead th,
.edit-attendees-table thead td
.edit-attendees-table thead th
{
color: #69939e;
font-size: 11px;
@ -614,6 +629,7 @@ a.miniColors-trigger {
border: 0;
border-bottom: 1px solid #ccc;
height: 18px;
line-height: 18px;
padding: 8px 7px 3px 7px;
}
@ -775,21 +791,26 @@ td.topalign {
margin-top: 0.5em;
}
.edit-attendees-table th.role,
.edit-attendees-table td.role {
width: 9em;
}
.edit-attendees-table th.availability,
.edit-attendees-table td.availability,
.edit-attendees-table th.confirmstate,
.edit-attendees-table td.confirmstate {
width: 4em;
}
.edit-attendees-table th.options,
.edit-attendees-table td.options {
width: 3em;
text-align: right;
padding-right: 4px;
}
.edit-attendees-table th.name,
.edit-attendees-table td.name {
width: auto;
white-space: nowrap;
@ -1120,7 +1141,7 @@ a.dropdown-link:after {
left: 0;
right: 0;
height: auto;
z-index: 200;
z-index: 10;
padding: 4px 5px;
border: 1px solid #c3c3c3;
border-top-color: #ddd;
@ -1376,11 +1397,21 @@ a.dropdown-link:after {
-o-box-shadow: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
outline: none;
}
.calendarmain .fc-header-left .fc-button:focus {
color: #fff;
text-shadow: 0px 1px 1px #666;
background-color: rgba(30,150,192, 0.5);
border-radius: 3px;
}
.calendarmain .fc-header-left .fc-button.fc-state-active {
font-weight: bold;
color: #222;
text-shadow: none;
background-color: transparent;
}
.calendarmain .fc-header-left .fc-button-agendaDay {
@ -1428,6 +1459,13 @@ a.dropdown-link:after {
font-size: 1em !important;
}
.calendarmain .fc-event:focus {
outline: 1px solid rgba(71,135,177, 0.4);
-webkit-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
-moz-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
-o-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
}
.fc-event-title {
font-weight: bold;
}

View file

@ -269,6 +269,10 @@ html .fc,
cursor: default;
}
.fc-event:focus {
outline: 2px solid ActiveBorder;
}
a.fc-event {
text-decoration: none;
}

View file

@ -9,9 +9,12 @@
<roundcube:include file="/includes/header.html" />
<h1 class="voice"><roundcube:label name="calendar.calendar" /></h1>
<div id="mainscreen">
<div id="calendarsidebar">
<div id="calendartoolbar" class="toolbar">
<h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
<div id="calendartoolbar" class="toolbar" role="toolbar" aria-labelledby="aria-label-toolbar">
<roundcube:button command="addevent" type="link" class="button addevent disabled" classAct="button addevent" classSel="button addevent pressed" label="calendar.new_event" title="calendar.new_event" />
<roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="calendar.print" title="calendar.printtitle" />
<roundcube:button command="events-import" type="link" class="button import disabled" classAct="button import" classSel="button import pressed" label="import" title="calendar.importevents" />
@ -19,35 +22,41 @@
<roundcube:container name="toolbar" id="calendartoolbar" />
</div>
<div id="datepicker" class="uibox"></div>
<h2 id="aria-label-minical" class="voice"><roundcube:label name="calendar.arialabelminical" /></h2>
<div id="datepicker" class="uibox" role="presentation"></div>
<div id="calendars" class="uibox listbox" style="visibility:hidden">
<h2 class="boxtitle"><roundcube:label name="calendar.calendars" />
<a class="iconbutton search" title="<roundcube:label name='calendar.findcalendars' />"></a>
<div id="calendars" class="uibox listbox" style="visibility:hidden" role="navigation" aria-labelledby="aria-label-calendarlist">
<h2 class="boxtitle" id="aria-label-calendarlist"><roundcube:label name="calendar.calendars" />
<a href="#calendars" class="iconbutton search" title="<roundcube:label name='calendar.findcalendars' />" tabindex="0"><roundcube:label name='calendar.findcalendars' /></a>
</h2>
<div class="listsearchbox">
<div class="searchbox">
<div class="searchbox" role="search" aria-labelledby="aria-label-calsearchform" aria-controls="calendarslist">
<h3 id="aria-label-calsearchform" class="voice"><roundcube:label name="calendar.arialabelcalsearchform" /></h3>
<label for="calendarlistsearch" class="voice"><roundcube:label name="calendar.searchterms" /></label>
<input type="text" name="q" id="calendarlistsearch" placeholder="<roundcube:label name='calendar.findcalendars' />" />
<a class="iconbutton searchicon"></a>
<roundcube:button command="reset-listsearch" id="calendarlistsearch-reset" class="iconbutton reset" title="resetsearch" content="x" />
<roundcube:button command="reset-listsearch" id="calendarlistsearch-reset" class="iconbutton reset" title="resetsearch" label="resetsearch" />
</div>
</div>
<div class="scroller withfooter">
<roundcube:object name="plugin.calendar_list" id="calendarslist" class="treelist listing" />
<roundcube:object name="plugin.calendar_list" id="calendarslist" class="treelist listing" />
</div>
<div class="boxfooter">
<roundcube:button command="calendar-create" type="link" title="calendar.createcalendar" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="calendaroptionslink" id="calendaroptionsmenulink" type="link" title="moreactions" class="listbutton groupactions" onclick="UI.show_popup('calendaroptionsmenu', undefined, { above:true });return false" innerClass="inner" content="&#9881;" />
<roundcube:button command="calendar-create" type="link" title="calendar.createcalendar" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="calendaroptionslink" id="calendaroptionsmenulink" type="link" title="moreactions" class="listbutton groupactions" onclick="return UI.toggle_popup('calendaroptionsmenu', event, { above:true })" innerClass="inner" label="calendar.calendaractions" aria-haspopup="true" aria-expanded="false" aria-owns="calendaroptionsmenu-menu" />
</div>
</div>
</div>
<div id="quicksearchbar">
<div id="quicksearchbar" class="searchbox" role="search" aria-labelledby="aria-label-searchform">
<h2 id="aria-label-searchform" class="voice"><roundcube:label name="calendar.arialabelsearchform" /></h2>
<label for="quicksearchbox" class="voice"><roundcube:label name="calendar.arialabelquicksearchbox" /></label>
<roundcube:object name="plugin.searchform" id="quicksearchbox" />
<a id="searchmenulink" class="iconbutton searchoptions" > </a>
<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
<a id="searchmenulink" class="iconbutton searchoptions" tabindex="-1"> </a>
<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" label="resetsearch" />
</div>
<div id="calendar">
<h2 id="aria-label-calendarview" class="voice"><roundcube:label name="calendar.arialabelcalendarview" /></h2>
<div id="calendar" role="main" aria-labelledby="aria-label-calendarview">
<roundcube:object name="plugin.angenda_options" class="boxfooter" id="agendaoptions" />
</div>
</div>
@ -56,18 +65,19 @@
<roundcube:object name="message" id="messagestack" />
<div id="calendaroptionsmenu" class="popupmenu">
<ul class="toolbarmenu">
<li><roundcube:button command="calendar-edit" label="calendar.edit" classAct="active" /></li>
<li><roundcube:button command="calendar-remove" label="calendar.remove" classAct="active" /></li>
<li><roundcube:button command="calendar-showurl" label="calendar.showurl" classAct="active" /></li>
<div id="calendaroptionsmenu" class="popupmenu" aria-hidden="true">
<h3 id="aria-label-calendaroptions" class="voice"><roundcube:label name="calendar.calendaractions" /></h3>
<ul id="calendaroptionsmenu-menu" class="toolbarmenu" role="menu" aria-labelledby="aria-label-calendaroptions">
<li role="menuitem"><roundcube:button command="calendar-edit" label="calendar.edit" classAct="active" /></li>
<li role="menuitem"><roundcube:button command="calendar-remove" label="calendar.remove" classAct="active" /></li>
<li role="menuitem"><roundcube:button command="calendar-showurl" label="calendar.showurl" classAct="active" /></li>
<roundcube:if condition="env:calendar_driver == 'kolab'" />
<li class="separator_above"><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
<li role="menuitem"><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
<roundcube:endif />
</ul>
</div>
<div id="eventshow" class="uidialog">
<div id="eventshow" class="uidialog" aria-hidden="true">
<h1 id="event-title">Event Title</h1>
<div class="event-section" id="event-location">Location</div>
<div class="event-section" id="event-date">From-To</div>
@ -125,14 +135,17 @@
<roundcube:include file="/templates/eventedit.html" />
<div id="eventresourcesdialog" class="uidialog">
<div id="eventresourcesdialog" class="uidialog" aria-hidden="true">
<div id="resource-dialog-left">
<div id="resource-selection" class="uibox listbox">
<div id="resource-selection" class="uibox listbox" role="navigation" aria-labelledby="aria-label-resourceselection">
<h2 class="voice" id="aria-label-resourceselection"><roundcube:label name="calendar.arialabelresourceselection" /></h2>
<div id="resourcequicksearch">
<div class="searchbox">
<div class="searchbox" role="search" aria-labelledby="aria-label-resourcesearchform" aria-controls="resources-list">
<h3 id="aria-label-resourcesearchform" class="voice"><roundcube:label name="calendar.arialabelresourcesearchform" /></h3>
<label for="resourcesearchbox" class="voice"><roundcube:label name="calendar.searchterms" /></label>
<roundcube:object name="plugin.resources_searchform" id="resourcesearchbox" />
<a id="resourcesearchmenulink" class="iconbutton searchoptions"> </a>
<roundcube:button command="reset-resource-search" id="resourcesearchreset" class="iconbutton reset" title="resetsearch" content=" " />
<roundcube:button command="reset-resource-search" id="resourcesearchreset" class="iconbutton reset" title="resetsearch" label="resetsearch" />
</div>
</div>
<div class="scroller">
@ -142,25 +155,25 @@
</div>
<div id="resource-dialog-right">
<div id="resource-info" class="uibox contentbox">
<h2 class="boxtitle"><roundcube:label name="calendar.resourcedetails" /></h2>
<div id="resource-info" class="uibox contentbox" role="region" aria-labelledby="aria-label-resourcedetails">
<h2 class="boxtitle" id="aria-label-resourcedetails"><roundcube:label name="calendar.resourcedetails" /></h2>
<div class="scroller">
<roundcube:object name="plugin.resource_info" id="resource-details" class="propform" />
<roundcube:object name="plugin.resource_info" id="resource-details" class="propform" aria-live="polite" aria-relevant="text" aria-atomic="true" />
</div>
</div>
<div id="resource-availability" class="uibox contentbox">
<h2 class="boxtitle"><roundcube:label name="calendar.resourceavailability" /></h2>
<div id="resource-availability" class="uibox contentbox" role="region" aria-labelledby="aria-label-resourceavailability">
<h2 class="boxtitle" id="aria-label-resourceavailability"><roundcube:label name="calendar.resourceavailability" /></h2>
<roundcube:object name="plugin.resource_calendar" id="resource-freebusy-calendar" />
<div class="boxpagenav">
<roundcube:button name="resource-cal-prev" id="resource-calendar-prev" type="link" class="icon prevpage" title="calendar.prevslot" content="&amp;lt;" />
<roundcube:button name="resource-cal-next" id="resource-calendar-next" type="link" class="icon nextpage" title="calendar.nextslot" content="&amp;gt;" />
<roundcube:button name="resource-cal-prev" id="resource-calendar-prev" type="link" class="icon prevpage" title="calendar.prevslot" label="calendar.prevweek" />
<roundcube:button name="resource-cal-next" id="resource-calendar-next" type="link" class="icon nextpage" title="calendar.nextslot" label="calendar.nextweek" />
</div>
</div>
</div>
</div>
<div id="eventfreebusy" class="uidialog">
<div id="eventfreebusy" class="uidialog" aria-hidden="true">
<roundcube:object name="plugin.attendees_freebusy_table" id="attendees-freebusy-table" cellpadding="0" />
<div class="schedule-options">
@ -203,7 +216,7 @@
</div>
</div>
<div id="calendarform" class="uidialog">
<div id="calendarform" class="uidialog" aria-hidden="true">
<roundcube:label name="loading" />
</div>
@ -262,6 +275,8 @@ $(document).ready(function(e){
// TODO: save state in localStorage
}
});
return false;
});
});
@ -275,8 +290,8 @@ function calendarview_splitter(p)
this.collapsed = false;
this.dragging = false;
this.threshold = 80;
this.lastpos = 0;
this._lastpos = 0;
this.lastpos = -1;
this._lastpos = -1;
this._min = p.min;
var me = this;
@ -316,6 +331,9 @@ function calendarview_splitter(p)
this.p1.resize();
this.lastpos = this.pos;
if (this._lastpos == -1)
this._lastpos = this.pos;
// also resize iframe covers
if (this.drag_active) {
$('iframe').each(function(i, elem) {

View file

@ -1,4 +1,4 @@
<div id="eventedit" class="uidialog uidialog-tabbed">
<div id="eventedit" class="uidialog uidialog-tabbed" aria-hidden="true">
<form id="eventtabs" action="#" method="post" enctype="multipart/form-data">
<ul>
<li><a href="#event-panel-summary"><roundcube:label name="calendar.tabsummary" /></a></li><li id="edit-tab-recurrence"><a href="#event-panel-recurrence"><roundcube:label name="calendar.tabrecurrence" /></a></li><li id="edit-tab-attendees"><a href="#event-panel-attendees"><roundcube:label name="calendar.tabattendees" /></a></li><li id="edit-tab-resources"><a href="#event-panel-resources"><roundcube:label name="calendar.tabresources" /></a></li><li id="edit-tab-attachments"><a href="#event-panel-attachments"><roundcube:label name="calendar.tabattachments" /></a></li>
@ -8,7 +8,7 @@
<div class="event-section">
<label for="edit-title"><roundcube:label name="calendar.title" /></label>
<br />
<input type="text" class="text" name="title" id="edit-title" size="40" />
<input type="text" class="text" name="title" id="edit-title" size="40" required="true" />
</div>
<div class="event-section">
<label for="edit-location"><roundcube:label name="calendar.location" /></label>
@ -28,21 +28,21 @@
<div class="event-section">
<label style="float:right;padding-right:0.5em"><input type="checkbox" name="allday" id="edit-allday" value="1" /><roundcube:label name="calendar.all-day" /></label>
<label for="edit-startdate"><roundcube:label name="calendar.start" /></label>
<input type="text" name="startdate" size="11" id="edit-startdate" /> &nbsp;
<input type="text" name="starttime" size="6" id="edit-starttime" />
<input type="text" name="startdate" size="11" id="edit-startdate" required="true" /> &nbsp;
<input type="text" name="starttime" size="6" id="edit-starttime" aria-label="<roundcube:label name='calendar.starttime' />" />
</div>
<div class="event-section">
<label for="edit-enddate"><roundcube:label name="calendar.end" /></label>
<input type="text" name="enddate" size="11" id="edit-enddate" /> &nbsp;
<input type="text" name="endtime" size="6" id="edit-endtime" />
<input type="text" name="enddate" size="11" id="edit-enddate" required="true" /> &nbsp;
<input type="text" name="endtime" size="6" id="edit-endtime" aria-label="<roundcube:label name='calendar.endtime' />" />
</div>
<div class="event-section" id="edit-alarms">
<div class="edit-alarm-item first">
<label><roundcube:label name="calendar.alarms" /></label>
<roundcube:object name="plugin.alarm_select" />
<label for="edit-alarm-item"><roundcube:label name="calendar.alarms" /></label>
<roundcube:object name="plugin.alarm_select" id="edit-alarm-item" />
<span class="edit-alarm-buttons">
<a href="#add" class="iconbutton add add-alarm">+</a>
<a href="#delete" class="iconbutton remove delete-alarm">-</a>
<a href="#add" class="iconbutton add add-alarm"><roundcube:label name="libcalendaring.addalarm" /></a>
<a href="#delete" class="iconbutton remove delete-alarm"><roundcube:label name="libcalendaring.removealarm" /></a>
</span>
</div>
</div>
@ -97,13 +97,15 @@
</div>
<!-- attendees list -->
<div id="event-panel-attendees">
<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table edit-attendees-table" coltitle="attendee" />
<h3 id="aria-label-attendeestable" class="voice"><roundcube:label name="calendar.arialabeleventattendees" /></h3>
<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table edit-attendees-table" coltitle="attendee" aria-labelledby="aria-label-attendeestable" />
<roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
<roundcube:include file="/templates/freebusylegend.html" />
</div>
<!-- resources list -->
<div id="event-panel-resources">
<roundcube:object name="plugin.attendees_list" id="edit-resources-table" class="records-table edit-attendees-table" coltitle="resource" />
<h3 id="aria-label-resourcestable" class="voice"><roundcube:label name="calendar.arialabeleventresources" /></h3>
<roundcube:object name="plugin.attendees_list" id="edit-resources-table" class="records-table edit-attendees-table" coltitle="resource" aria-labelledby="aria-label-resourcestable" />
<roundcube:object name="plugin.resources_form" id="edit-resources-form" />
<roundcube:include file="/templates/freebusylegend.html" />
</div>
@ -112,7 +114,8 @@
<div id="edit-attachments">
<roundcube:object name="plugin.attachments_list" id="attachment-list" class="attachmentslist" />
</div>
<div id="edit-attachments-form">
<div id="edit-attachments-form" role="region" aria-labelledby="aria-label-attachmentuploadform">
<h3 id="aria-label-attachmentuploadform" class="voice"><roundcube:label name="arialabelattachmentuploadform" /></h2>
<roundcube:object name="plugin.attachments_form" id="calendar-attachment-form" attachmentFieldSize="30" />
</div>
<roundcube:object name="plugin.filedroparea" id="event-panel-attachments" />

View file

@ -68,25 +68,26 @@ function rcube_libcalendaring(settings)
/**
* Create a nice human-readable string for the date/time range
*/
this.event_date_text = function(event)
this.event_date_text = function(event, voice)
{
if (!event.start)
return '';
if (!event.end)
event.end = event.start;
var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000;
var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000,
until = voice ? ' ' + rcmail.gettext('until','libcalendaring') + ' ' : ' — ';
if (event.allDay) {
fromto = this.format_datetime(event.start, 1)
+ (duration > 86400 || event.start.getDay() != event.end.getDay() ? ' &mdash; ' + this.format_datetime(event.end, 1) : '');
fromto = this.format_datetime(event.start, 1, voice)
+ (duration > 86400 || event.start.getDay() != event.end.getDay() ? until + this.format_datetime(event.end, 1, voice) : '');
}
else if (duration < 86400 && event.start.getDay() == event.end.getDay()) {
fromto = this.format_datetime(event.start, 0)
+ (duration > 0 ? ' &mdash; ' + this.format_datetime(event.end, 2) : '');
fromto = this.format_datetime(event.start, 0, voice)
+ (duration > 0 ? until + this.format_datetime(event.end, 2, voice) : '');
}
else {
fromto = this.format_datetime(event.start, 0)
+ (duration > 0 ? ' &mdash; ' + this.format_datetime(event.end, 0) : '');
fromto = this.format_datetime(event.start, 0, voice)
+ (duration > 0 ? until + this.format_datetime(event.end, 0, voice) : '');
}
return fromto;
@ -178,15 +179,18 @@ function rcube_libcalendaring(settings)
/**
* Format the given date object according to user's prefs
*/
this.format_datetime = function(date, mode)
this.format_datetime = function(date, mode, voice)
{
var res = '';
if (!mode || mode == 1)
res += $.datepicker.formatDate(datepicker_settings.dateFormat, date, datepicker_settings);
if (!mode)
res += ' ';
if (!mode || mode == 2)
res += this.format_time(date);
if (!mode || mode == 1) {
res += $.datepicker.formatDate(voice ? 'MM d yy' : datepicker_settings.dateFormat, date, datepicker_settings);
}
if (!mode) {
res += voice ? ' ' + rcmail.gettext('at','libcalendaring') + ' ' : ' ';
}
if (!mode || mode == 2) {
res += this.format_time(date, voice);
}
return res;
}
@ -194,7 +198,7 @@ function rcube_libcalendaring(settings)
/**
* Clone from fullcalendar.js
*/
this.format_time = function(date)
this.format_time = function(date, voice)
{
var zeroPad = function(n) { return (n < 10 ? '0' : '') + n; }
var formatters = {
@ -212,7 +216,8 @@ function rcube_libcalendaring(settings)
TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }
};
var i, i2, c, formatter, res = '', format = settings['time_format'];
var i, i2, c, formatter, res = '',
format = voice ? settings['time_format'].replace(':',' ').replace('HH','H').replace('hh','h').replace('mm','m').replace('ss','s') : settings['time_format'];
for (i=0; i < format.length; i++) {
c = format.charAt(i);
for (i2=Math.min(i+2, format.length); i2 > i; i2--) {
@ -316,10 +321,13 @@ function rcube_libcalendaring(settings)
.replace(/\n/g, "<br/>");
};
this.init_alarms_edit = function(prefix)
this.init_alarms_edit = function(prefix, index)
{
var edit_type = $(prefix+' select.edit-alarm-type'),
dom_id = edit_type.attr('id');
// register events on alarm fields
$(prefix+' select.edit-alarm-type').change(function(){
edit_type.change(function(){
$(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')]();
});
$(prefix+' select.edit-alarm-offset').change(function(){
@ -337,13 +345,20 @@ function rcube_libcalendaring(settings)
return false;
});
// set a unique id attribute and set label reference accordingly
if ((index || 0) > 0 && dom_id) {
dom_id += ':' + (new Date().getTime());
edit_type.attr('id', dom_id);
$(prefix+' label:first').attr('for', dom_id);
}
$(prefix).on('click', 'a.add-alarm', function(e){
var i = $(this).closest('.edit-alarm-item').siblings().length + 1;
var item = $(this).closest('.edit-alarm-item').clone(false)
.removeClass('first')
.appendTo(prefix);
me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')');
me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i);
$('select.edit-alarm-type, select.edit-alarm-offset', item).change();
return false;
});
@ -364,7 +379,7 @@ function rcube_libcalendaring(settings)
}
else {
domnode = $(prefix + ' .edit-alarm-item').eq(0).clone(false).removeClass('first').appendTo(prefix);
this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')');
this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i);
}
$('select.edit-alarm-type', domnode).val(alarm.action);

View file

@ -288,7 +288,7 @@ class libcalendaring extends rcube_plugin
public function alarm_select($attrib, $alarm_types, $absolute_time = true)
{
unset($attrib['name']);
$select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type'));
$select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type', 'id' => $attrib['id']));
$select_type->add($this->gettext('none'), '');
foreach ($alarm_types as $type)
$select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
@ -991,6 +991,7 @@ class libcalendaring extends rcube_plugin
'class' => 'delete',
'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", JS_OBJECT_NAME, $id),
'title' => $this->rc->gettext('delete'),
'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'],
), $button);
$content .= Q($attachment['name']);

View file

@ -2,6 +2,10 @@
$labels = array();
// words for spoken dates
$labels['until'] = 'until';
$labels['at'] = 'at';
// alarms related labels
$labels['alarmemail'] = 'Send Email';
$labels['alarmdisplay'] = 'Show message';
@ -18,7 +22,8 @@ $labels['trigger+M'] = 'minutes after';
$labels['trigger+H'] = 'hours after';
$labels['trigger+D'] = 'days after';
$labels['triggerattime'] = 'at time';
$labels['addalarm'] = 'add alarm';
$labels['addalarm'] = 'Add alarm';
$labels['removealarm'] = 'Remove alarm';
$labels['alarmtitle'] = 'Upcoming events';
$labels['dismissall'] = 'Dismiss all';

View file

@ -46,8 +46,9 @@ function kolab_folderlist(node, p)
if (results.length) {
// create treelist widget to present the search results
if (!search_results_widget) {
var list_id = (me.container.attr('id') || p.id_prefix || '0')
search_results_container = $('<div class="searchresults"></div>')
.html(p.search_title ? '<h2 class="boxtitle">' + p.search_title + '</h2>' : '')
.html(p.search_title ? '<h2 class="boxtitle" id="st:' + list_id + '">' + p.search_title + '</h2>' : '')
.insertAfter(me.container);
search_results_widget = new rcube_treelist_widget('<ul>', {
@ -55,7 +56,7 @@ function kolab_folderlist(node, p)
selectable: false
});
// copy classes from main list
search_results_widget.container.addClass(me.container.attr('class'));
search_results_widget.container.addClass(me.container.attr('class')).attr('aria-labelledby', 'st:' + list_id);
// register click handler on search result's checkboxes to select the given item for listing
search_results_widget.container
@ -87,6 +88,11 @@ function kolab_folderlist(node, p)
else {
li.remove();
}
// set focus to cloned checkbox
if (rcube_event.is_keyboard(e)) {
$(me.get_item(id, true)).find('input[type=checkbox]').first().focus();
}
});
}
@ -178,7 +184,7 @@ function kolab_folderlist(node, p)
}
if (listsearch_request) {
// ignore, let the currently runnung sequest finish
// ignore, let the currently running request finish
if (listsearch_request.query == search.query) {
return;
}
@ -196,7 +202,10 @@ function kolab_folderlist(node, p)
postdata: { action:'search', q:search.query, source:'%s' },
lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
onresponse: render_search_results,
whendone: function(e){ listsearch_request = null; }
whendone: function(data){
listsearch_request = null;
me.triggerEvent('search-complete', data);
}
});
listsearch_request = { id:reqid, query:search.query };