⚝
One Hat Cyber Team
⚝
Your IP:
216.73.216.50
Server IP:
41.128.143.86
Server:
Linux host.raqmix.cloud 6.8.0-1025-azure #30~22.04.1-Ubuntu SMP Wed Mar 12 15:28:20 UTC 2025 x86_64
Server Software:
Apache
PHP Version:
8.3.23
Buat File
|
Buat Folder
Eksekusi
Dir :
~
/
usr
/
share
/
psa-horde
/
kronolith
/
js
/
Edit File: kronolith.js
/** * kronolith.js - Base application logic. * * TODO: loadingImg() * * Copyright 2008-2017 Horde LLC (http://www.horde.org/) * * See the enclosed file COPYING for license information (GPL). If you * did not receive this file, see http://www.horde.org/licenses/gpl. * * @author Jan Schneider <jan@horde.org> */ /* Kronolith object. */ KronolithCore = { // Vars used and defaulting to null/false: // weekSizes, daySizes, // groupLoading, colorPicker, duration, timeMarker, monthDays, // allDays, eventsWeek, initialized view: '', ecache: $H(), cacheStart: null, cacheEnd: null, holidays: [], tcache: $H(), eventsLoading: {}, loading: 0, viewLoading: [], fbLoading: 0, redBoxLoading: false, date: Date.today(), tasktype: 'incomplete', knl: {}, wrongFormat: $H(), mapMarker: null, map: null, mapInitialized: false, freeBusy: $H(), search: 'future', effectDur: 0.4, macos: navigator.appVersion.indexOf('Mac') != -1, orstart: null, orend: null, lastRecurType: 'None', uatts: null, ucb: null, resourceACCache: { choices: [], map: $H() }, paramsCache: null, attendees: [], resources: [], /** * Flag that indicates if the event currently displayed in the event * properties window is a recurring event. * * @type boolean */ recurs: false, /** * The location that was open before the current location. * * @var string */ lastLocation: '', /** * The currently open location. * * @var string */ openLocation: '', /** * The current (main) location. * * This is different from openLocation as it isn't updated for any * locations that are opened in a popup view, e.g. events. * * @var string */ currentLocation: '', kronolithBody: $('kronolithBody'), onException: function(parentfunc, r, e) { /* Make sure loading images are closed. */ this.loading--; if (!this.loading) { $('kronolithLoading').hide(); } this.closeRedBox(); HordeCore.notify(HordeCore.text.ajax_error, 'horde.error'); parentfunc(r, e); }, setTitle: function(title) { document.title = Kronolith.conf.name + ' :: ' + title; return title; }, // url = (string) URL to redirect to // hash = (boolean) If true, url is treated as hash information to alter // on the current page redirect: function(url, hash) { if (hash) { window.location.hash = escape(url); window.location.reload(); } else { HordeCore.redirect(url); } }, go: function(fullloc, data) { if (!this.initialized) { this.go.bind(this, fullloc, data).defer(); return; } if (this.viewLoading.size()) { this.viewLoading.push([ fullloc, data ]); return; } var locParts = fullloc.split(':'); var loc = locParts.shift(); if (this.openLocation == fullloc) { return; } this.viewLoading.push([ fullloc, data ]); if (loc != 'search') { HordeTopbar.searchGhost.reset(); } this.switchTaskView(false); switch (loc) { case 'day': case 'week': case 'workweek': case 'month': case 'year': case 'agenda': case 'tasks': this.closeView(loc); var locCap = loc.capitalize(); $('kronolithNav' + locCap).up().addClassName('horde-active'); switch (loc) { case 'day': case 'agenda': case 'week': case 'workweek': case 'month': case 'year': var date = locParts.shift(); if (date) { date = this.parseDate(date); } else { date = this.date; } if (this.view != 'agenda' && this.view == loc && date.getYear() == this.date.getYear() && ((loc == 'year') || (loc == 'month' && date.getMonth() == this.date.getMonth()) || ((loc == 'week' || loc == 'workweek') && date.getRealWeek() == this.date.getRealWeek()) || ((loc == 'day' || loc == 'agenda') && date.dateString() == this.date.dateString()))) { this.setViewTitle(date, loc); this.addHistory(fullloc); this.loadNextView(); return; } this.addHistory(fullloc); this.view = loc; this.date = date; this.updateView(date, loc); var dates = this.viewDates(date, loc); this.loadEvents(dates[0], dates[1], loc); $('kronolithView' + locCap).appear({ duration: this.effectDur, queue: 'end', afterFinish: function() { if (loc == 'week' || loc == 'workweek' || loc == 'day') { this.calculateRowSizes(loc + 'Sizes', 'kronolithView' + locCap); if ($('kronolithTimeMarker')) { this.positionTimeMarker(); } if ($('kronolithTimeMarker')) { $('kronolithTimeMarker').show(); } // Scroll to the work day start time. $('kronolithView' + locCap).down('.kronolithViewBody').scrollTop = 9 * this[loc + 'Sizes'].height; } this.loadNextView(); }.bind(this) }); $('kronolithLoading' + loc).insert($('kronolithLoading').remove()); this.updateMinical(date, loc); break; case 'tasks': var tasktype = locParts.shift() || this.tasktype; this.switchTaskView(true); $('kronolithCurrent') .update(this.setTitle(Kronolith.text.tasks)); if (this.view == loc && this.tasktype == tasktype) { this.addHistory(fullloc); this.loadNextView(); return; } if (!$w('all complete incomplete future').include(tasktype)) { this.loadNextView(); return; } this.addHistory(fullloc); this.view = loc; this.tasktype = tasktype; $w('All Complete Incomplete Future').each(function(tasktype) { $('kronolithTasks' + tasktype).up().removeClassName('horde-active'); }); $('kronolithTasks' + this.tasktype.capitalize()).up().addClassName('horde-active'); this.loadTasks(this.tasktype); $('kronolithView' + locCap).appear({ duration: this.effectDur, queue: 'end', afterFinish: function() { this.loadNextView(); }.bind(this) }); $('kronolithLoading' + loc).insert($('kronolithLoading').remove()); this.updateMinical(this.date); break; default: if (!$('kronolithView' + locCap)) { break; } this.addHistory(fullloc); this.view = loc; $('kronolithView' + locCap).appear({ duration: this.effectDur, queue: 'end', afterFinish: function() { this.loadNextView(); }.bind(this) }); break; } break; case 'search': var cals = [], time = locParts[0], term = locParts[1], query = Object.toJSON({ title: term }); if (!($w('all past future').include(time))) { this.loadNextView(); return; } this.addHistory(fullloc); this.search = time; $w('All Past Future').each(function(time) { $('kronolithSearch' + time).up().removeClassName('horde-active'); }); $('kronolithSearch' + this.search.capitalize()).up().addClassName('horde-active'); this.closeView('agenda'); this.view = 'agenda'; this.updateView(null, 'search', term); $H(Kronolith.conf.calendars).each(function(type) { $H(type.value).each(function(calendar) { if (calendar.value.show) { cals.push(type.key + '|' + calendar.key); } }); }); $('kronolithAgendaNoItems').hide(); this.startLoading('search', query); HordeCore.doAction('searchEvents', { cals: Object.toJSON(cals), query: query, time: this.search }, { callback: function(r) { // Hide spinner. this.loading--; if (!this.loading) { $('kronolithLoading').hide(); } if (r.view != 'search' || r.query != this.eventsLoading.search) { return; } if (Object.isUndefined(r.events)) { $('kronolithAgendaNoItems').show(); return; } delete this.eventsLoading.search; $H(r.events).each(function(calendars) { $H(calendars.value).each(function(events) { this.createAgendaDay(events.key); $H(events.value).each(function(event) { event.value.calendar = calendars.key; event.value.start = Date.parse(event.value.s); event.value.end = Date.parse(event.value.e); this.insertEvent(event, events.key, 'agenda'); }, this); }, this); }, this); }.bind(this) }); $('kronolithViewAgenda').appear({ duration: this.effectDur, queue: 'end', afterFinish: function() { this.loadNextView(); }.bind(this) }); $('kronolithLoadingagenda').insert($('kronolithLoading').remove()); this.updateMinical(this.date); break; case 'event': // Load view first if necessary. if (!this.view ) { this.viewLoading.pop(); this.go(Kronolith.conf.login_view); this.go.bind(this, fullloc, data).defer(); return; } if (this.currentLocation == fullloc) { this.loadNextView(); return; } this.addHistory(fullloc, false); switch (locParts.length) { case 0: // New event. this.editEvent(); break; case 1: // New event on a certain date. this.editEvent(null, null, locParts[0]); break; default: // Editing event. var date = locParts.pop(), event = locParts.pop(), calendar = locParts.join(':'); this.editEvent(calendar, event, date); break; } this.loadNextView(); break; case 'task': // Load view first if necessary. if (!this.view ) { this.viewLoading.pop(); this.go('tasks'); this.go.bind(this, fullloc, data).defer(); return; } this.switchTaskView(true); switch (locParts.length) { case 0: this.addHistory(fullloc, false); this.editTask(); break; case 2: this.addHistory(fullloc, false); this.editTask(locParts[0], locParts[1]); break; } this.loadNextView(); break; case 'calendar': if (!this.view) { this.viewLoading.pop(); this.go(Kronolith.conf.login_view); this.go.bind(this, fullloc, data).defer(); return; } this.addHistory(fullloc, false); this.editCalendar(locParts.join(':')); this.loadNextView(); break; default: this.loadNextView(); break; } }, /** * Removes the last loaded view from the stack and loads the last added * view, if the stack is still not empty. * * We want to load views from a LIFO queue, because the queue is only * building up if the user switches to another view while the current view * still loads. In that case we can go directly to the most recently * clicked view and drop the remaining queue. */ loadNextView: function() { var current = this.viewLoading.shift(); if (this.viewLoading.size()) { var next = this.viewLoading.pop(); this.viewLoading = []; if (current[0] != next[0] || current[1] || next[1]) { this.go(next[0], next[1]); } } }, /** * Rebuilds one of the calendar views for a new date. * * @param Date date The date to show in the calendar. * @param string view The view that's rebuilt. * @param mixed data Any additional data that might be required. */ updateView: function(date, view, data) { this.holidays = []; switch (view) { case 'day': var today = Date.today(); this.dayEvents = []; this.dayGroups = []; this.allDayEvents = []; $('kronolithCurrent') .update(this.setViewTitle(date, view, data)); $('kronolithViewDay') .down('.kronolithAllDayContainer') .store('date', date.dateString()); $('kronolithEventsDay').store('date', date.dateString()); if (date.equals(today)) { this.addTimeMarker('kronolithEventsDay'); } break; case 'week': case 'workweek': this.dayEvents = []; this.dayGroups = []; this.allDayEvents = []; this.allDays = {}; this.eventsWeek = {}; var what = view == 'week' ? 'Week' : 'Workweek', div = $('kronolithEvents' + what).down('div'), th = $('kronolithView' + what + 'Head').down('.kronolithWeekDay'), td = $('kronolithView' + what + 'Head').down('tbody td').next('td'), hourRow = $('kronolithView' + what + 'Body').down('tr'), dates = this.viewDates(date, view), today = Date.today(), day, dateString, i, hourCol; $('kronolithCurrent') .update(this.setViewTitle(date, view, data)); for (i = 0; i < 24; i++) { day = dates[0].clone(); hourCol = hourRow.down('td').next('td'); while (hourCol) { hourCol.removeClassName('kronolith-today'); if (day.equals(today)) { hourCol.addClassName('kronolith-today'); } hourCol = hourCol.next('td'); day.next().day(); } hourRow = hourRow.next('tr'); } day = dates[0].clone(); for (i = 0; i < (view == 'week' ? 7 : 5); i++) { dateString = day.dateString(); this.allDays['kronolithAllDay' + dateString] = td.down('div'); this.eventsWeek['kronolithEvents' + what + dateString] = div; div.store('date', dateString) .writeAttribute('id', 'kronolithEvents' + what + dateString); th.store('date', dateString) .down('span').update(day.toString('dddd, d')); td.removeClassName('kronolith-today'); this.allDays['kronolithAllDay' + dateString] .writeAttribute('id', 'kronolithAllDay' + dateString) .store('date', dateString); if (day.equals(today)) { td.addClassName('kronolith-today'); this.addTimeMarker('kronolithEvents' + what + dateString); } new Drop(td.down('div')); div = div.next('div'); th = th.next('td'); td = td.next('td'); day.next().day(); } break; case 'month': var tbody = $('kronolith-month-body'), dates = this.viewDates(date, view), day = dates[0].clone(); $('kronolithCurrent') .update(this.setViewTitle(date, view, data)); // Remove old rows. Maybe we should only rebuild the calendars if // necessary. tbody.childElements().each(function(row) { if (row.identify() != 'kronolithRowTemplate') { row.purge(); row.remove(); } }); // Build new calendar view. this.monthDays = {}; while (!day.isAfter(dates[1])) { tbody.insert(this.createWeekRow(day, date.getMonth(), dates).show()); day.next().week(); } this.equalRowHeights(tbody); break; case 'year': var month; $('kronolithCurrent').update(this.setViewTitle(date, view, data)); // Build new calendar view. for (month = 0; month < 12; month++) { $('kronolithYear' + month).update(this.createYearMonth(date.getFullYear(), month, 'kronolithYear').show()); } break; case 'agenda': case 'search': // Agenda days are only created on demand, if there are any events // to add. if (view == 'agenda') { var dates = this.viewDates(date, view); $('kronolithCurrent') .update(this.setViewTitle(date, view, data)); $('kronolithSearchNavigation').up().up().hide(); } else { $('kronolithCurrent') .update(this.setViewTitle(date, view, data)); $('kronolithSearchNavigation').up().up().show(); } // Remove old rows. Maybe we should only rebuild the calendars if // necessary. tbody = $('kronolithViewAgendaBody').childElements().each(function(row) { if (row.identify() != 'kronolithAgendaTemplate' && row.identify() != 'kronolithAgendaNoItems') { row.purge(); row.remove(); } }); break; } }, /** * Sets the browser title of the calendar views. * * @param Date date The date to show in the calendar. * @param string view The view that's displayed. * @param mixed data Any additional data that might be required. */ setViewTitle: function(date, view, data) { switch (view) { case 'day': return this.setTitle(date.toString('D')); case 'week': case 'workweek': var dates = this.viewDates(date, view); return this.setTitle(dates[0].toString(Kronolith.conf.date_format) + ' - ' + dates[1].toString(Kronolith.conf.date_format)); case 'month': return this.setTitle(date.toString('MMMM yyyy')); case 'year': return this.setTitle(date.toString('yyyy')); case 'agenda': var dates = this.viewDates(date, view); return this.setTitle(dates[0].toString(Kronolith.conf.date_format) + ' - ' + dates[1].toString(Kronolith.conf.date_format)); case 'search': return this.setTitle(Kronolith.text.searching.interpolate({ term: data })).escapeHTML(); } }, /** * Closes the currently active view. */ closeView: function(loc) { $w('Day Workweek Week Month Year Tasks Agenda').each(function(a) { a = $('kronolithNav' + a); if (a) { a.up().removeClassName('horde-active'); } }); if (this.view && this.view != loc) { $('kronolithView' + this.view.capitalize()).fade({ duration: this.effectDur, queue: 'end' }); this.view = null; } }, /** * Creates a single row of day cells for usage in the month and multi-week * views. * * @param Date date The first day to show in the row. * @param integer month The current month. Days not from the current * month get the kronolith-other-month CSS class * assigned. * @param array viewDates Array of Date objects with the start and end * dates of the view. * * @return Element The element rendering a week row. */ createWeekRow: function(date, month, viewDates) { var day = date.clone(), today = new Date().dateString(), row, cell, dateString; // Create a copy of the row template. row = $('kronolithRowTemplate').clone(true); row.removeAttribute('id'); // Fill week number and day cells. cell = row.down() .setText(date.getRealWeek()) .store('date', date.dateString()) .next(); while (cell) { dateString = day.dateString(); this.monthDays['kronolithMonthDay' + dateString] = cell; cell.id = 'kronolithMonthDay' + dateString; cell.store('date', dateString); cell.removeClassName('kronolith-other-month').removeClassName('kronolith-today'); if (day.getMonth() != month) { cell.addClassName('kronolith-other-month'); } if (dateString == today) { cell.addClassName('kronolith-today'); } new Drop(cell); cell.store('date', dateString) .down('.kronolith-day') .store('date', dateString) .update(day.getDate()); cell = cell.next(); day.add(1).day(); } return row; }, /** * Creates a table row for a single day in the agenda view, if it doesn't * exist yet. * * @param string date The day to show in the row. * * @return Element The element rendering a week row. */ createAgendaDay: function(date) { // Exit if row exists already. if ($('kronolithAgendaDay' + date)) { return; } // Create a copy of the row template. var body = $('kronolithViewAgendaBody'), row = $('kronolithAgendaTemplate').clone(true); // Fill week number and day cells. row.store('date', date) .down() .setText(this.parseDate(date).toString('D')) .next() .writeAttribute('id', 'kronolithAgendaDay' + date); row.removeAttribute('id'); // Insert row. var nextRow; body.childElements().each(function(elm) { if (elm.retrieve('date') > date) { nextRow = elm; throw $break; } }); if (nextRow) { nextRow.insert({ before: row.show() }); } else { body.insert(row.show()); } return row; }, /** * Creates a table for a single month in the year view. * * @param integer year The year. * @param integer month The month. * @param string idPrefix If present, each day will get a DOM ID with this * prefix * * @return Element The element rendering a month table. */ createYearMonth: function(year, month, idPrefix) { // Create a copy of the month template. var table = $('kronolithYearTemplate').clone(true), tbody = table.down('tbody'); table.removeAttribute('id'); tbody.writeAttribute('id', 'kronolithYearTable' + month); // Set month name. table.down('tr.kronolith-minical-nav th') .store('date', year.toPaddedString(4) + (month + 1).toPaddedString(2) + '01') .update(Date.CultureInfo.monthNames[month]); // Build month table. this.buildMinical(tbody, new Date(year, month, 1), null, idPrefix, year); return table; }, equalRowHeights: function(tbody) { var children = tbody.childElements(); children.invoke('setStyle', { height: (100 / (children.size() - 1)) + '%' }); }, /** * Calculates some dimensions for the day and week view. * * @param string storage Property name where the dimensions are stored. * @param string view DOM node ID of the view. */ calculateRowSizes: function(storage, view) { if (!Object.isUndefined(this[storage])) { return; } var td = $(view).down('.kronolithViewBody tr td').next('td'), layout = td.getLayout(), spacing = td.up('table').getStyle('borderSpacing'); // FIXME: spacing is hardcoded for IE 7 because it doesn't know about // border-spacing, but still uses it. WTF? spacing = spacing ? parseInt($w(spacing)[1], 10) : 2; this[storage] = {}; this[storage].height = layout.get('margin-box-height') + spacing; this[storage].spacing = this[storage].height - layout.get('padding-box-height') - layout.get('border-bottom'); }, /** * Adds a horizontal ruler representing the current time to the specified * container. * * @param string|Element The container of the current day. */ addTimeMarker: function(container) { if ($('kronolithTimeMarker')) { $('kronolithTimeMarker').remove(); this.timeMarker.stop(); } $(container).insert(new Element('div', { id: 'kronolithTimeMarker' }).setStyle({ position: 'absolute' }).hide()); this.timeMarker = new PeriodicalExecuter(this.positionTimeMarker.bind(this), 60); }, /** * Updates the horizontal ruler representing the current time. */ positionTimeMarker: function() { var today = Date.today(), now; switch (this.view) { case 'day': if (!this.date.equals(today)) { $('kronolithTimeMarker').remove(); this.timeMarker.stop(); return; } break; case 'week': case 'workweek': if ($('kronolithTimeMarker').up().retrieve('date') != today.dateString()) { var newContainer = this.eventsWeek['kronolithEvents' + (this.view == 'week' ? 'Week' : 'Workweek') + today.dateString()]; $('kronolithTimeMarker').remove(); if (newContainer) { this.addTimeMarker(newContainer); } else { this.timeMarker.stop(); } return; } break; default: $('kronolithTimeMarker').remove(); this.timeMarker.stop(); return; } now = new Date(); $('kronolithTimeMarker').setStyle({ top: ((now.getHours() * 60 + now.getMinutes()) * this[this.view + 'Sizes'].height / 60 | 0) + 'px' }); }, /** * Rebuilds the mini calendar. * * @param Date date The date to show in the calendar. * @param string view The view that's displayed, determines which days in * the mini calendar are highlighted. */ updateMinical: function(date, view) { // Update header. $('kronolithMinicalDate') .store('date', date.dateString()) .update(date.toString('MMMM yyyy')); this.buildMinical($('kronolith-minical').down('tbody'), date, view); }, /** * Creates a mini calendar suitable for the navigation calendar and the * year view. * * @param Element tbody The table body to add the days to. * @param Date date The date to show in the calendar. * @param string view The view that's displayed, determines which days * in the mini calendar are highlighted. * @param string idPrefix If present, each day will get a DOM ID with this * prefix * @param integer year If present, generating mini calendars for the * year view of this year. */ buildMinical: function(tbody, date, view, idPrefix, year) { var dates = this.viewDates(date, 'month'), day = dates[0].clone(), date7 = date.clone().add(1).week(), today = Date.today(), week = this.viewDates(this.date, 'week'), workweek = this.viewDates(this.date, 'workweek'), dateString, td, tr, i; // Remove old calendar rows. Maybe we should only rebuild the minical // if necessary. tbody.childElements().invoke('remove'); for (i = 0; i < 42; i++) { dateString = day.dateString(); // Create calendar row and insert week number. if (day.getDay() == Kronolith.conf.week_start) { tr = new Element('tr'); tbody.insert(tr); td = new Element('td', { className: 'kronolith-minical-week' }) .store('weekdate', dateString); td.update(day.getRealWeek()); tr.insert(td); weekStart = day.clone(); weekEnd = day.clone(); weekEnd.add(6).days(); } // Insert day cell. td = new Element('td').store('date', dateString); if (day.getMonth() != date.getMonth()) { td.addClassName('kronolith-other-month'); } else if (!Object.isUndefined(idPrefix)) { td.id = idPrefix + dateString; } // Highlight days currently being displayed. if (view && ((view == 'month' && this.date.between(dates[0], dates[1])) || (view == 'week' && day.between(week[0], week[1])) || (view == 'workweek' && day.between(workweek[0], workweek[1])) || (view == 'day' && day.equals(this.date)) || (view == 'agenda' && !day.isBefore(date) && day.isBefore(date7)))) { td.addClassName('kronolith-selected'); } // Highlight today. if (day.equals(today) && (Object.isUndefined(year) || (day.getYear() + 1900 == year && date.getMonth() == day.getMonth()))) { td.addClassName('kronolith-today'); } td.insert(new Element('a').update(day.getDate())); tr.insert(td); day.next().day(); } }, /** * Inserts a calendar entry in the sidebar menu. * * @param string type The calendar type. * @param string id The calendar id. * @param object cal The calendar object. * @param Element div Container DIV where to add the entry (optional). */ insertCalendarInList: function(type, id, cal, div) { var noItems, calendar, link; if (!div) { div = this.getCalendarList(type, cal.owner); } noItems = div.previous(); if (noItems && noItems.tagName == 'DIV' && noItems.className == 'horde-info') { noItems.hide(); } link = new Element('span', { className: type != 'resourcegroup' ? (cal.show ? 'horde-resource-on' : 'horde-resource-off') : 'horde-resource-none' }) .insert(cal.name.escapeHTML()); calendar = new Element('div') .store('calendar', id) .store('calendarclass', type) .setStyle({ backgroundColor: cal.bg, color: cal.fg }); if (type != 'holiday' && type != 'external') { calendar.insert( new Element('span', { className: 'horde-resource-edit-' + cal.fg.substring(1) }) .setStyle({ backgroundColor: cal.bg, color: cal.fg }) .insert('►')); } calendar.insert( new Element('div', { className: 'horde-resource-link' }) .insert(link)); this.addShareIcon(cal, link); div.insert(calendar); if (cal.show) { this.addCalendarLegend(type, id, cal); } }, /** * Add the share icon after the calendar name in the calendar list. * * @param object cal A calendar object from Kronolith.conf.calendars. * @param Element element The calendar element in the list. */ addShareIcon: function(cal, element) { if (cal.owner && cal.perms) { $H(cal.perms).each(function(perm) { if (perm.key != 'type' && ((Object.isArray(perm.value) && perm.value.size()) || (!Object.isArray(perm.value) && perm.value))) { element.insert(' ').insert(new Element('img', { src: Kronolith.conf.images.attendees.replace(/fff/, cal.fg.substring(1)), title: Kronolith.text.shared })); throw $break; } }); } }, /** * Rebuilds the list of calendars. */ updateCalendarList: function() { var ext = $H(), extNames = $H(), extContainer = $('kronolithExternalCalendars'); $H(Kronolith.conf.calendars.internal).each(function(cal) { this.insertCalendarInList('internal', cal.key, cal.value); }, this); if (Kronolith.conf.tasks) { $H(Kronolith.conf.calendars.tasklists).each(function(cal) { this.insertCalendarInList('tasklists', cal.key, cal.value); }, this); } if (Kronolith.conf.calendars.resource) { $H(Kronolith.conf.calendars.resource).each(function(cal) { this.insertCalendarInList('resource', cal.key, cal.value); }, this); } if (Kronolith.conf.calendars.resourcegroup) { $H(Kronolith.conf.calendars.resourcegroup).each(function(cal) { this.insertCalendarInList('resourcegroup', cal.key, cal.value); }, this); } $H(Kronolith.conf.calendars.external).each(function(cal) { var parts = cal.key.split('/'), api = parts.shift(); if (!ext.get(api)) { ext.set(api, $H()); } ext.get(api).set(parts.join('/'), cal.value); extNames.set(api, cal.value.api ? cal.value.api : Kronolith.text.external_category); }); ext.each(function(api) { extContainer .insert(new Element('div', { className: 'horde-sidebar-split' })) .insert(new Element('div') .insert(new Element('h3') .insert(new Element('span', { className: 'horde-expand', title: HordeSidebar.text.expand }) .insert({ bottom: extNames.get(api.key).escapeHTML() }))) .insert(new Element('div', { id: 'kronolithExternalCalendar' + api.key, className: 'horde-resources', style: 'display:none' }))); api.value.each(function(cal) { this.insertCalendarInList('external', api.key + '/' + cal.key, cal.value, $('kronolithExternalCalendar' + api.key)); }, this); }, this); $H(Kronolith.conf.calendars.remote).each(function(cal) { this.insertCalendarInList('remote', cal.key, cal.value); }, this); if (Kronolith.conf.calendars.holiday) { $H(Kronolith.conf.calendars.holiday).each(function(cal) { if (cal.value.show) { this.insertCalendarInList('holiday', cal.key, cal.value); } }, this); } else { $('kronolithAddholiday').up().hide(); $('kronolithHolidayCalendars').hide(); } }, /** * Returns the DIV container that holds all calendars of a certain type. * * @param string type A calendar type * * @return Element The container of the calendar type. */ getCalendarList: function(type, personal) { switch (type) { case 'internal': return personal ? $('kronolithMyCalendars') : $('kronolithSharedCalendars'); case 'resource': return $('kronolithResourceCalendars'); case 'resourcegroup': return $('kronolithResourceGroups'); case 'tasklists': return personal ? $('kronolithMyTasklists') : $('kronolithSharedTasklists'); case 'external': return $('kronolithExternalCalendars'); case 'remote': return $('kronolithRemoteCalendars'); case 'holiday': return $('kronolithHolidayCalendars'); } }, /** * Loads a certain calendar, if the current view is still a calendar view. * * @param string type The calendar type. * @param string calendar The calendar id. */ loadCalendar: function(type, calendar) { if (Kronolith.conf.calendars[type][calendar].show && $w('day workweek week month year agenda').include(this.view)) { var dates = this.viewDates(this.date, this.view); this.deleteCache([type, calendar]); this.loadEvents(dates[0], dates[1], this.view, [[type, calendar]]); } }, /** * Toggles a calendars visibility. * * @param string type The calendar type. * @param string calendar The calendar id. */ toggleCalendar: function(type, calendar) { var elt = $('kronolithMenuCalendars').select('div').find(function(div) { return div.retrieve('calendarclass') == type && div.retrieve('calendar') == calendar; }).down('.horde-resource-link').down('span'); Kronolith.conf.calendars[type][calendar].show = !Kronolith.conf.calendars[type][calendar].show; elt.toggleClassName('horde-resource-on'); elt.toggleClassName('horde-resource-off'); if (Kronolith.conf.calendars[type][calendar].show) { this.addCalendarLegend(type, calendar, Kronolith.conf.calendars[type][calendar]); } else { this.deleteCalendarLegend(type, calendar); } switch (this.view) { case 'month': case 'agenda': if (Object.isUndefined(this.ecache.get(type)) || Object.isUndefined(this.ecache.get(type).get(calendar))) { this.loadCalendar(type, calendar); } else { var allEvents = this.kronolithBody.select('div').findAll(function(el) { return el.retrieve('calendar') == type + '|' + calendar; }); if (this.view == 'month' && Kronolith.conf.max_events) { var dates = this.viewDates(this.date, this.view); if (elt.hasClassName('horde-resource-off')) { var day, more, events, calendars = []; $H(Kronolith.conf.calendars).each(function(type) { $H(type.value).each(function(cal) { if (cal.value.show) { calendars.push(type.key + '|' + cal.key); } }); }); allEvents.each(function(el) { if (el.retrieve('calendar').startsWith('holiday|')) { this.holidays = this.holidays.without(el.retrieve('eventid')); } el.remove(); }, this); for (var date = dates[0]; !date.isAfter(dates[1]); date.add(1).days()) { day = this.monthDays['kronolithMonthDay' + date.dateString()]; more = day.select('.kronolithMore'); events = day.select('.kronolith-event'); if (more.size() && events.size() < Kronolith.conf.max_events) { more[0].purge(); more[0].remove(); events.invoke('remove'); calendars.each(function(calendar) { this.insertEvents([date, date], 'month', calendar); }, this); } } } else { this.insertEvents(dates, 'month', type + '|' + calendar); } } else { allEvents.invoke('toggle'); } } break; case 'year': case 'week': case 'workweek': case 'day': if (Object.isUndefined(this.ecache.get(type)) || Object.isUndefined(this.ecache.get(type).get(calendar))) { this.loadCalendar(type, calendar); } else { this.insertEvents(this.viewDates(this.date, this.view), this.view); } break; case 'tasks': if (type != 'tasklists') { break; } var tasklist = calendar.substr(6); if (elt.hasClassName('horde-resource-off')) { $('kronolithViewTasksBody').select('tr').findAll(function(el) { return el.retrieve('tasklist') == tasklist; }).invoke('remove'); } else { this.loadTasks(this.tasktype, [ tasklist ]); } break; } if ($w('tasklists remote external holiday resource').include(type)) { calendar = type + '_' + calendar; } HordeCore.doAction('saveCalPref', { toggle_calendar: calendar }); }, /** * Propagates a SELECT drop down list with the editable calendars. * * @param string id The id of the SELECT element. */ updateCalendarDropDown: function(id) { $(id).update(); ['internal', 'remote'].each(function(type) { $H(Kronolith.conf.calendars[type]).each(function(cal) { if (cal.value.edit) { $(id).insert(new Element('option', { value: type + '|' + cal.key }) .setStyle({ backgroundColor: cal.value.bg, color: cal.value.fg }) .update(cal.value.name.escapeHTML())); } }); }); }, /** * Adds a calendar entry to the print legend. * * @param string type The calendar type. * @param string id The calendar id. * @param object cal The calendar object. */ addCalendarLegend: function(type, id, cal) { $('kronolith-legend').insert( new Element('span') .insert(cal.name.escapeHTML()) .store('calendar', id) .store('calendarclass', type) .setStyle({ backgroundColor: cal.bg, color: cal.fg }) ); }, /** * Deletes a calendar entry from the print legend. * * @param string type The calendar type. * @param string id The calendar id. */ deleteCalendarLegend: function(type, id) { var legend = $('kronolith-legend').select('span').find(function(span) { return span.retrieve('calendarclass') == type && span.retrieve('calendar') == id; }); if (legend) { legend.remove(); } }, /** * Opens a tab in a form. * * @param Element The A element of a tab. */ openTab: function(elt) { var dialog = elt.up('form'), tab = $(elt.id.replace(/Link/, 'Tab')), field; dialog.select('.kronolithTabsOption').invoke('hide'); dialog.select('.tabset li').invoke('removeClassName', 'horde-active'); tab.show(); elt.up().addClassName('horde-active'); if (elt.id == 'kronolithEventLinkMap') { if (!this.mapInitialized) { this.initializeMap(); } } field = tab.down('textarea'); if (!field) { field = tab.down('input'); } if (field) { try { field.focus(); } catch (e) {} } switch (tab.identify()) { case 'kronolithEventTabAttendees': this.attendeeStartDateHandler(this.getFBDate()); break; case 'kronolithEventTabResources': this.resourceStartDateHandler(this.getFBDate()); break; } }, /** * Sets the load signature and show the loading spinner. * * @param string resource The loading resource. * @param string signatrue The signature for this request. */ startLoading: function(resource, signature) { this.eventsLoading[resource] = signature; this.loading++; $('kronolithLoading').show(); }, /** */ loadEvents: function(firstDay, lastDay, view, calendars) { var loading = false; if (typeof calendars == 'undefined') { calendars = []; $H(Kronolith.conf.calendars).each(function(type) { $H(type.value).each(function(cal) { if (cal.value.show) { calendars.push([type.key, cal.key]); } }); }); } calendars.each(function(cal) { var startDay = firstDay.clone(), endDay = lastDay.clone(), cals = this.ecache.get(cal[0]); if (typeof cals != 'undefined' && typeof cals.get(cal[1]) != 'undefined') { cals = cals.get(cal[1]); while (!Object.isUndefined(cals.get(startDay.dateString())) && startDay.isBefore(endDay)) { if (view != 'year') { this.insertEvents([startDay, startDay], view, cal.join('|')); } startDay.add(1).day(); } while (!Object.isUndefined(cals.get(endDay.dateString())) && (!startDay.isAfter(endDay))) { if (view != 'year') { this.insertEvents([endDay, endDay], view, cal.join('|')); } endDay.add(-1).day(); } if (startDay.compareTo(endDay) > 0) { return; } } var start = startDay.dateString(), end = endDay.dateString(), calendar = cal.join('|'); loading = true; this.startLoading(calendar, start + end); this.storeCache($H(), calendar, null, true); HordeCore.doAction('listEvents', { start: start, end: end, cal: calendar, sig: start + end, view: view }, { callback: function(r) { this.loadEventsCallback(r, true); }.bind(this) }); }, this); if (!loading && view == 'year') { this.insertEvents([firstDay, lastDay], 'year'); } }, /** * Callback method for inserting events in the current view. * * @param object r The ajax response object. * @param boolean createCache Whether to create a cache list entry for the * response, if none exists yet. Useful for * (not) adding individual events to the cache * if it doesn't match any cached views. */ loadEventsCallback: function(r, createCache) { // Hide spinner. this.loading--; if (!this.loading) { $('kronolithLoading').hide(); } var start = this.parseDate(r.sig.substr(0, 8)), end = this.parseDate(r.sig.substr(8, 8)), dates = [start, end], currentDates; this.storeCache(r.events || {}, r.cal, dates, createCache); // Check if this is the still the result of the most current request. if (r.sig != this.eventsLoading[r.cal]) { return; } delete this.eventsLoading[r.cal]; // Check if the calendar is still visible. var calendar = r.cal.split('|'); if (!Kronolith.conf.calendars[calendar[0]][calendar[1]].show) { return; } // Check if the result is still for the current view. currentDates = this.viewDates(this.date, this.view); if (r.view != this.view || !start.between(currentDates[0], currentDates[1])) { return; } if (this.view == 'day' || this.view == 'week' || this.view == 'workweek' || this.view == 'month' || this.view == 'agenda' || (this.view == 'year' && !$H(this.eventsLoading).size())) { this.insertEvents(dates, this.view, r.cal); } }, /** * Reads events from the cache and inserts them into the view. * * If inserting events into day and week views, the calendar parameter is * ignored, and events from all visible calendars are inserted instead. * This is necessary because the complete view has to be re-rendered if * events are not in chronological order. * The year view is specially handled too because there are no individual * events, only a summary of all events per day. * * @param Array dates Start and end of dates to process. * @param string view The view to update. * @param string calendar The calendar to update. */ insertEvents: function(dates, view, calendar) { switch (view) { case 'day': case 'week': case 'workweek': // The day and week views require the view to be completely // loaded, to correctly calculate the dimensions. if (this.viewLoading.size() || this.view != view) { this.insertEvents.bind(this, [dates[0].clone(), dates[1].clone()], view, calendar).defer(); return; } break; } var day = dates[0].clone(), viewDates = this.viewDates(this.date, this.view), date, more, title, titles, events, monthDay, busyHours; while (!day.isAfter(dates[1])) { // Skip if somehow events slipped in though the view is gone. if (!day.between(viewDates[0], viewDates[1])) { if (window.console) { window.console.trace(); } day.next().day(); continue; } date = day.dateString(); switch (view) { case 'day': case 'week': case 'workweek': this.dayEvents = []; this.dayGroups = []; this.allDayEvents = []; if (view == 'day') { $$('.kronolith-event').invoke('remove'); } else { this.eventsWeek['kronolithEvents' + (view == 'week' ? 'Week' : 'Workweek') + date] .select('.kronolith-event') .invoke('remove'); this.allDays['kronolithAllDay' + date] .childElements() .invoke('remove'); } break; case 'month': monthDay = this.monthDays['kronolithMonthDay' + date]; monthDay.select('div') .findAll(function(el) { return el.retrieve('calendar') == calendar; }) .invoke('remove'); break; case 'year': titles = []; busyHours = 0; } if (view == 'month' || view == 'agenda') { events = this.getCacheForDate(date, calendar); } else { events = this.getCacheForDate(date); } events.sortBy(this.sortEvents).each(function(event) { var insertBefore; switch (view) { case 'month': case 'agenda': if (calendar.startsWith('holiday|')) { if (this.holidays.include(event.key)) { return; } this.holidays.push(event.key); } if (view == 'month' && Kronolith.conf.max_events) { more = monthDay.down('.kronolithMore'); if (more) { more.purge(); more.remove(); } } if (view == 'month') { if (Kronolith.conf.max_events) { var events = monthDay.select('.kronolith-event'); if (events.size() >= Kronolith.conf.max_events) { if (date == (new Date().dateString())) { // This is today. if (event.value.al || event.value.end.isBefore()) { // No room for all-day or finished events. this.insertMore(date); return; } var remove, max; // Find an event that is earlier than now or // later then the current event. events.each(function(elm) { var calendar = elm.retrieve('calendar').split('|'), event = this.ecache.get(calendar[0]).get(calendar[1]).get(date).get(elm.retrieve('eventid')); if (event.start.isBefore()) { remove = elm; throw $break; } if (!max || event.start.isAfter(max)) { max = event.start; remove = elm; } }, this); if (remove) { remove.purge(); remove.remove(); insertBefore = this.findInsertBefore(events.without(remove), event, date); } else { this.insertMore(date); return; } } else { // Not today. var allDays = events.findAll(function(elm) { var calendar = elm.retrieve('calendar').split('|'); return this.ecache.get(calendar[0]).get(calendar[1]).get(date).get(elm.retrieve('eventid')).al; }.bind(this)); if (event.value.al) { // We want one all-day event. if (allDays.size()) { // There already is an all-day event. if (event.value.x == Kronolith.conf.status.confirmed || event.value.x == Kronolith.conf.status.tentative) { // But is there a less important // one? var status = [Kronolith.conf.status.free, Kronolith.conf.status.cancelled]; if (event.value.x == Kronolith.conf.status.confirmed) { status.push(Kronolith.conf.status.tentative); } var free = allDays.detect(function(elm) { var calendar = elm.retrieve('calendar').split('|'); return status.include(this.ecache.get(calendar[0]).get(calendar[1]).get(date).get(elm.retrieve('eventid')).x); }.bind(this)); if (!free) { this.insertMore(date); return; } insertBefore = free.next(); free.purge(); free.remove(); } else { // No. this.insertMore(date); return; } } else { // Remove the last event to make room // for this one. var elm = events.pop(); elm.purge(); elm.remove(); insertBefore = events.first(); } } else { if (allDays.size() > 1) { // We don't want more than one all-day // event. var elm = allDays.pop(); // Remove element from events as well. events = events.without(elm); elm.purge(); elm.remove(); insertBefore = this.findInsertBefore(events, event, date); } else { // This day is full. this.insertMore(date); return; } } } this.insertMore(date); } else { insertBefore = this.findInsertBefore(events, event, date); } } else { var events = monthDay.select('.kronolith-event'); insertBefore = this.findInsertBefore(events, event, date); } } break; case 'year': title = ''; if (event.value.al) { title += Kronolith.text.allday; } else { title += event.value.start.toString('t') + '-' + event.value.end.toString('t'); } if (event.value.t) { title += ': ' + event.value.t.escapeHTML(); } if (event.value.x == Kronolith.conf.status.tentative || event.value.x == Kronolith.conf.status.confirmed) { busyHours += event.value.start.getElapsed(event.value.end) / 3600000; } titles.push(title); return; } this.insertEvent(event, date, view, insertBefore); }, this); switch (view) { case 'agenda': if ($('kronolithViewAgendaBody').select('tr').length > 2) { $('kronolithAgendaNoItems').hide(); } else { $('kronolithAgendaNoItems').show(); } break; case 'year': var td = $('kronolithYear' + date); if (td.className == 'kronolith-minical-empty') { continue; } if (td.hasClassName('kronolith-today')) { td.className = 'kronolith-today'; } else { td.className = ''; } if (titles.length) { td.addClassName('kronolithHasEvents'); if (busyHours > 0) { td.addClassName(this.getHeatmapClass(busyHours)); busyHours = 0; } td.down('a').writeAttribute('nicetitle', Object.toJSON(titles)); } } day.next().day(); } // Workaround Firebug bug. Prototype.emptyFunction(); }, findInsertBefore: function(events, event, date) { var insertBefore, insertSort; events.each(function(elm) { var calendar = elm.retrieve('calendar').split('|'), existing = this.ecache .get(calendar[0]) .get(calendar[1]) .get(date) .get(elm.retrieve('eventid')); if (event.value.sort < existing.sort && (!insertSort || existing.sort < insertSort)) { insertBefore = elm; insertSort = existing.sort; } }, this); return insertBefore; }, getHeatmapClass: function(hours) { return 'heat' + Math.min(Math.ceil(hours / 2), 6); }, /** * Creates the DOM node for an event bubble and inserts it into the view. * * @param object event A Hash member with the event to insert. * @param string date The day to update. * @param string view The view to update. * @param Element before Insert the event before this element (month view). */ insertEvent: function(event, date, view, before) { var calendar = event.value.calendar.split('|'); event.value.nodeId = ('kronolithEvent' + view + event.value.calendar + date + event.key).replace(new RegExp('[^a-zA-Z0-9]', 'g'), ''); var _createElement = function(event) { var className ='kronolith-event'; switch (event.value.x) { case 3: className += ' kronolith-event-cancelled'; break; case 1: case 4: className += ' kronolith-event-tentative'; break; } var el = new Element('div', { id: event.value.nodeId, className: className }) .store('calendar', event.value.calendar) .store('eventid', event.key); if (!Object.isUndefined(event.value.aj)) { el.store('ajax', event.value.aj); } return el; }; switch (view) { case 'day': case 'week': case 'workweek': var storage = view + 'Sizes', what = view == 'week' ? 'Week' : 'Workweek', div = _createElement(event), margin = view == 'day' ? 1 : 3, style = { backgroundColor: Kronolith.conf.calendars[calendar[0]][calendar[1]].bg, color: Kronolith.conf.calendars[calendar[0]][calendar[1]].fg }; div.writeAttribute('title', event.value.t); if (event.value.al) { if (view == 'day') { $('kronolithViewDay').down('.kronolithAllDayContainer').insert(div.setStyle(style)); } else { var allDay = this.allDays['kronolithAllDay' + date], existing = allDay.childElements(), weekHead = $('kronolithView' + what + 'Head'); if (existing.size() == 3) { if (existing[2].className != 'kronolithMore') { existing[2].purge(); existing[2].remove(); allDay.insert({ bottom: new Element('span', { className: 'kronolithMore' }).store('date', date).insert(Kronolith.text.more) }); } } else { allDay.insert(div.setStyle(style)); if (event.value.pe) { div.addClassName('kronolithEditable'); var layout = div.getLayout(), minLeft = weekHead.down('.kronolith-first-col').getWidth() + this[storage].spacing + (parseInt(div.getStyle('marginLeft'), 10) || 0), minTop = weekHead.down('thead').getHeight() + this[storage].spacing + (parseInt(div.getStyle('marginTop'), 10) || 0), maxLeft = weekHead.getWidth() - layout.get('margin-box-width'), maxTop = weekHead.down('thead').getHeight() + weekHead.down('.kronolith-all-day').getHeight(), opts = { threshold: 5, parentElement: function() { return $('kronolithView' + what).down('.kronolith-view-head'); }, snap: function(x, y) { return [Math.min(Math.max(x, minLeft), maxLeft), Math.min(Math.max(y, minTop), maxTop - div.getHeight())]; } }; var d = new Drag(event.value.nodeId, opts); div.store('drags', []); Object.extend(d, { event: event, innerDiv: new Element('div'), midnight: this.parseDate(date) }); div.retrieve('drags').push(d); } } } break; } var midnight = this.parseDate(date), resizable = event.value.pe && (Object.isUndefined(event.value.vl) || event.value.vl), innerDiv = new Element('div', { className: 'kronolith-event-info' }), minHeight = 0, parentElement, draggerTop, draggerBottom, elapsed = (event.value.start.getHours() - midnight.getHours()) * 60 + (event.value.start.getMinutes() - midnight.getMinutes()); switch (view) { case 'day': parentElement = $('kronolithEventsDay'); break; case 'week': parentElement = this.eventsWeek['kronolithEventsWeek' + date]; break; case 'workweek': parentElement = this.eventsWeek['kronolithEventsWorkweek' + date]; break; } if (event.value.fi) { div.addClassName('kronolithFirst'); if (resizable) { draggerTop = new Element('div', { id: event.value.nodeId + 'top', className: 'kronolithDragger kronolithDraggerTop' }).setStyle(style); } } else { innerDiv.setStyle({ top: 0 }); } if (event.value.la) { div.addClassName('kronolithLast'); if (resizable) { draggerBottom = new Element('div', { id: event.value.nodeId + 'bottom', className: 'kronolithDragger kronolithDraggerBottom' }).setStyle(style); } } else { innerDiv.setStyle({ bottom: 0 }); } div.setStyle({ top: (elapsed * this[storage].height / 60 | 0) + 'px', width: 100 - margin + '%' }) .insert(innerDiv.setStyle(style)); if (draggerTop) { div.insert(draggerTop); } if (draggerBottom) { div.insert(draggerBottom); } parentElement.insert(div); if (draggerTop) { minHeight += draggerTop.getHeight(); } if (draggerBottom) { minHeight += draggerBottom.getHeight(); } if (!minHeight) { minHeight = parseInt(innerDiv.getStyle('lineHeight'), 10) + (parseInt(innerDiv.getStyle('paddingTop'), 10) || 0) + (parseInt(innerDiv.getStyle('paddingBottom'), 10) || 0); } div.setStyle({ height: Math.max(Math.round(event.value.start.getElapsed(event.value.end) / 60000) * this[storage].height / 60 - this[storage].spacing | 0, minHeight) + 'px' }); if (event.value.pe) { div.addClassName('kronolithEditable'); div.store('drags', []); // Number of pixels that cover 10 minutes. var step = this[storage].height / 6, stepX, minLeft, maxLeft, maxTop, minBottom, maxBottom, dragBottomHeight; if (draggerBottom) { // Height of bottom dragger dragBottomHeight = draggerBottom.getHeight(); } if (draggerTop) { // Bottom-most position (maximum y) of top dragger maxTop = div.offsetTop - draggerTop.getHeight() - parseInt(innerDiv.getStyle('lineHeight'), 10); if (draggerBottom) { maxTop += draggerBottom.offsetTop; } } if (draggerBottom) { // Top-most position (minimum y) of bottom dragger (upper // edge) minBottom = div.offsetTop + parseInt(innerDiv.getStyle('lineHeight'), 10); // Bottom-most position (maximum y) of bottom dragger // (upper edge) maxBottom = 24 * this[storage].height + dragBottomHeight; if (draggerTop) { minBottom += draggerTop.getHeight(); } } // Height of the whole event div var divHeight = div.getHeight(), // Maximum height of the whole event div maxDiv = 24 * this[storage].height - divHeight, // Whether the top dragger is dragged, vs. the bottom // dragger opts = { threshold: 5, constraint: 'vertical', scroll: this.kronolithBody, nodrop: true, parentElement: function() { return parentElement; } }; if (draggerTop) { opts.snap = function(x, y) { y = Math.max(0, step * (Math.min(maxTop, y - this.scrollTop) / step | 0)); return [0, y]; }.bind(this); var d = new Drag(event.value.nodeId + 'top', opts); Object.extend(d, { event: event, innerDiv: innerDiv, midnight: midnight }); div.retrieve('drags').push(d); } if (draggerBottom) { opts.snap = function(x, y) { y = Math.min(maxBottom + dragBottomHeight + KronolithCore[storage].spacing, step * ((Math.max(minBottom, y - this.scrollTop) + dragBottomHeight + KronolithCore[storage].spacing) / step | 0)) - dragBottomHeight - KronolithCore[storage].spacing; return [0, y]; }.bind(this); var d = new Drag(event.value.nodeId + 'bottom', opts); Object.extend(d, { event: event, innerDiv: innerDiv, midnight: midnight }); div.retrieve('drags').push(d); } if (view == 'week' || view == 'workweek') { var dates = this.viewDates(midnight, view); minLeft = this.eventsWeek['kronolithEvents' + what + dates[0].dateString()].offsetLeft - this.eventsWeek['kronolithEvents' + what + date].offsetLeft; maxLeft = this.eventsWeek['kronolithEvents' + what + dates[1].dateString()].offsetLeft - this.eventsWeek['kronolithEvents' + what + date].offsetLeft; stepX = (maxLeft - minLeft) / (view == 'week' ? 6 : 4); } var d = new Drag(div, { threshold: 5, nodrop: true, parentElement: function() { return parentElement; }, snap: function(x, y) { x = (view == 'week' || view == 'workweek') ? Math.max(minLeft, stepX * ((Math.min(maxLeft, x - (x < 0 ? stepX : 0)) + stepX / 2) / stepX | 0)) : 0; y = Math.max(0, step * (Math.min(maxDiv, y - this.scrollTop) / step | 0)); return [x, y]; }.bind(this) }); Object.extend(d, { divHeight: divHeight, startTop: div.offsetTop, event: event, midnight: midnight, stepX: stepX }); div.retrieve('drags').push(d); } var // The current column that we're probing for available space. column = 1, // The number of columns in the current conflict group. columns, // The column width in the current conflict group. width, // The first event that conflict with the current event. conflict = false, // The conflict group where this event should go. pos = this.dayGroups.length, // The event below that the current event fits. placeFound = false, // The minimum (virtual) duration of each event, defined by the // minimum height of an event DIV. minMinutes = (minHeight + this[storage].spacing) * 60 / this[storage].height; // this.dayEvents contains all events of the current day. // this.dayGroups contains conflict groups, i.e. all events that // conflict with each other and share a set of columns. // // Go through all events that have been added to this day already. this.dayEvents.each(function(ev) { // Due to the minimum height of an event DIV, events might // visually overlap, even if they physically don't. var minEnd = ev.start.clone().add(minMinutes).minutes(), end = ev.end.isAfter(minEnd) ? ev.end : minEnd; // If it doesn't conflict with the current event, go ahead. if (!end.isAfter(event.value.start)) { return; } // Found a conflicting event, now find its conflict group. for (pos = 0; pos < this.dayGroups.length; pos++) { if (this.dayGroups[pos].indexOf(ev) != -1) { // Increase column for each conflicting event in this // group. this.dayGroups[pos].each(function(ce) { var minEnd = ce.start.clone().add(minMinutes).minutes(), end = ce.end.isAfter(minEnd) ? ce.end : minEnd; if (end.isAfter(event.value.start)) { column++; } }); throw $break; } } }, this); event.value.column = event.value.columns = column; if (Object.isUndefined(this.dayGroups[pos])) { this.dayGroups[pos] = []; } this.dayGroups[pos].push(event.value); // See if the current event had to add yet another column. columns = Math.max(this.dayGroups[pos][0].columns, column); // Update the widths of all events in a conflict group. width = 100 / columns; this.dayGroups[pos].each(function(ev) { ev.columns = columns; $(ev.nodeId).setStyle({ width: width - margin + '%', left: (width * (ev.column - 1)) + '%' }); }); this.dayEvents.push(event.value); div = innerDiv; break; case 'month': var monthDay = this.monthDays['kronolithMonthDay' + date], div = _createElement(event) .setStyle({ backgroundColor: Kronolith.conf.calendars[calendar[0]][calendar[1]].bg, color: Kronolith.conf.calendars[calendar[0]][calendar[1]].fg }); div.writeAttribute('title', event.value.t); if (before) { before.insert({ before: div }); } else { monthDay.insert(div); } if (event.value.pe) { div.setStyle({ cursor: 'move' }); new Drag(event.value.nodeId, { threshold: 5, parentElement: function() { return $('kronolith-month-body'); }, snapToParent: true }); } if (Kronolith.conf.max_events) { var more = monthDay.down('.kronolithMore'); if (more) { monthDay.insert({ bottom: more.remove() }); } } break; case 'agenda': var div = _createElement(event) .setStyle({ backgroundColor: Kronolith.conf.calendars[calendar[0]][calendar[1]].bg, color: Kronolith.conf.calendars[calendar[0]][calendar[1]].fg }); this.createAgendaDay(date); $('kronolithAgendaDay' + date).insert(div); break; } this.setEventText(div, event.value, { time: view == 'agenda' || Kronolith.conf.show_time }) .observe('mouseover', div.addClassName.curry('kronolith-selected')) .observe('mouseout', div.removeClassName.curry('kronolith-selected')); }, /** * Re-renders the necessary parts of the current view, if any event changes * in those parts require re-rendering. * * @param Array dates The date strings of days to re-render. */ reRender: function(dates) { switch (this.view) { case 'week': case 'workweek': case 'day': dates.each(function(date) { date = this.parseDate(date); this.insertEvents([ date, date ], this.view); }, this); break; case 'month': dates.each(function(date) { var day = this.monthDays['kronolithMonthDay' + date]; day.select('.kronolith-event').each(function(event) { if (event.retrieve('calendar').startsWith('holiday')) { delete this.holidays[event.retrieve('eventid')]; } event.remove(); }, this); day.select('.kronolithMore').invoke('remove'); date = this.parseDate(date); this.loadEvents(date, date, 'month'); }, this); break; } }, /** * Returns all dates of the current view that contain (recurrences) of a * certain event. * * @param String cal A calendar string. * @param String eventid An event id. * * @return Array A list of date strings that contain a recurrence of the * event. */ findEventDays: function(cal, eventid) { cal = cal.split('|'); var cache = this.ecache.get(cal[0]).get(cal[1]), dates = this.viewDates(this.date, this.view), day = dates[0], days = [], dateString; while (!day.isAfter(dates[1])) { dateString = day.dateString(); if (cache.get(dateString).get(eventid)) { days.push(dateString); } day.add(1).days(); } return days; }, /** * Adds a "more..." button to the month view cell that links to the days, * or moves it to the buttom. * * @param string date The date string of the day cell. */ insertMore: function(date) { var monthDay = this.monthDays['kronolithMonthDay' + date], more = monthDay.down('.kronolithMore'); if (more) { monthDay.insert({ bottom: more.remove() }); } else { monthDay.insert({ bottom: new Element('span', { className: 'kronolithMore' }).store('date', date).insert(Kronolith.text.more) }); } }, setEventText: function(div, event, opts) { var calendar = event.calendar.split('|'), span = new Element('span'), time, end; opts = Object.extend({ time: false }, opts || {}); div.update(); if (event.ic) { div.insert(new Element('img', { src: event.ic, className: 'kronolithEventIcon' })); } if (opts.time && !event.al) { time = new Element('span', { className: 'kronolith-time' }) .insert(event.start.toString(Kronolith.conf.time_format)); if (!event.start.equals(event.end)) { end = event.end.clone(); if (end.getHours() == 23 && end.getMinutes() == 59 && end.getSeconds() == 59) { end.add(1).second(); } time.insert('-' + end.toString(Kronolith.conf.time_format)); } div.insert(time).insert(' '); } div.insert(event.t.escapeHTML()); div.insert(span); if (event.a) { span.insert(' ') .insert(new Element('img', { src: Kronolith.conf.images.alarm.replace(/fff/, Kronolith.conf.calendars[calendar[0]][calendar[1]].fg.substr(1)), title: Kronolith.text.alarm + ' ' + event.a })); } if (event.r) { span.insert(' ') .insert(new Element('img', { src: Kronolith.conf.images.recur.replace(/fff/, Kronolith.conf.calendars[calendar[0]][calendar[1]].fg.substr(1)), title: Kronolith.text.recur[event.r] })); } else if (event.bid) { div.store('bid', event.bid); span.insert(' ') .insert(new Element('img', { src: Kronolith.conf.images.exception.replace(/fff/, Kronolith.conf.calendars[calendar[0]][calendar[1]].fg.substr(1)), title: Kronolith.text.recur.exception })); } return div; }, /** * Finally removes events from the DOM and the cache. * * @param string calendar A calendar name. * @param string event An event id. If empty, all events from the * calendar are deleted. */ removeEvent: function(calendar, event) { this.deleteCache(calendar, event); this.kronolithBody.select('div.kronolith-event').findAll(function(el) { return el.retrieve('calendar') == calendar && (!event || el.retrieve('eventid') == event); }).invoke('remove'); }, /** * Removes all events that reprensent exceptions to the event series * represented by uid. * * @param string calendar A calendar name. * @param string uid An event uid. */ removeException: function(calendar, uid) { this.kronolithBody.select('div.kronolith-event').findAll(function(el) { if (el.retrieve('calendar') == calendar && el.retrieve('bid') == uid) { this.removeEvent(calendar, el.retrieve('eventid')); } }.bind(this)); }, /** * Calculates the event's start and end dates based on some drag and drop * information. */ calculateEventDates: function(event, storage, step, offset, height, start, end) { if (!Object.isUndefined(start)) { event.start = start; event.end = end; } event.start.set({ hour: offset / this[storage].height | 0, minute: Math.round(offset % this[storage].height / step) * 10 }); var hour = (offset + height + this[storage].spacing) / this[storage].height | 0, minute = Math.round((offset + height + this[storage].spacing) % this[storage].height / step) * 10, second = 0; if (hour == 24) { hour = 23; minute = 59; second = 59; } event.end.set({ hour: hour, minute: minute, second: second }); }, switchTaskView: function(on) { if (on) { $('kronolithNewEvent', 'kronolithNewTask').compact()[0] .replace(Kronolith.conf.new_task); $('kronolithQuickEvent').addClassName('kronolithNewTask'); $('kronolithHeader').down('.kronolithPrev').up().addClassName('disabled'); $('kronolithHeader').down('.kronolithNext').up().addClassName('disabled'); } else { $('kronolithNewEvent', 'kronolithNewTask').compact()[0] .replace(Kronolith.conf.new_event); $('kronolithQuickEvent').removeClassName('kronolithNewTask'); $('kronolithHeader').down('.kronolithPrev').up().removeClassName('disabled'); $('kronolithHeader').down('.kronolithNext').up().removeClassName('disabled'); } }, /** * Returns the task cache storage names that hold the tasks of the * requested task type. * * @param string tasktype The task type. * * @return array The list of task cache storage names. */ getTaskStorage: function(tasktype) { var tasktypes; if (tasktype == 'all' || tasktype == 'future') { tasktypes = [ 'complete', 'incomplete' ]; } else { tasktypes = [ tasktype ]; } return tasktypes; }, /** * Loads tasks, either from cache or from the server. * * @param integer tasktype The tasks type (all, incomplete, complete, or * future). * @param Array tasksLists The lists from where to obtain the tasks. */ loadTasks: function(tasktype, tasklists) { var tasktypes = this.getTaskStorage(tasktype), loading = false, spinner = $('kronolithLoading'); if (Object.isUndefined(tasklists)) { tasklists = []; $H(Kronolith.conf.calendars.tasklists).each(function(tasklist) { if (tasklist.value.show) { tasklists.push(tasklist.key.substring(6)); } }); } tasktypes.each(function(type) { tasklists.each(function(list) { if (Object.isUndefined(this.tcache.get(type)) || Object.isUndefined(this.tcache.get(type).get(list))) { loading = true; this.loading++; spinner.show(); HordeCore.doAction('listTasks', { type: type, list: list }, { callback: function(r) { this.loadTasksCallback(r, true); }.bind(this) }); } }, this); }, this); if (!loading) { tasklists.each(function(list) { this.insertTasks(tasktype, list); }, this); } }, /** * Callback method for inserting tasks in the current view. * * @param object r The ajax response object. * @param boolean createCache Whether to create a cache list entry for the * response, if none exists yet. Useful for * (not) adding individual tasks to the cache * without assuming to have all tasks of the * list. */ loadTasksCallback: function(r, createCache) { // Hide spinner. this.loading--; if (!this.loading) { $('kronolithLoading').hide(); } this.storeTasksCache(r.tasks || {}, r.type, r.list, createCache); // Check if result is still valid for the current view. // There could be a rare race condition where two responses for the // same task(s) arrive in the wrong order. Checking this too, like we // do for events seems not worth it. var tasktypes = this.getTaskStorage(this.tasktype), tasklist = Kronolith.conf.calendars.tasklists['tasks/' + r.list]; if (this.view != 'tasks' || !tasklist || !tasklist.show || !tasktypes.include(r.type)) { return; } this.insertTasks(this.tasktype, r.list); }, /** * Reads tasks from the cache and inserts them into the view. * * @param integer tasktype The tasks type (all, incomplete, complete, or * future). * @param string tasksList The task list to be drawn. */ insertTasks: function(tasktype, tasklist) { var tasktypes = this.getTaskStorage(tasktype), now = new Date(); $('kronolithViewTasksBody').select('tr').findAll(function(el) { return el.retrieve('tasklist') == tasklist; }).invoke('remove'); tasktypes.each(function(type) { if (!this.tcache.get(type)) { return; } var tasks = this.tcache.get(type).get(tasklist); $H(tasks).each(function(task) { switch (tasktype) { case 'complete': if (!task.value.cp) { return; } break; case 'incomplete': if (task.value.cp || (!Object.isUndefined(task.value.start) && task.value.start.isAfter(now))) { return; } break; case 'future': if (task.value.cp || Object.isUndefined(task.value.start) || !task.value.start.isAfter(now)) { return; } break; } this.insertTask(task); }, this); }, this); if ($('kronolithViewTasksBody').select('tr').length > 2) { $('kronolithTasksNoItems').hide(); } else { $('kronolithTasksNoItems').show(); } }, /** * Creates the DOM node for a task and inserts it into the view. * * @param object task A Hash with the task to insert */ insertTask: function(task) { var row = $('kronolithTasksTemplate').clone(true), col = row.down(), tagc; row.removeAttribute('id'); row.store('tasklist', task.value.l); row.store('taskid', task.key); col.addClassName('kronolithTask' + (!!task.value.cp ? 'Completed' : '')); col.setStyle({ backgroundColor: Kronolith.conf.calendars.tasklists['tasks/' + task.value.l].bg, color: Kronolith.conf.calendars.tasklists['tasks/' + task.value.l].fg, textIndent: task.value.i + 'em' }); col.insert(task.value.n.escapeHTML()); if (!Object.isUndefined(task.value.due)) { var now = new Date(); if (!now.isBefore(task.value.due)) { col.addClassName('kronolithTaskDue'); } col.insert(new Element('span', { className: 'kronolithSeparator' }).update(' · ')); col.insert(new Element('span', { className: 'kronolithDate' }).update(task.value.due.toString(Kronolith.conf.date_format))); if (task.value.r) { col.insert(' ') .insert(new Element('img', { src: Kronolith.conf.images.recur.replace(/fff/, Kronolith.conf.calendars.tasklists['tasks/' + task.value.l].fg.substr(1)), title: Kronolith.text.recur[task.value.r] })); } } if (!Object.isUndefined(task.value.sd)) { col.insert(new Element('span', { className: 'kronolithSeparator' }).update(' · ')); col.insert(new Element('span', { className: 'kronolithInfo' }).update(task.value.sd.escapeHTML())); } if (task.value.t && task.value.t.size() > 0) { tagc = new Element('ul', { className: 'horde-tags' }); task.value.t.each(function(x) { tagc.insert(new Element('li').update(x.escapeHTML())); }); col.insert(tagc); } row.insert(col.show()); this.insertTaskPosition(row, task); }, /** * Inserts the task row in the correct position. * * @param Element newRow The new row to be inserted. * @param object newTask A Hash with the task being added. */ insertTaskPosition: function(newRow, newTask) { var rows = $('kronolithViewTasksBody').select('tr'), rowTasklist, rowTaskId, rowTask, parentFound; // The first row is a template, ignoring. for (var i = 2; i < rows.length; i++) { rowTasklist = rows[i].retrieve('tasklist'); rowTaskId = rows[i].retrieve('taskid'); if (newTask.value.p) { if (rowTaskId == newTask.value.p) { parentFound = true; continue; } if (!parentFound) { continue; } } rowTask = this.tcache.inject(null, function(acc, list) { if (acc) { return acc; } if (!Object.isUndefined(list.value.get(rowTasklist))) { return list.value.get(rowTasklist).get(rowTaskId); } }); if (Object.isUndefined(rowTask)) { // TODO: Throw error return; } if (!this.isTaskAfter(newTask.value, rowTask)) { break; } } rows[--i].insert({ after: newRow.show() }); }, /** * Analyzes which task should be drawn first. * * TODO: Very incomplete, only a dummy version */ isTaskAfter: function(taskA, taskB) { // TODO: Make all ordering system if ((taskA.p || taskB.p) && taskA.p != taskB.p) { return !taskA.p; } return (taskA.pr >= taskB.pr); }, /** * Completes/uncompletes a task. * * @param string tasklist The task list to which the tasks belongs. * @param string taskid The id of the task. * @param boolean|string complete True if the task is completed, a * due date if there are still * incomplete recurrences. */ toggleCompletion: function(tasklist, taskid, complete) { // Update the cache. var task = this.tcache.inject(null, function(acc, list) { if (acc) { return acc; } if (!Object.isUndefined(list.value.get(tasklist))) { return list.value.get(tasklist).get(taskid); } }); if (Object.isUndefined(task)) { // This shouldn't happen. this.toggleCompletionClass(taskid); return; } if (Object.isUndefined(complete) || complete === true) { task.cp = !task.cp; } if (this.tcache.get(task.cp ? 'complete' : 'incomplete')) { this.tcache.get(task.cp ? 'complete' : 'incomplete').get(tasklist).set(taskid, task); } if (this.tcache.get(task.cp ? 'incomplete' : 'complete')) { this.tcache.get(task.cp ? 'incomplete' : 'complete').get(tasklist).unset(taskid); } // Remove row if necessary. var row = this.getTaskRow(taskid); if (!row) { return; } if ((this.tasktype == 'complete' && !task.cp) || ((this.tasktype == 'incomplete' || this.tasktype == 'future_incomplete') && task.cp) || ((complete === true) && (this.tasktype == 'future'))) { row.fade({ duration: this.effectDur, afterFinish: function() { row.purge(); row.remove(); //Check if items remained in interface if ($('kronolithViewTasksBody').select('tr').length < 3) { $('kronolithTasksNoItems').show(); } } }); } // Update due date if necessary. if (!Object.isUndefined(complete) && complete !== true) { var now = new Date(), due = Date.parse(complete); row.down('span.kronolithDate') .update(due.toString(Kronolith.conf.date_format)); if (now.isBefore(due)) { row.down('td.kronolithTaskCol') .removeClassName('kronolithTaskDue'); } } }, /** * Toggles the CSS class to show that a task is completed/uncompleted. * * @param string taskid The id of the task. */ toggleCompletionClass: function(taskid) { var row = this.getTaskRow(taskid); if (!row) { return; } var col = row.down('td.kronolithTaskCol'); col.toggleClassName('kronolithTask'); col.toggleClassName('kronolithTaskCompleted'); }, /** * Returns the table row of a task. * * @param string taskid The id of the task. * * @return Element The table row of the task list, if found. */ getTaskRow: function(taskid) { return $('kronolithViewTasksBody').select('tr').find(function(el) { return el.retrieve('taskid') == taskid; }); }, editTask: function(tasklist, id, desc) { if (this.redBoxLoading) { return; } if (Object.isUndefined(HordeImple.AutoCompleter.kronolithTaskTags)) { this.editTask.bind(this, tasklist, id, desc).defer(); return; } this.closeRedBox(); this.quickClose(); this.redBoxOnDisplay = RedBox.onDisplay; RedBox.onDisplay = function() { if (this.redBoxOnDisplay) { this.redBoxOnDisplay(); } try { $('kronolithTaskForm').focusFirstElement(); } catch(e) {} RedBox.onDisplay = this.redBoxOnDisplay; }.bind(this); this.openTab($('kronolithTaskForm').down('.tabset a.kronolithTabLink')); $('kronolithTaskForm').enable(); $('kronolithTaskForm').reset(); HordeImple.AutoCompleter.kronolithTaskTags.reset(); $('kronolithTaskSave').show().enable(); $('kronolithTaskDelete').show().enable(); $('kronolithTaskForm').down('.kronolithFormActions .kronolithSeparator').show(); this.updateTasklistDropDown(); this.disableAlarmMethods('Task'); this.knl.kronolithTaskDueTime.markSelected(); if (id) { RedBox.loading(); this.updateTaskParentDropDown(tasklist); this.updateTaskAssigneeDropDown(tasklist); HordeCore.doAction('getTask', { list: tasklist, id: id }, { callback: this.editTaskCallback.bind(this) }); $('kronolithTaskTopTags').update(); } else { $('kronolithTaskId').clear(); $('kronolithTaskOldList').clear(); $('kronolithTaskList').setValue(Kronolith.conf.tasks.default_tasklist); this.updateTaskParentDropDown(Kronolith.conf.tasks.default_tasklist); this.updateTaskAssigneeDropDown(Kronolith.conf.tasks.default_tasklist); $('kronolithTaskParent').setValue(''); $('kronolithTaskAssignee').setValue(''); //$('kronolithTaskLocation').setValue('http://'); HordeCore.doAction('listTopTags', {}, { callback: this.topTagsCallback.curry('kronolithTaskTopTags', 'kronolithTaskTag') }); $('kronolithTaskPriority').setValue(3); if (Kronolith.conf.tasks.default_due) { this.setDefaultDue(); } if (desc) { $('kronolithTaskDescription').setValue(desc); } this.toggleRecurrence(false, 'None'); $('kronolithTaskDelete').hide(); this.redBoxLoading = true; RedBox.showHtml($('kronolithTaskDialog').show()); } }, /** * Callback method for showing task forms. * * @param object r The ajax response object. */ editTaskCallback: function(r) { if (!r.task) { RedBox.close(); this.go(this.lastLocation); return; } var task = r.task; /* Basic information */ $('kronolithTaskId').setValue(task.id); $('kronolithTaskOldList').setValue(task.l); $('kronolithTaskList').setValue(task.l); $('kronolithTaskTitle').setValue(task.n); $('kronolithTaskParent').setValue(task.p); $('kronolithTaskAssignee').setValue(task.as); //$('kronolithTaskLocation').setValue(task.l); if (task.dd) { $('kronolithTaskDueDate').setValue(task.dd); } if (task.dt) { $('kronolithTaskDueTime').setValue(task.dt); this.knl.kronolithTaskDueTime.setSelected(task.dt); } $('kronolithTaskDescription').setValue(task.de); $('kronolithTaskPriority').setValue(task.pr); $('kronolithTaskCompleted').setValue(task.cp); /* Alarm */ if (task.a) { this.enableAlarm('Task', task.a); if (task.m) { $('kronolithTaskAlarmDefaultOff').checked = true; $H(task.m).each(function(method) { if (!$('kronolithTaskAlarm' + method.key)) { return; } $('kronolithTaskAlarm' + method.key).setValue(1); if ($('kronolithTaskAlarm' + method.key + 'Params')) { $('kronolithTaskAlarm' + method.key + 'Params').show(); } $H(method.value).each(function(param) { var input = $('kronolithTaskAlarmParam' + param.key); if (!input) { return; } if (input.type == 'radio') { input.up('form').select('input[type=radio]').each(function(radio) { if (radio.name == input.name && radio.value == param.value) { radio.setValue(1); throw $break; } }); } else { input.setValue(param.value); } }); }); } } else { $('kronolithTaskAlarmOff').setValue(true); } /* Recurrence */ if (task.r) { this.setRecurrenceFields(false, task.r); } else { this.toggleRecurrence(false, 'None'); } HordeImple.AutoCompleter.kronolithTaskTags.reset(task.t); if (!task.pe) { $('kronolithTaskSave').hide(); $('kronolithTaskForm').disable(); } else { HordeCore.doAction('listTopTags', {}, { callback: this.topTagsCallback.curry('kronolithTaskTopTags', 'kronolithTaskTag') }); } if (!task.pd) { $('kronolithTaskDelete').show(); } if (!task.pe && !task.pd) { $('kronolithTaskForm').down('.kronolithFormActions .kronolithSeparator').hide(); } this.setTitle(task.n); this.redBoxLoading = true; RedBox.showHtml($('kronolithTaskDialog').show()); /* Hide alarm message for this task. */ if (r.msgs) { r.msgs = r.msgs.reject(function(msg) { if (msg.type != 'horde.alarm') { return false; } var alarm = msg.flags.alarm; if (alarm.params && alarm.params.notify && alarm.params.notify.show && alarm.params.notify.show.tasklist && alarm.params.notify.show.task && alarm.params.notify.show.tasklist == task.l && alarm.params.notify.show.task == task.id) { return true; } return false; }); } }, /** * Propagates a SELECT drop down list with the editable task lists. */ updateTasklistDropDown: function() { var tasklist = $('kronolithTaskList'); tasklist.update(); $H(Kronolith.conf.calendars.tasklists).each(function(cal) { if (cal.value.edit) { tasklist.insert(new Element('option', { value: cal.key.substring(6) }) .setStyle({ backgroundColor: cal.value.bg, color: cal.value.fg }) .update(cal.value.name.escapeHTML())); } }); }, /** * Propagates a SELECT drop down list with the tasks of a task list. * * @param string list A task list ID. */ updateTaskParentDropDown: function(list) { var parents = $('kronolithTaskParent'); parents.update(new Element('option', { value: '' }) .update(Kronolith.text.no_parent)); HordeCore.doAction('listTasks', { type: 'future_incomplete', list: list }, { ajaxopts: { asynchronuous: false }, callback: function(r) { $H(r.tasks).each(function(task) { parents.insert(new Element('option', { value: task.key }) .setStyle({ textIndent: task.value.i + 'em' }) .update(task.value.n.escapeHTML())); }); }.bind(this) }); }, /** * Propagates a SELECT drop down list with the users of a task list. * * @param string list A task list ID. */ updateTaskAssigneeDropDown: function(list) { var assignee = $('kronolithTaskAssignee'); assignee.update(new Element('option', { value: '' }) .update(Kronolith.text.no_assignee)); $H(Kronolith.conf.calendars.tasklists['tasks/' + list].users).each(function(user) { assignee.insert(new Element('option', { value: user.key }) .update(user.value.escapeHTML())); }); }, /** * Sets the default due date and time for tasks. */ setDefaultDue: function() { if ($F('kronolithTaskDueDate') || $F('kronolithTaskDueTime')) { return; } $('kronolithTaskDueDate').setValue(new Date().add(Kronolith.conf.tasks.default_due_days).days().toString(Kronolith.conf.date_format)); if (Kronolith.conf.tasks.default_due_time == 'now') { $('kronolithTaskDueTime').setValue(new Date().toString(Kronolith.conf.time_format)); } else { var date = new Date(); date.setHours(Kronolith.conf.tasks.default_due_time.replace(/:.*$/, '')); date.setMinutes(0); $('kronolithTaskDueTime').setValue(date.toString(Kronolith.conf.time_format)); } }, /** * Finally removes tasks from the DOM and the cache. * * @param string list A task list name. * @param string task A task id. If empty, all tasks from the list are * deleted. */ removeTask: function(list, task) { this.deleteTasksCache(list, task); $('kronolithViewTasksBody').select('tr').findAll(function(el) { return el.retrieve('tasklist') == list && (!task || el.retrieve('taskid') == task); }).invoke('remove'); this.removeEvent('tasklists|tasks/' + list, task ? '_tasks' + task : null); if ($('kronolithViewTasksBody').select('tr').length > 2) { $('kronolithTasksNoItems').hide(); } else { $('kronolithTasksNoItems').show(); } }, /** * Submits the task edit form to create or update a task. */ saveTask: function() { if (this.wrongFormat.size() || (($F('kronolithTaskAlarmOn')) && $F('kronolithTaskDueDate').length == 0)) { HordeCore.notify(Kronolith.text.fix_form_values, 'horde.warning'); return; } var tasklist = $F('kronolithTaskOldList'), target = $F('kronolithTaskList'), taskid = $F('kronolithTaskId'), viewDates = this.viewDates(this.date, this.view), start = viewDates[0].dateString(), end = viewDates[1].dateString(); HordeImple.AutoCompleter.kronolithTaskTags.shutdown(); $('kronolithTaskSave').disable(); this.startLoading('tasklists|tasks/' + target, start + end + this.tasktype); this.loading++; HordeCore.doAction( 'saveTask', $H($('kronolithTaskForm').serialize({ hash: true })).merge({ sig: start + end + this.tasktype, view: this.view, view_start: start, view_end: end }), { callback: function(r) { if (r.tasks && taskid) { this.removeTask(tasklist, taskid); } this.loadTasksCallback(r, false); this.loadEventsCallback(r, false); if (r.tasks) { this.closeRedBox(); this.go(this.lastLocation); } else { $('kronolithTaskSave').enable(); } }.bind(this) } ); }, quickSaveTask: function() { var text = $F('kronolithQuicktaskQ'), viewDates = this.viewDates(this.date, 'tasks'), start = viewDates[0].dateString(), end = viewDates[1].dateString(), params = { sig: start + end + this.tasktype, view: 'tasks', view_start: start, view_end: end, tasklist: Kronolith.conf.tasks.default_tasklist, text: text }; this.closeRedBox(); this.startLoading('tasklists|tasks/' + Kronolith.conf.tasks.default_tasklist, params.sig); this.loading++; HordeCore.doAction('quickSaveTask', params, { callback: function(r) { this.loadTasksCallback(r, false); this.loadEventsCallback(r, false); if (!r.tasks || !$H(r.tasks).size()) { this.editTask(null, null, text); } else { $('kronolithQuicktaskQ').value = ''; } }.bind(this) }); }, /** * Opens the form for editing a calendar. * * @param string calendar Calendar type and calendar id, separated by '|'. */ editCalendar: function(calendar) { if (this.redBoxLoading) { return; } this.closeRedBox(); this.quickClose(); var type = calendar.split('|')[0], cal = calendar.split('|')[1]; if (!$w('internal tasklists remote holiday resource resourcegroup').include(type)) { return; } if (cal && (Object.isUndefined(Kronolith.conf.calendars[type]) || Object.isUndefined(Kronolith.conf.calendars[type][cal])) && (type == 'internal' || type == 'tasklists')) { HordeCore.doAction('getCalendar', { cal: cal }, { callback: function(r) { if (r.calendar) { Kronolith.conf.calendars[type][cal] = r.calendar; this.insertCalendarInList(type, cal, r.calendar); $('kronolithSharedCalendars').show(); this.editCalendar(type + '|' + cal); } else { this.go(this.lastLocation); } }.bind(this) }); return; } this.redBoxOnDisplay = RedBox.onDisplay; RedBox.onDisplay = function() { if (this.redBoxOnDisplay) { this.redBoxOnDisplay(); } try { $('kronolithCalendarForm' + type).focusFirstElement(); } catch(e) {} RedBox.onDisplay = this.redBoxOnDisplay; }.bind(this); if ($('kronolithCalendarDialog')) { this.redBoxLoading = true; RedBox.showHtml($('kronolithCalendarDialog').show()); this.editCalendarCallback(calendar); } else { RedBox.loading(); HordeCore.doAction('chunkContent', { chunk: 'calendar' }, { callback: function(r) { if (r.chunk) { this.redBoxLoading = true; RedBox.showHtml(r.chunk); ['internal', 'tasklists'].each(function(type) { $('kronolithC' + type + 'PGList').observe('change', function() { $('kronolithC' + type + 'PG').setValue(1); this.permsClickHandler(type, 'G'); }.bind(this)); }, this); this.editCalendarCallback(calendar); } else { this.closeRedBox(); } }.bind(this) }); } }, /** * Callback for editing a calendar. Fills the edit form with the correct * values. * * @param string calendar Calendar type and calendar id, separated by '|'. */ editCalendarCallback: function(calendar) { calendar = calendar.split('|'); var type = calendar[0]; calendar = calendar.length == 1 ? null : calendar[1]; var form = $('kronolithCalendarForm' + type), firstTab = form.down('.tabset a.kronolithTabLink'), info; form.enable(); form.reset(); if (firstTab) { this.openTab(firstTab); } $('kronolithCalendarDialog').select('.kronolithCalendarDiv').invoke('hide'); $('kronolithCalendar' + type + '1').show(); form.select('.kronolithCalendarContinue').invoke('enable'); $('kronolithC' + type + 'PUNew', 'kronolithC' + type + 'PGNew').compact().each(function(elm) { if (elm.tagName == 'SELECT') { $A(elm.options).each(function(option) { option.writeAttribute('disabled', false); }); } }); var newCalendar = !calendar; if (calendar && (Object.isUndefined(Kronolith.conf.calendars[type]) || Object.isUndefined(Kronolith.conf.calendars[type][calendar]))) { if (type != 'remote') { this.closeRedBox(); this.go(this.lastLocation); return; } newCalendar = true; } if (type == 'resourcegroup') { this.updateResourcegroupSelect(); } if (newCalendar) { switch (type) { case 'internal': HordeImple.AutoCompleter.kronolithCalendarinternalTags.reset(); // Fall through. case 'tasklists': $('kronolithCalendar' + type + 'LinkExport').up('span').hide(); break; case 'remote': if (calendar) { $('kronolithCalendarremoteUrl').setValue(calendar); $('kronolithCalendarremoteId').setValue(calendar); } break; case 'holiday': $('kronolithCalendarholidayDriver').update(); $H(Kronolith.conf.calendars.holiday).each(function(calendar) { if (calendar.value.show) { return; } $('kronolithCalendarholidayDriver').insert( new Element('option', { value: calendar.key }) .setStyle({ color: calendar.value.fg, backgroundColor: calendar.value.bg }) .insert(calendar.value.name.escapeHTML()) ); }); break; } $('kronolithCalendar' + type + 'Id').clear(); var color = '#', i; for (i = 0; i < 3; i++) { color += (Math.random() * 256 | 0).toColorPart(); } $('kronolithCalendar' + type + 'Color').setValue(color).setStyle({ backgroundColor: color, color: Color.brightness(Color.hex2rgb(color)) < 125 ? '#fff' : '#000' }); form.down('.kronolithCalendarDelete').hide(); $('kronolithCalendarinternalImportButton').hide(); } else { info = Kronolith.conf.calendars[type][calendar]; $('kronolithCalendar' + type + 'Id').setValue(calendar); $('kronolithCalendar' + type + 'Name').setValue(info.name); $('kronolithCalendar' + type + 'Color').setValue(info.bg).setStyle({ backgroundColor: info.bg, color: info.fg }); $('kronolithCalendarinternalImportButton').hide(); switch (type) { case 'internal': HordeImple.AutoCompleter.kronolithCalendarinternalTags.reset(Kronolith.conf.calendars.internal[calendar].tg); $('kronolithCalendar' + type + 'ImportCal').setValue('internal_' + calendar); if ($('kronolithCalendar' + type + 'LinkImport')) { if (info.edit) { $('kronolithCalendar' + type + 'LinkImport').up('li').show(); } else { $('kronolithCalendar' + type + 'LinkImport').up('li').hide(); } } $('kronolithCalendar' + type + 'UrlFeed').setValue(info.feed); $('kronolithCalendar' + type + 'EmbedUrl').setValue(info.embed); // Fall through. case 'tasklists': $('kronolithCalendar' + type + 'Description').setValue(info.desc); if ($('kronolithCalendar' + type + 'LinkExport')) { $('kronolithCalendar' + type + 'LinkExport').up('span').show(); $('kronolithCalendar' + type + 'Export').href = type == 'internal' ? Kronolith.conf.URI_CALENDAR_EXPORT.interpolate({ calendar: calendar }) : Kronolith.conf.tasks.URI_TASKLIST_EXPORT.interpolate({ tasklist: calendar.substring(6) }); } $('kronolithCalendar' + type + 'LinkUrls').up().show(); if (info.caldav) { $('kronolithCalendar' + type + 'UrlCaldav').setValue(info.caldav); $('kronolithCalendar' + type + 'Caldav').show(); } else { $('kronolithCalendar' + type + 'Caldav').hide(); } $('kronolithCalendar' + type + 'UrlWebdav').setValue(info.sub); break; case 'remote': $('kronolithCalendarremoteUrl').setValue(calendar); $('kronolithCalendarremoteDescription').setValue(info.desc); $('kronolithCalendarremoteUsername').setValue(info.user); $('kronolithCalendarremotePassword').setValue(info.password); break; case 'resourcegroup': $('kronolithCalendarresourcegroupDescription').setValue(info.desc); $('kronolithCalendarresourcegroupmembers').setValue(info.members); break; case 'resource': $('kronolithCalendarresourceDescription').setValue(info.desc); $('kronolithCalendarresourceResponseType').setValue(info.response_type); $('kronolithCalendarresourceExport').href = Kronolith.conf.URI_RESOURCE_EXPORT.interpolate({ calendar: calendar }); } } if (newCalendar || info.owner) { if (type == 'internal' || type == 'tasklists') { this.updateGroupDropDown([['kronolithC' + type + 'PGList', this.updateGroupPerms.bind(this, type)], ['kronolithC' + type + 'PGNew']]); $('kronolithC' + type + 'PBasic').show(); $('kronolithC' + type + 'PAdvanced').hide(); $('kronolithC' + type + 'PNone').setValue(1); if ($('kronolithC' + type + 'PAllShow')) { $('kronolithC' + type + 'PAllShow').disable(); } $('kronolithC' + type + 'PGList').disable(); $('kronolithC' + type + 'PGPerms').disable(); $('kronolithC' + type + 'PUList').disable(); $('kronolithC' + type + 'PUPerms').disable(); $('kronolithC' + type + 'PAdvanced').select('tr').findAll(function(tr) { return tr.retrieve('remove'); }).invoke('remove'); $('kronolithCalendar' + type + 'LinkUrls').up().show(); form.down('.kronolithColorPicker').show(); if (type == 'internal') { HordeCore.doAction('listTopTags', {}, { callback: this.topTagsCallback.curry('kronolithCalendarinternalTopTags', 'kronolithCalendarTag') }); } form.down('.kronolithCalendarSubscribe').hide(); form.down('.kronolithCalendarUnsubscribe').hide(); if ($('kronolithCalendar' + type + 'LinkPerms')) { $('kronolithCalendar' + type + 'LinkPerms').up('span').show(); } if (!Object.isUndefined(info) && info.owner) { this.setPermsFields(type, info.perms); } } if (type == 'remote' || type == 'internal' || type == 'tasklists') { if (newCalendar || (type == 'internal' && calendar == Kronolith.conf.user) || (type == 'tasklists' && calendar == 'tasks/' + Kronolith.conf.user)) { form.select('.kronolithCalendarDelete').invoke('hide'); } else { form.select('.kronolithCalendarDelete').invoke('show'); } } form.down('.kronolithCalendarSave').show(); form.down('.kronolithFormActions .kronolithSeparator').show(); } else { form.disable(); form.down('.kronolithColorPicker').hide(); form.down('.kronolithCalendarDelete').hide(); form.down('.kronolithCalendarSave').hide(); if (type == 'internal' || type == 'tasklists') { $('kronolithCalendar' + type + 'UrlCaldav').enable(); $('kronolithCalendar' + type + 'UrlAccount').enable(); $('kronolithCalendar' + type + 'UrlWebdav').enable(); if (type == 'internal') { $('kronolithCalendar' + type + 'UrlFeed').enable(); $('kronolithCalendar' + type + 'EmbedUrl').enable(); if (info.edit) { $('kronolithCalendarinternalImport').enable(); if (info.del) { $('kronolithCalendarinternalImportOver').enable(); } $('kronolithCalendarinternalImportButton').show().enable(); } } HordeImple.AutoCompleter.kronolithCalendarinternalTags.disable(); if (Kronolith.conf.calendars[type][calendar].show) { form.down('.kronolithCalendarSubscribe').hide(); form.down('.kronolithCalendarUnsubscribe').show().enable(); } else { form.down('.kronolithCalendarSubscribe').show().enable(); form.down('.kronolithCalendarUnsubscribe').hide(); } form.down('.kronolithFormActions .kronolithSeparator').show(); if ($('kronolithCalendar' + type + 'LinkPerms')) { $('kronolithCalendar' + type + 'LinkPerms').up('span').hide(); } } else { form.down('.kronolithFormActions .kronolithSeparator').hide(); } } }, /** * Updates the select list in the resourcegroup calendar dialog. */ updateResourcegroupSelect: function() { if (!Kronolith.conf.calendars.resource) { return; } $('kronolithCalendarresourcegroupmembers').update(); $H(Kronolith.conf.calendars.resource).each(function(r) { var o = new Element('option', { value: r.value.id }).update(r.value.name.escapeHTML()); $('kronolithCalendarresourcegroupmembers').insert(o); }); }, /** * Handles clicks on the radio boxes of the basic permissions screen. * * @param string type The calendar type, 'internal' or 'taskslists'. * @param string perm The permission to activate, 'None', 'All', or * 'Group'. */ permsClickHandler: function(type, perm) { $('kronolithC' + type + 'PAdvanced') .select('input[type=checkbox]') .invoke('setValue', 0); $('kronolithC' + type + 'PAdvanced').select('tr').findAll(function(tr) { return tr.retrieve('remove'); }).invoke('remove'); switch (perm) { case 'None': if ($('kronolithC' + type + 'PAllShow')) { $('kronolithC' + type + 'PAllShow').disable(); } $('kronolithC' + type + 'PGList').disable(); $('kronolithC' + type + 'PGPerms').disable(); $('kronolithC' + type + 'PUList').disable(); $('kronolithC' + type + 'PUPerms').disable(); break; case 'All': $('kronolithC' + type + 'PAllShow').enable(); $('kronolithC' + type + 'PGList').disable(); $('kronolithC' + type + 'PGPerms').disable(); $('kronolithC' + type + 'PUList').disable(); $('kronolithC' + type + 'PUPerms').disable(); var perms = { 'default': Kronolith.conf.perms.read, 'guest': Kronolith.conf.perms.read }; if ($F('kronolithC' + type + 'PAllShow')) { perms['default'] |= Kronolith.conf.perms.show; perms['guest'] |= Kronolith.conf.perms.show; } this.setPermsFields(type, perms); break; case 'G': if ($('kronolithC' + type + 'PAllShow')) { $('kronolithC' + type + 'PAllShow').disable(); } $('kronolithC' + type + 'PGList').enable(); $('kronolithC' + type + 'PGPerms').enable(); $('kronolithC' + type + 'PUList').disable(); $('kronolithC' + type + 'PUPerms').disable(); var group = $F('kronolithC' + type + 'PGSingle') ? $F('kronolithC' + type + 'PGSingle') : $F('kronolithC' + type + 'PGList'); this.insertGroupOrUser(type, 'group', group, true); $('kronolithC' + type + 'PGshow_' + group).setValue(1); $('kronolithC' + type + 'PGread_' + group).setValue(1); if ($F('kronolithC' + type + 'PGPerms') == 'edit') { $('kronolithC' + type + 'PGedit_' + group).setValue(1); } else { $('kronolithC' + type + 'PGedit_' + group).setValue(0); } $('kronolithC' + type + 'PGdel_' + group).setValue(0); if ($('kronolithC' + type + 'PGdelegate_' + group)) { $('kronolithC' + type + 'PGdelegate_' + group).setValue(0); } break; case 'U': if ($('kronolithC' + type + 'PAllShow')) { $('kronolithC' + type + 'PAllShow').disable(); } $('kronolithC' + type + 'PGList').disable(); $('kronolithC' + type + 'PGPerms').disable(); $('kronolithC' + type + 'PUList').enable(); $('kronolithC' + type + 'PUPerms').enable(); var users = $F('kronolithC' + type + 'PUList').strip(); users = users ? users.split(/\s*(?:,|\n)\s*/) : []; users.each(function(user) { if (!this.insertGroupOrUser(type, 'user', user, true)) { return; } $('kronolithC' + type + 'PUshow_' + user).setValue(1); $('kronolithC' + type + 'PUread_' + user).setValue(1); if ($F('kronolithC' + type + 'PUPerms') == 'edit') { $('kronolithC' + type + 'PUedit_' + user).setValue(1); } else { $('kronolithC' + type + 'PUedit_' + user).setValue(0); } $('kronolithC' + type + 'PUdel_' + user).setValue(0); if ($('kronolithC' + type + 'PUdelegate_' + user)) { $('kronolithC' + type + 'PUdelegate_' + user).setValue(0); } }, this); break; } }, /** * Populates the permissions field matrix. * * @param string type The calendar type, 'internal' or 'taskslists'. * @param object perms An object with the resource permissions. */ setPermsFields: function(type, perms) { if (this.groupLoading) { this.setPermsFields.bind(this, type, perms).defer(); return; } var allperms = $H(Kronolith.conf.perms), advanced = false, users = [], basic, same, groupPerms, groupId, userPerms; $H(perms).each(function(perm) { switch (perm.key) { case 'default': case 'guest': if (Object.isUndefined(same)) { same = perm.value; } else if (Object.isUndefined(basic) && same == perm.value && (perm.value == Kronolith.conf.perms.read || perm.value == (Kronolith.conf.perms.read | Kronolith.conf.perms.show))) { basic = perm.value == Kronolith.conf.perms.read ? 'all_read' : 'all_show'; } else if (perm.value != 0) { advanced = true; } break; case 'creator': if (perm.value != 0) { advanced = true; } break; case 'groups': if (!Object.isArray(perm.value)) { $H(perm.value).each(function(group) { if (!this.insertGroupOrUser(type, 'group', group.key)) { return; } if (!$('kronolithC' + type + 'PGshow_' + group.key)) { // Group doesn't exist anymore. delete perm.value[group.key]; return; } groupPerms = group.value; groupId = group.key; }, this); if (Object.isUndefined(basic) && $H(perm.value).size() == 1 && (groupPerms == (Kronolith.conf.perms.show | Kronolith.conf.perms.read) || groupPerms == (Kronolith.conf.perms.show | Kronolith.conf.perms.read | Kronolith.conf.perms.edit))) { basic = groupPerms == (Kronolith.conf.perms.show | Kronolith.conf.perms.read) ? 'group_read' : 'group_edit'; } else { advanced = true; } } break; case 'users': if (!Object.isArray(perm.value)) { $H(perm.value).each(function(user) { if (user.key != Kronolith.conf.user) { if (!this.insertGroupOrUser(type, 'user', user.key)) { return; } if (!$('kronolithC' + type + 'PUshow_' + user.key)) { // User doesn't exist anymore. delete perm.value[user.key]; return; } // Check if we already have other basic permissions. if (Object.isUndefined(userPerms) && !Object.isUndefined(basic)) { advanced = true; } // Check if all users have the same permissions. if (!Object.isUndefined(userPerms) && userPerms != user.value) { advanced = true; } userPerms = user.value; if (!advanced && (userPerms == (Kronolith.conf.perms.show | Kronolith.conf.perms.read) || userPerms == (Kronolith.conf.perms.show | Kronolith.conf.perms.read | Kronolith.conf.perms.edit))) { basic = userPerms == (Kronolith.conf.perms.show | Kronolith.conf.perms.read) ? 'user_read' : 'user_edit'; users.push(user.key); } else { advanced = true; } } }, this); } break; } allperms.each(function(baseperm) { if (baseperm.key == 'all') { return; } switch (perm.key) { case 'default': case 'guest': case 'creator': if (baseperm.value & perm.value) { $('kronolithC' + type + 'P' + perm.key + baseperm.key).setValue(1); } break; case 'groups': $H(perm.value).each(function(group) { if (baseperm.value & group.value) { $('kronolithC' + type + 'PG' + baseperm.key + '_' + group.key).setValue(1); } }); break; case 'users': $H(perm.value).each(function(user) { if (baseperm.value & user.value && user.key != Kronolith.conf.user) { $('kronolithC' + type + 'PU' + baseperm.key + '_' + user.key).setValue(1); } }); break; } }); }.bind(this)); if (advanced) { this.activateAdvancedPerms(type); } else { switch (basic) { case 'all_read': $('kronolithC' + type + 'PAll').setValue(1); $('kronolithC' + type + 'PAllShow').setValue(0); $('kronolithC' + type + 'PAllShow').enable(); break; case 'all_show': $('kronolithC' + type + 'PAll').setValue(1); $('kronolithC' + type + 'PAllShow').setValue(1); $('kronolithC' + type + 'PAllShow').enable(); break; case 'group_read': case 'group_edit': var setGroup = function(group) { if ($('kronolithC' + type + 'PGList').visible()) { $('kronolithC' + type + 'PGList').setValue(group); if ($('kronolithC' + type + 'PGList').getValue() != group) { // Group no longer exists. this.permsClickHandler(type, 'None'); } } else if ($('kronolithC' + type + 'PGSingle').getValue() != group) { // Group no longer exists. this.permsClickHandler(type, 'None'); } }.bind(this, groupId); if (this.groupLoading) { setGroup.defer(); } else { setGroup(); } $('kronolithC' + type + 'PG').setValue(1); $('kronolithC' + type + 'PGPerms').setValue(basic.substring(6)); $('kronolithC' + type + 'PAdvanced').hide(); $('kronolithC' + type + 'PBasic').show(); $('kronolithC' + type + 'PGPerms').enable(); break; case 'user_read': case 'user_edit': $('kronolithC' + type + 'PUList').enable().setValue(users.join(', ')); $('kronolithC' + type + 'PU').setValue(1); $('kronolithC' + type + 'PUPerms').setValue(basic.substring(5)); $('kronolithC' + type + 'PAdvanced').hide(); $('kronolithC' + type + 'PBasic').show(); $('kronolithC' + type + 'PUPerms').enable(); break; } } }, /** * Propagates a SELECT drop down list with the groups. * * @param array params A two-dimensional array with the following values * in each element: * - The id of the SELECT element. * - A callback method that is invoked with the group * list passes as an argument. */ updateGroupDropDown: function(params) { this.groupLoading = true; params.each(function(param) { var elm = $(param[0]), options = elm.childElements(); options.invoke('remove'); elm.up('form').disable(); }); HordeCore.doAction('listGroups', {}, { callback: function(r) { var groups; if (r.groups) { groups = $H(r.groups); params.each(function(param) { groups.each(function(group) { $(param[0]).insert(new Element('option', { value: group.key }).update(group.value.escapeHTML())); }); }); } params.each(function(param) { $(param[0]).up('form').enable(); if (param[1]) { param[1](groups); } }); this.groupLoading = false; }.bind(this) }); }, /** * Updates the group permission interface after the group list has * been loaded. * * @param string type The calendar type, 'internal' or 'taskslists'. * @param Hash groups The list of groups. */ updateGroupPerms: function(type, groups) { $('kronolithC' + type + 'PGSingle').clear(); if (!groups) { $('kronolithC' + type + 'PGNew').up('div').hide(); $('kronolithC' + type + 'PG').up('span').hide(); } else { $('kronolithC' + type + 'PGNew').up('div').show(); $('kronolithC' + type + 'PG').up('span').show(); if (groups.size() == 1) { $('kronolithC' + type + 'PGName') .update('"' + groups.values()[0].escapeHTML() + '"') .show(); $('kronolithC' + type + 'PGSingle').setValue(groups.keys()[0]); $('kronolithC' + type + 'PGList').hide(); } else { $('kronolithC' + type + 'PGName').hide(); $('kronolithC' + type + 'PGList').show(); } } }, /** * Inserts a group or user row into the advanced permissions interface. * * @param string type The calendar type, 'internal' or * 'taskslists'. * @param what string Either 'group' or 'user'. * @param group string The group id or user name to insert. * Defaults to the value of the drop down. * @param stay_basic boolean Enforces to NOT switch to the advanced * permissions screen. * * @return boolean Whether a row has been inserted. */ insertGroupOrUser: function(type, what, id, stay_basic) { var elm = $(what == 'user' ? 'kronolithC' + type + 'PUNew' : 'kronolithC' + type + 'PGNew'); if (id) { elm.setValue(id); } var value = elm.getValue(); if (!value) { if (id) { HordeCore.notify(Kronolith.text.invalid_user + ': ' + id, 'horde.error'); } return false; } var tr = elm.up('tr'), row = tr.clone(true).store('remove', true), td = row.down('td'), clearName = elm.tagName == 'SELECT' ? elm.options[elm.selectedIndex].text: elm.getValue(); td.update(); td.insert(clearName.escapeHTML()) .insert(new Element('input', { type: 'hidden', name: (what == 'user' ? 'u' : 'g') + '_names[' + value + ']', value: value })); row.select('input[type=checkbox]').each(function(input) { input.writeAttribute('name', input.name.replace(/\[.*?$/, '[' + value + ']')) .writeAttribute('id', input.id.replace(/new/, value)) .next() .writeAttribute('for', input.id); }); tr.insert({ before: row }); if (elm.tagName == 'SELECT') { elm.options[elm.selectedIndex].writeAttribute('disabled', true); elm.selectedIndex = 0; } else { elm.clear(); } if (!stay_basic) { this.activateAdvancedPerms(type); } return true; }, /** * Activates the advanced permissions. * * @param string type The calendar type, 'internal' or 'taskslists'. */ activateAdvancedPerms: function(type) { [$('kronolithC' + type + 'PNone'), $('kronolithC' + type + 'PU'), $('kronolithC' + type + 'PG')].each(function(radio) { radio.checked = false; }); if ($('kronolithC' + type + 'PAll')) { $('kronolithC' + type + 'PAll').checked = false; } $('kronolithC' + type + 'PBasic').hide(); $('kronolithC' + type + 'PAdvanced').show(); }, /** * Opens the next screen of the calendar management wizard. * * @param string type The calendar type. */ calendarNext: function(type) { var i = 1; while (!$('kronolithCalendar' + type + i).visible()) { i++; } $('kronolithCalendar' + type + i).hide(); $('kronolithCalendar' + type + (++i)).show(); if (this.colorPicker) { this.colorPicker.hide(); } }, /** * Submits the calendar form to save the calendar data. * * @param Element form The form node. * * @return boolean Whether the save request was successfully sent. */ saveCalendar: function(form) { if (this.colorPicker) { this.colorPicker.hide(); } var data = form.serialize({ hash: true }); if (data.type == 'holiday') { this.insertCalendarInList('holiday', data.driver, Kronolith.conf.calendars.holiday[data.driver]); this.toggleCalendar('holiday', data.driver); form.down('.kronolithCalendarSave').enable(); this.closeRedBox(); this.go(this.lastLocation); return; } if (data.name.empty()) { HordeCore.notify(data.type == 'tasklists' ? Kronolith.text.no_tasklist_title : Kronolith.text.no_calendar_title, 'horde.warning'); $('kronolithCalendar' + data.type + 'Name').focus(); return false; } HordeCore.doAction('saveCalendar', data, { callback: this.saveCalendarCallback.bind(this, form, data) }); return true; }, calendarImport: function(form, disableForm) { if ($F('kronolithCalendarinternalImport')) { HordeCore.notify(Kronolith.text.import_warning, 'horde.message'); this.loading++; $('kronolithLoading').show(); var name = 'kronolithIframe' + Math.round(Math.random() * 1000), iframe = new Element('iframe', { src: 'about:blank', name: name, id: name }).setStyle({ display: 'none' }); document.body.insert(iframe); form.enable(); form.target = name; form.submit(); if (disableForm) { form.disable(); } } }, /** * Callback method after saving a calendar. * * @param Element form The form node. * @param object data The serialized form data. * @param object r The ajax response object. */ saveCalendarCallback: function(form, data, r) { var type = form.id.replace(/kronolithCalendarForm/, ''); // If saving the calendar changed the owner, we need to delete // and re-insert the calendar. if (r.deleted) { this.deleteCalendar(type, data.calendar); delete data.calendar; } if (r.saved) { this.calendarImport(form, false); var cal = r.calendar, id; if (data.calendar) { var color = { backgroundColor: cal.bg, color: cal.fg }, legendSpan; id = data.calendar; this.getCalendarList(type, cal.owner).select('div').each(function(element) { if (element.retrieve('calendar') == id) { var link = element.down('.horde-resource-link span'); element.setStyle(color); link.update(cal.name.escapeHTML()); this.addShareIcon(cal, link); throw $break; } }, this); this.kronolithBody.select('div').each(function(el) { if (el.retrieve('calendar') == type + '|' + id) { el.setStyle(color); } }); legendSpan = $('kronolith-legend').select('span') .find(function(span) { return span.retrieve('calendarclass') == type && span.retrieve('calendar') == id; }); if (legendSpan) { legendSpan.setStyle(color).update(cal.name.escapeHTML()); } Kronolith.conf.calendars[type][id] = cal; } else { id = r.id; if (!Kronolith.conf.calendars[type]) { Kronolith.conf.calendars[type] = []; } Kronolith.conf.calendars[type][id] = cal; this.insertCalendarInList(type, id, cal); this.storeCache($H(), [type, id], this.viewDates(this.date, this.view), true); if (type == 'tasklists') { this.storeTasksCache($H(), this.tasktype, id.replace(/^tasks\//, ''), true); } } if (type == 'remote') { this.loadCalendar(type, id); } } form.down('.kronolithCalendarSave').enable(); this.closeRedBox(); this.go(this.lastLocation); }, /** * Deletes a calendar and all its events from the interface and cache. * * @param string type The calendar type. * @param string calendar The calendar id. */ deleteCalendar: function(type, calendar) { var container = this.getCalendarList(type, Kronolith.conf.calendars[type][calendar].owner), noItems = container.previous(), div = container.select('div').find(function(element) { return element.retrieve('calendar') == calendar; }), arrow = div.down('span'); arrow.purge(); arrow.remove(); div.purge(); div.remove(); if (noItems && noItems.tagName == 'DIV' && noItems.className == 'horde-info' && !container.childElements().size()) { noItems.show(); } this.deleteCalendarLegend(type, calendar); this.removeEvent(type + '|' + calendar); this.deleteCache([type, calendar]); if (type == 'tasklists' && this.view == 'tasks') { this.removeTask(calendar.replace(/^tasks\//, '')); } delete Kronolith.conf.calendars[type][calendar]; }, /** * Parses a date attribute string into a Date object. * * For other strings use Date.parse(). * * @param string date A yyyyMMdd date string. * * @return Date A date object. */ parseDate: function(date) { var d = new Date(date.substr(0, 4), date.substr(4, 2) - 1, date.substr(6, 2)); if (date.length == 12) { d.setHours(date.substr(8, 2)); d.setMinutes(date.substr(10, 2)); } return d; }, /** * Calculates first and last days being displayed. * * @var Date date The date of the view. * @var string view A view name. * * @return array Array with first and last day of the view. */ viewDates: function(date, view) { var start = date.clone(), end = date.clone(); switch (view) { case 'week': case 'workweek': if (view == 'workweek') { start.add(1).days(); } start.moveToBeginOfWeek(view == 'week' ? Kronolith.conf.week_start : 1); end = start.clone(); end.moveToEndOfWeek(Kronolith.conf.week_start); if (view == 'workweek') { end.add(Kronolith.conf.week_start == 0 ? -1 : -2).days(); } break; case 'month': start.setDate(1); start.moveToBeginOfWeek(Kronolith.conf.week_start); end.moveToLastDayOfMonth(); end.moveToEndOfWeek(Kronolith.conf.week_start); break; case 'year': start.setDate(1); start.setMonth(0); end.setMonth(11); end.moveToLastDayOfMonth(); break; case 'agenda': end.add(6).days(); break; } return [start, end]; }, /** * Stores a set of events in the cache. * * For dates in the specified date ranges that don't contain any events, * empty cache entries are created so that those dates aren't re-fetched * each time. * * @param object events A list of calendars and events as returned * from an ajax request. * @param string calendar A calendar string or array. * @param string dates A date range in the format yyyymmddyyyymmdd * as used in the ajax response signature. * @param boolean createCache Whether to create a cache list entry for the * response, if none exists yet. */ storeCache: function(events, calendar, dates, createCache) { if (Object.isString(calendar)) { calendar = calendar.split('|'); } // Create cache entry for the calendar. if (!this.ecache.get(calendar[0])) { if (!createCache) { return; } this.ecache.set(calendar[0], $H()); } if (!this.ecache.get(calendar[0]).get(calendar[1])) { if (!createCache) { return; } this.ecache.get(calendar[0]).set(calendar[1], $H()); } var calHash = this.ecache.get(calendar[0]).get(calendar[1]); // Create empty cache entries for all dates. if (!!dates) { var day = dates[0].clone(), date; while (!day.isAfter(dates[1])) { date = day.dateString(); if (!calHash.get(date)) { if (!createCache) { return; } if (!this.cacheStart || this.cacheStart.isAfter(day)) { this.cacheStart = day.clone(); } if (!this.cacheEnd || this.cacheEnd.isBefore(day)) { this.cacheEnd = day.clone(); } calHash.set(date, $H()); } day.add(1).day(); } } var cal = calendar.join('|'); $H(events).each(function(date) { // We might not have a cache for this date if the event lasts // longer than the current view if (!calHash.get(date.key)) { return; } // Store calendar string and other useful information in event // objects. $H(date.value).each(function(event) { event.value.calendar = cal; event.value.start = Date.parse(event.value.s); event.value.end = Date.parse(event.value.e); }); // Store events in cache. calHash.set(date.key, calHash.get(date.key).merge(date.value)); }); }, /** * Stores a set of tasks in the cache. * * @param Hash tasks The tasks to be stored. * @param string tasktypes The task type that's being stored. * @param string tasklist The task list to which the tasks belong. * @param boolean createCache Whether to create a cache list entry for the * response, if none exists yet. */ storeTasksCache: function(tasks, tasktypes, tasklist, createCache) { var taskHashes = {}, cacheExists = {}; if (tasktypes == 'all' || tasktypes == 'future') { tasktypes = [ 'complete', 'incomplete' ]; } else { tasktypes = [ tasktypes ]; } tasktypes.each(function(tasktype) { cacheExists[tasktype] = false; if (!this.tcache.get(tasktype)) { if (!createCache) { return; } this.tcache.set(tasktype, $H()); } if (!tasklist) { return; } if (!this.tcache.get(tasktype).get(tasklist)) { if (!createCache) { return; } this.tcache.get(tasktype).set(tasklist, $H()); cacheExists[tasktype] = true; } else { cacheExists[tasktype] = true; } taskHashes[tasktype] = this.tcache.get(tasktype).get(tasklist); }, this); $H(tasks).each(function(task) { var tasktype = task.value.cp ? 'complete' : 'incomplete'; if (!cacheExists[tasktype]) { return; } if (!Object.isUndefined(task.value.s)) { task.value.start = Date.parse(task.value.s); } if (!Object.isUndefined(task.value.du)) { task.value.due = Date.parse(task.value.du); } taskHashes[tasktype].set(task.key, task.value); }); }, /** * Deletes an event or a complete calendar from the cache. * * @param string calendar A calendar string or array. * @param string event An event ID or empty if deleting the calendar. * @param string day A specific day to delete in yyyyMMdd form. */ deleteCache: function(calendar, event, day) { if (Object.isString(calendar)) { calendar = calendar.split('|'); } if (!this.ecache.get(calendar[0]) || !this.ecache.get(calendar[0]).get(calendar[1])) { return; } if (event) { this.ecache.get(calendar[0]).get(calendar[1]).each(function(day) { day.value.unset(event); }); } else if (day) { this.ecache.get(calendar[0]).get(calendar[1]).unset(day); } else { this.ecache.get(calendar[0]).unset(calendar[1]); } }, /** * Deletes tasks from the cache. * * @param string list A task list string. * @param string task A task ID. If empty, all tasks from the list are * deleted. */ deleteTasksCache: function(list, task) { this.deleteCache([ 'external', 'tasks/' + list ], task); $w('complete incomplete').each(function(type) { if (!Object.isUndefined(this.tcache.get(type)) && !Object.isUndefined(this.tcache.get(type).get(list))) { if (task) { this.tcache.get(type).get(list).unset(task); } else { this.tcache.get(type).unset(list); } } }, this); }, /** * Return all events for a single day from all displayed calendars merged * into a single hash. * * @param string date A yyyymmdd date string. * * @return Hash An event hash which event ids as keys and event objects as * values. */ getCacheForDate: function(date, calendar) { if (calendar) { var cals = calendar.split('|'); if (!this.ecache.get(cals[0]) || !this.ecache.get(cals[0]).get(cals[1])) { return $H(); } var x = this.ecache.get(cals[0]).get(cals[1]).get(date); return this.ecache.get(cals[0]).get(cals[1]).get(date); } var events = $H(); this.ecache.each(function(type) { type.value.each(function(cal) { if (!Kronolith.conf.calendars[type.key][cal.key].show) { return; } events = events.merge(cal.value.get(date)); }); }); return events; }, /** * Helper method for Enumerable.sortBy to sort events first by start time, * second by end time reversed. * * @param Hash event A hash entry with the event object as the value. * * @return string A comparable string. */ sortEvents: function(event) { return event.value.sort; }, /** * Adds a new location to the history and displays it in the URL hash. * * This is not really a history, because only the current and the last * location are stored. * * @param string loc The location to save. * @param boolean save Whether to actually save the location. This should * be false for any location that are displayed on top * of another location, i.e. in a popup view. */ addHistory: function(loc, save) { location.hash = encodeURIComponent(loc); this.lastLocation = this.currentLocation; if (Object.isUndefined(save) || save) { this.currentLocation = loc; } this.openLocation = loc; }, /** * Loads an external page. * * @param string loc The URL of the page to load. */ loadPage: function(loc) { window.location.assign(loc); }, searchSubmit: function(e) { this.go('search:' + this.search + ':' + $F('horde-search-input')); }, searchReset: function(e) { HordeTopbar.searchGhost.reset(); }, /** * Event handler for HordeCore:showNotifications events. */ showNotification: function(e) { if (!e.memo.flags || !e.memo.flags.alarm || !e.memo.flags.growl || !e.memo.flags.alarm.params.notify.ajax) { return; } var growl = e.memo.flags.growl, link = growl.down('A'); if (link) { link.observe('click', function(ee) { ee.stop(); HordeCore.Growler.ungrowl(growl); this.go(e.memo.flags.alarm.params.notify.ajax); }.bind(this)); } }, /* Keydown event handler */ keydownHandler: function(e) { if (e.stopped) { return; } var kc = e.keyCode || e.charCode, form = e.findElement('FORM'), trigger = e.findElement(); switch (trigger.id) { case 'kronolithEventLocation': if (kc == Event.KEY_RETURN && $F('kronolithEventLocation')) { this.initializeMap(true); this.geocode($F('kronolithEventLocation')); e.stop(); return; } break; case 'kronolithCalendarinternalUrlCaldav': case 'kronolithCalendarinternalUrlWebdav': case 'kronolithCalendarinternalUrlAccount': case 'kronolithCalendarinternalUrlFeed': case 'kronolithCalendartasklistsUrlCaldav': case 'kronolithCalendartasklistsUrlWebdav': case 'kronolithCalendartasklistsUrlAccount': if (String.fromCharCode(kc) != 'C' || (this.macos && !e.metaKey) || (!this.macos && !e.ctrlKey)) { e.stop(); return; } break; } if (form) { switch (kc) { case Event.KEY_RETURN: switch (form.identify()) { case 'kronolithEventForm': if (e.element().tagName != 'TEXTAREA') { this.saveEvent(); e.stop(); } break; case 'kronolithTaskForm': if (e.element().tagName != 'TEXTAREA') { this.saveTask(); e.stop(); } break; case 'kronolithQuickinsertForm': this.quickSaveEvent(); e.stop(); break; case 'kronolithCalendarForminternal': case 'kronolithCalendarFormtasklists': case 'kronolithCalendarFormremote': // Disabled for now, we have to also catch Continue buttons. //var saveButton = form.down('.kronolithCalendarSave'); //saveButton.disable(); //if (!this.saveCalendar(form)) { // saveButton.enable(); //} //e.stop(); break; } break; case Event.KEY_ESC: switch (form.identify()) { case 'kronolithQuickinsertForm': case 'kronolithQuicktaskForm': this.quickClose(); break; case 'kronolithEventForm': case 'kronolithTaskForm': Horde_Calendar.hideCal(); this.closeRedBox(); this.go(this.lastLocation); break; } break; } return; } switch (kc) { case Event.KEY_ESC: Horde_Calendar.hideCal(); this.closeRedBox(); break; } }, keyupHandler: function(e) { switch (e.element().readAttribute('id')) { case 'kronolithEventLocation': if ($F('kronolithEventLocation') && Kronolith.conf.maps.driver) { $('kronolithEventMapLink').show(); } else if (Kronolith.conf.maps.driver) { $('kronolithEventMapLink').hide(); this.removeMapMarker(); } return; case 'kronolithEventStartTime': case 'kronolithEventEndTime': var field = $(e.element().readAttribute('id')), kc = e.keyCode; switch(e.keyCode) { case Event.KEY_UP: case Event.KEY_DOWN: case Event.KEY_RIGHT: case Event.KEY_LEFT: return; default: if ($F(field) !== this.knl[field.identify()].getCurrentEntry()) { this.knl[field.identify()].markSelected(null); } return; } } }, clickHandler: function(e, dblclick) { if (e.isRightClick() || typeof e.element != 'function') { return; } var elt = e.element(), orig = e.element(), id, tmp, calendar; while (Object.isElement(elt)) { id = elt.readAttribute('id'); switch (id) { case 'kronolithNewEvent': this.go('event'); e.stop(); return; case 'kronolithNewTask': this.go('task'); e.stop(); return; case 'kronolithQuickEvent': if (this.view == 'tasks') { RedBox.showHtml($('kronolithQuicktask').show()); } else { this.updateCalendarDropDown('kronolithQuickinsertCalendars'); $('kronolithQuickinsertCalendars').setValue(Kronolith.conf.default_calendar); RedBox.showHtml($('kronolithQuickinsert').show()); } e.stop(); return; case 'kronolithQuickinsertSave': this.quickSaveEvent(); e.stop(); return; case 'kronolithQuicktaskSave': this.quickSaveTask(); e.stop(); return; case 'kronolithQuickinsertCancel': case 'kronolithQuicktaskCancel': this.quickClose(); e.stop(); return; case 'kronolithGotoToday': var view = this.view; if (!$w('day workweek week month year agenda').include(view)) { view = Kronolith.conf.login_view; } this.go(view + ':' + new Date().dateString()); e.stop(); return; case 'kronolithEventAllday': this.toggleAllDay(); break; case 'kronolithEventAlarmDefaultOn': this.disableAlarmMethods('Event'); break; case 'kronolithTaskAlarmDefaultOn': this.disableAlarmMethods('Task'); break; case 'kronolithEventAlarmPrefs': HordeCore.redirect(HordeCore.addURLParam( Kronolith.conf.prefs_url, { group: 'notification' } )); e.stop(); break; case 'kronolithTaskAlarmPrefs': if (Kronolith.conf.tasks.prefs_url) { HordeCore.redirect(HordeCore.addURLParam( Kronolith.conf.tasks.prefs_url, { group: 'notification' } )); } e.stop(); break; case 'kronolithEventLinkNone': case 'kronolithEventLinkDaily': case 'kronolithEventLinkWeekly': case 'kronolithEventLinkMonthly': case 'kronolithEventLinkYearly': case 'kronolithEventLinkLength': case 'kronolithTaskLinkNone': case 'kronolithTaskLinkDaily': case 'kronolithTaskLinkWeekly': case 'kronolithTaskLinkMonthly': case 'kronolithTaskLinkYearly': case 'kronolithTaskLinkLength': this.toggleRecurrence( id.startsWith('kronolithEvent'), id.substring(id.startsWith('kronolithEvent') ? 18 : 17)); break; case 'kronolithEventRepeatDaily': case 'kronolithEventRepeatWeekly': case 'kronolithEventRepeatMonthly': case 'kronolithEventRepeatYearly': case 'kronolithEventRepeatLength': case 'kronolithTaskRepeatDaily': case 'kronolithTaskRepeatWeekly': case 'kronolithTaskRepeatMonthly': case 'kronolithTaskRepeatYearly': case 'kronolithTaskRepeatLength': this.toggleRecurrence( id.startsWith('kronolithEvent'), id.substring(id.startsWith('kronolithEvent') ? 20 : 19)); break; case 'kronolithEventSave': if (!elt.disabled) { this._checkDate($('kronolithEventStartDate')); this._checkDate($('kronolithEventEndDate')); if ($F('kronolithEventAttendees') && $F('kronolithEventId')) { $('kronolithEventSendUpdates').setValue(0); $('kronolithEventDiv').hide(); $('kronolithUpdateDiv').show(); e.stop(); break; } } case 'kronolithEventSendUpdateYes': if (this.uatts) { this.uatts.u = true; } else { $('kronolithEventSendUpdates').setValue(1); } case 'kronolithEventSendUpdateNo': if (this.uatts) { this.doDragDropUpdate(this.uatts, this.ucb); this.uatts = null; this.ucb = null; this.closeRedBox(); $('kronolithUpdateDiv').hide(); $('kronolithEventDiv').show(); } else if (!elt.disabled) { this.saveEvent(); } e.stop(); break; case 'kronolithEventConflictYes': this.doSaveEvent(); e.stop(); break; case 'kronolithEventConflictNo': $('kronolithConflictDiv').hide(); $('kronolithEventDiv').show(); e.stop(); break; case 'kronolithEventSaveAsNew': if (!elt.disabled) { $('kronolithEventSendUpdates').setValue(1); this.saveEvent(true); } e.stop(); break; case 'kronolithTaskSave': if (!elt.disabled) { this.saveTask(); } e.stop(); break; case 'kronolithEventDeleteCancel': $('kronolithDeleteDiv').hide(); $('kronolithEventDiv').show(); e.stop(); return; case 'kronolithEventSendCancellationYes': $('kronolithRecurDeleteAll').enable(); $('kronolithRecurDeleteCurrent').enable(); $('kronolithRecurDeleteFuture').enable(); this.paramsCache.sendupdates = 1; case 'kronolithEventSendCancellationNo': $('kronolithRecurDeleteAll').enable(); $('kronolithRecurDeleteCurrent').enable(); $('kronolithRecurDeleteFuture').enable(); $('kronolithCancellationDiv').hide(); this.delete_verified = true; case 'kronolithEventDelete': if ((Kronolith.conf.confirm_delete || this.recurs) && !this.delete_verified) { $('kronolithEventDiv').hide(); $('kronolithDeleteDiv').show(); e.stop(); break; } else { $('kronolithEventDiv').hide(); this.delete_verified = false; } // Fallthrough case 'kronolithRecurDeleteAll': case 'kronolithRecurDeleteCurrent': case 'kronolithRecurDeleteFuture': case 'kronolithEventDeleteConfirm': if (elt.disabled) { e.stop(); break; } elt.disable(); var cal = $F('kronolithEventCalendar'), eventid = $F('kronolithEventId'); if (id != 'kronolithEventSendCancellationNo' && id != 'kronolithEventSendCancellationYes') { this.paramsCache = { cal: cal, id: eventid, rstart: $F('kronolithEventRecurOStart'), cstart: this.cacheStart.toISOString(), cend: this.cacheEnd.toISOString() }; switch (id) { case 'kronolithRecurDeleteAll': this.paramsCache.r = 'all'; break; case 'kronolithRecurDeleteCurrent': this.paramsCache.r = 'current'; break; case 'kronolithRecurDeleteFuture': this.paramsCache.r = 'future'; break; } } if (id != 'kronolithEventSendCancellationNo' && id != 'kronolithEventSendCancellationYes' && $F('kronolithEventAttendees')) { $('kronolithDeleteDiv').hide(); $('kronolithCancellationDiv').show(); e.stop(); break; } this.kronolithBody.select('div').findAll(function(el) { return el.retrieve('calendar') == cal && el.retrieve('eventid') == eventid; }).invoke('hide'); var viewDates = this.viewDates(this.date, this.view), start = viewDates[0].toString('yyyyMMdd'), end = viewDates[1].toString('yyyyMMdd'); this.paramsCache.sig = start + end + (Math.random() + '').slice(2); this.paramsCache.view_start = start; this.paramsCache.view_end = end; HordeCore.doAction('deleteEvent', this.paramsCache, { callback: function(elt,r) { if (r.deleted) { var days; if (this.view == 'month' || this.view == 'week' || this.view == 'workweek' || this.view == 'day') { days = this.findEventDays(cal, eventid); days.each(function(day) { this.refreshResources(day, cal, eventid); }.bind(this)); } this.removeEvent(cal, eventid); if (r.uid) { this.removeException(cal, r.uid); } this.loadEventsCallback(r, false); if (days && days.length) { this.reRender(days); } } else { this.kronolithBody.select('div').findAll(function(el) { return el.retrieve('calendar') == cal && el.retrieve('eventid') == eventid; }).invoke('show'); } elt.enable(); }.curry(elt).bind(this) }); $('kronolithDeleteDiv').hide(); $('kronolithEventDiv').show(); this.closeRedBox(); this.go(this.lastLocation); e.stop(); break; case 'kronolithTaskDelete': if (elt.disabled) { e.stop(); break; } elt.disable(); var tasklist = $F('kronolithTaskOldList'), taskid = $F('kronolithTaskId'); HordeCore.doAction('deleteTask', { list: tasklist, id: taskid }, { callback: function(r) { if (r.deleted) { this.removeTask(tasklist, taskid); } else { elt.enable(); $('kronolithViewTasksBody').select('tr').find(function(el) { return el.retrieve('tasklist') == tasklist && el.retrieve('taskid') == taskid; }).toggle(); } }.bind(this) }); var taskrow = $('kronolithViewTasksBody').select('tr').find(function(el) { return el.retrieve('tasklist') == tasklist && el.retrieve('taskid') == taskid; }); if (taskrow) { taskrow.hide(); } this.closeRedBox(); this.go(this.lastLocation); e.stop(); break; case 'kronolithCinternalPMore': case 'kronolithCinternalPLess': case 'kronolithCtasklistsPMore': case 'kronolithCtasklistsPLess': var type = id.match(/kronolithC(.*)P/)[1]; $('kronolithC' + type + 'PBasic').toggle(); $('kronolithC' + type + 'PAdvanced').toggle(); e.stop(); break; case 'kronolithCinternalPNone': case 'kronolithCinternalPAll': case 'kronolithCinternalPG': case 'kronolithCinternalPU': case 'kronolithCtasklistsPNone': case 'kronolithCtasklistsPAll': case 'kronolithCtasklistsPG': case 'kronolithCtasklistsPU': var info = id.match(/kronolithC(.*)P(.*)/); this.permsClickHandler(info[1], info[2]); break; case 'kronolithCinternalPAllShow': case 'kronolithCtasklistsPAllShow': var type = id.match(/kronolithC(.*)P/)[1]; this.permsClickHandler(type, 'All'); break; case 'kronolithCinternalPAdvanced': case 'kronolithCtasklistsPAdvanced': var type = id.match(/kronolithC(.*)P/)[1]; if (orig.tagName != 'INPUT') { break; } this.activateAdvancedPerms(type); if (orig.name.match(/u_.*||new/)) { this.insertGroupOrUser(type, 'user'); } break; case 'kronolithCinternalPUAdd': case 'kronolithCinternalPGAdd': case 'kronolithCtasklistsPUAdd': case 'kronolithCtasklistsPGAdd': var info = id.match(/kronolithC(.*)P(.)/); this.insertGroupOrUser(info[1], info[2] == 'U' ? 'user' : 'group'); break; case 'kronolithNavDay': case 'kronolithNavWeek': case 'kronolithNavWorkweek': case 'kronolithNavMonth': case 'kronolithNavYear': case 'kronolithNavAgenda': this.go(id.substring(12).toLowerCase() + ':' + this.date.dateString()); e.stop(); return; case 'kronolithNavTasks': this.go('tasks'); e.stop(); return; case 'kronolithTasksAll': case 'kronolithTasksComplete': case 'kronolithTasksIncomplete': case 'kronolithTasksFuture': this.go('tasks:' + id.substring(14).toLowerCase()); e.stop(); return; case 'kronolithMinicalDate': this.go('month:' + orig.retrieve('date')); e.stop(); return; case 'kronolith-minical': if (orig.id == 'kronolith-minical-prev') { var date = this.parseDate($('kronolithMinicalDate').retrieve('date')); date.previous().month(); this.updateMinical(date, date.getMonth() == this.date.getMonth() ? this.view : undefined); e.stop(); return; } if (orig.id == 'kronolith-minical-next') { var date = this.parseDate($('kronolithMinicalDate').retrieve('date')); date.next().month(); this.updateMinical(date, date.getMonth() == this.date.getMonth() ? this.view : null); e.stop(); return; } var tmp = orig; if (tmp.tagName.toLowerCase() != 'td') { tmp = tmp.up('td'); } if (tmp) { if (tmp.retrieve('weekdate') && tmp.hasClassName('kronolith-minical-week')) { this.go('week:' + tmp.retrieve('weekdate')); } else if (tmp.retrieve('date') && !tmp.hasClassName('empty')) { this.go('day:' + tmp.retrieve('date')); } } e.stop(); return; case 'kronolithEventsDay': var date = this.date.clone(); date.add(Math.round((e.pointerY() - elt.cumulativeOffset().top + elt.up('.kronolithViewBody').scrollTop) / this.daySizes.height * 2) * 30).minutes(); this.go('event:' + date.toString('yyyyMMddHHmm')); e.stop(); return; case 'kronolithViewMonth': if (orig.hasClassName('kronolith-first-col')) { var date = orig.retrieve('date'); if (date) { this.go('week:' + date); e.stop(); return; } } e.stop(); return; case 'kronolithViewYear': var tmp = orig; if (tmp.tagName.toLowerCase() != 'td' && tmp.tagName.toLowerCase() != 'th') { tmp = tmp.up('td'); } if (tmp) { if (tmp.retrieve('weekdate') && tmp.hasClassName('kronolith-minical-week')) { this.go('week:' + tmp.retrieve('weekdate')); } else if (tmp.hasClassName('kronolithMinicalDate')) { this.go('month:' + tmp.retrieve('date')); } else if (tmp.retrieve('date') && !tmp.hasClassName('empty')) { this.go('day:' + tmp.retrieve('date')); } } e.stop(); return; case 'kronolithViewAgendaBody': var tmp = orig; if (tmp.tagName != 'TR') { tmp = tmp.up('tr'); } if (tmp && tmp.retrieve('date')) { this.go('day:' + tmp.retrieve('date')); } e.stop(); return; case 'kronolithSearchButton': this.go('search:' + this.search + ':' + $F('horde-search-input')); e.stop(); break; case 'kronolithSearchFuture': if (this.search != 'future') { this.go('search:future:' + $F('horde-search-input')); } e.stop(); break; case 'kronolithSearchPast': if (this.search != 'past') { this.go('search:past:' + $F('horde-search-input')); } e.stop(); break; case 'kronolithSearchAll': if (this.search != 'all') { this.go('search:all:' + $F('horde-search-input')); } e.stop(); break; case 'kronolithEventToTimeslice': var params = $H(); params.set('e', $('kronolithEventId').value); params.set('cal', $('kronolithEventCalendar').value); params.set('t', $('kronolithEventTimesliceType').value); params.set('c', $('kronolithEventTimesliceClient').value); HordeCore.doAction('toTimeslice', params); break; case 'kronolithEventDialog': case 'kronolithTaskDialog': Horde_Calendar.hideCal(); return; case 'kronolithCalendarDialog': if (this.colorPicker) { this.colorPicker.hide(); } return; case 'kronolithEditRecurCurrent': case 'kronolithEditRecurFuture': $('kronolithEventStartDate').setValue(this.orstart); $('kronolithEventEndDate').setValue(this.orend); if (id == 'kronolithEditRecurCurrent') { this.toggleRecurrence('Exception'); } else { this.toggleRecurrence(this.lastRecurType); } return; case 'kronolithEditRecurAll': this.toggleRecurrence(this.lastRecurType); break; case 'kronolithEventUrlToggle': $('kronolithEventUrlDisplay').hide(); $('kronolithEventUrl').show(); e.stop(); return; case 'kronolithCalendarinternalImportButton': // Used when user has edit perms to a shared calendar. this.calendarImport(elt.up('form'), true); break; } // Caution, this only works if the element has definitely only a // single CSS class. switch (elt.className) { case 'kronolithPrev': case 'kronolithNext': var newDate = this.date.clone(), offset = elt.className == 'kronolithPrev' ? -1 : 1; switch (this.view) { case 'day': case 'agenda': newDate.add(offset).day(); break; case 'week': case 'workweek': newDate.add(offset).week(); break; case 'month': newDate.add(offset).month(); break; case 'year': newDate.add(offset).year(); break; } this.go(this.view + ':' + newDate.dateString()); e.stop(); return; case 'horde-add': this.go('calendar:' + id.replace(/kronolithAdd/, '')); e.stop(); return; case 'kronolithTabLink': this.openTab(elt); e.stop(); break; case 'horde-cancel': this.closeRedBox(); this.resetMap(); this.go(this.lastLocation); e.stop(); break; case 'kronolithEventTag': HordeImple.AutoCompleter.kronolithEventTags.addNewItemNode(elt.getText()); e.stop(); break; case 'kronolithCalendarTag': HordeImple.AutoCompleter.kronolithCalendarinternalTags.addNewItemNode(elt.getText()); e.stop(); break; case 'kronolithTaskTag': HordeImple.AutoCompleter.kronolithTaskTags.addNewItemNode(elt.getText()); e.stop(); break; case 'kronolithEventGeo': this.initializeMap(true); this.geocode($F('kronolithEventLocation')); e.stop(); break; case 'kronolithTaskRow': if (elt.retrieve('taskid')) { this.go('task:' + elt.retrieve('tasklist') + ':' + elt.retrieve('taskid')); } e.stop(); return; case 'horde-resource-edit-000': case 'horde-resource-edit-fff': this.go('calendar:' + elt.up().retrieve('calendarclass') + '|' + elt.up().retrieve('calendar')); e.stop(); return; case 'kronolithMore': this.go('day:' + elt.retrieve('date')); e.stop(); return; case 'kronolithDatePicker': id = elt.readAttribute('id'); Horde_Calendar.open(id, Date.parseExact($F(id.replace(/Picker$/, 'Date')), Kronolith.conf.date_format)); e.stop(); return; case 'kronolithColorPicker': var input = elt.previous(); this.colorPicker = new ColorPicker({ color: $F(input), offsetParent: elt, update: [[input, 'value'], [input, 'background']] }); e.stop(); return; } if (elt.hasClassName('kronolith-event')) { if (!Object.isUndefined(elt.retrieve('ajax'))) { this.go(elt.retrieve('ajax')); } else { this.go('event:' + elt.retrieve('calendar') + ':' + elt.retrieve('eventid') + ':' + elt.up().retrieve('date')); } e.stop(); return; } else if (elt.hasClassName('kronolithMonthDay')) { if (orig.hasClassName('kronolith-day')) { var date = orig.retrieve('date'); if (date) { this.go('day:' + date); e.stop(); return; } } this.go('event:' + elt.retrieve('date')); e.stop(); return; } else if (elt.hasClassName('kronolithWeekDay')) { this.go('day:' + elt.retrieve('date')); e.stop(); return; } else if (elt.hasClassName('kronolithEventsWeek') || elt.hasClassName('kronolithEventsWorkweek') || elt.hasClassName('kronolithAllDayContainer')) { var date = elt.retrieve('date'); if (elt.hasClassName('kronolithAllDayContainer')) { date += 'all'; } else { date = this.parseDate(date); date.add(Math.round((e.pointerY() - elt.cumulativeOffset().top + elt.up('.kronolithViewBody').scrollTop) / (elt.hasClassName('kronolithEventsWeek') ? this.weekSizes.height : this.workweekSizes.height) * 2) * 30).minutes(); date = date.toString('yyyyMMddHHmm'); } this.go('event:' + date); e.stop(); return; } else if (elt.hasClassName('kronolithTaskCheckbox')) { var taskid = elt.up('tr.kronolithTaskRow', 0).retrieve('taskid'), tasklist = elt.up('tr.kronolithTaskRow', 0).retrieve('tasklist'); this.toggleCompletionClass(taskid); HordeCore.doAction('toggleCompletion', { list: tasklist, id: taskid }, { callback: function(r) { if (r.toggled) { this.toggleCompletion(tasklist, taskid, r.toggled); if (r.toggled !== true) { this.toggleCompletionClass(taskid); } } else { this.toggleCompletionClass(taskid); } }.bind(this) }); e.stop(); return; } else if (elt.hasClassName('kronolithCalendarSave')) { if (!elt.disabled) { elt.disable(); if (!this.saveCalendar(elt.up('form'))) { elt.enable(); } } e.stop(); break; } else if (elt.hasClassName('kronolithCalendarContinue')) { if (elt.disabled) { e.stop(); break; } elt.disable(); var form = elt.up('form'), type = form.id.replace(/kronolithCalendarForm/, ''), i = 1; while (!$('kronolithCalendar' + type + i).visible()) { i++; } if (type == 'remote') { var params = { url: $F('kronolithCalendarremoteUrl') }; if (i == 1) { if (!$F('kronolithCalendarremoteUrl')) { HordeCore.notify(Kronolith.text.no_url, 'horde.warning'); e.stop(); break; } HordeCore.doAction('getRemoteInfo', params, { asynchronous: false, callback: function(r) { if (r.success) { if (r.name) { $('kronolithCalendarremoteName').setValue(r.name); } if (r.desc) { $('kronolithCalendarremoteDescription').setValue(r.desc); } this.calendarNext(type); this.calendarNext(type); } else if (r.auth) { this.calendarNext(type); } else { elt.enable(); } }.bind(this) }); } if (i == 2) { if ($F('kronolithCalendarremoteUsername')) { params.user = $F('kronolithCalendarremoteUsername'); params.password = $F('kronolithCalendarremotePassword'); } HordeCore.doAction('getRemoteInfo', params, { callback: function(r) { if (r.success) { if (r.name && !$F('kronolithCalendarremoteName')) { $('kronolithCalendarremoteName').setValue(r.name); } if (r.desc && !$F('kronolithCalendarremoteDescription')) { $('kronolithCalendarremoteDescription').setValue(r.desc); } this.calendarNext(type); } else { if (r.auth) { HordeCore.notify(Kronolith.text.wrong_auth, 'horde.warning'); } elt.enable(); } }.bind(this) }); } e.stop(); break; } this.calendarNext(type); e.stop(); break; } else if (elt.hasClassName('kronolithCalendarDelete')) { var form = elt.up('form'), type = form.id.replace(/kronolithCalendarForm/, ''), calendar = $F('kronolithCalendar' + type + 'Id'); if ((type == 'tasklists' && !window.confirm(Kronolith.text.delete_tasklist)) || (type != 'tasklists' && !window.confirm(Kronolith.text.delete_calendar))) { e.stop(); break; } if (!elt.disabled) { elt.disable(); HordeCore.doAction('deleteCalendar', { type: type, calendar: calendar }, { callback: function(r) { if (r.deleted) { this.deleteCalendar(type, calendar); } this.closeRedBox(); this.go(this.lastLocation); }.bind(this) }); } e.stop(); break; } else if (elt.hasClassName('kronolithCalendarSubscribe') || elt.hasClassName('kronolithCalendarUnsubscribe')) { var form = elt.up('form'); this.toggleCalendar($F(form.down('input[name=type]')), $F(form.down('input[name=calendar]'))); this.closeRedBox(); this.go(this.lastLocation); e.stop(); break; } else if (elt.tagName == 'INPUT' && (elt.name == 'event_alarms[]' || elt.name == 'task[alarm_methods][]')) { if (elt.name == 'event_alarms[]') { $('kronolithEventAlarmOn').setValue(1); $('kronolithEventAlarmDefaultOff').setValue(1); } else { $('kronolithTaskAlarmOn').setValue(1); $('kronolithTaskAlarmDefaultOff').setValue(1); } if ($(elt.id + 'Params')) { if (elt.getValue()) { $(elt.id + 'Params').show(); } else { $(elt.id + 'Params').hide(); } } break; } var calClass = elt.retrieve('calendarclass'); if (calClass) { this.toggleCalendar(calClass, elt.retrieve('calendar')); e.stop(); return; } elt = elt.up(); } // Workaround Firebug bug. Prototype.emptyFunction(); }, /** * Handles date selections from a date picker. */ datePickerHandler: function(e) { var field = e.element().previous(); field.setValue(e.memo.toString(Kronolith.conf.date_format)); this.updateTimeFields(field.identify()); }, /** * Handles moving an event to a different day in month view and all day * events in weekly/daily views. */ onDrop: function(e) { var drop = e.element(), el = e.memo.element; if (drop == el.up()) { return; } var lastDate = this.parseDate(el.up().retrieve('date')), newDate = this.parseDate(drop.retrieve('date')), diff = newDate.subtract(lastDate), eventid = el.retrieve('eventid'), cal = el.retrieve('calendar'), viewDates = this.viewDates(this.date, this.view), start = viewDates[0].toString('yyyyMMdd'), end = viewDates[1].toString('yyyyMMdd'), sig = start + end + (Math.random() + '').slice(2), events = this.getCacheForDate(lastDate.toString('yyyyMMdd'), cal), attributes = $H({ offDays: diff }), event = events.find(function(e) { return e.key == eventid; }); drop.insert(el); this.startLoading(cal, sig); if (event.value.r) { attributes.set('rday', lastDate); attributes.set('cstart', this.cacheStart); attributes.set('cend', this.cacheEnd); } var uatts = { cal: cal, id: eventid, view: this.view, sig: sig, view_start: start, view_end: end, att: Object.toJSON(attributes) }, callback = function(r) { if (r.events) { // Check if this is the still the result of the // most current request. if (r.sig == this.eventsLoading[r.cal]) { var days; if ((this.view == 'month' && Kronolith.conf.max_events) || this.view == 'week' || this.view == 'workweek' || this.view == 'day') { days = this.findEventDays(cal, eventid); } this.removeEvent(cal, eventid); if (days && days.length) { this.reRender(days); } } $H(r.events).each(function(days) { $H(days.value).each(function(event) { if (event.value.c.startsWith('tasks/')) { var tasklist = event.value.c.substr(6), task = event.key.substr(6), taskObject; if (this.tcache.get('incomplete') && this.tcache.get('incomplete').get(tasklist) && this.tcache.get('incomplete').get(tasklist).get(task)) { taskObject = this.tcache.get('incomplete').get(tasklist).get(task); taskObject.due = Date.parse(event.value.s); this.tcache.get('incomplete').get(tasklist).set(task, taskObject); } } }, this); }, this); } this.loadEventsCallback(r, false); $H(r.events).each(function(days) { $H(days.value).each(function(event) { if (event.key == eventid) { this.refreshResources(days.key, cal, eventid, lastDate.toString('yyyyMMdd'), event); } }.bind(this)) }.bind(this)); }.bind(this); if (event.value.mt) { $('kronolithEventDiv').hide(); $('kronolithUpdateDiv').show(); RedBox.showHtml($('kronolithEventDialog').show()); this.uatts = uatts; this.ucb = callback; } else { this.doDragDropUpdate(uatts, callback); } }, onDragStart: function(e) { if (this.view == 'month') { return; } var elt = e.element(); if (elt.hasClassName('kronolithDragger')) { elt.up().addClassName('kronolith-selected'); DragDrop.Drags.getDrag(elt).top = elt.cumulativeOffset().top; } else if (elt.hasClassName('kronolithEditable')) { elt.addClassName('kronolith-selected').setStyle({ left: 0, width: (this.view == 'week' || this.view == 'workweek') ? '90%' : '95%', zIndex: 1 }); } this.scrollTop = $('kronolithView' + this.view.capitalize()) .down('.kronolithViewBody') .scrollTop; this.scrollLast = this.scrollTop; }, onDrag: function(e) { if (this.view == 'month') { return; } var elt = e.element(), drag = DragDrop.Drags.getDrag(elt); storage = this.view + 'Sizes', step = this[storage].height / 6; if (!drag.event) { return; } var event = drag.event.value; if (elt.hasClassName('kronolithDragger')) { // Resizing the event. var div = elt.up(), top = drag.ghost.cumulativeOffset().top, scrollTop = $('kronolithView' + this.view.capitalize()).down('.kronolithViewBody').scrollTop, offset = 0, height; // Check if view has scrolled since last call. if (scrollTop != this.scrollLast) { offset = scrollTop - this.scrollLast; this.scrollLast = scrollTop; } if (elt.hasClassName('kronolithDraggerTop')) { offset += top - drag.top; height = div.offsetHeight - offset; div.setStyle({ top: (div.offsetTop + offset) + 'px' }); offset = drag.ghost.offsetTop; drag.top = top; } else { offset += top - drag.top; height = div.offsetHeight + offset; offset = div.offsetTop; drag.top = top; } div.setStyle({ height: height + 'px' }); this.calculateEventDates(event, storage, step, offset, height); drag.innerDiv.update('(' + event.start.toString(Kronolith.conf.time_format) + ' - ' + event.end.toString(Kronolith.conf.time_format) + ') ' + event.t.escapeHTML()); } else if (elt.hasClassName('kronolithEditable')) { // Moving the event. if (Object.isUndefined(drag.innerDiv)) { drag.innerDiv = drag.ghost.down('.kronolith-event-info'); } if ((this.view == 'week') || (this.view == 'workweek')) { var offsetX = Math.round(drag.ghost.offsetLeft / drag.stepX); event.offsetDays = offsetX; this.calculateEventDates(event, storage, step, drag.ghost.offsetTop, drag.divHeight, event.start.clone().addDays(offsetX), event.end.clone().addDays(offsetX)); } else { event.offsetDays = 0; this.calculateEventDates(event, storage, step, drag.ghost.offsetTop, drag.divHeight); } event.offsetTop = drag.ghost.offsetTop - drag.startTop; drag.innerDiv.update('(' + event.start.toString(Kronolith.conf.time_format) + ' - ' + event.end.toString(Kronolith.conf.time_format) + ') ' + event.t.escapeHTML()); elt.clonePosition(drag.ghost, { offsetLeft: (this.view == 'week' || this.view == 'workweek') ? -2 : 0 }); } }, onDragEnd: function(e) { if (this.view == 'month') { return; } if (!e.element().hasClassName('kronolithDragger') && !e.element().hasClassName('kronolithEditable')) { return; } var div = e.element(), drag = DragDrop.Drags.getDrag(div), event = drag.event; if (event.value.al) { return; } var date = drag.midnight, storage = this.view + 'Sizes', step = this[storage].height / 6, dates = this.viewDates(date, this.view), start = dates[0].dateString(), end = dates[1].dateString(), sig = start + end + (Math.random() + '').slice(2), element, attributes; div.removeClassName('kronolith-selected'); if (!Object.isUndefined(drag.innerDiv)) { this.setEventText(drag.innerDiv, event.value); } this.startLoading(event.value.calendar, sig); if (!Object.isUndefined(event.value.offsetTop)) { attributes = $H({ offDays: event.value.offsetDays, offMins: Math.round(event.value.offsetTop / step) * 10 }); element = div; } else if (div.hasClassName('kronolithDraggerTop')) { attributes = $H({ start: event.value.start }); element = div.up(); } else if (div.hasClassName('kronolithDraggerBottom')) { attributes = $H({ end: event.value.end }); element = div.up(); } else { attributes = $H({ start: event.value.start, end: event.value.end }); element = div; } if (event.value.r) { attributes.set('rstart', event.value.s); attributes.set('rend', event.value.e); attributes.set('cstart', this.cacheStart); attributes.set('cend', this.cacheEnd); } element.retrieve('drags').invoke('destroy'); var uatts = { cal: event.value.calendar, id: event.key, view: this.view, sig: sig, view_start: start, view_end: end, att: Object.toJSON(attributes) }, callback = function(r) { // Check if this is the still the result of the most current // request. if (r.events && r.sig == this.eventsLoading[r.cal]) { if (event.value.rs) { var d = new Date(event.value.s); this.refreshResources(d.toString('yyyyMMdd'), event.value.calendar, event.key) } this.removeEvent(event.value.calendar, event.key); } this.loadEventsCallback(r, false); }.bind(this); if (event.value.mt) { $('kronolithEventDiv').hide(); $('kronolithUpdateDiv').show(); RedBox.showHtml($('kronolithEventDialog').show()); this.uatts = uatts; this.ucb = callback; } else { this.doDragDropUpdate(uatts, callback); } }, doDragDropUpdate: function(att, cb) { HordeCore.doAction('updateEvent', att, { callback: cb }); }, /** * Refresh any resource calendars bound to the given just-updated event. * Clears the old resource event from UI and cache, and clears the cache * for the days of the new event, in order to allow listEvents to refresh * the UI. * * @param string dt The current/new date for the event (yyyyMMdd). * @param string cal The calendar the event exists in. * @param string eventid The eventid that is changing. * @param string last_dt The previous date for the event, if known. (yyyyMMdd). * @param object event The event object (if a new event) dt is ignored. * */ refreshResources: function(dt, cal, eventid, last_dt, event) { var events = this.getCacheForDate(dt, cal), update_cals = [], r_dates; if (!event) { event = events.find(function(e) { return e.key == eventid; }); } if (!dt) { dt = new Date(event.value.s); } else { dt = this.parseDate(dt); } if (event) { $H(event.value.rs).each(function(r) { var r_cal = ['resource', r.value.calendar], r_events = this.getCacheForDate(last_dt, r_cal.join('|')), r_event, day, end; if (r_events) { r_event = r_events.find(function(e) { return e.value.uid == event.value.uid }); if (r_event) { this.removeEvent(r_cal, r_event.key); day = new Date(r_event.value.s); end = new Date(r_event.value.s); while (!day.isAfter(end)) { this.deleteCache(r_cal, null, day.toString('yyyyMMdd')); day.add(1).day(); } day = new Date(event.value.s); end = new Date(event.value.e); while (!day.isAfter(end)) { this.deleteCache(r_cal, null, day.toString('yyyyMMdd')); day.add(1).day(); } } else { // Don't know the previous date/time so just nuke the cache. this.deleteCache(r_cal); } } else { this.deleteCache(r_cal); } update_cals.push(r_cal); }.bind(this)); if (update_cals.length) { dates = this.viewDates(dt, this.view); // Ensure we also grab the full length of the events. if (dates[0].isAfter(dt)) { dates[0] = dt; } var dt_end = new Date(event.value.e); if (dt_end.isAfter(dates[1])) { dates[1] = dt_end; } this.loadEvents(dates[0], dates[1], this.view, update_cals); } } }, editEvent: function(calendar, id, date, title) { if (this.redBoxLoading) { return; } if (Object.isUndefined(HordeImple.AutoCompleter.kronolithEventTags)) { this.editEvent.bind(this, calendar, id, date).defer(); return; } this.closeRedBox(); this.quickClose(); this.redBoxOnDisplay = RedBox.onDisplay; RedBox.onDisplay = function() { if (this.redBoxOnDisplay) { this.redBoxOnDisplay(); } try { $('kronolithEventForm').focusFirstElement(); } catch(e) {} if (Kronolith.conf.maps.driver && $('kronolithEventLinkMap').up().hasClassName('horde-active') && !this.mapInitialized) { this.initializeMap(); } RedBox.onDisplay = this.redBoxOnDisplay; }.bind(this); this.attendees = []; this.resources = []; this.updateCalendarDropDown('kronolithEventTarget'); this.toggleAllDay(false); this.openTab($('kronolithEventForm').down('.tabset a.kronolithTabLink')); this.disableAlarmMethods('Event'); this.knl.kronolithEventStartTime.markSelected(); this.knl.kronolithEventEndTime.markSelected(); $('kronolithEventForm').reset(); this.resetMap(); HordeImple.AutoCompleter.kronolithEventAttendees.reset(); HordeImple.AutoCompleter.kronolithEventTags.reset(); HordeImple.AutoCompleter.kronolithEventResources.reset(); if (Kronolith.conf.maps.driver) { $('kronolithEventMapLink').hide(); } $('kronolithEventSave').show().enable(); $('kronolithEventSaveAsNew').show().enable(); $('kronolithEventDelete').show().enable(); $('kronolithEventDeleteConfirm').enable(); $('kronolithEventTarget').show(); $('kronolithEventTargetRO').hide(); $('kronolithEventForm').down('.kronolithFormActions .kronolithSeparator').show(); $('kronolithEventExceptions').clear(); if (id) { // An id passed to this function indicates we are editing an event. RedBox.loading(); var attributes = { cal: calendar, id: id, date: date }; // Need the current st and et of this instance. var events = this.getCacheForDate(date.toString('yyyyMMdd'), calendar); if (events) { var ev = events.find(function(e) { return e.key == id; }); if (ev[1].r) { attributes.rsd = ev[1].start.dateString(); attributes.red = ev[1].end.dateString(); } } HordeCore.doAction('getEvent', attributes, { callback: this.editEventCallback.bind(this) }); $('kronolithEventTopTags').update(); } else { // This is a new event. HordeCore.doAction('listTopTags', {}, { callback: this.topTagsCallback.curry('kronolithEventTopTags', 'kronolithEventTag') }); var d; if (date) { if (date.endsWith('all')) { date = date.substring(0, date.length - 3); $('kronolithEventAllday').setValue(true); this.toggleAllDay(true); } d = this.parseDate(date); } else { d = new Date(); } if (title) { $('kronolithEventTitle').setValue(title); } $('kronolithEventId').clear(); $('kronolithEventCalendar').clear(); $('kronolithEventTarget').setValue(Kronolith.conf.default_calendar); $('kronolithEventDelete').hide(); $('kronolithEventStartDate').setValue(d.toString(Kronolith.conf.date_format)); $('kronolithEventStartTime').setValue(d.toString(Kronolith.conf.time_format)); this.updateFBDate(d); d.add(1).hour(); this.duration = 60; $('kronolithEventEndDate').setValue(d.toString(Kronolith.conf.date_format)); $('kronolithEventEndTime').setValue(d.toString(Kronolith.conf.time_format)); $('kronolithEventLinkExport').up('span').hide(); $('kronolithEventSaveAsNew').hide(); $('kronolithEventUrlDisplay').hide(); $('kronolithEventUrl').show(); this.toggleRecurrence(true, 'None'); $('kronolithEventEditRecur').hide(); this.enableAlarm('Event', Kronolith.conf.default_alarm); this.redBoxLoading = true; RedBox.showHtml($('kronolithEventDialog').show()); } }, /** * Generates ajax request parameters for requests to save events. * * @return object An object with request parameters. */ saveEventParams: function() { var viewDates = this.viewDates(this.date, this.view), params = { sig: viewDates[0].dateString() + viewDates[1].dateString(), view: this.view }; if (this.cacheStart) { params.view_start = this.cacheStart.dateString(); params.view_end = this.cacheEnd.dateString(); } return params; }, /** * Submits the event edit form to create or update an event. */ saveEvent: function(asnew) { this.validateEvent(asnew); }, /** * Perform any preliminary checks necessary. doSaveEvent will be called from * the callback if checks are successful. * */ validateEvent: function(asnew) { if (this.wrongFormat.size()) { HordeCore.notify(Kronolith.text.fix_form_values, 'horde.warning'); return; } // Check that there are no conflicts. if (Kronolith.conf.has_resources && $F('kronolithEventResourceIds')) { HordeCore.doAction( 'checkResources', { s: this.getDate('start').toISOString(), e: this.getDate('end').toISOString(), i: $F('kronolithEventId'), c: $F('kronolithEventCalendar'), r: $F('kronolithEventResourceIds') }, { callback: this.validateEventCallback.curry(asnew).bind(this) } ); } else { this.validateEventCallback(asnew, {}); } }, validateEventCallback: function(asnew, r) { var conflict = false; $H(r).each(function(a) { // 3 == Kronolith::RESPONSE_DECLINED if (a.value == 3) { $('kronolithEventDiv').hide(); $('kronolithConflictDiv').show(); conflict = true; return; } }); if (!conflict) { this.doSaveEvent(asnew); } }, doSaveEvent: function(asnew) { var cal = $F('kronolithEventCalendar'), target = $F('kronolithEventTarget'), eventid = $F('kronolithEventId'), params; if (this.mapInitialized) { $('kronolithEventMapZoom').value = this.map.getZoom(); } params = $H($('kronolithEventForm').serialize({ hash: true })) .merge(this.saveEventParams()); params.set('as_new', asnew ? 1 : 0); if (this.cacheStart) { params.set('cstart', this.cacheStart.toISOString()); params.set('cend', this.cacheEnd.toISOString()); } HordeImple.AutoCompleter.kronolithEventTags.shutdown(); $('kronolithEventSave').disable(); $('kronolithEventSaveAsNew').disable(); $('kronolithEventDelete').disable(); this.startLoading(target, params.get('sig')); HordeCore.doAction('saveEvent', params, { callback: function(r) { if (!asnew && r.events && eventid) { this.removeEvent(cal, eventid); } this.loadEventsCallback(r, false); // Refresh bound exceptions var calendar = cal.split('|'), refreshed = false; $H(r.events).each(function(d) { $H(d.value).each(function(evt) { if (evt.value.bid) { var cache = this.getCacheForDate(this.findEventDays(cal, evt.key, cal)); cache.each(function(entry) { if (entry.value.bid == evt.value.bid && evt.value.c != calendar[1]) { this.removeEvent(cal, entry.key); } }.bind(this)); } if (!refreshed && ((evt.key == eventid) || !eventid) && evt.value.rs) { this.refreshResources(null, cal, eventid, false, evt); refreshed = true; } }.bind(this)) }.bind(this)); if (r.events) { this.resetMap(); this.closeRedBox(); this.go(this.lastLocation); } else { $('kronolithEventSave').enable(); $('kronolithEventSaveAsNew').enable(); $('kronolithEventDelete').enable(); } $('kronolithUpdateDiv').hide(); $('kronolithConflictDiv').hide(); $('kronolithEventDiv').show(); }.bind(this) }); }, quickSaveEvent: function() { var text = $F('kronolithQuickinsertQ'), cal = $F('kronolithQuickinsertCalendars'), params; params = $H($('kronolithEventForm').serialize({ hash: true })) .merge(this.saveEventParams()); params.set('text', text); params.set('cal', cal); this.closeRedBox(); this.startLoading(cal, params.get('sig')); HordeCore.doAction('quickSaveEvent', params, { callback: function(r) { this.loadEventsCallback(r, false); if (r.error) { this.editEvent(null, null, null, text); } else { $('kronolithQuickinsertQ').value = ''; } }.bind(this) }); }, /** * Closes and resets the quick event form. */ quickClose: function() { $('kronolithQuickinsertQ').value = ''; if ($('kronolithQuicktaskQ')) { $('kronolithQuicktaskQ').value = ''; } this.closeRedBox(); }, topTagsCallback: function(update, tagclass, r) { $('kronolithEventTabTags').select('label').invoke('show'); if (!r.tags) { $(update).update(); return; } var t = new Element('ul', { className: 'horde-tags' }); r.tags.each(function(tag) { if (tag == null) { return; } t.insert(new Element('li', { className: tagclass }).update(tag.escapeHTML())); }); $(update).update(t); }, /** * Callback method for showing event forms. * * @param object r The ajax response object. */ editEventCallback: function(r) { if (!r.event) { RedBox.close(); this.go(this.lastLocation); return; } var ev = r.event; if (!Object.isUndefined(ev.ln)) { this.loadPage(ev.ln); this.closeRedBox(); return; } /* Basic information */ $('kronolithEventId').setValue(ev.id); $('kronolithEventCalendar').setValue(ev.ty + '|' + ev.c); $('kronolithEventTarget').setValue(ev.ty + '|' + ev.c); $('kronolithEventTargetRO').update(Kronolith.conf.calendars[ev.ty][ev.c].name.escapeHTML()); $('kronolithEventTitle').setValue(ev.t); $('kronolithEventLocation').setValue(ev.l); $('kronolithEventTimezone').setValue(ev.tz); if (ev.l && Kronolith.conf.maps.driver) { $('kronolithEventMapLink').show(); } if (ev.uhl) { $('kronolithEventUrlDisplay').down().update(ev.uhl); $('kronolithEventUrlDisplay').show(); $('kronolithEventUrl').hide(); } else { $('kronolithEventUrlDisplay').hide(); $('kronolithEventUrl').show(); } if (ev.u) { $('kronolithEventUrl').setValue(ev.u); } $('kronolithEventAllday').setValue(ev.al); if (ev.r && ev.rsd && ev.red) { // Save the original datetime, so we can properly create the // exception. var osd = Date.parse(ev.rsd + ' ' + ev.st); var oed = Date.parse(ev.red + ' ' + ev.et); $('kronolithEventRecurOStart').setValue(osd.toString('s')); $('kronolithEventRecurOEnd').setValue(oed.toString('s')); // ...and put the same value in the form field to replace the // date of the initial series. $('kronolithEventStartDate').setValue(ev.sd); $('kronolithEventEndDate').setValue(ev.ed); // Save the current datetime in case we are not editing 'all' this.orstart = ev.rsd; this.orend = ev.red; } else { $('kronolithEventStartDate').setValue(ev.sd); $('kronolithEventEndDate').setValue(ev.ed); $('kronolithEventRecurEnd').clear(); $('kronolithEventRecurOStart').clear(); $('kronolithEventRecurOEnd').clear(); this.orstart = null; this.orend = null; } $('kronolithEventStartTime').setValue(ev.st); this.knl.kronolithEventStartTime.setSelected(ev.st); this.updateFBDate(Date.parseExact(ev.sd, Kronolith.conf.date_format)); $('kronolithEventEndTime').setValue(ev.et); this.knl.kronolithEventEndTime.setSelected(ev.et); this.duration = Math.abs(Date.parse(ev.e).getTime() - Date.parse(ev.s).getTime()) / 60000; this.toggleAllDay(ev.al); $('kronolithEventStatus').setValue(ev.x); $('kronolithEventDescription').setValue(ev.d); $('kronolithEventPrivate').setValue(ev.pv); $('kronolithEventLinkExport').up('span').show(); $('kronolithEventExport').href = Kronolith.conf.URI_EVENT_EXPORT.interpolate({ id: ev.id, calendar: ev.c, type: ev.ty }); /* Alarm */ if (ev.a) { this.enableAlarm('Event', ev.a); if (ev.m) { $('kronolithEventAlarmDefaultOff').checked = true; $H(ev.m).each(function(method) { $('kronolithEventAlarm' + method.key).setValue(1); if ($('kronolithEventAlarm' + method.key + 'Params')) { $('kronolithEventAlarm' + method.key + 'Params').show(); $H(method.value).each(function(param) { var input = $('kronolithEventAlarmParam' + param.key); if (input.type == 'radio') { input.up('form').select('input[type=radio]').each(function(radio) { if (radio.name == input.name && radio.value == param.value) { radio.setValue(1); throw $break; } }); } else { input.setValue(param.value); } }); } }); } } else { $('kronolithEventAlarmOff').setValue(true); } /* Recurrence */ if (ev.r) { this.setRecurrenceFields(true, ev.r); $('kronolithRecurDelete').show(); $('kronolithNoRecurDelete').hide(); $('kronolithEventEditRecur').show(); this.recurs = true; } else if (ev.bid) { $('kronolithRecurDelete').hide(); $('kronolithNoRecurDelete').show(); $('kronolithEventEditRecur').hide(); var div = $('kronolithEventRepeatException'); div.down('span').update(ev.eod); this.toggleRecurrence(true, 'Exception'); this.recurs = false; } else { $('kronolithRecurDelete').hide(); $('kronolithNoRecurDelete').show(); $('kronolithEventEditRecur').hide(); this.toggleRecurrence(true, 'None'); this.recurs = false; } /* Attendees */ if (!Object.isUndefined(ev.at)) { HordeImple.AutoCompleter.kronolithEventAttendees.reset(ev.at.pluck('l')); ev.at.each(this.addAttendee.bind(this)); if (this.fbLoading) { $('kronolithFBLoading').show(); } } /* Resources */ if (!Object.isUndefined(ev.rs)) { var rs = $H(ev.rs); HordeImple.AutoCompleter.kronolithEventResources.reset(rs.values().pluck('name')); rs.each(function(r) { this.addResource(r.value, r.key); }.bind(this)); if (this.fbLoading) { $('kronolithResourceFBLoading').show(); } } /* Tags */ HordeImple.AutoCompleter.kronolithEventTags.reset(ev.tg); /* Geo */ if (ev.gl) { $('kronolithEventLocationLat').value = ev.gl.lat; $('kronolithEventLocationLon').value = ev.gl.lon; $('kronolithEventMapZoom').value = Math.max(1, ev.gl.zoom); } if (!ev.pe) { $('kronolithEventSave').hide(); HordeImple.AutoCompleter.kronolithEventTags.disable(); $('kronolithEventTabTags').select('label').invoke('hide'); } else { HordeCore.doAction('listTopTags', {}, { callback: this.topTagsCallback.curry('kronolithEventTopTags', 'kronolithEventTag') }); } if (!ev.pd) { $('kronolithEventDelete').hide(); $('kronolithEventTarget').hide(); $('kronolithEventTargetRO').show(); } this.setTitle(ev.t); this.redBoxLoading = true; RedBox.showHtml($('kronolithEventDialog').show()); /* Hide alarm message for this event. */ if (r.msgs) { r.msgs = r.msgs.reject(function(msg) { if (msg.type != 'horde.alarm') { return false; } var alarm = msg.flags.alarm; if (alarm.params && alarm.params.notify && alarm.params.notify.show && alarm.params.notify.show.calendar && alarm.params.notify.show.event && alarm.params.notify.show.calendar == ev.c && alarm.params.notify.show.event == ev.id) { return true; } return false; }); } }, /** * Adds an attendee row to the free/busy table. * * @param object attendee An attendee object with the properties: * - e: email address * - l: the display name of the attendee */ addAttendee: function(attendee) { if (typeof attendee == 'string') { if (attendee.include('@')) { HordeCore.doAction('parseEmailAddress', { email: attendee }, { callback: function (r) { if (r.email) { this.addAttendee({ e: r.email, l: attendee }); } }.bind(this) }); return; } else { attendee = { l: attendee }; } } if (attendee.e) { this.attendees.push(attendee); this.fbLoading++; HordeCore.doAction('getFreeBusy', { email: attendee.e }, { callback: function(r) { this.fbLoading--; if (!this.fbLoading) { $('kronolithFBLoading').hide(); } if (!Object.isUndefined(r.fb)) { this.freeBusy.get(attendee.l)[1] = r.fb; this.insertFreeBusy(attendee.l, this.getFBDate()); } }.bind(this) }); } var tr = new Element('tr'), response, i; this.freeBusy.set(attendee.l, [ tr ]); attendee.r = attendee.r || 1; switch (attendee.r) { case 1: response = 'None'; break; case 2: response = 'Accepted'; break; case 3: response = 'Declined'; break; case 4: response = 'Tentative'; break; } tr.insert(new Element('td') .writeAttribute('title', attendee.l) .addClassName('kronolithAttendee' + response) .insert(attendee.e ? attendee.e.escapeHTML() : attendee.l.escapeHTML())); for (i = 0; i < 24; i++) { tr.insert(new Element('td', { className: 'kronolithFBUnknown' })); } $('kronolithEventAttendeesList').down('tbody').insert(tr); }, resetFBRows: function() { this.attendees.each(function(attendee) { var row = this.freeBusy.get(attendee.l)[0]; row.update(); attendee.r = attendee.r || 1; switch (attendee.r) { case 1: response = 'None'; break; case 2: response = 'Accepted'; break; case 3: response = 'Declined'; break; case 4: response = 'Tentative'; break; } row.insert(new Element('td') .writeAttribute('title', attendee.l) .addClassName('kronolithAttendee' + response) .insert(attendee.e ? attendee.e.escapeHTML() : attendee.l.escapeHTML())); for (i = 0; i < 24; i++) { row.insert(new Element('td', { className: 'kronolithFBUnknown' })); } }.bind(this)); this.resources.each(function(resource) { var row = this.freeBusy.get(resource)[0], tdone = row.down('td'); row.update(); row.update(tdone); for (i = 0; i < 24; i++) { row.insert(new Element('td', { className: 'kronolithFBUnknown' })); } }.bind(this)); }, addResource: function(resource, id) { var v, response = 1; if (!id) { // User entered this.resourceACCache.choices.each(function(i) { if (i.name == resource) { v = i.code; throw $break; } else { v = false; } }.bind(this)); } else { // Populating from an edit event action v = id; response = resource.response; resource = resource.name; } switch (response) { case 1: response = 'None'; break; case 2: response = 'Accepted'; break; case 3: response = 'Declined'; break; case 4: response = 'Tentative'; break; } var att = { 'resource': v }, tr, i; if (att.resource) { this.fbLoading++; HordeCore.doAction('getFreeBusy', att, { callback: this.addResourceCallback.curry(resource).bind(this) }); tr = new Element('tr'); this.freeBusy.set(resource, [ tr ]); tr.insert(new Element('td') .writeAttribute('title', resource) .addClassName('kronolithAttendee' + response) .insert(resource.escapeHTML())); for (i = 0; i < 24; i++) { tr.insert(new Element('td', { className: 'kronolithFBUnknown' })); } $('kronolithEventResourcesList').down('tbody').insert(tr); this.resourceACCache.map.set(resource, v); $('kronolithEventResourceIds').value = this.resourceACCache.map.values(); } else { HordeCore.notify(Kronolith.text.unknown_resource + ': ' + resource, 'horde.error'); } }, removeResource: function(resource) { var row = this.freeBusy.get(resource)[0]; row.purge(); row.remove(); this.resourceACCache.map.unset(resource); $('kronolithEventResourceIds').value = this.resourceACCache.map.values(); }, addResourceCallback: function(resource, r) { this.fbLoading--; if (!this.fbLoading) { $('kronolithResourceFBLoading').hide(); } if (Object.isUndefined(r.fb)) { return; } this.resources.push(resource); this.freeBusy.get(resource)[1] = r.fb; this.insertFreeBusy(resource); }, /** * Removes an attendee row from the free/busy table. * * @param string attendee The display name of the attendee. */ removeAttendee: function(attendee) { var row = this.freeBusy.get(attendee)[0]; row.purge(); row.remove(); }, normalizeAttendee: function(attendee) { var pattern = /:(.*);/; var match = pattern.exec(attendee); if (match) { return match[1].split(','); } return [attendee]; }, checkOrganizerAsAttendee: function() { if (HordeImple.AutoCompleter.kronolithEventAttendees.selectedItems.length == 1 && HordeImple.AutoCompleter.kronolithEventAttendees.selectedItems.first().rawValue != Kronolith.conf.email) { // Invite the organizer of this event to the new event. HordeImple.AutoCompleter.kronolithEventAttendees.addNewItemNode(Kronolith.conf.email); this.addAttendee(Kronolith.conf.email); } }, getFBDate: function () { var startDate = $('kronolithFBDate').innerHTML.split(' '); if (startDate.length > 1) { startDate = startDate[1]; } else { startDate = startDate[0]; } return Date.parseExact(startDate, Kronolith.conf.date_format); }, /** * Updates rows with free/busy information in the attendees table. * * @param string attendee An attendee display name as the free/busy * identifier. * @param date start An optinal start date for f/b info. If omitted, * $('kronolithEventStartDate') is used. */ insertFreeBusy: function(attendee, start) { if (!$('kronolithEventDialog').visible() || !this.freeBusy.get(attendee)) { return; } var fb = this.freeBusy.get(attendee)[1], tr = this.freeBusy.get(attendee)[0], td = tr.select('td')[1], div = td.down('div'), start; if (!fb) { return; } if (!td.getWidth()) { this.insertFreeBusy.bind(this, attendee, start).defer(); return; } if (div) { div.purge(); div.remove(); } if (!start) { start = Date.parseExact($F('kronolithEventStartDate'), Kronolith.conf.date_format); } var end = start.clone().add(1).days(), width = td.getWidth(), fbs = this.parseDate(fb.s), fbe = this.parseDate(fb.e); if (start.isBefore(fbs) || end.isBefore(fbs) || start.isAfter(fbe)) { return; } tr.select('td').each(function(td, i) { if (i != 0) { td.className = 'kronolithFBFree'; } i++; }); div = new Element('div').setStyle({ position: 'relative', height: td.offsetHeight + 'px' }); td.insert(div); $H(fb.b).each(function(busy) { var left, from = Date.parse(busy.key).addSeconds(1), to = Date.parse(busy.value).addSeconds(1); if (!end.isAfter(from) || to.isBefore(start)) { return; } if (from.isBefore(start)) { from = start.clone(); } if (to.isAfter(end)) { to = end.clone(); } if (to.getHours() === 0 && to.getMinutes() === 0) { to.add(-1).minutes(); } left = from.getHours() + from.getMinutes() / 60; div.insert(new Element('div', { className: 'kronolithFBBusy' }).setStyle({ zIndex: 1, top: 0, left: (left * width) + 'px', width: (((to.getHours() + to.getMinutes() / 60) - left) * width) + 'px' })); }); }, fbStartDateOnChange: function() { if (!$F('kronolithEventStartDate')) { this._checkDate($('kronolithEventStartDate')); return; } this.fbStartDateHandler(Date.parseExact($F('kronolithEventStartDate'), Kronolith.conf.date_format)); }, /** * @param Date start The start date. */ fbStartDateHandler: function(start) { this.updateFBDate(start); this.resetFBRows(); // Need to check visisbility - multiple changes will break the display // due to the use of .defer() in insertFreeBusy(). if ($('kronolithEventTabAttendees').visible()) { this.attendeeStartDateHandler(start); } if ($('kronolithEventTabResources').visible()) { this.resourceStartDateHandler(start); } }, attendeeStartDateHandler: function(start) { this.attendees.each(function(attendee) { this.insertFreeBusy(attendee.l, start); }, this); }, resourceStartDateHandler: function(start) { this.resources.each(function(resource) { this.insertFreeBusy(resource, start); }, this); }, nextFreebusy: function() { this.fbStartDateHandler(this.getFBDate().addDays(1)); }, prevFreebusy: function() { this.fbStartDateHandler(this.getFBDate().addDays(-1)); }, /** * @start Date object */ updateFBDate: function(start) { $('kronolithFBDate').update(start.toString('dddd') + ' ' + start.toString(Kronolith.conf.date_format)); $('kronolithResourceFBDate').update(start.toString('dddd') + ' ' + start.toString(Kronolith.conf.date_format)); }, /** * Toggles the start and end time fields of the event edit form on and off. * * @param boolean on Whether the event is an all-day event, i.e. the time * fields should be turned off. If not specified, the * current state is toggled. */ toggleAllDay: function(on) { var end = this.getDate('end'), old = $('kronolithEventStartTimeLabel').getStyle('visibility') == 'hidden'; if (Object.isUndefined(on)) { on = $('kronolithEventStartTimeLabel').getStyle('visibility') == 'visible'; } if (end) { if (on) { if (end.getHours() == 0 && end.getMinutes() == 0) { end.add(-1).minute(); } } else if (old) { end.setHours(23); end.setMinutes(59); } $('kronolithEventEndDate').setValue(end.toString(Kronolith.conf.date_format)); $('kronolithEventEndTime').setValue(end.toString(Kronolith.conf.time_format)); } $('kronolithEventStartTimeLabel').setStyle({ visibility: on ? 'hidden' : 'visible' }); $('kronolithEventEndTimeLabel').setStyle({ visibility: on ? 'hidden' : 'visible' }); }, /** * Enables the alarm in the event or task form and sets the correct value * and unit. * * @param string type The object type, either 'Event' or 'Task'. * @param integer alarm The alarm time in seconds. */ enableAlarm: function(type, alarm) { if (!alarm) { return; } type = 'kronolith' + type + 'Alarm'; $(type + 'On').setValue(true); [10080, 1440, 60, 1].each(function(unit) { if (alarm % unit === 0) { $(type + 'Value').setValue(alarm / unit); $(type + 'Unit').setValue(unit); throw $break; } }); }, /** * Disables all custom alarm methods in the event form. */ disableAlarmMethods: function(type) { $('kronolith' + type + 'TabReminder').select('input').each(function(input) { if (input.name == (type == 'Event' ? 'event_alarms[]' : 'task[alarm_methods][]')) { input.setValue(0); if ($(input.id + 'Params')) { $(input.id + 'Params').hide(); } } }); }, /** * Toggles the recurrence fields of the event and task edit forms. * * @param boolean event Whether to use the event form. * @param string recur The recurrence part of the field name, i.e. 'None', * 'Daily', etc. */ toggleRecurrence: function(event, recur) { var prefix = 'kronolith' + (event ? 'Event' : 'Task'); if (recur == 'Exception') { if (!$(prefix + 'RepeatException').visible()) { $(prefix + 'TabRecur').select('div').invoke('hide'); $(prefix + 'RepeatException').show(); } } else if (recur != 'None') { var div = $(prefix + 'Repeat' + recur), length = $(prefix + 'RepeatLength'); this.lastRecurType = recur; if (!div.visible()) { $(prefix + 'TabRecur').select('div').invoke('hide'); div.show(); length.show(); $(prefix + 'RepeatType').show(); } switch (recur) { case 'Daily': case 'Weekly': case 'Monthly': case 'Yearly': var recurLower = recur.toLowerCase(); if (div.down('input[name=recur_' + recurLower + '][value=1]').checked) { div.down('input[name=recur_' + recurLower + '_interval]').disable(); } else { div.down('input[name=recur_' + recurLower + '_interval]').enable(); } break; } if (length.down('input[name=recur_end_type][value=date]').checked) { $(prefix + 'RecurDate').enable(); $(prefix + 'RecurPicker').setStyle({ visibility: 'visible' }); } else { $(prefix + 'RecurDate').disable(); $(prefix + 'RecurPicker').setStyle({ visibility: 'hidden' }); } if (length.down('input[name=recur_end_type][value=count]').checked) { $(prefix + 'RecurCount').enable(); } else { $(prefix + 'RecurCount').disable(); } } else { $(prefix + 'TabRecur').select('div').invoke('hide'); $(prefix + 'RepeatType').show(); } }, /** * Fills the recurrence fields of the event and task edit forms. * * @param boolean event Whether to use the event form. * @param object recur The recurrence object from the ajax response. */ setRecurrenceFields: function(event, recur) { var scheme = Kronolith.conf.recur[recur.t], schemeLower = scheme.toLowerCase(), prefix = 'kronolith' + (event ? 'Event' : 'Task'), div = $(prefix + 'Repeat' + scheme); $(prefix + 'Link' + scheme).setValue(true); if (scheme == 'Monthly' || scheme == 'Yearly') { div.down('input[name=recur_' + schemeLower + '_scheme][value=' + recur.t + ']').setValue(true); } if (scheme == 'Weekly') { div.select('input[type=checkbox]').each(function(input) { if (input.name == 'weekly[]' && input.value & recur.d) { input.setValue(true); } }); } if (recur.i == 1) { div.down('input[name=recur_' + schemeLower + '][value=1]').setValue(true); } else { div.down('input[name=recur_' + schemeLower + '][value=0]').setValue(true); div.down('input[name=recur_' + schemeLower + '_interval]').setValue(recur.i); } if (!Object.isUndefined(recur.e)) { $(prefix + 'RepeatLength').down('input[name=recur_end_type][value=date]').setValue(true); $(prefix + 'RecurDate').setValue(Date.parse(recur.e).toString(Kronolith.conf.date_format)); } else if (!Object.isUndefined(recur.c)) { $(prefix + 'RepeatLength').down('input[name=recur_end_type][value=count]').setValue(true); $(prefix + 'RecurCount').setValue(recur.c); } else { $(prefix + 'RepeatLength').down('input[name=recur_end_type][value=none]').setValue(true); } $(prefix + 'Exceptions').setValue(recur.ex || ''); if ($(prefix + 'Completions')) { $(prefix + 'Completions').setValue(recur.co || ''); } this.toggleRecurrence(event, scheme); }, /** * Returns the Date object representing the date and time specified in the * event form's start or end fields. * * @param string what Which fields to parse, either 'start' or 'end'. * * @return Date The date object or null if the fields can't be parsed. */ getDate: function(what) { var dateElm, timeElm, date, time; if (what == 'start') { dateElm = 'kronolithEventStartDate'; timeElm = 'kronolithEventStartTime'; } else { dateElm = 'kronolithEventEndDate'; timeElm = 'kronolithEventEndTime'; } date = Date.parseExact($F(dateElm), Kronolith.conf.date_format) || Date.parse($F(dateElm)); if (date) { time = Date.parseExact($F(timeElm), Kronolith.conf.time_format); if (!time) { time = Date.parse($F(timeElm)); } if (time) { date.setHours(time.getHours()); date.setMinutes(time.getMinutes()); } } return date; }, checkDate: function(e) { this._checkDate(e.element()); }, _checkDate: function(elm) { if ($F(elm)) { var date = Date.parseExact($F(elm), Kronolith.conf.date_format) || Date.parse($F(elm)); if (date) { elm.setValue(date.toString(Kronolith.conf.date_format)); this.wrongFormat.unset(elm.id); } else { HordeCore.notify(Kronolith.text.wrong_date_format.interpolate({ wrong: $F(elm), right: new Date().toString(Kronolith.conf.date_format) }), 'horde.warning'); this.wrongFormat.set(elm.id, true); } } else { HordeCore.notify(Kronolith.text.wrong_date_format.interpolate({ wrong: $F(elm), right: new Date().toString(Kronolith.conf.date_format) }), 'horde.warning'); this.wrongFormat.set(elm.id, true); } }, /** * Attaches a KeyNavList drop down to one of the time fields. * * @param string|Element field A time field (id). * * @return KeyNavList The drop down list object. */ attachTimeDropDown: function(field) { var list = [], d = new Date(), time, opts; d.setHours(0); d.setMinutes(0); do { time = d.toString(Kronolith.conf.time_format); list.push({ l: time, v: time }); d.add(30).minutes(); } while (d.getHours() !== 0 || d.getMinutes() !== 0); field = $(field); opts = { list: list, domParent: field.up('.kronolithDialog'), onChoose: function(value) { if (value) { field.setValue(value); } this.updateTimeFields(field.identify()); }.bind(this) }; this.knl[field.id] = new KeyNavList(field, opts); return this.knl[field.id]; }, checkTime: function(e) { var elm = e.element(); if ($F(elm)) { var time = Date.parseExact(new Date().toString(Kronolith.conf.date_format) + ' ' + $F(elm), Kronolith.conf.date_format + ' ' + Kronolith.conf.time_format) || Date.parse(new Date().toString('yyyy-MM-dd ') + $F(elm)); if (time) { elm.setValue(time.toString(Kronolith.conf.time_format)); this.wrongFormat.unset(elm.id); } else { HordeCore.notify(Kronolith.text.wrong_time_format.interpolate({ wrong: $F(elm), right: new Date().toString(Kronolith.conf.time_format) }), 'horde.warning'); this.wrongFormat.set(elm.id, true); } } }, /** * Updates the start time in the event form after changing the end time. */ updateStartTime: function(date) { var start = this.getDate('start'), end = this.getDate('end'); if (!start) { return; } if (!date) { date = end; } if (!date) { return; } if (start.isAfter(end)) { $('kronolithEventStartDate').setValue(date.toString(Kronolith.conf.date_format)); $('kronolithEventStartTime').setValue($F('kronolithEventEndTime')); } this.duration = Math.abs(date.getTime() - start.getTime()) / 60000; }, /** * Updates the end time in the event form after changing the start time. */ updateEndTime: function() { var date = this.getDate('start'); if (!date) { return; } date.add(this.duration).minutes(); $('kronolithEventEndDate').setValue(date.toString(Kronolith.conf.date_format)); $('kronolithEventEndTime').setValue(date.toString(Kronolith.conf.time_format)); }, /** * Event handler for scrolling the mouse over the date field. * * @param Event e The mouse event. * @param string field The field name. */ scrollDateField: function(e, field) { var date = Date.parseExact($F(field), Kronolith.conf.date_format); if (!date || (!e.wheelData && !e.detail)) { return; } date.add(e.wheelData > 0 || e.detail < 0 ? 1 : -1).days(); $(field).setValue(date.toString(Kronolith.conf.date_format)); switch (field) { case 'kronolithEventStartDate': this.updateEndTime(); break; case 'kronolithEventEndDate': this.updateStartTime(date); break; } }, /** * Event handler for scrolling the mouse over the time field. * * @param Event e The mouse event. * @param string field The field name. */ scrollTimeField: function(e, field) { var time = Date.parseExact($F(field), Kronolith.conf.time_format) || Date.parse($F(field)), newTime, minute; if (!time || (!e.wheelData && !e.detail)) { return; } newTime = time.clone(); newTime.add(e.wheelData > 0 || e.detail < 0 ? 10 : -10).minutes(); minute = newTime.getMinutes(); if (minute % 10) { if (e.wheelData > 0 || e.detail < 0) { minute = minute / 10 | 0; } else { minute = (minute - 10) / 10 | 0; } minute *= 10; newTime.setMinutes(minute); } if (newTime.getDate() != time.getDate()) { if (newTime.isAfter(time)) { newTime = time.clone().set({ hour: 23, minute: 59 }); } else { newTime = time.clone().set({ hour: 0, minute: 0 }); } } $(field).setValue(newTime.toString(Kronolith.conf.time_format)); this.updateTimeFields(field); /* Mozilla bug https://bugzilla.mozilla.org/show_bug.cgi?id=502818 * Need to stop or else multiple scroll events may be fired. We * lose the ability to have the mousescroll bubble up, but that is * more desirable than having the wrong scrolling behavior. */ if (Prototype.Browser.Gecko && !e.stop) { Event.stop(e); } }, /** * Updates the time fields of the event dialog after either has been * changed. * * @param string field The id of the field that has been changed. */ updateTimeFields: function(field) { switch (field) { case 'kronolithEventStartDate': this.fbStartDateHandler(Date.parseExact($F(field), Kronolith.conf.date_format)); case 'kronolithEventStartTime': this.updateEndTime(); break; case 'kronolithEventEndDate': case 'kronolithEventEndTime': this.updateStartTime(); this.fbStartDateHandler(Date.parseExact($F('kronolithEventStartDate'), Kronolith.conf.date_format)); break; } }, /** * Closes a RedBox overlay, after saving its content to the body. */ closeRedBox: function() { if (!RedBox.getWindow()) { return; } var content = RedBox.getWindowContents(); if (content) { document.body.insert(content.hide()); } RedBox.close(); }, // By default, no context onShow action contextOnShow: Prototype.emptyFunction, // By default, no context onClick action contextOnClick: Prototype.emptyFunction, // Map initializeMap: function(ignoreLL) { if (this.mapInitialized) { return; } var layers = []; if (Kronolith.conf.maps.providers) { Kronolith.conf.maps.providers.each(function(l) { var p = new HordeMap[l](); $H(p.getLayers()).values().each(function(e) {layers.push(e);}); }); } this.map = new HordeMap.Map[Kronolith.conf.maps.driver]({ elt: 'kronolithEventMap', delayed: true, layers: layers, markerDragEnd: this.onMarkerDragEnd.bind(this), mapClick: this.afterClickMap.bind(this) }); if ($('kronolithEventLocationLat').value && !ignoreLL) { var ll = { lat:$('kronolithEventLocationLat').value, lon: $('kronolithEventLocationLon').value }; // Note that we need to cast the value of zoom to an integer here, // otherwise the map display breaks. this.placeMapMarker(ll, true, $('kronolithEventMapZoom').value - 0); } //@TODO: check for Location field - and if present, but no lat/lon value, attempt to // geocode it. this.map.display(); this.mapInitialized = true; }, resetMap: function() { this.mapInitialized = false; $('kronolithEventLocationLat').value = null; $('kronolithEventLocationLon').value = null; $('kronolithEventMapZoom').value = null; if (this.mapMarker) { this.map.removeMarker(this.mapMarker, {}); this.mapMarker = null; } if (this.map) { this.map.destroy(); this.map = null; } }, /** * Callback for handling marker drag end. * * @param object r An object that implenents a getLonLat() method to obtain * the new location of the marker. */ onMarkerDragEnd: function(r) { var ll = r.getLonLat(); $('kronolithEventLocationLon').value = ll.lon; $('kronolithEventLocationLat').value = ll.lat; var gc = new HordeMap.Geocoder[Kronolith.conf.maps.geocoder](this.map.map, 'kronolithEventMap'); gc.reverseGeocode(ll, this.onReverseGeocode.bind(this), this.onGeocodeError.bind(this) ); }, /** * Callback for handling a reverse geocode request. * * @param array r An array of objects containing the results. Each object in * the array is {lat:, lon:, address} */ onReverseGeocode: function(r) { if (!r.length) { return; } $('kronolithEventLocation').value = r[0].address; }, onGeocodeError: function(r) { $('kronolithEventGeo_loading_img').toggle(); HordeCore.notify(Kronolith.text.geocode_error + ' ' + r, 'horde.error'); }, /** * Callback for geocoding calls. */ onGeocode: function(r) { $('kronolithEventGeo_loading_img').toggle(); r = r.shift(); if (r.precision) { zoom = r.precision * 2; } else { zoom = null; } this.ensureMap(true); this.placeMapMarker({ lat: r.lat, lon: r.lon }, true, zoom); }, geocode: function(a) { if (!a) { return; } $('kronolithEventGeo_loading_img').toggle(); var gc = new HordeMap.Geocoder[Kronolith.conf.maps.geocoder](this.map.map, 'kronolithEventMap'); gc.geocode(a, this.onGeocode.bind(this), this.onGeocodeError); }, /** * Place the event marker on the map, at point ll, ensuring it exists. * Optionally center the map on the marker and zoom. Zoom only honored if * center is set, and if center is set, but zoom is null, we zoomToFit(). * */ placeMapMarker: function(ll, center, zoom) { if (!this.mapMarker) { this.mapMarker = this.map.addMarker( ll, { draggable: true }, { context: this, dragend: this.onMarkerDragEnd }); } else { this.map.moveMarker(this.mapMarker, ll); } if (center) { this.map.setCenter(ll, zoom); if (!zoom) { this.map.zoomToFit(); } } $('kronolithEventLocationLon').value = ll.lon; $('kronolithEventLocationLat').value = ll.lat; }, /** * Remove the event marker from the map. Called after clearing the location * field. */ removeMapMarker: function() { if (this.mapMarker) { this.map.removeMarker(this.mapMarker, {}); $('kronolithEventLocationLon').value = null; $('kronolithEventLocationLat').value = null; } this.mapMarker = false; }, /** * Ensures the map tab is visible and sets UI elements accordingly. */ ensureMap: function(ignoreLL) { if (!this.mapInitialized) { this.initializeMap(ignoreLL); } var dialog = $('kronolithEventForm'); dialog.select('.kronolithTabsOption').invoke('hide'); dialog.select('.tabset li').invoke('removeClassName', 'horde-active'); $('kronolithEventTabMap').show(); $('kronolithEventLinkMap').up().addClassName('horde-active'); }, /** * Callback that gets called after a new marker has been placed on the map * due to a single click on the map. * * @return object o { lonlat: } */ afterClickMap: function(o) { this.placeMapMarker(o.lonlat, false); var gc = new HordeMap.Geocoder[Kronolith.conf.maps.geocoder](this.map.map, 'kronolithEventMap'); gc.reverseGeocode(o.lonlat, this.onReverseGeocode.bind(this), this.onGeocodeError.bind(this) ); }, /* Onload function. */ onDomLoad: function() { var dateFields, timeFields; /* Initialize the starting page. */ var tmp = location.hash; if (!tmp.empty() && tmp.startsWith('#')) { tmp = (tmp.length == 1) ? '' : tmp.substring(1); } if (tmp.empty()) { this.updateView(this.date, Kronolith.conf.login_view); $('kronolithView' + Kronolith.conf.login_view.capitalize()).show(); } HordeCore.doAction('listCalendars', {}, { callback: this.initialize.bind(this, tmp) }); RedBox.onDisplay = function() { this.redBoxLoading = false; }.bind(this); RedBox.duration = this.effectDur; $('kronolithEventStartDate', 'kronolithEventEndDate', 'kronolithTaskDueDate').compact().invoke('observe', 'blur', this.checkDate.bind(this)); var timeFields = $('kronolithEventStartTime', 'kronolithEventEndTime', 'kronolithTaskDueTime').compact(); timeFields.invoke('observe', 'blur', this.checkTime.bind(this)); timeFields.each(function(field) { var dropDown = this.attachTimeDropDown(field); field.observe('click', function() { dropDown.show(); }); }, this); $('kronolithEventStartDate', 'kronolithEventStartTime').invoke('observe', 'change', this.updateEndTime.bind(this)); $('kronolithEventEndDate', 'kronolithEventEndTime').invoke('observe', 'change', function() { this.updateStartTime(); }.bind(this)); if (Kronolith.conf.has_tasks) { $('kronolithTaskDueDate', 'kronolithTaskDueTime').compact().invoke('observe', 'focus', this.setDefaultDue.bind(this)); $('kronolithTaskList').observe('change', function() { this.updateTaskParentDropDown($F('kronolithTaskList')); this.updateTaskAssigneeDropDown($F('kronolithTaskList')); }.bind(this)); } document.observe('keydown', KronolithCore.keydownHandler.bindAsEventListener(KronolithCore)); document.observe('keyup', KronolithCore.keyupHandler.bindAsEventListener(KronolithCore)); document.observe('click', KronolithCore.clickHandler.bindAsEventListener(KronolithCore)); document.observe('dblclick', KronolithCore.clickHandler.bindAsEventListener(KronolithCore, true)); // Mouse wheel handler. dateFields = [ 'kronolithEventStartDate', 'kronolithEventEndDate' ]; timeFields = [ 'kronolithEventStartTime', 'kronolithEventEndTime' ]; if (Kronolith.conf.has_tasks) { dateFields.push('kronolithTaskDueDate'); timeFields.push('kronolithTaskDueTime'); } dateFields.each(function(field) { $(field).observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousewheel', this.scrollDateField.bindAsEventListener(this, field)); }, this); timeFields.each(function(field) { $(field).observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousewheel', this.scrollTimeField.bindAsEventListener(this, field)); }, this); $('kronolithEventStartDate').observe('change', this.fbStartDateOnChange.bind(this)); $('kronolithFBDatePrev').observe('click', this.prevFreebusy.bind(this)); $('kronolithFBDateNext').observe('click', this.nextFreebusy.bind(this)); $('kronolithResourceFBDatePrev').observe('click', this.prevFreebusy.bind(this)); $('kronolithResourceFBDateNext').observe('click', this.nextFreebusy.bind(this)); this.updateMinical(this.date); }, initialize: function(location, r) { Kronolith.conf.calendars = r.calendars; this.updateCalendarList(); HordeSidebar.refreshEvents(); $('kronolithLoadingCalendars').hide(); $('kronolithMenuCalendars').show(); this.initialized = true; /* Initialize the starting page. */ if (!location.empty()) { this.go(decodeURIComponent(location)); } else { this.go(Kronolith.conf.login_view); } /* Start polling. */ new PeriodicalExecuter(function() { HordeCore.doAction('poll'); $(kronolithGotoToday).update(Date.today().toString(Kronolith.conf.date_format)); }, 60 ); } }; /* Initialize global event handlers. */ document.observe('dom:loaded', KronolithCore.onDomLoad.bind(KronolithCore)); document.observe('DragDrop2:drag', KronolithCore.onDrag.bindAsEventListener(KronolithCore)); document.observe('DragDrop2:drop', KronolithCore.onDrop.bindAsEventListener(KronolithCore)); document.observe('DragDrop2:end', KronolithCore.onDragEnd.bindAsEventListener(KronolithCore)); document.observe('DragDrop2:start', KronolithCore.onDragStart.bindAsEventListener(KronolithCore)); document.observe('Horde_Calendar:select', KronolithCore.datePickerHandler.bindAsEventListener(KronolithCore)); document.observe('FormGhost:reset', KronolithCore.searchReset.bindAsEventListener(KronolithCore)); document.observe('FormGhost:submit', KronolithCore.searchSubmit.bindAsEventListener(KronolithCore)); document.observe('HordeCore:showNotifications', KronolithCore.showNotification.bindAsEventListener(KronolithCore)); if (Prototype.Browser.IE) { $('kronolithBody').observe('selectstart', Event.stop); } /* Extend AJAX exception handling. */ HordeCore.onException = HordeCore.onException.wrap(KronolithCore.onException.bind(KronolithCore));
Simpan