diff --git a/calendar/js/app.ts b/calendar/js/app.ts new file mode 100644 index 0000000000..13137f359e --- /dev/null +++ b/calendar/js/app.ts @@ -0,0 +1,4144 @@ +/** + * EGroupware - Calendar - Javascript UI + * + * @link http://www.egroupware.org + * @package calendar + * @author Hadi Nategh + * @author Nathan Gray + * @copyright (c) 2008-16 by Ralf Becker + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +/*egw:uses + /api/js/jsapi/egw_app.js; + /etemplate/js/etemplate2.js; + /calendar/js/View.js; + /calendar/js/et2_widget_owner.js; + /calendar/js/et2_widget_timegrid.js; + /calendar/js/et2_widget_planner.js; + /vendor/bower-asset/jquery-touchswipe/jquery.touchSwipe.js; +*/ + +import {EgwApp} from "../../api/js/jsapi/egw_app"; +import {etemplate2} from "../../api/js/etemplate/etemplate2"; +import {et2_container} from "../../api/js/etemplate/et2_core_baseWidget"; +import {et2_date} from "../../api/js/etemplate/et2_widget_date"; +import {day, day4, listview, month, planner, week, weekN} from "./View"; +import {et2_calendar_view} from "./et2_widget_view"; +import {et2_calendar_timegrid} from "./et2_widget_timegrid"; +import {et2_calendar_daycol} from "./et2_widget_daycol"; +import {et2_calendar_planner_row} from "./et2_widget_planner_row"; +import {et2_calendar_event} from "./et2_widget_event"; + +/** + * UI for calendar + * + * Calendar has multiple different views of the same data. All the templates + * for the different view are loaded at the start, then the view objects + * in CalendarApp.views are used to manage the different views. + * update_state() is used to change the state between the different views, as + * well as adjust only a single part of the state while keeping the rest unchanged. + * + * The event widgets (and the nextmatch) get the data from egw.data, and they + * register update callbacks to automatically update when the data changes. This + * means that when we update something on the server, to update the UI we just need + * to send back the new data and if the event widget still exists it will update + * itself. See calendar_uiforms->ajax_status(). + * + * To reduce server calls, we also keep a map of day => event IDs. This allows + * us to quickly change views (week to day, for example) without requesting additional + * data from the server. We keep that map as long as only the date (and a few + * others - see update_state()) changes. If user or any of the other filters are + * changed, we discard the daywise cache and ask the server for the filtered events. + * + */ +class CalendarApp extends EgwApp +{ + /** + * application name + */ + public readonly appname = 'calendar'; + + /** + * Needed for JSON callback + */ + public readonly prefix = 'calendar'; + + /** + * etemplate for the sidebox filters + */ + sidebox_et2: et2_container = null; + + /** + * Current internal state + * + * If you need to change state, you can pass just the fields to change to + * update_state(). + */ + state = { + date: new Date(), + view: egw.preference('saved_states','calendar') ? + // @ts-ignore + egw.preference('saved_states','calendar').view : + egw.preference('defaultcalendar','calendar') || 'day', + owner: egw.user('account_id'), + keywords: '', + last: undefined + }; + + /** + * These are the keys we keep to set & remember the status, others are discarded + */ + static readonly states_to_save = ['owner','status_filter','filter','cat_id','view','sortby','planner_view','weekend']; + + // If you are in one of these views and select a date in the sidebox, the view + // will change as needed to show the date. Other views will only change the + // date in the current view. + public readonly sidebox_changes_views = ['day','week','month']; + + static views = { + day: day, + day4: day4, + week: week, + weekN: weekN, + month: month, + planner: planner, + listview: listview + }; + + /** + * This is the data cache prefix for the daywise event index cache + * Daywise cache IDs look like: calendar_daywise::20150101 and + * contain a list of event IDs for that day (or empty array) + */ + public static readonly DAYWISE_CACHE_ID = 'calendar_daywise'; + + // Calendar allows other apps to hook into the sidebox. We keep these etemplates + // up to date as state is changed. + sidebox_hooked_templates = []; + + // List of queries in progress, to prevent home from requesting the same thing + _queries_in_progress = []; + + // Calendar-wide autorefresh + _autorefresh_timer : number = null; + + // Flag if the state is being updated + private state_update_in_progress: boolean = false; + + // Data for quick add dialog + private quick_add: any; + + /** + * Constructor + * + */ + constructor() + { + /* + // categories have nothing to do with calendar, but eT2 objects loads calendars app.js + if (window.framework && framework.applications.calendar.browser && + framework.applications.calendar.browser.currentLocation.match('menuaction=preferences\.preferences_categories_ui\.index')) + { + this._super.apply(this, arguments); + return; + } + else// make calendar object available, even if not running in top window, as sidebox does + if (window.top !== window && !egw(window).is_popup() && window.top.app.calendar) + { + window.app.calendar = window.top.app.calendar; + return; + } + else if (window.top == window && !egw(window).is_popup()) + { + // Show loading div + egw.loading_prompt( + this.appname,true,egw.lang('please wait...'), + typeof framework !== 'undefined' ? framework.applications.calendar.tab.contentDiv : false, + egwIsMobile()?'horizental':'spinner' + ); + } + */ + // call parent + super(); + + // Scroll + jQuery(jQuery.proxy(this._scroll,this)); + jQuery.extend(this.state, this.egw.preference('saved_states','calendar')); + + // Set custom color for events without category + if(this.egw.preference('no_category_custom_color','calendar')) + { + this.egw.css( + '.calendar_calEvent:not([class*="cat_"])', + 'background-color: '+this.egw.preference('no_category_custom_color','calendar')+' !important' + ); + } + } + + /** + * Destructor + */ + destroy() + { + // call parent + super.destroy(this.appname); + + // remove top window reference + // @ts-ignore + if (window.top !== window && window.top.app.calendar === this) + { + // @ts-ignore + delete window.top.app.calendar; + } + jQuery('body').off('.calendar'); + + if(this.sidebox_et2) + { + var date = this.sidebox_et2.getWidgetById('date'); + jQuery(window).off('resize.calendar'+date.dom_id); + } + this.sidebox_hooked_templates = null; + + egw_unregisterGlobalShortcut(jQuery.ui.keyCode.PAGE_UP, false, false, false); + egw_unregisterGlobalShortcut(jQuery.ui.keyCode.PAGE_DOWN, false, false, false); + + // Stop autorefresh + if(this._autorefresh_timer) + { + window.clearInterval(this._autorefresh_timer); + this._autorefresh_timer = null; + } + } + + /** + * This function is called when the etemplate2 object is loaded + * and ready. If you must store a reference to the et2 object, + * make sure to clean it up in destroy(). + * + * @param {etemplate2} _et2 newly ready et2 object + * @param {string} _name name of template + */ + et2_ready(_et2 : etemplate2, _name) + { + // call parent + super.et2_ready(_et2, _name); + + // Avoid many problems with home + if(_et2.app !== 'calendar' || _name == 'admin.categories.index') + { + egw.loading_prompt(this.appname,false); + return; + } + + // Re-init sidebox, since it was probably initialized too soon + var sidebox = jQuery('#favorite_sidebox_'+this.appname); + if(sidebox.length == 0 && egw_getFramework() != null) + { + var egw_fw = egw_getFramework(); + sidebox= jQuery('#favorite_sidebox_'+this.appname,egw_fw.sidemenuDiv); + } + + var content = this.et2.getArrayMgr('content'); + + switch (_name) + { + case 'calendar.sidebox': + this.sidebox_et2 = _et2.widgetContainer; + this.sidebox_hooked_templates.push(this.sidebox_et2); + jQuery(_et2.DOMContainer).hide(); + + // Set client side holiday cache for this year + // @ts-ignore + if(egw.window.et2_calendar_view) + { + // @ts-ignore + egw.window.et2_calendar_view.holiday_cache[content.data.year] = content.data.holidays; + delete content.data.holidays; + delete content.data.year; + } + + this._setup_sidebox_filters(); + + this.state = content.data; + break; + + case 'calendar.add': + this.et2.getWidgetById('title').input.select(); + // Fall through to get all the edit stuff too + case 'calendar.edit': + if (typeof content.data['conflicts'] == 'undefined') + { + //Check if it's fallback from conflict window or it's from edit window + if (content.data['button_was'] != 'freetime') + { + this.set_enddate_visibility(); + this.check_recur_type(); + this.edit_start_change(null, this.et2.getWidgetById('start')); + if(this.et2.getWidgetById('recur_exception')) + { + this.et2.getWidgetById('recur_exception').set_disabled(!content.data.recur_exception || + typeof content.data.recur_exception[0] == 'undefined'); + } + } + else + { + this.freetime_search(); + } + //send Syncronus ajax request to the server to unlock the on close entry + //set onbeforeunload with json request to send request when the window gets close by X button + if (content.data.lock_token) + { + window.onbeforeunload = function () { + this.egw.json('calendar.calendar_uiforms.ajax_unlock', + [content.data.id, content.data.lock_token],null,true,null,null).sendRequest(true); + }; + } + } + this.alarm_custom_date(); + + // If title is pre-filled for a new (no ID) event, highlight it + if(content.data && !content.data.id && content.data.title) + { + this.et2.getWidgetById('title').input.select(); + } + + // Disable loading prompt (if loaded nopopup) + egw.loading_prompt(this.appname,false); + break; + + case 'calendar.freetimesearch': + this.set_enddate_visibility(); + break; + case 'calendar.list': + // Wait until _et2_view_init is done + window.setTimeout(jQuery.proxy(function() { + this.filter_change(); + },this),0); + break; + case 'calendar.category_report': + this.category_report_init(); + break; + } + + // Record the templates for the views so we can switch between them + this._et2_view_init(_et2,_name); + } + + /** + * Observer method receives update notifications from all applications + * + * App is responsible for only reacting to "messages" it is interested in! + * + * Calendar binds listeners to the data cache, so if the data is updated, the widget + * will automatically update itself. + * + * @param {string} _msg message (already translated) to show, eg. 'Entry deleted' + * @param {string} _app application name + * @param {(string|number)} _id id of entry to refresh or null + * @param {string} _type either 'update', 'edit', 'delete', 'add' or null + * - update: request just modified data from given rows. Sorting is not considered, + * so if the sort field is changed, the row will not be moved. + * - edit: rows changed, but sorting may be affected. Requires full reload. + * - delete: just delete the given rows clientside (no server interaction neccessary) + * - add: requires full reload for proper sorting + * @param {string} _msg_type 'error', 'warning' or 'success' (default) + * @param {object|null} _links app => array of ids of linked entries + * or null, if not triggered on server-side, which adds that info + * @return {false|*} false to stop regular refresh, thought all observers are run + */ + observer(_msg, _app, _id, _type, _msg_type, _links) + { + var do_refresh = false; + if(this.state.view === 'listview') + { + // @ts-ignore + CalendarApp.views.listview.etemplates[0].widgetContainer.getWidgetById('nm').refresh(_id,_type); + } + switch(_app) + { + case 'infolog': + jQuery('.calendar_calDayTodos') + .find('a') + .each(function(i,a : HTMLAnchorElement){ + var match = a.href.split(/&info_id=/); + if (match && typeof match[1] !="undefined") + { + if (match[1]== _id) do_refresh = true; + } + }); + + // Unfortunately we do not know what type this infolog is here, + // but we can tell if it's turned off entirely + if(egw.preference('calendar_integration','infolog') !== '0') + { + if (jQuery('div [data-app="infolog"][data-app_id="'+_id+'"]').length > 0) do_refresh = true; + switch (_type) + { + case 'add': + do_refresh = true; + break; + } + } + if (do_refresh) + { + // Discard cache + this._clear_cache(); + + // Calendar is the current application, refresh now + if(framework.activeApp.appName === this.appname) + { + this.setState({state: this.state}); + } + // Bind once to trigger a refresh when tab is activated again + else if(framework.applications.calendar && framework.applications.calendar.tab && + framework.applications.calendar.tab.contentDiv) + { + jQuery(framework.applications.calendar.tab.contentDiv) + .off('show.calendar') + .one('show.calendar', + jQuery.proxy(function() {this.setState({state: this.state});},this) + ); + } + } + break; + case 'calendar': + // Regular refresh + let event = null; + if(_id) + { + event = egw.dataGetUIDdata('calendar::'+_id); + } + if(event && event.data && event.data.date || _type === 'delete') + { + // Intelligent refresh without reloading everything + var recurrences = Object.keys(egw.dataSearchUIDs(new RegExp('^calendar::'+_id+':'))); + var ids = event && event.data && event.data.recur_type && typeof _id === 'string' && _id.indexOf(':') < 0 || recurrences.length ? + recurrences : + ['calendar::'+_id]; + + if(_type === 'delete') + { + for(var i in ids) + { + egw.dataStoreUID(ids[i], null); + } + } + // Updates are handled by events themselves through egw.data + else if (_type !== 'update') + { + this._update_events(this.state, ids); + } + return false; + } + else + { + this._clear_cache(); + + // Force redraw to current state + this.setState({state: this.state}); + return false; + } + break; + default: + return undefined; + } + } + + /** + * Link hander for jDots template to just reload our iframe, instead of reloading whole admin app + * + * @param {String} _url + * @return {boolean|string} true, if linkHandler took care of link, false for default processing or url to navigate to + */ + linkHandler(_url) + { + if (_url == 'about:blank' || _url.match('menuaction=preferences\.preferences_categories_ui\.index')) + { + return false; + } + if (_url.match('menuaction=calendar\.calendar_uiviews\.')) + { + var view = _url.match(/calendar_uiviews\.([^&?]+)/); + view = view && view.length > 1 ? view[1] : null; + + // Get query + var q : any = {}; + _url.split('?')[1].split('&').forEach(function(i){ + q[i.split('=')[0]]=unescape(i.split('=')[1]); + }); + delete q.ajax; + delete q.menuaction; + if(!view && q.view || q.view != view && view == 'index') view = q.view; + + // No specific view requested, looks like a reload from framework + if(this.sidebox_et2 && typeof view === 'undefined') + { + this._clear_cache(); + this.setState({state: this.state}); + return false; + } + + if (this.sidebox_et2 && typeof CalendarApp.views[view] == 'undefined' && view != 'index') + { + if(q.owner) + { + q.owner = q.owner.split(','); + q.owner = q.owner.reduce(function(p,c) {if(p.indexOf(c)<0) p.push(c);return p;},[]); + q.owner = q.owner.join(','); + } + q.menuaction = 'calendar.calendar_uiviews.index'; + (this.sidebox_et2.getWidgetById('iframe')).set_src(egw.link('/index.php',q)); + jQuery(this.sidebox_et2.parentNode).show(); + return true; + } + // Known AJAX view + else if(CalendarApp.views[view]) + { + // Reload of known view? + if(view == 'index') + { + var pref = this.egw.preference('saved_states','calendar'); + view = pref.view || 'day'; + } + // View etemplate not loaded + if(typeof CalendarApp.views[view].etemplates[0] == 'string') + { + return _url + '&ajax=true'; + } + // Already loaded, we'll just apply any variables to our current state + var set = jQuery.extend({view: view},q); + this.update_state(set); + return true; + } + } + else if (this.sidebox_et2) + { + var iframe = this.sidebox_et2.getWidgetById('iframe'); + if(!iframe) return false; + iframe.set_src(_url); + jQuery(this.sidebox_et2.parentNode).show(); + // Hide other views + for(var _view in CalendarApp.views) + { + for(var i = 0; i < CalendarApp.views[_view].etemplates.length; i++) + { + jQuery(CalendarApp.views[_view].etemplates[i].DOMContainer).hide(); + } + } + this.state.view = ''; + return true; + } + // can not load our own index page, has to be done by framework + return false; + } + + /** + * Handle actions from the toolbar + * + * @param {egwAction} action Action from the toolbar + */ + toolbar_action(action) + { + // Most can just provide state change data + if(action.data && action.data.state) + { + var state = jQuery.extend({},action.data.state); + if(state.view == 'planner' && app.calendar.state.view != 'planner') { + state.planner_view = app.calendar.state.view; + } + this.update_state(state); + } + // Special handling + switch(action.id) + { + case 'add': + // Default date/time to start of next hour + var tempDate = new Date(); + if(tempDate.getMinutes() !== 0) + { + tempDate.setHours(tempDate.getHours()+1); + tempDate.setMinutes(0); + } + var today = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(),tempDate.getHours(),-tempDate.getTimezoneOffset(),0); + return egw.open(null,"calendar","add", {start: today}); + case 'weekend': + this.update_state({weekend: action.checked}); + break; + case 'today': + var tempDate = new Date(); + var today = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(),0,-tempDate.getTimezoneOffset(),0); + var change = {date: today.toJSON()}; + app.calendar.update_state(change); + break; + case 'next': + case 'previous': + var delta = action.id == 'previous' ? -1 : 1; + var view = CalendarApp.views[app.calendar.state.view] || false; + var start = new Date(app.calendar.state.date); + if (view) + { + start = view.scroll(delta); + app.calendar.update_state({date:app.calendar.date.toString(start)}); + } + break; + } + } + + /** + * Set the app header + * + * Because the toolbar takes some vertical space and has some horizontal space, + * we don't use the system app header, but our own that is in the toolbar + * + * @param {string} header Text to display + */ + set_app_header( header) + { + var template = etemplate2.getById('calendar-toolbar'); + var widget = template ? template.widgetContainer.getWidgetById('app_header') : false; + if(widget) + { + widget.set_value(header); + egw_app_header('','calendar'); + } + else + { + egw_app_header(header,'calendar'); + } + } + + /** + * Setup and handle sortable calendars. + * + * You can only sort calendars if there is more than one owner, and the calendars + * are not combined (many owners, multi-week or month views) + * @returns {undefined} + */ + _sortable( ) + { + // Calender current state + var state = this.getState(); + // Day / month sortables + var daily = jQuery('#calendar-view_view .calendar_calGridHeader > div:first'); + var weekly = jQuery('#calendar-view_view tbody'); + if(state.view == 'day') + { + var sortable = daily; + if(weekly.sortable('instance')) weekly.sortable('disable'); + } + else + { + var sortable = weekly; + if(daily.sortable('instance')) daily.sortable('disable'); + } + if(!sortable.sortable('instance')) + { + sortable.sortable({ + cancel: "#divAppboxHeader, .calendar_calWeekNavHeader, .calendar_plannerHeader", + handle: '.calendar_calGridHeader', + //placeholder: "srotable_cal_wk_ph", + axis:"y", + revert: true, + helper:"clone", + create: function () + { + var $sortItem = jQuery(this); + }, + start: function (event, ui) + { + jQuery('.calendar_calTimeGrid',ui.helper).css('position', 'absolute'); + // Put owners into row IDs + CalendarApp.views[app.calendar.state.view].etemplates[0].widgetContainer.iterateOver(function(widget) { + if(widget.options.owner && !widget.disabled) + { + widget.div.parents('tr').attr('data-owner',widget.options.owner); + } + else + { + widget.div.parents('tr').removeAttr('data-owner'); + } + },this,et2_calendar_timegrid); + }, + stop: function () + { + }, + update: function () + { + var state = app.calendar.getState(); + if (state && typeof state.owner !== 'undefined') + { + var sortedArr = sortable.sortable('toArray', {attribute:"data-owner"}); + // No duplicates, no empties + sortedArr = sortedArr.filter(function(value, index, self) { + return value !== '' && self.indexOf(value) === index; + }); + + var parent = null; + var children = []; + if(state.view == 'day') + { + // If in day view, the days need to be re-ordered, avoiding + // the current sort order + CalendarApp.views.day.etemplates[0].widgetContainer.iterateOver(function(widget) { + var idx = sortedArr.indexOf(widget.options.owner.toString()); + // Move the event holding div + widget.set_left((parseInt(widget.options.width) * idx) + 'px'); + // Re-order the children, or it won't stay + parent = widget._parent; + children.splice(idx,0,widget); + },this,et2_calendar_daycol); + parent.day_widgets.sort(function(a,b) { + return children.indexOf(a) - children.indexOf(b); + }); + } + else + { + // Re-order the children, or it won't stay + CalendarApp.views.day.etemplates[0].widgetContainer.iterateOver(function(widget) { + parent = widget._parent; + var idx = sortedArr.indexOf(widget.options.owner); + children.splice(idx,0,widget); + widget.resize(); + },this,et2_calendar_timegrid); + } + parent._children.sort(function(a,b) { + return children.indexOf(a) - children.indexOf(b); + }); + // Directly update, since there is no other changes needed, + // and we don't want the current sort order applied + app.calendar.state.owner = sortedArr; + parent.options.owner = sortedArr; + } + } + }); + } + + // Enable or disable + if(state.owner.length > 1 && ( + state.view == 'day' && state.owner.length < parseInt(''+egw.preference('day_consolidate','calendar')) || + state.view == 'week' && state.owner.length < parseInt(''+egw.preference('week_consolidate','calendar')) + )) + { + sortable.sortable('enable') + .sortable("refresh") + .disableSelection(); + var options = {}; + switch (state.view) + { + case "day": + options = { + placeholder:"srotable_cal_day_ph", + axis:"x", + handle: '> div:first', + helper( event, element) + { + var scroll = element.parentsUntil('.calendar_calTimeGrid').last().next(); + var helper = jQuery(document.createElement('div')) + .append(element.clone()) + .css('height',scroll.parent().css('height')) + .css('background-color','white') + .css('width', element.css('width')); + return helper; + } + }; + sortable.sortable('option', options); + break; + case "week": + options = { + placeholder:"srotable_cal_wk_ph", + axis:"y", + handle: '.calendar_calGridHeader', + helper: 'clone' + }; + sortable.sortable('option', options); + break; + } + } + else + { + sortable.sortable('disable'); + } + } + + /** + * Bind scroll event + * When the user scrolls, we'll move enddate - startdate days + */ + _scroll( ) + { + /** + * Function we can pass all this off to + * + * @param {String} direction up, down, left or right + * @param {number} delta Integer for how many we're moving, should be +/- 1 + */ + var scroll_animate = function(direction, delta) + { + // Scrolling too fast? + if(app.calendar._scroll_disabled) return; + + // Find the template + var id = jQuery(this).closest('.et2_container').attr('id'); + if(id) + { + var template = etemplate2.getById(id); + } + else + { + template = CalendarApp.views[app.calendar.state.view].etemplates[0]; + } + if(!template) return; + + // Prevent scrolling too fast + app.calendar._scroll_disabled = true; + + // Animate the transition, if possible + var widget = null; + template.widgetContainer.iterateOver(function(w) { + if (w.getDOMNode() == this) widget = w; + },this,et2_widget); + if(widget == null) + { + template.widgetContainer.iterateOver(function(w) { + widget = w; + },this, et2_calendar_timegrid); + if(widget == null) return; + } + /* Disabled + * + // We clone the nodes so we can animate the transition + var original = jQuery(widget.getDOMNode()).closest('.et2_grid'); + var cloned = original.clone(true).attr("id","CLONE"); + + // Moving this stuff around scrolls things around too + // We need this later + var scrollTop = jQuery('.calendar_calTimeGridScroll',original).scrollTop(); + + // This is to hide the scrollbar + var wrapper = original.parent(); + if(direction == "right" || direction == "left") + { + original.css({"display":"inline-block","width":original.width()+"px"}); + cloned.css({"display":"inline-block","width":original.width()+"px"}); + } + else + { + original.css("height",original.height() + "px"); + cloned.css("height",original.height() + "px"); + } + var original_size = {height: wrapper.parent().css('height'), width: wrapper.parent().css('width')}; + wrapper.parent().css({overflow:'hidden', height:original.outerHeight()+"px", width:original.outerWidth() + "px"}); + wrapper.height(direction == "up" || direction == "down" ? 2 * original.outerHeight() : original.outerHeight()); + wrapper.width(direction == "left" || direction == "right" ? 2 * original.outerWidth() : original.outerWidth()); + + // Re-scroll to previous to avoid "jumping" + jQuery('.calendar_calTimeGridScroll',original).scrollTop(scrollTop); + switch(direction) + { + case "up": + case "left": + // Scrolling up + // Apply the reverse quickly, then let it animate as the changes are + // removed, leaving things where they should be. + + original.parent().append(cloned); + // Makes it jump to destination + wrapper.css({ + "transition-duration": "0s", + "transition-delay": "0s", + "transform": direction == "up" ? "translateY(-50%)" : "translateX(-50%)" + }); + // Stop browser from caching style by forcing reflow + if(wrapper[0]) wrapper[0].offsetHeight; + + wrapper.css({ + "transition-duration": "", + "transition-delay": "" + }); + break; + case "down": + case "right": + // Scrolling down + original.parent().prepend(cloned); + break; + } + // Scroll clone to match to avoid "jumping" + jQuery('.calendar_calTimeGridScroll',cloned).scrollTop(scrollTop); + + // Remove + var remove = function() { + // Starting animation + wrapper.addClass("calendar_slide"); + var translate = direction == "down" ? "translateY(-50%)" : (direction == "right" ? "translateX(-50%)" : ""); + wrapper.css({"transform": translate}); + window.setTimeout(function() { + + cloned.remove(); + + // Makes it jump to destination + wrapper.css({ + "transition-duration": "0s", + "transition-delay": "0s" + }); + + // Clean up from animation + wrapper + .removeClass("calendar_slide") + .css({"transform": '',height: '', width:'',overflow:''}); + wrapper.parent().css({overflow: '', width: original_size.width, height: original_size.height}); + original.css("display",""); + if(wrapper.length) + { + wrapper[0].offsetHeight; + } + wrapper.css({ + "transition-duration": "", + "transition-delay": "" + }); + + // Re-scroll to start of day + template.widgetContainer.iterateOver(function(w) { + w.resizeTimes(); + },this, et2_calendar_timegrid); + + window.setTimeout(function() { + if(app.calendar) + { + app.calendar._scroll_disabled = false; + } + }, 100); + },2000); + } + // If detecting the transition end worked, we wouldn't need to use a timeout. + window.setTimeout(remove,100); + */ + window.setTimeout(function() { + if(app.calendar) + { + app.calendar._scroll_disabled = false; + } + }, 2000); + // Get the view to calculate - this actually loads the new data + // Using a timeout make it a little faster (in Chrome) + window.setTimeout(function() { + var view = CalendarApp.views[app.calendar.state.view] || false; + var start = new Date(app.calendar.state.date); + if (view && view.etemplates.indexOf(template) !== -1) + { + start = view.scroll(delta); + app.calendar.update_state({date:app.calendar.date.toString(start)}); + } + else + { + // Home - always 1 week + // TODO + return false; + } + },0); + }; + + // Bind only once, to the whole thing + /* Disabled + jQuery('body').off('.calendar') + //.on('wheel','.et2_container:#calendar-list,#calendar-sidebox)', + .on('wheel.calendar','.et2_container .calendar_calTimeGrid, .et2_container .calendar_plannerWidget', + function(e) + { + // Consume scroll if in the middle of something + if(app.calendar._scroll_disabled) return false; + + // Ignore if they're going the other way + var direction = e.originalEvent.deltaY > 0 ? 1 : -1; + var at_bottom = direction !== -1; + var at_top = direction !== 1; + + jQuery(this).children(":not(.calendar_calGridHeader)").each(function() { + // Check for less than 2px from edge, as sometimes we can't scroll anymore, but still have + // 2px left to go + at_bottom = at_bottom && Math.abs(this.scrollTop - (this.scrollHeight - this.offsetHeight)) <= 2; + }).each(function() { + at_top = at_top && this.scrollTop === 0; + }); + if(!at_bottom && !at_top) return; + + e.preventDefault(); + + scroll_animate.call(this, direction > 0 ? "down" : "up", direction); + + return false; + } + ); + */ + if(typeof framework !== 'undefined' && framework.applications.calendar && framework.applications.calendar.tab) + { + jQuery(framework.applications.calendar.tab.contentDiv) + .swipe('destroy'); + + jQuery(framework.applications.calendar.tab.contentDiv) + .swipe({ + //Generic swipe handler for all directions + swipe:function(event, direction, distance, duration, fingerCount) { + if(direction == "up" || direction == "down") + { + if(fingerCount <= 1) return; + var at_bottom = direction !== -1; + var at_top = direction !== 1; + + jQuery(this).children(":not(.calendar_calGridHeader)").each(function() { + // Check for less than 2px from edge, as sometimes we can't scroll anymore, but still have + // 2px left to go + at_bottom = at_bottom && Math.abs(this.scrollTop - (this.scrollHeight - this.offsetHeight)) <= 2; + }).each(function() { + at_top = at_top && this.scrollTop === 0; + }); + } + + var delta = direction == "down" || direction == "right" ? -1 : 1; + // But we animate in the opposite direction to the swipe + var opposite = {"down": "up", "up": "down", "left": "right", "right": "left"}; + direction = opposite[direction]; + scroll_animate.call(jQuery(event.target).closest('.calendar_calTimeGrid, .calendar_plannerWidget')[0], direction, delta); + return false; + }, + allowPageScroll: jQuery.fn.swipe.pageScroll.VERTICAL, + threshold: 100, + fallbackToMouseEvents: false, + triggerOnTouchEnd: false + }); + + // Page up & page down + egw_registerGlobalShortcut(jQuery.ui.keyCode.PAGE_UP, false, false, false, function() { + if(app.calendar.state.view == 'listview') + { + return false; + } + scroll_animate.call(this,"up", -1); + return true; + }, this); + egw_registerGlobalShortcut(jQuery.ui.keyCode.PAGE_DOWN, false, false, false, function() { + if(app.calendar.state.view == 'listview') + { + return false; + } + scroll_animate.call(this,"down", 1); + return true; + }, this); + } + } + + /** + * Handler for changes generated by internal user interactions, like + * drag & drop inside calendar and resize. + * + * @param {Event} event + * @param {et2_calendar_event} widget Widget for the event + * @param {string} dialog_button - 'single', 'series', or 'exception', based on the user's answer + * in the popup + * @returns {undefined} + */ + event_change(event, widget, dialog_button) + { + // Add loading spinner - not visible if the body / gradient is there though + widget.div.addClass('loading'); + + // Integrated infolog event + //Get infologID if in case if it's an integrated infolog event + if (widget.options.value.app == 'infolog') + { + // If it is an integrated infolog event we need to edit infolog entry + egw().json( + 'stylite_infolog_calendar_integration::ajax_moveInfologEvent', + [widget.options.value.app_id, widget.options.value.start, widget.options.value.duration], + // Remove loading spinner + function() {if(widget.div) widget.div.removeClass('loading');} + ).sendRequest(); + } + else + { + var _send = function() { + egw().json( + 'calendar.calendar_uiforms.ajax_moveEvent', + [ + dialog_button == 'exception' ? widget.options.value.app_id : widget.options.value.id, + widget.options.value.owner, + widget.options.value.start, + widget.options.value.owner, + widget.options.value.duration, + dialog_button == 'series' ? widget.options.value.start : null + ], + // Remove loading spinner + function() {if(widget && widget.div) widget.div.removeClass('loading');} + ).sendRequest(true); + }; + if(dialog_button == 'series' && widget.options.value.recur_type) + { + widget.series_split_prompt(function(_button_id) + { + if (_button_id == et2_dialog.OK_BUTTON) + { + _send(); + } + } + ); + } + else + { + _send(); + } + } + } + + /** + * open the freetime search popup + * + * @param {string} _link + */ + freetime_search_popup(_link) + { + this.egw.open_link(_link,'ft_search','700x500') ; + } + + /** + * send an ajax request to server to set the freetimesearch window content + * + */ + freetime_search() + { + var content = this.et2.getArrayMgr('content').data; + content['start'] = this.et2.getWidgetById('start').get_value(); + content['end'] = this.et2.getWidgetById('end').get_value(); + content['duration'] = this.et2.getWidgetById('duration').get_value(); + + var request = this.egw.json('calendar.calendar_uiforms.ajax_freetimesearch', [content],null,null,null,null); + request.sendRequest(); + } + + /** + * Function for disabling the recur_data multiselect box + * + */ + check_recur_type() + { + var recurType = this.et2.getWidgetById('recur_type'); + var recurData = this.et2.getWidgetById('recur_data'); + + if(recurType && recurData) + { + recurData.set_disabled(recurType.get_value() != 2 && recurType.get_value() != 4); + } + } + + /** + * Actions for when the user changes the event start date in edit dialog + * + * @returns {undefined} + */ + edit_start_change(input, widget) + { + if(!widget) + { + widget = etemplate2.getById('calendar-edit').widgetContainer.getWidgetById('start'); + } + + // Update settings for querying participants + this.edit_update_participant(widget); + + // Update recurring date limit, if not set it can't be before start + if(widget) + { + var recur_end = widget.getRoot().getWidgetById('recur_enddate'); + if(recur_end && recur_end.getValue && !recur_end.getValue()) + { + recur_end.set_min(widget.getValue()); + } + } + // Update currently selected alarm time + this.alarm_custom_date(); + } + + /** + * Show/Hide end date, for both edit and freetimesearch popups, + * based on if "use end date" selected or not. + * + */ + set_enddate_visibility() + { + var duration = this.et2.getWidgetById('duration'); + var start = this.et2.getWidgetById('start'); + var end = this.et2.getWidgetById('end'); + var content = this.et2.getArrayMgr('content').data; + + if (typeof duration != 'undefined' && typeof end != 'undefined') + { + end.set_disabled(duration.get_value()!==''); + + // Only set end date if not provided, adding seconds fails with DST + if (!end.disabled && !content.end) + { + end.set_value(start.get_value()); + if (typeof content.duration != 'undefined') end.set_value("+"+content.duration); + } + } + this.edit_update_participant(start); + } + + /** + * Update query parameters for participants + * + * This allows for resource conflict checking + * + * @param {DOMNode|et2_widget} input Either the input node, or the widget + * @param {et2_widget} [widget] If input is an input node, widget will have + * the widget, otherwise it will be undefined. + */ + edit_update_participant(input, widget?) + { + if(typeof widget === 'undefined') widget = input; + var content = widget.getInstanceManager().getValues(widget.getRoot()); + var participant = widget.getRoot().getWidgetById('participant'); + if(!participant) return; + + participant.set_autocomplete_params({exec:{ + start: content.start, + end: content.end, + duration: content.duration, + whole_day: content.whole_day, + }}); + } + + /** + * handles actions selectbox in calendar edit popup + * + * @param {mixed} _event + * @param {et2_base_widget} widget "actions selectBox" in edit popup window + */ + actions_change(_event, widget) + { + var event = this.et2.getArrayMgr('content').data; + if (widget) + { + var id = this.et2.getArrayMgr('content').data['id']; + switch (widget.get_value()) + { + case 'print': + this.egw.open_link('calendar.calendar_uiforms.edit&cal_id='+id+'&print=1','_blank','700x700'); + break; + case 'mail': + this.egw.json('calendar.calendar_uiforms.ajax_custom_mail', [event, !event['id'], false],null,null,null,null).sendRequest(); + this.et2._inst.submit(); + break; + case 'sendrequest': + this.egw.json('calendar.calendar_uiforms.ajax_custom_mail', [event, !event['id'], true],null,null,null,null).sendRequest(); + this.et2._inst.submit(); + break; + case 'infolog': + this.egw.open_link('infolog.infolog_ui.edit&action=calendar&action_id='+(jQuery.isPlainObject(event)?event['id']:event),'_blank','700x600','infolog'); + this.et2._inst.submit(); + break; + case 'ical': + this.et2._inst.postSubmit(); + break; + default: + this.et2._inst.submit(); + } + } + } + + /** + * open mail compose popup window + * + * @param {Array} vars + * @todo need to provide right mail compose from server to custom_mail function + */ + custom_mail (vars) + { + this.egw.open_link(this.egw.link("/index.php",vars),'_blank','700x700'); + } + + /** + * control delete_series popup visibility + * + * @param {et2_widget} widget + * @param {Array} exceptions an array contains number of exception entries + * + */ + delete_btn(widget,exceptions) + { + var content = this.et2.getArrayMgr('content').data; + + if (exceptions) + { + var buttons = [ + { + button_id: 'keep', + title: this.egw.lang('All exceptions are converted into single events.'), + text: this.egw.lang('Keep exceptions'), + id: 'button[delete_keep_exceptions]', + image: 'keep', "default":true + }, + { + button_id: 'delete', + title: this.egw.lang('The exceptions are deleted together with the series.'), + text: this.egw.lang('Delete exceptions'), + id: 'button[delete_exceptions]', + image: 'delete' + }, + { + button_id: 'cancel', + text: this.egw.lang('Cancel'), + id: 'dialog[cancel]', + image: 'cancel' + } + + ]; + var self = this; + et2_dialog.show_dialog + ( + function(_button_id) + { + if (_button_id != 'dialog[cancel]') + { + widget.getRoot().getWidgetById('delete_exceptions').set_value(_button_id == 'button[delete_exceptions]'); + widget.getInstanceManager().submit('button[delete]'); + return true; + } + else + { + return false; + } + }, + this.egw.lang("Do you want to keep the series exceptions in your calendar?"), + this.egw.lang("This event is part of a series"), {}, buttons , et2_dialog.WARNING_MESSAGE + ); + } + else if (content['recur_type'] !== 0) + { + et2_dialog.confirm(widget,'Delete this series of recurring events','Delete Series'); + } + else + { + et2_dialog.confirm(widget,'Delete this event','Delete'); + } + } + + /** + * On change participant event, try to set add button status based on + * participant field value. Additionally, disable/enable quantity field + * if there's none resource value or there are more than one resource selected. + * + */ + participantOnChange() + { + var add = this.et2.getWidgetById('add'); + var quantity = this.et2.getWidgetById('quantity'); + var participant = this.et2.getWidgetById('participant'); + + // array of participants + var value = participant.get_value(); + + add.set_readonly(value.length <= 0); + + quantity.set_readonly(false); + + // number of resources + var nRes = 0; + + for (var i=0;i( CalendarApp.views['listview'].etemplates[0]).widgetContainer || null; + const nm = view ? view.getWidgetById('nm') : null; + const filter = view && nm ? nm.getWidgetById('filter') : null; + const dates = view ? view.getWidgetById('calendar.list.dates') : null; + + // Update state when user changes it + if(view && filter) + { + app.calendar.state.filter = filter.getValue(); + // Change sort order for before - this is just the UI, server does the query + if(app.calendar.state.filter == 'before') + { + nm.sortBy('cal_start',false, false); + } + else + { + nm.sortBy('cal_start',true, false); + } + } + else + { + delete app.calendar.state.filter; + } + if (filter && dates) + { + dates.set_disabled(filter.getValue() !== "custom"); + if (filter.getValue() == "custom" && !this.state_update_in_progress) + { + // Copy state dates over, without causing [another] state update + var actual = this.state_update_in_progress; + this.state_update_in_progress = true; + (view.getWidgetById('startdate')).set_value(app.calendar.state.first); + (view.getWidgetById('enddate')).set_value(app.calendar.state.last); + this.state_update_in_progress = actual; + + jQuery((view.getWidgetById('startdate')).getDOMNode()).find('input').focus(); + } + } + } + + /** + * Application links from non-list events + * + * The ID looks like calendar:: or calendar::: + * For processing the links: + * '$app' gets replaced with 'calendar' + * '$id' gets replaced with + * '$app_id gets replaced with : + * + * Use either $id or $app_id depending on if you want the series [beginning] + * or a particular date. + * + * @param {egwAction} _action + * @param {egwActionObject[]} _events + */ + action_open(_action, _events) + { + var id = _events[0].id.split('::'); + var app = id[0]; + var app_id = id[1]; + if(app_id && app_id.indexOf(':')) + { + var split = id[1].split(':'); + id = split[0]; + } + else + { + id = app_id; + } + if(_action.data.open) + { + var open = JSON.parse(_action.data.open) || {}; + var extra = open.extra || ''; + + extra = extra.replace(/(\$|%24)app/,app).replace(/(\$|%24)app_id/,app_id) + .replace(/(\$|%24)id/,id); + + // Get a little smarter with the context + if(!extra) + { + var context : any = {}; + if(egw.dataGetUIDdata(_events[0].id) && egw.dataGetUIDdata(_events[0].id).data) + { + // Found data in global cache + context = egw.dataGetUIDdata(_events[0].id).data; + extra = {}; + } + else if (_events[0].iface.getWidget() && _events[0].iface.getWidget()._get_time_from_position && + _action.menu_context && _action.menu_context.event + ) + { + // Non-row space in planner + // Context menu has position information, but target is not what we expact + let target = jQuery('.calendar_plannerGrid',_action.menu_context.event.currentTarget); + var y = _action.menu_context.event.pageY - target.offset().top; + var x = _action.menu_context.event.pageX - target.offset().left; + var date = _events[0].iface.getWidget()._get_time_from_position(x, y); + if(date) + { + context.start = date.toJSON(); + } + } + else if (_events[0].iface.getWidget() && _events[0].iface.getWidget().instanceOf(et2_calendar_planner_row)) + { + // Empty space on a planner row + var widget = _events[0].iface.getWidget(); + var parent = widget.getParent(); + if(parent.options.group_by == 'month') + { + var date = parent._get_time_from_position(_action.menu_context.event.clientX, _action.menu_context.event.clientY); + } + else + { + var date = parent._get_time_from_position(_action.menu_context.event.offsetX, _action.menu_context.event.offsetY); + } + if(date) + { + context.start = date.toJSON(); + } + jQuery.extend(context, widget.getDOMNode().dataset); + + } + else if (_events[0].iface.getWidget() && _events[0].iface.getWidget().instanceOf(et2_valueWidget)) + { + // Able to extract something from the widget + context = _events[0].iface.getWidget().getValue ? + _events[0].iface.getWidget().getValue() : + _events[0].iface.getWidget().options.value || {}; + extra = {}; + } + // Try to pull whatever we can from the event + else if (jQuery.isEmptyObject(context) && _action.menu_context && (_action.menu_context.event.target)) + { + let target = _action.menu_context.event.target; + while(target != null && target.parentNode && jQuery.isEmptyObject(target.dataset)) + { + target = target.parentNode; + } + + context = extra = jQuery.extend({},target.dataset); + var owner = jQuery(target).closest('[data-owner]').get(0); + if(owner && owner.dataset.owner && owner.dataset.owner != this.state.owner) + { + extra.owner = owner.dataset.owner.split(','); + } + } + if(context.date) extra.date = context.date; + if(context.app) extra.app = context.app; + if(context.app_id) extra.app_id = context.app_id; + } + + this.egw.open(open.id_data||'',open.app,open.type,extra ? extra : context); + } + else if (_action.data.url) + { + var url = _action.data.url; + url = url.replace(/(\$|%24)app/,app).replace(/(\$|%24)app_id/,app_id) + .replace(/(\$|%24)id/,id); + this.egw.open_link(url); + } + } + + /** + * Context menu action (on a single event) in non-listview to generate ical + * + * Since nextmatch is all ready to handle that, we pass it through + * + * @param {egwAction} _action + * @param {egwActionObject[]} _events + */ + ical(_action, _events) + { + // Send it through nextmatch + _action.data.nextmatch = etemplate2.getById('calendar-list').widgetContainer.getWidgetById('nm'); + var ids = {ids:[]}; + for(var i = 0; i < _events.length; i++) + { + ids.ids.push(_events[i].id); + } + nm_action(_action, _events, null, ids); + } + + /** + * Change status (via AJAX) + * + * @param {egwAction} _action + * @param {egwActionObject} _events + */ + status(_action, _events) + { + // Should be a single event, but we'll do it for all + for(var i = 0; i < _events.length; i++) + { + var event_widget = _events[i].iface.getWidget() || false; + if(!event_widget) continue; + + event_widget.recur_prompt(jQuery.proxy(function(button_id,event_data) { + switch(button_id) + { + case 'exception': + egw().json( + 'calendar.calendar_uiforms.ajax_status', + [event_data.app_id, egw.user('account_id'), _action.data.id] + ).sendRequest(true); + break; + case 'series': + case 'single': + egw().json( + 'calendar.calendar_uiforms.ajax_status', + [event_data.id, egw.user('account_id'), _action.data.id] + ).sendRequest(true); + break; + case 'cancel': + default: + break; + } + },this)); + } + + } + + /** + * this function try to fix ids which are from integrated apps + * + * @param {egwAction} _action + * @param {egwActionObject[]} _senders + */ + cal_fix_app_id(_action, _senders) + { + var app = 'calendar'; + var id = _senders[0].id; + var matches = id.match(/^(?:calendar::)?([0-9]+)(:([0-9]+))?$/); + if (matches) + { + id = matches[1]; + } + else + { + matches = id.match(/^([a-z_-]+)([0-9]+)/i); + if (matches) + { + app = matches[1]; + id = matches[2]; + } + } + var backup_url = _action.data.url; + + _action.data.url = _action.data.url.replace(/(\$|%24)id/,id); + _action.data.url = _action.data.url.replace(/(\$|%24)app/,app); + + nm_action(_action, _senders,false,{ids:[id]}); + + _action.data.url = backup_url; // restore url + } + + /** + * Open a smaller dialog/popup to add a new entry + * + * This is opened inside a dialog widget, not a popup. This causes issues + * with the submission, handling of the response, and cleanup. + * + * @param {Object} options Array of values for new + * @param {et2_calendar_event} event Placeholder showing where new event goes + */ + add(options, event) + { + if(this.egw.preference('new_event_dialog', 'calendar') === 'edit') + { + // Set this to open the add template in a popup + //options.template = 'calendar.add'; + return this.egw.open(null, 'calendar', 'edit', options, '_blank', 'calendar'); + } + + // Open dialog to use as target + var add_dialog = et2_dialog.show_dialog(null, '', ' ', null, [], et2_dialog.PLAIN_MESSAGE, this.egw); + + // Call the server, get it into the dialog + options = jQuery.extend({menuaction: 'calendar.calendar_uiforms.ajax_add', template: 'calendar.add'}, options); + this.egw.json( + this.egw.link('/json.php', options), + //menuaction + options.join('&'), + [options], + function(data) { + if(data.type) return false; + var content = { + html: data[0], + js: '' + }; + + egw_seperateJavaScript(content); + + // Check for right template in the response + if(content.html.indexOf('calendar-add') <= 0) return false; + + // Insert the content + jQuery(add_dialog.div).append(content.html); + + // Run the javascript code + jQuery(add_dialog.div).append(content.js); + + // Re-position after load + jQuery('form', add_dialog.div).one('load', function() { + // Hide close button + jQuery(".ui-dialog-titlebar-close", add_dialog.div.parent()).hide(); + + // Position by event + add_dialog.div.dialog('widget').position({ + my: 'center top', at: event ? 'bottom' : 'center', of: event ? event.node : window, + collision: 'flipfit' + }); + }); + } + ).sendRequest(); + + add_dialog.div.dialog({ + close: function( ev, ui ) { + // Wait a bit to make sure etemplate button finishes processing, or it will error + window.setTimeout(function() { + var template = etemplate2.getById('calendar-add'); + if(template && template.name === 'calendar.add') + { + template.clear(); + this.dialog.destroy(); + delete app.calendar.quick_add; + } + else if (template) + { + // Open conflicts + var data = jQuery.extend({},template.widgetContainer.getArrayMgr('content').data, app.calendar.quick_add); + + egw.openPopup( + egw.link( + '/index.php?menuaction=calendar.calendar_uiforms.ajax_conflicts', + data + ), + 850, 300, + 'conflicts', 'calendar' + ); + + delete app.calendar.quick_add; + + // Close the JS dialog + this.dialog.destroy(); + + // Do not submit this etemplate + return false; + } + + }.bind({dialog: add_dialog, event: ev}), 1000); + } + }); + } + + /** + * Callback for save button in add dialog + * + * @param {Event} event + * @param {et2_button} widget + * @returns {Boolean} + */ + add_dialog_save(event, widget) + { + // Include all sent values so we can pass on things that we don't have UI widgets for + this.quick_add = this._add_dialog_values(widget); + + // Close the dialog + jQuery(widget.getInstanceManager().DOMContainer.parentNode).dialog('close'); + + // Mess with opener so update opener in response works + window.opener = window; + window.setTimeout(function() {window.opener = null;},1000); + + // Proceed with submit + return true; + } + + + /** + * Callback for edit button in add dialog + * + * @param {Event} event + * @param {et2_button} widget + * @returns {Boolean} + */ + add_dialog_edit(event, widget) + { + var title=widget.getRoot().getWidgetById('title'); + if(title && !title.get_value()) + { + title.set_value(title.egw().lang('Event')); + } + + // Open regular edit + egw.open(null,'calendar','edit',this._add_dialog_values(widget)); + + // Close the dialog + jQuery(widget.getInstanceManager().DOMContainer.parentNode).dialog('close'); + + // Do not submit this etemplate + return false; + } + + /** + * Include some additional values so we can pass on things that we don't have + * UI widgets for in the add template + * + * @param {et2_widget} widget + * @returns {Object} + */ + _add_dialog_values(widget) + { + // Some select things to pass on, since not everything will fit + var mgr = widget.getRoot().getArrayMgr('content'); + var values : any = { + owner: typeof mgr.getEntry('owner') == 'object' ? mgr.getEntry('owner') : (mgr.getEntry('owner')+'').split(','), + participants: [], + whole_day: mgr.getEntry('whole_day') + }; + if(mgr.getEntry('link_to') && typeof mgr.getEntry('link_to').to_id === 'object') + { + var links = mgr.getEntry('link_to').to_id; + + values.link_app = []; + values.link_id = []; + for( var id in links) + { + values.link_app.push(links[id].app); + values.link_id.push(links[id].id); + } + } + for(var id in mgr.getEntry('participants')) + { + var participant = mgr.getEntry('participants')[id]; + if (participant && participant.uid) + { + values.participants.push(participant.uid); + } + } + return jQuery.extend( + values, + widget.getInstanceManager().getValues(widget.getRoot()) + ); + } + + /** + * Open calendar entry, taking into accout the calendar integration of other apps + * + * calendar_uilist::get_rows sets var js_calendar_integration object + * + * @param _action + * @param _senders + * + */ + cal_open(_action, _senders) + { + // Try for easy way - find a widget + if(_senders[0].iface.getWidget) + { + var widget = _senders[0].iface.getWidget(); + return widget.recur_prompt(); + } + + // Nextmatch in list view does not have a widget, but we can pull + // the data by ID + // Check for series + var id = _senders[0].id; + var data = egw.dataGetUIDdata(id); + if (data && data.data) + { + et2_calendar_event.recur_prompt(data.data); + return; + } + var matches = id.match(/^(?:calendar::)?([0-9]+):([0-9]+)$/); + + // Check for other app integration data sent from server + var backup = _action.data; + if(_action.parent.data && _action.parent.data.nextmatch) + { + var js_integration_data = _action.parent.data.nextmatch.options.settings.js_integration_data || this.et2.getArrayMgr('content').data.nm.js_integration_data; + if(typeof js_integration_data == 'string') + { + js_integration_data = JSON.parse(js_integration_data); + } + } + matches = id.match(/^calendar::([a-z_-]+)([0-9]+)/i); + if (matches && js_integration_data && js_integration_data[matches[1]]) + { + var app = matches[1]; + _action.data.url = window.egw_webserverUrl+'/index.php?'; + var get_params = js_integration_data[app].edit; + get_params[js_integration_data[app].edit_id] = matches[2]; + for(var name in get_params) + _action.data.url += name+"="+encodeURIComponent(get_params[name])+"&"; + + if (js_integration_data[app].edit_popup) + { + egw.open_link(_action.data.url,'_blank',js_integration_data[app].edit_popup,app); + + _action.data = backup; // restore url, width, height, nm_action + return; + } + } + else + { + // Other app integration using link registry + var data = egw.dataGetUIDdata(_senders[0].id); + if(data && data.data) + { + return egw.open(data.data.app_id, data.data.app, 'edit'); + } + } + // Regular, single event + egw.open(id.replace(/^calendar::/g,''),'calendar','edit'); + } + + /** + * Delete (a single) calendar entry over ajax. + * + * Used for the non-list views + * + * @param {egwAction} _action + * @param {egwActionObject} _events + */ + delete(_action, _events) + { + // Should be a single event, but we'll do it for all + for(var i = 0; i < _events.length; i++) + { + var event_widget = _events[i].iface.getWidget() || false; + if(!event_widget) continue; + + event_widget.recur_prompt(jQuery.proxy(function(button_id,event_data) { + switch(button_id) + { + case 'exception': + egw().json( + 'calendar.calendar_uiforms.ajax_delete', + [event_data.app_id] + ).sendRequest(true); + break; + case 'series': + case 'single': + egw().json( + 'calendar.calendar_uiforms.ajax_delete', + [event_data.id] + ).sendRequest(true); + break; + case 'cancel': + default: + break; + } + },this)); + } + } + + /** + * Delete calendar entry, asking if you want to delete series or exception + * + * Used for nextmatch + * + * @param _action + * @param _senders + */ + cal_delete(_action, _senders) + { + var backup = _action.data; + var matches = false; + + // Loop so we ask if any of the selected entries is part of a series + for(var i = 0; i < _senders.length; i++) + { + var id = _senders[i].id; + if(!matches) + { + matches = id.match(/^(?:calendar::)?([0-9]+):([0-9]+)$/); + } + } + if (matches) + { + var popup = jQuery('#calendar-list_delete_popup').get(0); + if (typeof popup != 'undefined') + { + // nm action - show popup + nm_open_popup(_action,_senders); + } + return; + } + + nm_action(_action, _senders); + } + + /** + * Confirmation dialog for moving a series entry + * + * @param {object} _DOM + * @param {et2_widget} _button button Save | Apply + */ + move_edit_series(_DOM,_button) + { + var content = this.et2.getArrayMgr('content').data; + var start_date = this.et2.getWidgetById('start').get_value(); + var end_date = this.et2.getWidgetById('end').get_value(); + var whole_day = this.et2.getWidgetById('whole_day'); + var duration = ''+this.et2.getWidgetById('duration').get_value(); + var is_whole_day = whole_day && whole_day.get_value() == whole_day.options.selected_value; + var button = _button; + var that = this; + + let instance_date_regex = window.location.search.match(/date=(\d{4}-\d{2}-\d{2}(?:.+Z)?)/); + let instance_date; + if(instance_date_regex && instance_date_regex.length && instance_date_regex[1]) + { + instance_date = new Date(unescape(instance_date_regex[1])); + instance_date.setUTCMinutes(instance_date.getUTCMinutes() +instance_date.getTimezoneOffset()); + } + if (typeof content != 'undefined' && content.id != null && + typeof content.recur_type != 'undefined' && content.recur_type != null && content.recur_type != 0 + ) + { + if (content.start != start_date || + content.whole_day != is_whole_day || + (duration && ''+content.duration != duration || + // End date might ignore seconds, and be 59 seconds off for all day events + !duration && Math.abs(new Date(end_date) - new Date(content.end)) > 60000) + ) + { + et2_calendar_event.series_split_prompt( + content, instance_date, function(_button_id) + { + if (_button_id == et2_dialog.OK_BUTTON) + { + that.et2.getInstanceManager().submit(button); + + } + } + ); + } + else + { + return true; + } + } + else + { + return true; + } + } + + /** + * Send a mail or meeting request to event participants + * + * @param {egwAction} _action + * @param {egwActionObject[]} _selected + */ + action_mail(_action, _selected) + { + var data = egw.dataGetUIDdata(_selected[0].id) || {data:{}}; + var event = data.data; + this.egw.json('calendar.calendar_uiforms.ajax_custom_mail', + [event, false, _action.id==='sendrequest'], + null,null,null,null + ).sendRequest(); + } + + /** + * Insert selected event(s) into a document + * + * Actually, just pass it off to the nextmatch + * + * @param {egwAction} _action + * @param {egwActionObject[]} _selected + */ + action_merge(_action, _selected) + { + var ids = {ids:[]}; + for(var i = 0; i < _selected.length; i++) + { + ids.ids.push(_selected[i].id); + } + nm_action(egw_getActionManager(this.appname,false,1) + .getActionById('nm').getActionById(_action.id), _selected, null, ids); + } + + /** + * Sidebox merge + * + * Manage the state and pass the request to the correct place. Since the nextmatch + * and the sidebox have different ideas of the 'current' timespan (sidebox + * always has a start and end date) we need to call merge on the nextmatch + * if the current view is listview, so the user gets the results they expect. + * + * @param {Event} event UI event + * @param {et2_widget} widget Should be the merge selectbox + */ + sidebox_merge(event, widget) + { + if(!widget || !widget.getValue()) return false; + + if(this.state.view == 'listview') + { + // If user is looking at the list, pretend they used the context + // menu and process it through the nextmatch + var nm = etemplate2.getById('calendar-list').widgetContainer.getWidgetById('nm') || false; + var selected = nm ? nm.controller._objectManager.getSelectedLinks() : []; + var action = nm.controller._actionManager.getActionById('document_'+widget.getValue()); + if(nm && (!selected || !selected.length)) + { + nm.controller._selectionMgr.selectAll(true); + } + if(action && selected) + { + action.execute(selected); + } + } + else + { + // Set the hidden inputs to the current time span & submit + widget.getRoot().getWidgetById('first').set_value(app.calendar.state.first); + widget.getRoot().getWidgetById('last').set_value(app.calendar.state.last); + if(widget.getRoot().getArrayMgr('content').getEntry('collabora_enabled')) + { + widget.getInstanceManager().submit(); + } + else + { + widget.getInstanceManager().postSubmit(); + window.setTimeout(function() {widget.set_value('');},100); + } + } + + return false; + } + + /** + * Method to set state for JSON requests (jdots ajax_exec or et2 submits can NOT use egw.js script tag) + * + * @param {object} _state + */ + set_state(_state) + { + if (typeof _state == 'object') + { + // If everything is loaded, handle the changes + if(this.sidebox_et2 !== null) + { + this.update_state(_state); + } + else + { + // Things aren't loaded yet, just set it + this.state = _state; + } + } + } + + /** + * Change only part of the current state. + * + * The passed state options (filters) are merged with the current state, so + * this is the one that should be used for most calls, as setState() requires + * the complete state. + * + * @param {Object} _set New settings + */ + update_state(_set) + { + // Make sure we're running in top window + // @ts-ignore + if(window !== window.top && window.top.app.calendar) + { + // @ts-ignore + return window.top.app.calendar.update_state(_set); + } + if(this.state_update_in_progress) return; + + var changed = []; + var new_state = jQuery.extend({}, this.state); + if (typeof _set === 'object') + { + for(var s in _set) + { + if (new_state[s] !== _set[s] && (typeof new_state[s] == 'string' || typeof new_state[s] !== 'string' && new_state[s]+'' !== _set[s]+'')) + { + changed.push(s + ': ' + new_state[s] + ' -> ' + _set[s]); + new_state[s] = _set[s]; + } + } + } + if(changed.length && !this.state_update_in_progress) + { + // This activates calendar app if you call setState from a different app + // such as home. If we change state while not active, sizing is wrong. + if(typeof framework !== 'undefined' && framework.applications.calendar && framework.applications.calendar.hasSideboxMenuContent) + { + framework.setActiveApp(framework.applications.calendar); + } + + console.log('Calendar state changed',changed.join("\n")); + // Log + this.egw.debug('navigation','Calendar state changed', changed.join("\n")); + this.setState({state: new_state}); + } + } + + /** + * Return state object defining current view + * + * Called by favorites to query current state. + * + * @return {object} description + */ + getState() + { + var state = jQuery.extend({},this.state); + + if (!state) + { + var egw_script_tag = document.getElementById('egw_script_id'); + state = egw_script_tag.getAttribute('data-calendar-state'); + state = state ? JSON.parse(state) : {}; + } + + // Don't store current user in state to allow admins to create favourites for all + // Should make no difference for normal users. + if(state.owner == egw.user('account_id')) + { + // 0 is always the current user, so if an admin creates a default favorite, + // it will work for other users too. + state.owner = 0; + } + + // Keywords are only for list view + if(state.view == 'listview') + { + var listview : et2_nextmatch = typeof CalendarApp.views.listview.etemplates[0] !== 'string' && + CalendarApp.views.listview.etemplates[0].widgetContainer && + CalendarApp.views.listview.etemplates[0].widgetContainer.getWidgetById('nm'); + if(listview && listview.activeFilters && listview.activeFilters.search) + { + state.keywords = listview.activeFilters.search; + } + } + + // Don't store date or first and last + delete state.date; + delete state.first; + delete state.last; + delete state.startdate; + delete state.enddate; + delete state.start_date; + delete state.end_date; + + return state; + } + + /** + * Set a state previously returned by getState + * + * Called by favorites to set a state saved as favorite. + * + * @param {object} state containing "name" attribute to be used as "favorite" GET parameter to a nextmatch + */ + setState(state) + { + // State should be an object, not a string, but we'll parse + if(typeof state == "string") + { + if(state.indexOf('{') != -1 || state =='null') + { + state = JSON.parse(state); + } + } + if(typeof state.state !== 'object' || !state.state.view) + { + state.state = {view: 'week'}; + } + // States with no name (favorites other than No filters) default to + // today. Applying a favorite should keep the current date. + if(!state.state.date) + { + state.state.date = state.name ? this.state.date : new Date(); + } + if(typeof state.state.weekend == 'undefined') + { + state.state.weekend = true; + } + + // Hide other views + var view = CalendarApp.views[state.state.view]; + for(var _view in CalendarApp.views) + { + if(state.state.view != _view && CalendarApp.views[_view]) + { + for(var i = 0; i < CalendarApp.views[_view].etemplates.length; i++) + { + if(typeof CalendarApp.views[_view].etemplates[i] !== 'string' && + view.etemplates.indexOf(CalendarApp.views[_view].etemplates[i]) == -1) + { + jQuery(CalendarApp.views[_view].etemplates[i].DOMContainer).hide(); + } + } + } + } + if(this.sidebox_et2) + { + jQuery(this.sidebox_et2.getInstanceManager().DOMContainer).hide(); + } + + // Check for valid cache + var cachable_changes = ['date','weekend','view','days','planner_view','sortby']; + // @ts-ignore + let keys = (Object.keys(this.state).concat(Object.keys(state.state))).filter( + function(value, index, self) + { + return self.indexOf(value) === index; + } + ); + for(var i = 0; i < keys.length; i++) + { + var s = keys[i]; + if (this.state[s] !== state.state[s]) + { + if(cachable_changes.indexOf(s) === -1) + { + // Expire daywise cache + var daywise = egw.dataKnownUIDs(CalendarApp.DAYWISE_CACHE_ID); + + // Can't delete from here, as that would disconnect the existing widgets listening + for(var i = 0; i < daywise.length; i++) + { + egw.dataStoreUID(CalendarApp.DAYWISE_CACHE_ID + '::' + daywise[i],null); + } + break; + } + } + } + + // Check for a supported client-side view + if(CalendarApp.views[state.state.view] && + // Check that the view is instanciated + typeof CalendarApp.views[state.state.view].etemplates[0] !== 'string' && CalendarApp.views[state.state.view].etemplates[0].widgetContainer + ) + { + // Doing an update - this includes the selected view, and the sidebox + // We set a flag to ignore changes from the sidebox which would + // cause infinite loops. + this.state_update_in_progress = true; + + // Sanitize owner so it's always an array + if(state.state.owner === null || !state.state.owner || + (typeof state.state.owner.length != 'undefined' && state.state.owner.length == 0) + ) + { + state.state.owner = undefined; + } + switch(typeof state.state.owner) + { + case 'undefined': + state.state.owner = [this.egw.user('account_id')]; + break; + case 'string': + state.state.owner = state.state.owner.split(','); + break; + case 'number': + state.state.owner = [state.state.owner]; + break; + case 'object': + // An array-like Object or an Array? + if(!state.state.owner.filter) + { + state.state.owner = jQuery.map(state.state.owner, function(owner) {return owner;}); + } + } + // Remove duplicates + state.state.owner = state.state.owner.filter(function(value, index, self) { + return self.indexOf(value) === index; + }); + // Make sure they're all strings + state.state.owner = state.state.owner.map(function(owner) { return ''+owner;}); + // Keep sort order + if(typeof this.state.owner === 'object') + { + var owner = []; + this.state.owner.forEach(function(key) { + var found = false; + state.state.owner = state.state.owner.filter(function(item) { + if(!found && item == key) { + owner.push(item); + found = true; + return false; + } else + return true; + }); + }); + // Add in any new owners + state.state.owner = owner.concat(state.state.owner); + } + if (state.state.owner.indexOf('0') >= 0) + { + state.state.owner[state.state.owner.indexOf('0')] = this.egw.user('account_id'); + } + + // Show the correct number of grids + var grid_count = 0; + switch(state.state.view) + { + case 'day': + grid_count = 1; + break; + case 'day4': + case 'week': + grid_count = state.state.owner.length >= parseInt(''+this.egw.preference('week_consolidate','calendar')) ? 1 : state.state.owner.length; + break; + case 'weekN': + grid_count = parseInt(''+this.egw.preference('multiple_weeks','calendar')) || 3; + break; + // Month is calculated individually for the month + } + + var grid = view.etemplates[0].widgetContainer.getWidgetById('view'); + + // Show the templates for the current view + // Needs to be visible while updating so sizing works + for(var i = 0; i < view.etemplates.length; i++) + { + jQuery(view.etemplates[i].DOMContainer).show(); + } + + /* + If the count is different, we need to have the correct number + If the count is > 1, it's either because there are multiple date spans (weekN, month) and we need the correct span + per row, or there are multiple owners and we need the correct owner per row. + */ + if(grid) + { + // Show loading div to hide redrawing + egw.loading_prompt( + this.appname,true,egw.lang('please wait...'), + typeof framework !== 'undefined' ? framework.applications.calendar.tab.contentDiv : false, + egwIsMobile()?'horizontal':'spinner' + ); + + var loading = false; + + + var value = []; + state.state.first = view.start_date(state.state).toJSON(); + // We'll modify this one, so it needs to be a new object + var date = new Date(state.state.first); + + // Hide all but the first day header + jQuery(grid.getDOMNode()).toggleClass( + 'hideDayColHeader', + state.state.view == 'week' || state.state.view == 'day4' + ); + + // Determine the different end date & varying values + switch(state.state.view) + { + case 'month': + var end = state.state.last = view.end_date(state.state); + grid_count = Math.ceil((end - date.valueOf()) / (1000 * 60 * 60 * 24) / 7); + // fall through + case 'weekN': + for(var week = 0; week < grid_count; week++) + { + var val = { + id: CalendarApp._daywise_cache_id(date,state.state.owner), + start_date: date.toJSON(), + end_date: new Date(date.toJSON()), + owner: state.state.owner + }; + val.end_date.setUTCHours(24*7-1); + val.end_date.setUTCMinutes(59); + val.end_date.setUTCSeconds(59); + val.end_date = val.end_date.toJSON(); + value.push(val); + date.setUTCHours(24*7); + } + state.state.last=val.end_date; + break; + case 'day': + var end = state.state.last = view.end_date(state.state).toJSON(); + value.push({ + id: CalendarApp._daywise_cache_id(date,state.state.owner), + start_date: state.state.first, + end_date: state.state.last, + owner: view.owner(state.state) + }); + break; + default: + var end = state.state.last = view.end_date(state.state).toJSON(); + for(let owner = 0; owner < grid_count && owner < state.state.owner.length; owner++) + { + var _owner = grid_count > 1 ? state.state.owner[owner] || 0 : state.state.owner; + value.push({ + id: CalendarApp._daywise_cache_id(date,_owner), + start_date: date, + end_date: end, + owner: _owner + }); + } + break; + } + // If we have cached data for the timespan, pass it along + // Single day with multiple owners still needs owners split to satisfy + // caching keys, otherwise they'll fetch & cache consolidated + if(state.state.view == 'day' && state.state.owner.length < parseInt(''+this.egw.preference('day_consolidate','calendar'))) + { + var day_value = []; + for(var i = 0; i < state.state.owner.length; i++) + { + day_value.push({ + start_date: state.state.first, + end_date: state.state.last, + owner: state.state.owner[i] + }); + } + loading = this._need_data(day_value,state.state); + } + else + { + loading = this._need_data(value,state.state); + } + + var row_index = 0; + + // Find any matching, existing rows - they can be kept + grid.iterateOver(function(widget) { + for(var i = 0; i < value.length; i++) + { + if(widget.id == value[i].id) + { + // Keep it, but move it + if(i > row_index) + { + for(var j = i-row_index; j > 0; j--) + { + // Move from the end to the start + grid._children.unshift(grid._children.pop()); + + // Swap DOM nodes + var a = grid._children[0].getDOMNode().parentNode.parentNode; + let a_scroll = jQuery('.calendar_calTimeGridScroll',a).scrollTop(); + var b = grid._children[1].getDOMNode().parentNode.parentNode; + a.parentNode.insertBefore(a,b); + + // Moving nodes changes scrolling, so set it back + jQuery('.calendar_calTimeGridScroll',a).scrollTop(a_scroll); + } + } + else if (row_index > i) + { + // Swap DOM nodes + var a = grid._children[row_index].getDOMNode().parentNode.parentNode; + let a_scroll = jQuery('.calendar_calTimeGridScroll',a).scrollTop(); + var b = grid._children[i].getDOMNode().parentNode.parentNode; + + // Simple scroll forward, put top on the bottom + // This makes it faster if they scroll back next + if(i==0 && row_index == 1) + { + jQuery(b).appendTo(b.parentNode); + grid._children.push(grid._children.shift()); + } + else + { + grid._children.splice(i,0,widget); + grid._children.splice(row_index+1,1); + a.parentNode.insertBefore(a,b); + } + + // Moving nodes changes scrolling, so set it back + jQuery('.calendar_calTimeGridScroll',a).scrollTop(a_scroll); + } + break; + } + } + row_index++; + },this,et2_calendar_view); + row_index = 0; + + // Set rows that need it + var was_disabled = []; + grid.iterateOver(function(widget) { + was_disabled[row_index] = false; + if(row_index < value.length) + { + was_disabled[row_index] = widget.options.disabled; + widget.set_disabled(false); + } + else + { + widget.set_disabled(true); + } + row_index++; + },this, et2_calendar_view); + row_index = 0; + grid.iterateOver(function(widget) { + if(row_index >= value.length) return; + if(widget.set_show_weekend) + { + widget.set_show_weekend(view.show_weekend(state.state)); + } + if(widget.set_granularity) + { + if(widget.loader) widget.loader.show(); + widget.set_granularity(view.granularity(state.state)); + } + if(widget.id == value[row_index].id && + widget.get_end_date().getUTCFullYear() == value[row_index].end_date.substring(0,4) && + widget.get_end_date().getUTCMonth()+1 == value[row_index].end_date.substring(5,7) && + widget.get_end_date().getUTCDate() == value[row_index].end_date.substring(8,10) + ) + { + // Do not need to re-set this row, but we do need to re-do + // the times, as they may have changed + widget.resizeTimes(); + window.setTimeout(jQuery.proxy(widget.set_header_classes, widget),0); + + // If disabled while the daycols were loaded, they won't load their events + for(var day = 0; was_disabled[row_index] && day < widget.day_widgets.length; day++) + { + egw.dataStoreUID( + widget.day_widgets[day].registeredUID, + egw.dataGetUIDdata(widget.day_widgets[day].registeredUID).data + ); + } + widget.set_owner(value[row_index].owner); + + // Hide loader + widget.loader.hide(); + row_index++; + return; + } + if(widget.set_value) + { + widget.set_value(value[row_index++]); + } + },this, et2_calendar_view); + } + else if(state.state.view !== 'listview') + { + // Simple, easy case - just one widget for the selected time span. (planner) + // Update existing view's special attribute filters, defined in the view list + for(var updater in view) + { + if(typeof view[updater] === 'function') + { + let value = view[updater].call(this,state.state); + if(updater === 'start_date') state.state.first = this.date.toString(value); + if(updater === 'end_date') state.state.last = this.date.toString(value); + + // Set value + for(var i = 0; i < view.etemplates.length; i++) + { + view.etemplates[i].widgetContainer.iterateOver(function(widget) { + if(typeof widget['set_'+updater] === 'function') + { + widget['set_'+updater](value); + } + }, this, et2_calendar_view); + } + } + } + let value = [{start_date: state.state.first, end_date: state.state.last}]; + loading = this._need_data(value,state.state); + } + // Include first & last dates in state, mostly for server side processing + if(state.state.first && state.state.first.toJSON) state.state.first = state.state.first.toJSON(); + if(state.state.last && state.state.last.toJSON) state.state.last = state.state.last.toJSON(); + + // Toggle todos + if((state.state.view == 'day' || this.state.view == 'day') && jQuery(view.etemplates[0].DOMContainer).is(':visible')) + { + if(state.state.view == 'day' && state.state.owner.length === 1 && !isNaN(state.state.owner) && state.state.owner[0] >= 0 && !egwIsMobile()) + { + // Set width to 70%, otherwise if a scrollbar is needed for the view, it will conflict with the todo list + jQuery((CalendarApp.views.day.etemplates[0]).DOMContainer).css("width","70%"); + jQuery(view.etemplates[1].DOMContainer).css({"left":"70%", "height":(jQuery(framework.tabsUi.activeTab.contentDiv).height()-30)+'px'}); + // TODO: Maybe some caching here + this.egw.jsonq('calendar_uiviews::ajax_get_todos', [state.state.date, state.state.owner[0]], function(data) { + this.getWidgetById('label').set_value(data.label||''); + this.getWidgetById('todos').set_value({content:data.todos||''}); + },view.etemplates[1].widgetContainer); + view.etemplates[0].resize(); + } + else + { + jQuery((CalendarApp.views.day.etemplates[1]).DOMContainer).css("left","100%"); + jQuery((CalendarApp.views.day.etemplates[1]).DOMContainer).hide(); + jQuery((CalendarApp.views.day.etemplates[0]).DOMContainer).css("width","100%"); + view.etemplates[0].widgetContainer.iterateOver(function(w) { + w.set_width('100%'); + },this,et2_calendar_timegrid); + } + } + else if(jQuery(view.etemplates[0].DOMContainer).is(':visible')) + { + jQuery(view.etemplates[0].DOMContainer).css("width",""); + view.etemplates[0].widgetContainer.iterateOver(function(w) { + w.set_width('100%'); + },this,et2_calendar_timegrid); + } + + // List view (nextmatch) has slightly different fields + if(state.state.view === 'listview') + { + state.state.startdate = state.state.date; + if(state.state.startdate.toJSON) + { + state.state.startdate = state.state.startdate.toJSON(); + } + + if(state.state.end_date) + { + state.state.enddate = state.state.end_date; + } + if(state.state.enddate && state.state.enddate.toJSON) + { + state.state.enddate = state.state.enddate.toJSON(); + } + state.state.col_filter = {participant: state.state.owner}; + state.state.search = state.state.keywords ? state.state.keywords : state.state.search; + delete state.state.keywords; + + var nm = view.etemplates[0].widgetContainer.getWidgetById('nm'); + + // 'Custom' filter needs an end date + if(nm.activeFilters.filter === 'custom' && !state.state.end_date) + { + state.state.enddate = state.state.last; + } + if(state.state.enddate && state.state.startdate && state.state.startdate > state.state.enddate) + { + state.state.enddate = state.state.startdate; + } + nm.applyFilters(state.state); + + // Try to keep last value up to date with what's in nextmatch + if(nm.activeFilters.enddate) + { + this.state.last = nm.activeFilters.enddate; + } + // Updates the display of start & end date + this.filter_change(); + } + else + { + // Turn off nextmatch's automatic stuff - it won't work while it + // is hidden, and can cause an infinite loop as it tries to layout. + // (It will automatically re-start when shown) + try + { + var nm = (CalendarApp.views.listview.etemplates[0]).widgetContainer.getWidgetById('nm'); + nm.controller._grid.doInvalidate = false; + } catch (e) {} + // Other views do not search + delete state.state.keywords; + } + this.state = jQuery.extend({},state.state); + + /* Update re-orderable calendars */ + this._sortable(); + + /* Update sidebox widgets to show current value*/ + if(this.sidebox_hooked_templates.length) + { + for(var j = 0; j < this.sidebox_hooked_templates.length; j++) + { + var sidebox = this.sidebox_hooked_templates[j]; + // Remove any destroyed or not valid templates + if(!sidebox.getInstanceManager || !sidebox.getInstanceManager()) + { + this.sidebox_hooked_templates.splice(j,1,0); + continue; + } + sidebox.iterateOver(function(widget) { + if(widget.id == 'view') + { + // View widget has a list of state settings, which require special handling + for(var i = 0; i < widget.options.select_options.length; i++) + { + var option_state = JSON.parse(widget.options.select_options[i].value) || []; + var match = true; + for(var os_key in option_state) + { + // Sometimes an optional state variable is not yet defined (sortby, days, etc) + match = match && (option_state[os_key] == this.state[os_key] || typeof this.state[os_key] == 'undefined'); + } + if(match) + { + widget.set_value(widget.options.select_options[i].value); + return; + } + } + } + else if (widget.id == 'keywords') + { + widget.set_value(''); + } + else if(typeof state.state[widget.id] !== 'undefined' && state.state[widget.id] != widget.getValue()) + { + // Update widget. This may trigger an infinite loop of + // updates, so we do it after changing this.state and set a flag + try + { + widget.set_value(state.state[widget.id]); + } + catch(e) + { + widget.set_value(''); + } + } + else if (widget.instanceOf(et2_inputWidget) && typeof state.state[widget.id] == 'undefined') + { + // No value, clear it + widget.set_value(''); + } + },this,et2_valueWidget); + } + } + + // If current state matches a favorite, hightlight it + this.highlight_favorite(); + + // Update app header + this.set_app_header(view.header(state.state)); + + // Reset auto-refresh timer + this._set_autorefresh(); + + // Sidebox is updated, we can clear the flag + this.state_update_in_progress = false; + + // Update saved state in preferences + var save = {}; + for(var i = 0; i < CalendarApp.states_to_save.length; i++) + { + save[CalendarApp.states_to_save[i]] = this.state[CalendarApp.states_to_save[i]]; + } + egw.set_preference('calendar','saved_states', save); + + // Trigger resize to get correct sizes, as they may have sized while + // hidden + for(var i = 0; i < view.etemplates.length; i++) + { + view.etemplates[i].resize(); + } + + // If we need to fetch data from the server, it will hide the loader + // when done but if everything is in the cache, hide from here. + if(!loading) + { + window.setTimeout(jQuery.proxy(function() { + + egw.loading_prompt(this.appname,false); + },this),500); + } + + return; + } + // old calendar state handling on server-side (incl. switching to and from listview) + var menuaction = 'calendar.calendar_uiviews.index'; + if (typeof state.state != 'undefined' && (typeof state.state.view == 'undefined' || state.state.view == 'listview')) + { + if (state.name) + { + // 'blank' is the special name for no filters, send that instead of the nice translated name + state.state.favorite = jQuery.isEmptyObject(state) || jQuery.isEmptyObject(state.state||state.filter) ? 'blank' : state.name.replace(/[^A-Za-z0-9-_]/g, '_'); + // set date for "No Filter" (blank) favorite to todays date + if (state.state.favorite == 'blank') + state.state.date = jQuery.datepicker.formatDate('yymmdd', new Date); + } + menuaction = 'calendar.calendar_uilist.listview'; + state.state.ajax = 'true'; + // check if we already use et2 / are in listview + if (this.et2 || etemplate2 && etemplate2.getByApplication('calendar')) + { + // current calendar-code can set regular calendar states only via a server-request :( + // --> check if we only need to set something which can be handeled by nm internally + // or we need a redirect + // ToDo: pass them via nm's get_rows call to server (eg. by passing state), so we dont need a redirect + var current_state = this.getState(); + var need_redirect = false; + for(var attr in current_state) + { + switch(attr) + { + case 'cat_id': + case 'owner': + case 'filter': + if (state.state[attr] != current_state[attr]) + { + need_redirect = true; + // reset of attributes managed on server-side + if (state.state.favorite === 'blank') + { + switch(attr) + { + case 'cat_id': + state.state.cat_id = 0; + break; + case 'owner': + state.state.owner = egw.user('account_id'); + break; + case 'filter': + state.state.filter = 'default'; + break; + } + } + break; + } + break; + + case 'view': + // "No filter" (blank) favorite: if not in listview --> stay in that view + if (state.state.favorite === 'blank' && current_state.view != 'listview') + { + menuaction = 'calendar.calendar_uiviews.index'; + delete state.state.ajax; + need_redirect = true; + } + } + } + if (!need_redirect) + { + return super.setState([state]); + } + } + } + // setting internal state now, that linkHandler does not intercept switching from listview to any old view + this.state = jQuery.extend({},state.state); + if(this.sidebox_et2) + { + jQuery(this.sidebox_et2.getInstanceManager().DOMContainer).show(); + } + + var query = jQuery.extend({menuaction: menuaction},state.state||{}); + + // prepend an owner 0, to reset all owners and not just set given resource type + if(typeof query.owner != 'undefined') + { + query.owner = '0,'+ (typeof query.owner == 'object' ? query.owner.join(',') : (''+query.owner).replace('0,','')); + } + + this.egw.open_link(this.egw.link('/index.php',query), 'calendar'); + + // Stop the normal bubbling if this is called on click + return false; + } + + /** + * Check to see if any of the selected is an event widget + * Used to separate grid actions from event actions + * + * @param {egwAction} _action + * @param {egwActioObject[]} _selected + * @returns {boolean} Is any of the selected an event widget + */ + is_event(_action, _selected) + { + var is_widget = false; + for(var i = 0; i < _selected.length; i++) + { + if(_selected[i].iface.getWidget() && _selected[i].iface.getWidget().instanceOf(et2_calendar_event)) + { + is_widget = true; + } + + // Also check classes, usually indicating permission + if(_action.data && _action.data.enableClass) + { + is_widget = is_widget && (jQuery( _selected[i].iface.getDOMNode()).hasClass(_action.data.enableClass)); + } + if(_action.data && _action.data.disableClass) + { + is_widget = is_widget && !(jQuery( _selected[i].iface.getDOMNode()).hasClass(_action.data.disableClass)); + } + + } + return is_widget; + } + + /** + * Enable/Disable custom Date-time for set Alarm + * + * @param {widget object} _widget new_alarm[options] selectbox + */ + alarm_custom_date (_widget?) + { + var alarm_date = this.et2.getWidgetById('new_alarm[date]'); + var alarm_options = _widget || this.et2.getWidgetById('new_alarm[options]'); + var start = this.et2.getWidgetById('start'); + + if (alarm_date && alarm_options + && start) + { + if (alarm_options.get_value() != '0') + { + alarm_date.set_class('calendar_alarm_date_display'); + } + else + { + alarm_date.set_class(''); + } + var startDate = typeof start.get_value != 'undefined'?start.get_value():start.value; + if (startDate) + { + var date = new Date(startDate); + date.setTime(date.getTime() - 1000 * parseInt(alarm_options.get_value())); + alarm_date.set_value(date); + } + } + } + + /** + * Set alarm options based on WD/Regular event user preferences + * Gets fired by wholeday checkbox. This is mainly for display purposes, + * the default alarm is calculated on the server as well. + * + * @param {egw object} _egw + * @param {widget object} _widget whole_day checkbox + */ + set_alarmOptions_WD (_egw,_widget) + { + var alarm = this.et2.getWidgetById('alarm'); + if (!alarm) return; // no default alarm + var content = this.et2.getArrayMgr('content').data; + var start = this.et2.getWidgetById('start'); + var self= this; + var time = alarm.cells[1][0].widget; + var event = alarm.cells[1][1].widget; + // Convert a seconds of time to a translated label + var _secs_to_label = function (_secs) + { + var label=''; + if (_secs < 3600) + { + label = self.egw.lang('%1 minutes', _secs/60); + } + else if(_secs < 86400) + { + label = self.egw.lang('%1 hours', _secs/3600); + } + else + { + label = self.egw.lang('%1 days', _secs/(3600*24)); + } + return label; + }; + if (typeof content['alarm'][1]['default'] == 'undefined') + { + // user deleted alarm --> nothing to do + } + else + { + var def_alarm = this.egw.preference(_widget.get_value() === "true" ? + 'default-alarm-wholeday' : 'default-alarm', 'calendar'); + if (!def_alarm && def_alarm !== 0) // no alarm + { + jQuery('#calendar-edit_alarm > tbody :nth-child(1)').hide(); + } + else + { + jQuery('#calendar-edit_alarm > tbody :nth-child(1)').show(); + start.set_hours(0); + start.set_minutes(0); + time.set_value(start.get_value()); + time.set_value(new Date(new Date(start.get_value()).valueOf() - (60*def_alarm*1000)).toJSON()); + event.set_value(_secs_to_label(60 * def_alarm)); + } + } + } + + + /** + * Clear all calendar data from egw.data cache + */ + _clear_cache( ) + { + // Full refresh, clear the caches + var events = egw.dataKnownUIDs('calendar'); + for(var i = 0; i < events.length; i++) + { + egw.dataDeleteUID('calendar::' + events[i]); + } + var daywise = egw.dataKnownUIDs(CalendarApp.DAYWISE_CACHE_ID); + for(var i = 0; i < daywise.length; i++) + { + // Empty to clear existing widgets + egw.dataStoreUID(CalendarApp.DAYWISE_CACHE_ID + '::' + daywise[i], null); + } + } + + /** + * Take the date range(s) in the value and decide if we need to fetch data + * for the date ranges, or if they're already cached fill them in. + * + * @param {Object} value + * @param {Object} state + * + * @return {boolean} Data was requested + */ + _need_data(value, state) + { + var need_data = false; + + // Determine if we're showing multiple owners seperate or consolidated + var seperate_owners = false; + var last_owner = value.length ? value[0].owner || 0 : 0; + for(var i = 0; i < value.length && !seperate_owners; i++) + { + seperate_owners = seperate_owners || (last_owner !== value[i].owner); + } + + for(var i = 0; i < value.length; i++) + { + var t = new Date(value[i].start_date); + var end = new Date(value[i].end_date); + do + { + // Cache is by date (and owner, if seperate) + var date = t.getUTCFullYear() + sprintf('%02d',t.getUTCMonth()+1) + sprintf('%02d',t.getUTCDate()); + var cache_id = CalendarApp._daywise_cache_id(date, seperate_owners && value[i].owner ? value[i].owner : state.owner||false); + + if(egw.dataHasUID(cache_id)) + { + var c = egw.dataGetUIDdata(cache_id); + if(c.data && c.data !== null) + { + // There is data, pass it along now + value[i][date] = []; + for(var j = 0; j < c.data.length; j++) + { + if(egw.dataHasUID('calendar::'+c.data[j])) + { + value[i][date].push(egw.dataGetUIDdata('calendar::'+c.data[j]).data); + } + else + { + need_data = true; + } + } + } + else + { + need_data = true; + // Assume it's empty, if there is data it will be filled later + egw.dataStoreUID(cache_id, []); + } + } + else + { + need_data = true; + // Assume it's empty, if there is data it will be filled later + egw.dataStoreUID(cache_id, []); + } + t.setUTCDate(t.getUTCDate() + 1); + } + while(t < end); + + // Some data is missing for the current owner, go get it + if(need_data && seperate_owners) + { + this._fetch_data( + jQuery.extend({}, state, {owner: value[i].owner, selected_owners: state.owner}), + this.sidebox_et2 ? null : this.et2.getInstanceManager() + ); + need_data = false; + } + } + + // Some data was missing, go get it + if(need_data && !seperate_owners) + { + this._fetch_data( + state, + this.sidebox_et2 ? null : this.et2.getInstanceManager() + ); + } + + return need_data; + } + + /** + * Use the egw.data system to get data from the calendar list for the + * selected time span. + * + * As long as the other filters are the same (category, owner, status) we + * cache the data. + * + * @param {Object} state + * @param {etemplate2} [instance] If the full calendar app isn't loaded + * (home app), pass a different instance to use it to get the data + * @param {number} [start] Result offset. Internal use only + */ + _fetch_data(state, instance, start?) + { + if(!this.sidebox_et2 && !instance) + { + return; + } + + if(typeof start === 'undefined') + { + start = 0; + } + + // Category needs to be false if empty, not an empty array or string + var cat_id = state.cat_id ? state.cat_id : false; + if(cat_id && typeof cat_id.join != 'undefined') + { + if(cat_id.join('') == '') cat_id = false; + } + // Make sure cat_id reaches to server in array format + if (cat_id && typeof cat_id == 'string' && cat_id != "0") cat_id = cat_id.split(','); + + var query = jQuery.extend({}, { + get_rows: 'calendar.calendar_uilist.get_rows', + row_id:'row_id', + startdate:state.first || state.date, + enddate:state.last, + // Participant must be an array or it won't work + col_filter: {participant: (typeof state.owner == 'string' || typeof state.owner == 'number' ? [state.owner] : state.owner)}, + filter:'custom', // Must be custom to get start & end dates + status_filter: state.status_filter, + cat_id: cat_id, + csv_export: false, + selected_owners: state.selected_owners + }); + // Show ajax loader + if(typeof framework !== 'undefined') + { + framework.applications.calendar.sidemenuEntry.showAjaxLoader(); + } + + if(state.view === 'planner' && state.sortby === 'user') + { + query.order = 'participants'; + } + else if (state.view === 'planner' && state.sortby === 'category') + { + query.order = 'categories'; + } + + // Already in progress? + var query_string = JSON.stringify(query); + if(this._queries_in_progress.indexOf(query_string) != -1) + { + return; + } + this._queries_in_progress.push(query_string); + + this.egw.dataFetch( + instance ? instance.etemplate_exec_id : + this.sidebox_et2.getInstanceManager().etemplate_exec_id, + {start: start, num_rows:400}, + query, + this.appname, + function calendar_handleResponse(data) { + var idx = this._queries_in_progress.indexOf(query_string); + if(idx >= 0) + { + this._queries_in_progress.splice(idx,1); + } + //console.log(data); + + // Look for any updated select options + if(data.rows && data.rows.sel_options && this.sidebox_et2) + { + for(var field in data.rows.sel_options) + { + var widget = this.sidebox_et2.getWidgetById(field); + if(widget && widget.set_select_options) + { + // Merge in new, update label of existing + for(var i in data.rows.sel_options[field]) + { + var found = false; + var option = data.rows.sel_options[field][i]; + for(var j in widget.options.select_options) + { + if(option.value == widget.options.select_options[j].value) + { + widget.options.select_options[j].label = option.label; + found = true; + break; + } + } + if(!found) + { + if(!widget.options.select_options.push) + { + widget.options.select_options = []; + } + widget.options.select_options.push(option); + } + } + var in_progress = app.calendar.state_update_in_progress; + app.calendar.state_update_in_progress = true; + widget.set_select_options(widget.options.select_options); + widget.set_value(widget.getValue()); + + app.calendar.state_update_in_progress = in_progress; + } + } + } + + if(data.order && data.total) + { + this._update_events(state, data.order); + } + + // More rows? + if(data.order.length + start < data.total) + { + // Wait a bit, let UI do something. + window.setTimeout( function() { + app.calendar._fetch_data(state, instance, start + data.order.length); + }, 100); + } + // Hide AJAX loader + else if(typeof framework !== 'undefined') + { + framework.applications.calendar.sidemenuEntry.hideAjaxLoader(); + egw.loading_prompt('calendar',false) + + } + }, this,null + ); + } + + /** + * We have a list of calendar UIDs of events that need updating. + * + * The event data should already be in the egw.data cache, we just need to + * figure out where they need to go, and update the needed parent objects. + * + * Already existing events will have already been updated by egw.data + * callbacks. + * + * @param {Object} state Current state for update, used to determine what to update + * @param data + */ + _update_events( state, data) + { + var updated_days = {}; + + // Events can span for longer than we are showing + var first = new Date(state.first); + var last = new Date(state.last); + var bounds = { + first: ''+first.getUTCFullYear() + sprintf('%02d',first.getUTCMonth()+1) + sprintf('%02d',first.getUTCDate()), + last: ''+last.getUTCFullYear() + sprintf('%02d',last.getUTCMonth()+1) + sprintf('%02d',last.getUTCDate()) + }; + // Seperate owners, or consolidated? + var multiple_owner = typeof state.owner != 'string' && + state.owner.length > 1 && + (state.view == 'day' && state.owner.length < parseInt(''+this.egw.preference('day_consolidate','calendar')) || + ['week','day4'].indexOf(state.view) !== -1 && state.owner.length < parseInt(''+this.egw.preference('week_consolidate','calendar'))); + + + for(var i = 0; i < data.length; i++) + { + var record = this.egw.dataGetUIDdata(data[i]); + if(record && record.data) + { + if(typeof updated_days[record.data.date] === 'undefined') + { + // Check to make sure it's in range first, record.data.date is start date + // and could be before our start + if(record.data.date >= bounds.first && record.data.date <= bounds.last || + // Or it's for a day we already have + typeof this.egw.dataGetUIDdata('calendar_daywise::'+record.data.date) !== 'undefined' + ) + { + updated_days[record.data.date] = []; + } + } + if(typeof updated_days[record.data.date] != 'undefined') + { + // Copy, to avoid unwanted changes by reference + updated_days[record.data.date].push(record.data.row_id); + } + + // Check for multi-day events listed once + // Date must stay a string or we might cause problems with nextmatch + var dates = { + start: typeof record.data.start === 'string' ? record.data.start : record.data.start.toJSON(), + end: typeof record.data.end === 'string' ? record.data.end : record.data.end.toJSON() + }; + if(dates.start.substr(0,10) !== dates.end.substr(0,10)) + { + var end = new Date(Math.min(new Date(record.data.end).valueOf(), new Date(state.last).valueOf())); + end.setUTCHours(23); + end.setUTCMinutes(59); + end.setUTCSeconds(59); + var t = new Date(Math.max(new Date(record.data.start).valueOf(), new Date(state.first).valueOf())); + + do + { + var expanded_date = ''+t.getUTCFullYear() + sprintf('%02d',t.getUTCMonth()+1) + sprintf('%02d',t.getUTCDate()); + + // Avoid events ending at midnight having a 0 length event the next day + if(t.toJSON().substr(0,10) === dates.end.substr(0,10) && dates.end.substr(11,8) === '00:00:00') break; + + if(typeof(updated_days[expanded_date]) === 'undefined') + { + // Check to make sure it's in range first, expanded_date could be after our end + if(expanded_date >= bounds.first && expanded_date <= bounds.last) + { + updated_days[expanded_date] = []; + } + } + if(record.data.date !== expanded_date && typeof updated_days[expanded_date] !== 'undefined') + { + // Copy, to avoid unwanted changes by reference + updated_days[expanded_date].push(record.data.row_id); + } + t.setUTCDate(t.getUTCDate() + 1); + } + while(end >= t ) + } + } + } + + // Now we know which days changed, so we pass it on + for(var day in updated_days) + { + // Might be split by user, so we have to check that too + for(var i = 0; i < (typeof state.owner == 'object' ? state.owner.length : 1); i++) + { + var owner = multiple_owner ? state.owner[i] : state.owner; + var cache_id = CalendarApp._daywise_cache_id(day, owner); + if(egw.dataHasUID(cache_id)) + { + // Don't lose any existing data, just append + var c = egw.dataGetUIDdata(cache_id); + if(c.data && c.data !== null) + { + // Avoid duplicates + var data = c.data.concat(updated_days[day]).filter(function(value, index, self) { + return self.indexOf(value) === index; + }); + this.egw.dataStoreUID(cache_id,data); + } + } + else + { + this.egw.dataStoreUID(cache_id, updated_days[day]); + } + if(!multiple_owner) break; + } + } + + egw.loading_prompt(this.appname,false); + } + + /** + * Some handy date calculations + * All take either a Date object or full date with timestamp (Z) + */ + date = { + toString: function(date) : string + { + // Ensure consistent formatting using UTC, avoids problems with comparison + // and timezones + if(typeof date === 'string') date = new Date(date); + return date.getUTCFullYear() +'-'+ + sprintf("%02d",date.getUTCMonth()+1) + '-'+ + sprintf("%02d",date.getUTCDate()) + 'T'+ + sprintf("%02d",date.getUTCHours()) + ':'+ + sprintf("%02d",date.getUTCMinutes()) + ':'+ + sprintf("%02d",date.getUTCSeconds()) + 'Z'; + }, + + /** + * Formats one or two dates (range) as long date (full monthname), optionaly with a time + * + * Take care of any timezone issues before you pass the dates in. + * + * @param {Date} first first date + * @param {Date} last =0 last date for range, or false for a single date + * @param {boolean} display_time =false should a time be displayed too + * @param {boolean} display_day =false should a day-name prefix the date, eg. monday June 20, 2006 + * @return string with formatted date + */ + long_date: function(first, last, display_time, display_day) : string + { + if(!first) return ''; + if(typeof first === 'string') + { + first = new Date(first); + } + var first_format = new Date(first.valueOf() + first.getTimezoneOffset() * 60 * 1000); + + if(typeof last == 'string' && last) + { + last = new Date(last); + } + if(!last || typeof last !== 'object') + { + last = false; + } + if(last) + { + var last_format = new Date(last.valueOf() + last.getTimezoneOffset() * 60 * 1000); + } + + if(!display_time) display_time = false; + if(!display_day) display_day = false; + + var range = ''; + + var datefmt = egw.preference('dateformat'); + var timefmt = egw.preference('timeformat') === '12' ? 'h:i a' : 'H:i'; + + var month_before_day = datefmt[0].toLowerCase() == 'm' || + datefmt[2].toLowerCase() == 'm' && datefmt[4] == 'd'; + + if (display_day) + { + range = jQuery.datepicker.formatDate('DD',first_format)+(datefmt[0] != 'd' ? ' ' : ', '); + } + for (var i = 0; i < 5; i += 2) + { + switch(datefmt[i]) + { + case 'd': + range += first.getUTCDate()+ (datefmt[1] == '.' ? '.' : ''); + if (last && (first.getUTCMonth() != last.getUTCMonth() || first.getUTCFullYear() != last.getUTCFullYear())) + { + if (!month_before_day) + { + range += jQuery.datepicker.formatDate('MM',first_format); + } + if (first.getFullYear() != last.getFullYear() && datefmt[0] != 'Y') + { + range += (datefmt[0] != 'd' ? ', ' : ' ') + first.getFullYear(); + } + if (display_time) + { + range += ' '+jQuery.datepicker.formatDate(dateTimeFormat(timefmt),first_format); + } + if (!last) + { + return range; + } + range += ' - '; + + if (first.getFullYear() != last.getFullYear() && datefmt[0] == 'Y') + { + range += last.getUTCFullYear() + ', '; + } + + if (month_before_day) + { + range += jQuery.datepicker.formatDate('MM',last_format); + } + } + else if (last) + { + if (display_time) + { + range += ' '+jQuery.datepicker.formatDate(dateTimeFormat(timefmt),last_format); + } + if(last) + { + range += ' - '; + } + } + if(last) + { + range += ' ' + last.getUTCDate() + (datefmt[1] == '.' ? '.' : ''); + } + break; + case 'm': + case 'M': + range += ' '+jQuery.datepicker.formatDate('MM',month_before_day || !last ? first_format : last_format) + ' '; + break; + case 'Y': + if (datefmt[0] != 'm') + { + range += ' ' + (datefmt[0] == 'Y' ? first.getUTCFullYear()+(datefmt[2] == 'd' ? ', ' : ' ') : last.getUTCFullYear()+' '); + } + break; + } + } + if (display_time && last) + { + range += ' '+jQuery.datepicker.formatDate(dateTimeFormat(timefmt),last_format); + } + if (datefmt[4] == 'Y' && datefmt[0] == 'm') + { + range += ', ' + last.getUTCFullYear(); + } + return range; + }, + /** + * Calculate iso8601 week-number, which is defined for Monday as first day of week only + * + * We adjust the day, if user prefs want a different week-start-day + * + * @param {string|Date} _date + * @return string + */ + week_number: function(_date) + { + var d = new Date(_date); + var day = d.getUTCDay(); + + + // if week does not start Monday and date is Sunday --> add one day + if (egw.preference('weekdaystarts','calendar') != 'Monday' && !day) + { + d.setUTCDate(d.getUTCDate() + 1); + } + // if week does start Saturday and $time is Saturday --> add two days + else if (egw.preference('weekdaystarts','calendar') == 'Saturday' && day == 6) + { + d.setUTCDate(d.getUTCDate() + 2); + } + + return jQuery.datepicker.iso8601Week(new Date(d.valueOf() + d.getTimezoneOffset() * 60 * 1000)); + }, + start_of_week: function(date) + { + var d = new Date(date); + var day = d.getUTCDay(); + var diff = 0; + switch(egw.preference('weekdaystarts','calendar')) + { + case 'Saturday': + diff = day === 6 ? 0 : day === 0 ? -1 : -(day + 1); + break; + case 'Monday': + diff = day === 0 ? -6 : 1-day; + break; + case 'Sunday': + default: + diff = -day; + } + d.setUTCDate(d.getUTCDate() + diff); + return d; + }, + end_of_week: function(date) + { + var d = app.calendar.date.start_of_week(date); + d.setUTCDate(d.getUTCDate() + 6); + return d; + } + }; + + /** + * The sidebox filters use some non-standard and not-exposed options. They + * are set up here. + * + */ + _setup_sidebox_filters() + { + // Further date customizations + var date_widget = this.sidebox_et2.getWidgetById('date'); + if(date_widget) + { + // Dynamic resize of sidebox calendar to fill sidebox + var preferred_width = jQuery('#calendar-sidebox_date .ui-datepicker-inline').outerWidth(); + var font_ratio = 12 / parseFloat(jQuery('#calendar-sidebox_date .ui-datepicker-inline').css('font-size')); + var go_button_widget = date_widget.getRoot().getWidgetById('header_go'); + var auto_update = this.egw.preference('auto_update_on_sidebox_change', 'calendar') === '1'; + var calendar_resize = function() { + try { + var percent = 1+((jQuery(date_widget.getDOMNode()).width() - preferred_width) / preferred_width); + percent *= font_ratio; + jQuery('#calendar-sidebox_date .ui-datepicker-inline') + .css('font-size',(percent*100)+'%'); + + // Position go and today + go_button_widget.set_disabled(false); + var buttons = jQuery('#calendar-sidebox_date .ui-datepicker-header a span'); + if(today.length && go_button.length) + { + go_button.position({my: 'left+8px center', at: 'right center-1',of: jQuery('#calendar-sidebox_date .ui-datepicker-year')}); + buttons.position({my: 'center', at: 'center', of: go_button}) + .css('left', ''); + today.position({my: 'top', at: 'top', of: buttons}); + today.css({ + 'left': (buttons.first().offset().left + buttons.last().offset().left)/2 - Math.ceil(today.outerWidth()/2), + }); + } + if(auto_update) + { + go_button_widget.set_disabled(true); + } + } catch (e){ + // Resize didn't work + } + }; + + var datepicker = date_widget.input_date.datepicker("option", { + showButtonPanel: false, + onChangeMonthYear: function(year, month, inst) + { + // Update month button label + if(go_button_widget) + { + var temp_date = new Date(year, month-1, 1,0,0,0); + //temp_date.setUTCMinutes(temp_date.getUTCMinutes() + temp_date.getTimezoneOffset()); + go_button_widget.btn.attr('title',egw.lang(date('F',temp_date))); + + // Store current _displayed_ date in date button for clicking + temp_date.setUTCMinutes(temp_date.getUTCMinutes() - temp_date.getTimezoneOffset()); + go_button_widget.btn.attr('data-date', temp_date.toJSON()); + } + if(auto_update) + { + go_button_widget.click(); + } + window.setTimeout(calendar_resize,0); + }, + // Mark holidays + beforeShowDay: function (date) + { + var holidays = et2_calendar_view.get_holidays({day_class_holiday: function() {}}, date.getFullYear()); + var day_holidays = holidays[''+date.getFullYear() + + sprintf("%02d",date.getMonth()+1) + + sprintf("%02d",date.getDate())]; + var css_class = ''; + var tooltip = ''; + if(typeof day_holidays !== 'undefined' && day_holidays.length) + { + for(var i = 0; i < day_holidays.length; i++) + { + if (typeof day_holidays[i]['birthyear'] !== 'undefined') + { + css_class +='calendar_calBirthday '; + } + else + { + css_class += 'calendar_calHoliday '; + } + tooltip += day_holidays[i]['name'] + "\n"; + } + } + return [true, css_class, tooltip]; + } + }); + + // Clickable week numbers + date_widget.input_date.on('mouseenter','.ui-datepicker-week-col', function() { + jQuery(this).siblings().find('a').addClass('ui-state-hover'); + }) + .on('mouseleave','.ui-datepicker-week-col', function() { + jQuery(this).siblings().find('a').removeClass('ui-state-hover'); + }) + .on('click', '.ui-datepicker-week-col', function() { + var view = app.calendar.state.view; + var days = app.calendar.state.days; + + // Avoid a full state update, we just want the calendar to update + // Directly update to avoid change event from the sidebox calendar + var date = new Date(this.nextSibling.dataset.year,this.nextSibling.dataset.month,this.nextSibling.firstChild.textContent,0,0,0); + date.setUTCMinutes(date.getUTCMinutes() - date.getTimezoneOffset()); + date = app.calendar.date.toString(date); + + // Set to week view, if in one of the views where we change view + if(app.calendar.sidebox_changes_views.indexOf(view) >= 0) + { + app.calendar.update_state({view: 'week', date: date, days: days}); + } + else if (view == 'planner') + { + // Clicked a week, show just a week + app.calendar.update_state({date: date, planner_view: 'week'}); + } + else if (view == 'listview') + { + app.calendar.update_state({ + date: date, + end_date: app.calendar.date.toString(CalendarApp.views.week.end_date({date:date})), + filter: 'week' + }); + } + else + { + app.calendar.update_state({date: date}); + } + }); + + + // Set today button + var today = jQuery('#calendar-sidebox_header_today'); + today.attr('title',egw.lang('today')); + + // Set go button + var go_button_widget = date_widget.getRoot().getWidgetById('header_go'); + if(go_button_widget && go_button_widget.btn) + { + var go_button = go_button_widget.btn; + var temp_date = new Date(date_widget.get_value()); + temp_date.setUTCDate(1); + temp_date.setUTCMinutes(temp_date.getUTCMinutes() + temp_date.getTimezoneOffset()); + + go_button.attr('title', egw.lang(date('F',temp_date))); + // Store current _displayed_ date in date button for clicking + temp_date.setUTCMinutes(temp_date.getUTCMinutes() - temp_date.getTimezoneOffset()); + go_button.attr('data-date', temp_date.toJSON()); + + } + } + + jQuery(window).on('resize.calendar'+date_widget.dom_id,calendar_resize).trigger('resize'); + + // Avoid wrapping owner icons if user has group + search + var button = jQuery('#calendar-sidebox_owner ~ span.et2_clickable'); + if(button.length == 1) + { + button.parent().css('margin-right',button.outerWidth(true)+2); + button.parent().parent().css('white-space','nowrap'); + } + jQuery(window).on('resize.calendar-owner', function() { + var preferred_width = jQuery('#calendar-et2_target').children().first().outerWidth()||0; + if(app.calendar && app.calendar.sidebox_et2) + { + var owner = app.calendar.sidebox_et2.getWidgetById('owner'); + if(preferred_width && owner.input.hasClass("chzn-done")) + { + owner.input.next().css('width',preferred_width); + } + } + }); + window.setTimeout(calendar_resize,50); + } + + /** + * Record view templates so we can quickly switch between them. + * + * @param {etemplate2} _et2 etemplate2 template that was just loaded + * @param {String} _name Name of the template + */ + _et2_view_init(_et2, _name) + { + var hidden = typeof this.state.view !== 'undefined'; + var all_loaded = this.sidebox_et2 !== null; + + // Avoid home portlets using our templates, and get them right + if(_et2.uniqueId.indexOf('portlet') === 0) return; + if(_et2.uniqueId === 'calendar-add') return; + + // Flag to make sure we don't hide non-view templates + var view_et2 = false; + + for(var view in CalendarApp.views) + { + var index = CalendarApp.views[view].etemplates.indexOf(_name); + if(index > -1) + { + view_et2 = true; + CalendarApp.views[view].etemplates[index] = _et2; + // If a template disappears, we want to release it + jQuery(_et2.DOMContainer).one('clear',jQuery.proxy(function() { + this.view.etemplates[this.index] = _name; + },jQuery.extend({},{view: CalendarApp.views[view], index: ""+index, name: _name}))); + + if(this.state.view === view) + { + hidden = false; + } + } + CalendarApp.views[view].etemplates.forEach(function(et) {all_loaded = all_loaded && typeof et !== 'string';}); + } + + // Add some extras to the nextmatch so it can keep the dates in sync with + // those in the sidebox calendar. Care must be taken to not trigger any + // sort of refresh or update, as that may resulte in infinite loops so these + // are only used for the 'week' and 'month' filters, and we just update the + // date range + if(_name == 'calendar.list') + { + var nm = _et2.widgetContainer.getWidgetById('nm'); + if(nm) + { + // Avoid unwanted refresh immediately after load + nm.controller._grid.doInvalidate = false; + + // Preserve pre-set search + if(nm.activeFilters.search) + { + this.state.keywords = nm.activeFilters.search; + } + // Bind to keep search up to date + jQuery(nm.getWidgetById('search').getDOMNode()).on('change', function() { + app.calendar.state.search = jQuery('input',this).val(); + }); + nm.set_startdate = jQuery.proxy(function(date) { + this.state.first = this.date.toString(new Date(date)); + },this); + nm.set_enddate = jQuery.proxy(function(date) { + this.state.last = this.date.toString(new Date(date)); + },this); + } + } + + // Start hidden, except for current view + if(view_et2) + { + if(hidden) + { + jQuery(_et2.DOMContainer).hide(); + } + } + else + { + var app_name = _name.split('.')[0]; + if(app_name && app_name != 'calendar' && egw.app(app_name)) + { + // A template from another application? Keep it up to date as state changes + this.sidebox_hooked_templates.push(_et2.widgetContainer); + // If it leaves (or reloads) remove it + jQuery(_et2.DOMContainer).one('clear',jQuery.proxy(function() { + if(app.calendar) + { + app.calendar.sidebox_hooked_templates.splice(this,1,0); + } + },this.sidebox_hooked_templates.length -1)); + } + } + if(all_loaded) + { + jQuery(window).trigger('resize'); + this.setState({state:this.state}); + + // Hide loader after 1 second as a fallback, it will also be hidden + // after loading is complete. + window.setTimeout(jQuery.proxy(function() { + egw.loading_prompt(this.appname,false); + }, this),1000); + + // Start calendar-wide autorefresh timer to include more than just nm + this._set_autorefresh(); + } + } + + /** + * Set a refresh timer that works for the current view. + * The nextmatch goes into an infinite loop if we let it autorefresh while + * hidden. + */ + _set_autorefresh( ) + { + // Listview not loaded + if(typeof CalendarApp.views.listview.etemplates[0] == 'string') return; + + var nm = CalendarApp.views.listview.etemplates[0].widgetContainer.getWidgetById('nm'); + // nextmatch missing + if(!nm) return; + + var refresh_preference = "nextmatch-" + nm.options.settings.columnselection_pref + "-autorefresh"; + var time = this.egw.preference(refresh_preference, 'calendar'); + + if(this.state.view == 'listview' && time) + { + nm._set_autorefresh(time); + return; + } + else + { + window.clearInterval(nm._autorefresh_timer); + } + var self = this; + var refresh = function() { + // Deleted events are not coming properly, so clear it all + self._clear_cache(); + // Force redraw to current state + self.setState({state: self.state}); + + // This is a fast update, but misses deleted events + //app.calendar._fetch_data(app.calendar.state); + }; + + // Start / update timer + if (this._autorefresh_timer) + { + window.clearInterval(this._autorefresh_timer); + this._autorefresh_timer = null; + } + if(time > 0) + { + this._autorefresh_timer = setInterval(jQuery.proxy(refresh, this), time * 1000); + } + + // Bind to tab show/hide events, so that we don't bother refreshing in the background + jQuery(nm.getInstanceManager().DOMContainer.parentNode).on('hide.calendar', jQuery.proxy(function(e) { + // Stop + window.clearInterval(this._autorefresh_timer); + jQuery(e.target).off(e); + + if(!time) return; + + // If the autorefresh time is up, bind once to trigger a refresh + // (if needed) when tab is activated again + this._autorefresh_timer = setTimeout(jQuery.proxy(function() { + // Check in case it was stopped / destroyed since + if(!this._autorefresh_timer) return; + + jQuery(nm.getInstanceManager().DOMContainer.parentNode).one('show.calendar', + // Important to use anonymous function instead of just 'this.refresh' because + // of the parameters passed + jQuery.proxy(function() {refresh();},this) + ); + },this), time*1000); + },this)); + jQuery(nm.getInstanceManager().DOMContainer.parentNode).on('show.calendar', jQuery.proxy(function(e) { + // Start normal autorefresh timer again + this._set_autorefresh(this.egw.preference(refresh_preference, 'calendar')); + jQuery(e.target).off(e); + },this)); + } + + /** + * Initialization function in order to set/unset + * categories status. + * + */ + category_report_init () + { + var content = this.et2.getArrayMgr('content').data; + for (var i=1;i'+jQuery.datepicker.formatDate('DD',formatDate)+ - ''+jQuery.datepicker.formatDate('D',formatDate)+''+ - jQuery.datepicker.formatDate('d',formatDate)); - } - this.title - .attr("data-date", new_date) - .toggleClass('et2_label', !!this.options.label); - this.header - .attr('data-date',new_date) - .attr('data-whole_day',true); - - // Avoid redrawing if date is the same - if(new_date === this.options.date && - this.display_settings.granularity === this._parent.options.granularity && - !force_redraw - ) - { - return; - } - - var cache_id = app.classes.calendar._daywise_cache_id(new_date,this.options.owner); - if(this.options.date && this.registeredUID && - cache_id !== this.registeredUID) - { - egw.dataUnregisterUID(this.registeredUID,false,this); - - // Remove existing events - while(this._children.length > 0) - { - var node = this._children[this._children.length-1]; - this.removeChild(node); - node.free(); - } - } - - this.options.date = new_date; - - // Set holiday and today classes - this.day_class_holiday(); - - // Update all the little boxes - this._draw(); - - - // Register for updates on events for this day - if(this.registeredUID !== cache_id) - { - this.registeredUID = cache_id; - egw.dataRegisterUID(this.registeredUID, this._data_callback,this,this.getInstanceManager().execId,this.id); - } - }, - - /** - * Set the owner of this day - * - * @param {number|number[]|string|string[]} _owner - Owner ID, which can - * be an account ID, a resource ID (as defined in calendar_bo, not - * necessarily an entry from the resource app), or a list containing a - * combination of both. - */ - set_owner: function(_owner) { - - this.title - .attr("data-owner", _owner); - this.header.attr('data-owner',_owner); - this.div.attr('data-owner',_owner); - - // Simple comparison, both numbers - if(_owner === this.options.owner) return; - - // More complicated comparison, one or the other is an array - if((typeof _owner == 'object' || typeof this.options.owner == 'object') && - _owner.toString() == this.options.owner.toString()) - { - return; - } - - this.options.owner = typeof _owner !== 'object' ? [_owner] : _owner; - - var cache_id = app.classes.calendar._daywise_cache_id(this.options.date,_owner); - if(this.options.date && this.registeredUID && - cache_id !== this.registeredUID) - { - egw.dataUnregisterUID(this.registeredUID,false,this); - } - - if(this.registeredUID !== cache_id) - { - this.registeredUID = cache_id; - egw.dataRegisterUID(this.registeredUID, this._data_callback,this,this.getInstanceManager().execId,this.id); - } - }, - - set_class: function(classnames) { - this.header.removeClass(this.class); - this._super.apply(this, arguments); - this.header.addClass(classnames); - }, - - /** - * Callback used when the daywise data changes - * - * Events should update themselves when their data changes, here we are - * dealing with a change in which events are displayed on this day. - * - * @param {String[]} event_ids - * @returns {undefined} - */ - _data_callback: function(event_ids) { - var events = []; - if(event_ids == null || typeof event_ids.length == 'undefined') event_ids = []; - for(var i = 0; i < event_ids.length; i++) - { - var event = egw.dataGetUIDdata('calendar::'+event_ids[i]); - event = event && event.data || false; - if(event && event.date && et2_calendar_event.owner_check(event, this) && ( - event.date === this.options.date || - // Accept multi-day events - new Date(event.start) <= this.date //&& new Date(event.end) >= this.date - )) - { - events.push(event); - } - else if (event) - { - // Got an ID that doesn't belong - event_ids.splice(i--,1); - } - } - if(!this._parent.disabled) - this._update_events(events); - }, - - set_label: function(label) { - this.options.label = label; - this.title.text(label); - this.title.toggleClass('et2_clickable et2_link',label === ''); - }, - set_left: function(left) { - if(this.div) - { - this.div.css('left',left); - } - }, - set_width: function(width) { - this.options.width = width; - - if(this.div) - { - this.div.outerWidth(this.options.width); - this.header.outerWidth(this.options.width); - } - }, - - /** - * Applies class for today, and any holidays for current day - */ - day_class_holiday: function() { - this.title - // Remove all special day classes - .removeClass('calendar_calToday calendar_calBirthday calendar_calHoliday') - // Except this one... - .addClass("et2_clickable et2_link"); - this.title.attr('data-holiday',''); - - // Set today class - note +1 when dealing with today, as months in JS are 0-11 - var today = new Date(); - today.setUTCMinutes(today.getUTCMinutes() - today.getTimezoneOffset()); - - this.title.toggleClass("calendar_calToday", this.options.date === ''+today.getUTCFullYear()+ - sprintf("%02d",today.getUTCMonth()+1)+ - sprintf("%02d",today.getUTCDate()) - ); - - // Holidays and birthdays - var holidays = et2_calendar_view.get_holidays(this,this.options.date.substring(0,4)); - var holiday_list = []; - var holiday_pref = (egw.preference('birthdays_as_events','calendar')||[]); - if(typeof holiday_pref === 'string') - { - holiday_pref = holiday_pref.split(','); - } - else - { - holiday_pref = jQuery.extend([], holiday_pref); - } - - // Show holidays as events on mobile or by preference - var holidays_as_events = egwIsMobile() || egw.preference('birthdays_as_events','calendar') === true || - holiday_pref.indexOf('holiday') >= 0; - - var birthdays_as_events = egwIsMobile() || holiday_pref.indexOf('birthday') >= 0; - - if(holidays && holidays[this.options.date]) - { - holidays = holidays[this.options.date]; - for(var i = 0; i < holidays.length; i++) - { - if (typeof holidays[i]['birthyear'] !== 'undefined') - { - // Show birthdays as events on mobile or by preference - if(birthdays_as_events) - { - // Create event - this._parent.date_helper.set_value(this.options.date.substring(0,4)+'-'+ - (this.options.date.substring(4,6))+'-'+this.options.date.substring(6,8)+ - 'T00:00:00Z'); - var event = et2_createWidget('calendar-event',{ - id:'event_'+holidays[i].name, - value: { - title: holidays[i].name, - whole_day: true, - whole_day_on_top: true, - start: new Date(this._parent.date_helper.get_value()), - end: this.options.date, - owner: this.options.owner, - participants: this.options.owner, - app: 'calendar', - class: 'calendar_calBirthday' - }, - readonly: true, - class: 'calendar_calBirthday' - },this); - event.doLoadingFinished(); - event._update(); - } - if (!egwIsMobile()) - { - //If the birthdays are already displayed as event, don't - //show them in the caption - this.title.addClass('calendar_calBirthday'); - holiday_list.push(holidays[i]['name']); - } - } - else - { - // Show holidays as events on mobile - if(holidays_as_events) - { - // Create event - this._parent.date_helper.set_value(this.options.date.substring(0,4)+'-'+ - (this.options.date.substring(4,6))+'-'+this.options.date.substring(6,8)+ - 'T00:00:00Z'); - var event = et2_createWidget('calendar-event',{ - id:'event_'+holidays[i].name, - value: { - title: holidays[i].name, - whole_day: true, - whole_day_on_top: true, - start: new Date(this._parent.date_helper.get_value()), - end: this.options.date, - owner: this.options.owner, - participants: this.options.owner, - app: 'calendar', - class: 'calendar_calHoliday' - }, - readonly: true, - class: 'calendar_calHoliday' - },this); - event.doLoadingFinished(); - event._update(); - } - else - { - this.title.addClass('calendar_calHoliday'); - this.title.attr('data-holiday', holidays[i]['name']); - - //If the birthdays are already displayed as event, don't - //show them in the caption - if (!this.options.display_holiday_as_event) - { - holiday_list.push(holidays[i]['name']); - } - } - } - } - } - this.title.attr('title', holiday_list.join(', ')); - }, - - /** - * Load the event data for this day and create event widgets for each. - * - * If event information is not provided, it will be pulled from the content array. - * - * @param {Object[]} [_events] Array of event information, one per event. - */ - _update_events: function(_events) - { - var events = _events || this.getArrayMgr('content').getEntry(this.options.date) || []; - - // Remove extra events - while(this._children.length > 0) - { - var node = this._children[this._children.length-1]; - this.removeChild(node); - node.free(); - } - - // Make sure children are in cronological order, or columns are backwards - events.sort(function(a,b) { - var start = new Date(a.start) - new Date(b.start); - var end = new Date(a.end) - new Date(b.end); - // Whole day events sorted by ID, normal events by start / end time - if(a.whole_day && b.whole_day) - { - return (a.app_id - b.app_id); - } - else if (a.whole_day || b.whole_day) - { - return a.whole_day ? -1 : 1; - } - return start ? start : end; - }); - - for(var c = 0; c < events.length; c++) - { - // Create event - var event = et2_createWidget('calendar-event',{ - id:'event_'+events[c].id, - value: events[c] - },this); - } - - // Seperate loop so column sorting finds all children in the right place - for(var c = 0; c < events.length && c < this._children.length; c++) - { - var event = this.getWidgetById('event_'+events[c].id); - if(!event) continue; - if(this.isInTree()) - { - event.doLoadingFinished(); - } - } - - - // Show holidays as events on mobile or by preference - if(egwIsMobile() || egw.preference('birthdays_as_events','calendar')) - { - this.day_class_holiday(); - } - - // Apply styles to hidden events - this._out_of_view(); - }, - - /** - * Apply styles for out-of-view and partially hidden events - * - * There are 3 different states or modes of display: - * - * - 'Normal' - When showing events positioned by time, the indicator is just - * a bar colored by the last category color. On hover it shows either the - * title of a single event or "x event(s)" if more than one are hidden. - * Clicking adjusts the current view to show the earliest / latest hidden - * event - * - * - Fixed - When showing events positioned by time but in a fixed-height - * week (not auto-sized to fit screen) the indicator is the same as sized. - * On hover it shows the titles of the hidden events, clicking changes - * the view to the selected day. - * - * - GridList - When showing just a list, the indicator shows "x event(s)", - * and on hover shows the category color, title & time. Clicking changes - * the view to the selected day, and opens the event for editing. - */ - _out_of_view: function() - { - // Reset - this.header.children('.hiddenEventBefore').remove(); - this.div.children('.hiddenEventAfter').remove(); - this.event_wrapper.css('overflow','visible'); - this.all_day.removeClass('overflown'); - jQuery('.calendar_calEventBody', this.div).css({'padding-top': '','margin-top':''}); - - var timegrid = this._parent; - - // elem is jquery div of event - function isHidden(elem) { - // Add an extra 5px top and bottom to include events just on the - // edge of visibility - var docViewTop = timegrid.scrolling.scrollTop() + 5, - docViewBottom = docViewTop + ( - this.display_settings.granularity === 0 ? - this.event_wrapper.height() : - timegrid.scrolling.height() - 10 - ), - elemTop = elem.position().top, - elemBottom = elemTop + elem.outerHeight(true); - if((elemBottom <= docViewBottom) && (elemTop >= docViewTop)) - { - // Entirely visible - return false; - } - var visible = { - hidden: elemTop > docViewTop ? 'bottom' : 'top', - completely: false - }; - visible.completely = visible.hidden == 'top' ? elemBottom < docViewTop : elemTop > docViewBottom; - return visible; - } - - // In gridlist view, we can quickly check if we need it at all - if(this.display_settings.granularity === 0 && this._children.length) - { - jQuery('div.calendar_calEvent',this.div).show(0); - if(Math.ceil(this.div.height() / this._children[0].div.height()) > this._children.length) - { - return; - } - } - // Check all day overflow - this.all_day.toggleClass('overflown', - this.all_day[0].scrollHeight - this.all_day.innerHeight() > 5 - ); - - // Check each event - this.iterateOver(function(event) { - // Skip whole day events and events missing value - if(this.display_settings.granularity && ( - (!event.options || !event.options.value || event.options.value.whole_day_on_top)) - ) - { - return; - } - // Reset - event.title.css({'top':'','background-color':''}); - event.body.css({'padding-top':'','margin-top':''}); - var hidden = isHidden.call(this,event.div); - var day = this; - if(!hidden) - { - return; - } - // Only top is hidden, move label - // Bottom hidden is fine - if(hidden.hidden === 'top' && !hidden.completely && !event.div.hasClass('calendar_calEventSmall')) - { - var title_height = event.title.outerHeight(); - event.title.css({ - 'top': timegrid.scrolling.scrollTop() - event.div.position().top, - 'background-color': 'transparent' - }); - event.body.css({ - 'padding-top': timegrid.scrolling.scrollTop() - event.div.position().top + title_height, - 'margin-top' : -title_height - }); - } - // Too many in gridlist view, show indicator - else if (this.display_settings.granularity === 0 && hidden) - { - if(jQuery('.hiddenEventAfter',this.div).length == 0) - { - this.event_wrapper.css('overflow','hidden'); - } - this._hidden_indicator(event, false, function() { - app.calendar.update_state({view: 'day', date: day.date}); - }); - // Avoid partially visible events - // We need to hide all, or the next row will be visible - event.div.hide(0); - } - // Completely out of view, show indicator - else if (hidden.completely) - { - this._hidden_indicator(event, hidden.hidden == 'top',false); - } - }, this, et2_calendar_event); - }, - - /** - * Show an indicator that there are hidden events - * - * The indicator works 3 different ways, depending on if the day can be - * scrolled, is fixed, or if in gridview. - * - * @see _out_of_view() - * - * @param {et2_calendar_event} event Event we're creating the indicator for - * @param {boolean} top Events hidden at the top (true) or bottom (false) - * @param {function} [onclick] Callback for when user clicks on the indicator - */ - _hidden_indicator: function _hidden_indicator(event, top, onclick) - { - var indicator = ''; - var day = this; - var timegrid = this._parent; - var fixed_height = timegrid.div.hasClass('calendar_calTimeGridFixed'); - - // Event is before the displayed times - if(top) - { - // Create if not already there - if(jQuery('.hiddenEventBefore',this.header).length === 0) - { - indicator = jQuery('
') - .appendTo(this.header) - .attr('data-hidden_count', 1); - if(!fixed_height) - { - indicator - .text(event.options.value.title) - .on('click', typeof onclick === 'function' ? onclick : function() { - jQuery('.calendar_calEvent',day.div).first()[0].scrollIntoView(); - return false; - }); - } - } - else - { - indicator = jQuery('.hiddenEventBefore',this.header); - indicator.attr('data-hidden_count', parseInt(indicator.attr('data-hidden_count')) + 1); - - if (!fixed_height) - { - indicator.text(day.egw().lang('%1 event(s) %2',indicator.attr('data-hidden_count'),'')); - } - } - } - // Event is after displayed times - else - { - indicator = jQuery('.hiddenEventAfter',this.div); - // Create if not already there - if(indicator.length === 0) - { - indicator = jQuery('
') - .attr('data-hidden_count', 0) - .appendTo(this.div); - if(!fixed_height) - { - indicator - .on('click', typeof onclick === 'function' ? onclick : function() { - jQuery('.calendar_calEvent',day.div).last()[0].scrollIntoView(false); - // Better re-run this to clean up - day._out_of_view(); - return false; - }); - } - else - { - indicator - .on('mouseover', function() { - indicator.css({ - 'height': (indicator.attr('data-hidden_count')*1.2) + 'em', - 'margin-top': -(indicator.attr('data-hidden_count')*1.2) + 'em' - }); - }) - .on('mouseout', function() { - indicator.css({ - 'height': '', - 'margin-top': '' - }); - }); - } - } - var count = parseInt(indicator.attr('data-hidden_count')) + 1; - indicator.attr('data-hidden_count', count); - if(this.display_settings.granularity === 0) - { - indicator.append(event.div.clone()); - indicator.attr('data-hidden_label', day.egw().lang('%1 event(s) %2',indicator.attr('data-hidden_count'),'')); - } - else if (!fixed_height) - { - indicator.text(day.egw().lang('%1 event(s) %2',indicator.attr('data-hidden_count'),'')); - } - indicator.css('top',timegrid.scrolling.height() + timegrid.scrolling.scrollTop()-indicator.innerHeight()); - } - // Show different stuff for fixed height - if(fixed_height) - { - indicator - .append("
"+ - event.options.value.title+ - "
" - ); - } - // Match color to the event - if(indicator !== '') - { - // Avoid white, which is hard to see - // Use border-bottom-color, Firefox doesn't give a value with border-color - var color = jQuery.Color(event.div.css('background-color')).toString() !== jQuery.Color('white').toString() ? - event.div.css('background-color') : event.div.css('border-bottom-color'); - if(color !== 'rgba(0, 0, 0, 0)') - { - indicator.css('border-color', color); - } - } - }, - - /** - * Sort a day's events into minimally overlapping columns - * - * @returns {Array[]} Events sorted into columns - */ - _spread_events: function() - { - if(!this.date) return []; - - var day_start = this.date.valueOf() / 1000; - var dst_check = new Date(this.date); - dst_check.setUTCHours(12); - - // if daylight saving is switched on or off, correct $day_start - // gives correct times after 2am, times between 0am and 2am are wrong - var daylight_diff = day_start + 12*60*60 - (dst_check.valueOf()/1000); - if(daylight_diff) - { - day_start -= daylight_diff; - } - - var eventCols = [], col_ends = []; - - // Make sure children are in cronological order, or columns are backwards - this._children.sort(function(a,b) { - var start = new Date(a.options.value.start) - new Date(b.options.value.start); - var end = new Date(a.options.value.end) - new Date(b.options.value.end); - // Whole day events sorted by ID, normal events by start / end time - if(a.options.value.whole_day && b.options.value.whole_day) - { - // Longer duration comes first so we have nicer bars across the top - var duration = - (new Date(b.options.value.end) - new Date(b.options.value.start)) - - (new Date(a.options.value.end) - new Date(a.options.value.start)); - - return duration ? duration : (a.options.value.app_id - b.options.value.app_id); - } - else if (a.options.value.whole_day || b.options.value.whole_day) - { - return a.options.value.whole_day ? -1 : 1; - } - return start ? start : end; - }); - - for(var i = 0; i < this._children.length; i++) - { - var event = this._children[i].options.value || false; - if(!event) continue; - if(event.date && event.date != this.options.date && - // Multi-day events date may be different - (new Date(event.start) >= this.date || new Date(event.end) < this.date ) - ) - { - // Still have a child event that has changed date (DnD) - this._children[i].destroy(); - this.removeChild(this._children[i]); - continue; - } - - var c = 0; - event['multiday'] = false; - if(typeof event.start !== 'object') - { - event.start = new Date(event.start); - } - if(typeof event.end !== 'object') - { - event.end = new Date(event.end); - } - event['start_m'] = (event.start.valueOf()/1000 - day_start) / 60 - if (event['start_m'] < 0) - { - event['start_m'] = 0; - event['multiday'] = true; - } - event['end_m'] = (event.end.valueOf()/1000 - day_start) / 60; - if (event['end_m'] >= 24*60) - { - event['end_m'] = 24*60-1; - event['multiday'] = true; - } - if(!event.start.getUTCHours() && !event.start.getUTCMinutes() && event.end.getUTCHours() == 23 && event.end.getUTCMinutes() == 59) - { - event.whole_day_on_top = (event.non_blocking && event.non_blocking != '0'); - } - if (!event['whole_day_on_top']) - { - for(c = 0; event['start_m'] < col_ends[c]; ++c); - col_ends[c] = event['end_m']; - } - if(typeof eventCols[c] === 'undefined') - { - eventCols[c] = []; - } - eventCols[c].push(this._children[i]); - } - return eventCols; - }, - - /** - * Position the event according to its time and how this widget is laid - * out. - * - * @param {et2_calendar_event} [event] - Event to be updated - * If a single event is not provided, all events are repositioned. - */ - position_event: function(event) - { - // If hidden, skip it - it takes too long - if(!this.div.is(':visible')) return; - - // Sort events into minimally-overlapping columns - var columns = this._spread_events(); - - for(var c = 0; c < columns.length; c++) - { - // Calculate horizontal positioning - var left = Math.ceil(5 + (1.5 * 100 / (parseFloat(this.options.width) || 100))); - var right = 2; - if (columns.length !== 1) - { - right = !c ? 30 : 2; - left += c * (100.0-left) / columns.length; - } - - for(var i = 0; (columns[c].indexOf(event) >= 0 || !event) && i < columns[c].length; i++) - { - // Calculate vertical positioning - var top = 0; - var height = 0; - // Position the event - if(this.display_settings.granularity === 0) - { - if(this.all_day.has(columns[c][i].div).length) - { - columns[c][i].div.prependTo(this.event_wrapper); - } - columns[c][i].div.css('top', ''); - columns[c][i].div.css('height', ''); - columns[c][i].div.css('left', ''); - columns[c][i].div.css('right', ''); - // Strip out of view padding - columns[c][i].body.css('padding-top',''); - continue; - } - if(columns[c][i].options.value.whole_day_on_top) - { - if(!this.all_day.has(columns[c][i].div).length) - { - columns[c][i].div.css('top', ''); - columns[c][i].div.css('height',''); - columns[c][i].div.css('left', ''); - columns[c][i].div.css('right', ''); - columns[c][i].body.css('padding-top',''); - columns[c][i].div - .appendTo(this.all_day); - this._parent.resizeTimes(); - } - continue; - } - else - { - if(this.all_day.has(columns[c][i].div).length) - { - columns[c][i].div.appendTo(this.event_wrapper); - this._parent.resizeTimes(); - } - top = this._time_to_position(columns[c][i].options.value.start_m); - height = this._time_to_position(columns[c][i].options.value.end_m)-top; - } - - // Position the event - if(event && columns[c].indexOf(event) >= 0 || !event) - { - columns[c][i].div.css('top', top+'%'); - columns[c][i].div.css('height', height+'%'); - // Remove spacing from border, but only if visible or the height will be wrong - if(columns[c][i].div.is(':visible')) - { - var border_diff = columns[c][i].div.outerHeight() - columns[c][i].div.height(); - columns[c][i].div.css('height','calc('+height+'% - ' +border_diff+')'); - } - // This gives the wrong height - //columns[c][i].div.outerHeight(height+'%'); - columns[c][i].div.css('left', left.toFixed(1)+'%'); - columns[c][i].div.css('right', right.toFixed(1)+'%'); - columns[c][i].div.css('z-index',parseInt(20)+c); - columns[c][i]._small_size(); - } - } - // Only wanted to position this event, leave the other columns alone - if(event && columns[c].indexOf(event) >= 0) - { - return; - } - } - }, - - /** - * Calculates the vertical position based on the time - * - * This calculation is a percentage from 00:00 to 23:59 - * - * @param {int} time in minutes from midnight - * @return {float} position in percent - */ - _time_to_position: function(time) - { - var pos = 0.0; - - // 24h - pos = ((time / 60) / 24) * 100; - - pos = pos.toFixed(1); - - return pos; - }, - - attachToDOM: function() - { - this._super.apply(this, arguments); - - // Remove the binding for the click handler, unless there's something - // custom here. - if (!this.onclick) - { - jQuery(this.node).off("click"); - } - // But we do want to listen to certain clicks, and handle them internally - jQuery(this.node).on('click.et2_daycol', - '.calendar_calDayColHeader,.calendar_calAddEvent', - jQuery.proxy(this.click, this) - ); - }, - - /** - * Click handler calling custom handler set via onclick attribute to this.onclick, - * or the default which is to open a new event at that time. - * - * Normally, you don't bind to this one, but the attribute is supported if you - * can get a reference to the widget. - * - * @param {Event} _ev - * @returns {boolean} - */ - click: function(_ev) - { - if(this._parent.options.readonly ) return; - - // Drag to create in progress - if(this._parent.drag_create.start !== null) return; - - // Click on the title - if (jQuery(_ev.target).hasClass('calendar_calAddEvent')) - { - if(this.header.has(_ev.target).length == 0 && !_ev.target.dataset.whole_day) - { - // Default handler to open a new event at the selected time - var options = { - date: _ev.target.dataset.date || this.options.date, - hour: _ev.target.dataset.hour || this._parent.options.day_start, - minute: _ev.target.dataset.minute || 0, - owner: this.options.owner - }; - app.calendar.add(options); - return false; - } - // Header, all day non-blocking - else if (this.header.has(_ev.target).length && !jQuery('.hiddenEventBefore',this.header).has(_ev.target).length || - this.header.is(_ev.target) - ) - { - // Click on the header, but not the title. That's an all-day non-blocking - var end = this.date.getFullYear() + '-' + (this.date.getUTCMonth()+1) + '-' + this.date.getUTCDate() + 'T23:59'; - var options = { - start: this.date.toJSON(), - end: end, - non_blocking: true, - owner: this.options.owner - } - app.calendar.add(options); - return false; - } - } - // Day label - else if(this.title.is(_ev.target) || this.title.has(_ev.target).length) - { - app.calendar.update_state({view: 'day',date: this.date.toJSON()}); - return false; - } - - }, - - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes: function(_attrs) { - - }, - - getDetachedNodes: function() { - return [this.getDOMNode()]; - }, - - setDetachedAttributes: function(_nodes, _values) { - - }, - - // Resizable interface - /** - * Resize - * - * Parent takes care of setting proper width & height for the containing div - * here we just need to adjust the events to fit the new size. - */ - resize: function () - { - if(this.disabled || !this.div.is(':visible') || this._parent.disabled) - { - return; - } - - if(this.display_settings.granularity !== this._parent.options.granularity) - { - // Layout has changed - this._draw(); - - // Resize & position all events - this.position_event(); - } - else - { - // Don't need to resize & reposition, just clear some stuff - // to reset for _out_of_view() - this.iterateOver(function(widget) { - widget._small_size(); - }, this, et2_calendar_event); - } - this._out_of_view(); - } -});}).call(this); - -et2_register_widget(et2_calendar_daycol, ["calendar-daycol"]); +var et2_calendar_daycol = /** @class */ (function (_super) { + __extends(et2_calendar_daycol, _super); + /** + * Constructor + */ + function et2_calendar_daycol(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_calendar_daycol._attributes, _child || {})) || this; + _this.registeredUID = null; + // Init to defaults, just in case - they will be updated from parent + _this.display_settings = { + wd_start: 60 * 9, + wd_end: 60 * 17, + granularity: 30, + rowsToDisplay: 10, + rowHeight: 20, + // Percentage; not yet available + titleHeight: 2.0 + }; + // Main container + _this.div = jQuery(document.createElement("div")) + .addClass("calendar_calDayCol") + .css('width', _this.options.width) + .css('left', _this.options.left); + _this.header = jQuery(document.createElement('div')) + .addClass("calendar_calDayColHeader") + .css('width', _this.options.width) + .css('left', _this.options.left); + _this.title = jQuery(document.createElement('div')) + .addClass('et2_clickable et2_link') + .appendTo(_this.header); + _this.user_spacer = jQuery(document.createElement('div')) + .addClass("calendar_calDayColHeader_spacer") + .appendTo(_this.header); + _this.all_day = jQuery(document.createElement('div')) + .addClass("calendar_calDayColAllDay") + .css('max-height', (egw.preference('limit_all_day_lines', 'calendar') || 3) * 1.4 + 'em') + .appendTo(_this.header); + _this.event_wrapper = jQuery(document.createElement('div')) + .addClass("event_wrapper") + .appendTo(_this.div); + _this.setDOMNode(_this.div[0]); + // Used for its date calculations - note this is a datetime, parent + // uses just a date + _this._date_helper = et2_createWidget('date-time', {}, null); + _this._date_helper.loadingFinished(); + return _this; + } + et2_calendar_daycol.prototype.doLoadingFinished = function () { + var result = _super.prototype.doLoadingFinished.call(this); + // Parent will have everything we need, just load it from there + if (this.getParent() && this.getParent().options.owner) { + this.set_owner(this.getParent().options.owner); + } + if (this.title.text() === '' && this.options.date && + this.getParent() && this.getParent().instanceOf(et2_widget_timegrid_1.et2_calendar_timegrid)) { + // Forces an update + var date_1 = this.options.date; + this.options.date = ''; + this.set_date(date_1); + } + return result; + }; + et2_calendar_daycol.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.div.off(); + this.header.off().remove(); + this.title.off(); + this.div = null; + this.header = null; + this.title = null; + this.user_spacer = null; + // date_helper has no parent, so we must explicitly remove it + this._date_helper.destroy(); + this._date_helper = null; + egw.dataUnregisterUID(this.registeredUID, null, this); + }; + et2_calendar_daycol.prototype.getDOMNode = function (sender) { + if (!sender || sender === this) + return this.div[0]; + if (sender.instanceOf && sender.instanceOf(et2_widget_event_1.et2_calendar_event)) { + if (this.display_settings.granularity === 0) { + return this.event_wrapper[0]; + } + if (sender.options.value.whole_day_on_top || + sender.options.value.whole_day && sender.options.value.non_blocking === true) { + return this.all_day[0]; + } + return this.div[0]; + } + }; + /** + * Draw the individual divs for clicking to add an event + */ + et2_calendar_daycol.prototype._draw = function () { + // Remove any existing + jQuery('.calendar_calAddEvent', this.div).remove(); + // Grab real values from parent + if (this.getParent() && this.getParent().instanceOf(et2_widget_timegrid_1.et2_calendar_timegrid)) { + this.display_settings.wd_start = 60 * this.getParent().options.day_start; + this.display_settings.wd_end = 60 * this.getParent().options.day_end; + this.display_settings.granularity = this.getParent().options.granularity; + var header = this.getParent().dayHeader.children(); + // Figure out insert index + var idx = 0; + var siblings = this.getParent().getDOMNode(this).childNodes; + while (idx < siblings.length && siblings[idx] != this.getDOMNode()) { + idx++; + } + // Stick header in the right place + if (idx == 0) { + this.getParent().dayHeader.prepend(this.header); + } + else if (header.length) { + header.eq(Math.min(header.length, idx) - 1).after(this.header); + } + } + this.div.attr('data-date', this.options.date); + }; + et2_calendar_daycol.prototype.getDate = function () { + return this.date; + }; + Object.defineProperty(et2_calendar_daycol.prototype, "date_helper", { + get: function () { + return this._date_helper; + }, + enumerable: true, + configurable: true + }); + /** + * Set the date + * + * @param {string|Date} _date New date + * @param {Object[]} events =false List of event data to be displayed, or false to + * automatically fetch data from content array + * @param {boolean} force_redraw =false Redraw even if the date is the same. + * Used for when new data is available. + */ + et2_calendar_daycol.prototype.set_date = function (_date, events, force_redraw) { + if (typeof events === 'undefined' || !events) { + events = false; + } + if (typeof force_redraw === 'undefined' || !force_redraw) { + force_redraw = false; + } + if (!this.getParent() || !this.getParent().date_helper) { + egw.debug('warn', 'Day col widget "' + this.id + '" is missing its parent.'); + return false; + } + if (typeof _date === "object") { + this.getParent().date_helper.set_value(_date); + } + else if (typeof _date === "string") { + // Need a new date to avoid invalid month/date combinations when setting + // month then day. Use a string to avoid browser timezone. + this.getParent().date_helper.set_value(_date.substring(0, 4) + '-' + (_date.substring(4, 6)) + '-' + _date.substring(6, 8) + 'T00:00:00Z'); + } + this.date = new Date(this.getParent().date_helper.getValue()); + // Keep internal option in Ymd format, it gets passed around in this format + var new_date = "" + this.getParent().date_helper.get_year() + + sprintf("%02d", this.getParent().date_helper.get_month()) + + sprintf("%02d", this.getParent().date_helper.get_date()); + // Set label + if (!this.options.label) { + // Add timezone offset back in, or formatDate will lose those hours + var formatDate = new Date(this.date.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); + this.title.html('' + jQuery.datepicker.formatDate('DD', formatDate) + + '' + jQuery.datepicker.formatDate('D', formatDate) + '' + + jQuery.datepicker.formatDate('d', formatDate)); + } + this.title + .attr("data-date", new_date) + .toggleClass('et2_label', !!this.options.label); + this.header + .attr('data-date', new_date) + .attr('data-whole_day', true); + // Avoid redrawing if date is the same + if (new_date === this.options.date && + this.display_settings.granularity === this.getParent().options.granularity && + !force_redraw) { + return; + } + var cache_id = CalendarApp._daywise_cache_id(new_date, this.options.owner); + if (this.options.date && this.registeredUID && + cache_id !== this.registeredUID) { + egw.dataUnregisterUID(this.registeredUID, null, this); + // Remove existing events + while (this._children.length > 0) { + var node = this._children[this._children.length - 1]; + this.removeChild(node); + node.destroy(); + } + } + this.options.date = new_date; + // Set holiday and today classes + this.day_class_holiday(); + // Update all the little boxes + this._draw(); + // Register for updates on events for this day + if (this.registeredUID !== cache_id) { + this.registeredUID = cache_id; + egw.dataRegisterUID(this.registeredUID, this._data_callback, this, this.getInstanceManager().execId, this.id); + } + }; + /** + * Set the owner of this day + * + * @param {number|number[]|string|string[]} _owner - Owner ID, which can + * be an account ID, a resource ID (as defined in calendar_bo, not + * necessarily an entry from the resource app), or a list containing a + * combination of both. + */ + et2_calendar_daycol.prototype.set_owner = function (_owner) { + this.title + .attr("data-owner", _owner); + this.header.attr('data-owner', _owner); + this.div.attr('data-owner', _owner); + // Simple comparison, both numbers + if (_owner === this.options.owner) + return; + // More complicated comparison, one or the other is an array + if ((typeof _owner == 'object' || typeof this.options.owner == 'object') && + _owner.toString() == this.options.owner.toString()) { + return; + } + this.options.owner = typeof _owner !== 'object' ? [_owner] : _owner; + var cache_id = CalendarApp._daywise_cache_id(this.options.date, _owner); + if (this.options.date && this.registeredUID && + cache_id !== this.registeredUID) { + egw.dataUnregisterUID(this.registeredUID, null, this); + } + if (this.registeredUID !== cache_id) { + this.registeredUID = cache_id; + egw.dataRegisterUID(this.registeredUID, this._data_callback, this, this.getInstanceManager().execId, this.id); + } + }; + et2_calendar_daycol.prototype.set_class = function (classnames) { + this.header.removeClass(this.class); + _super.prototype.set_class.call(this, classnames); + this.header.addClass(classnames); + }; + /** + * Callback used when the daywise data changes + * + * Events should update themselves when their data changes, here we are + * dealing with a change in which events are displayed on this day. + * + * @param {String[]} event_ids + * @returns {undefined} + */ + et2_calendar_daycol.prototype._data_callback = function (event_ids) { + var events = []; + if (event_ids == null || typeof event_ids.length == 'undefined') + event_ids = []; + for (var i = 0; i < event_ids.length; i++) { + var event_1 = egw.dataGetUIDdata('calendar::' + event_ids[i]); + event_1 = event_1 && event_1.data || false; + if (event_1 && event_1.date && et2_widget_event_1.et2_calendar_event.owner_check(event_1, this) && (event_1.date === this.options.date || + // Accept multi-day events + new Date(event_1.start) <= this.date //&& new Date(event.end) >= this.date + )) { + events.push(event_1); + } + else if (event_1) { + // Got an ID that doesn't belong + event_ids.splice(i--, 1); + } + } + if (!this.getParent().disabled) + this._update_events(events); + }; + et2_calendar_daycol.prototype.set_label = function (label) { + this.options.label = label; + this.title.text(label); + this.title.toggleClass('et2_clickable et2_link', label === ''); + }; + et2_calendar_daycol.prototype.set_left = function (left) { + if (this.div) { + this.div.css('left', left); + } + }; + et2_calendar_daycol.prototype.set_width = function (width) { + this.options.width = width; + if (this.div) { + this.div.outerWidth(this.options.width); + this.header.outerWidth(this.options.width); + } + }; + /** + * Applies class for today, and any holidays for current day + */ + et2_calendar_daycol.prototype.day_class_holiday = function () { + this.title + // Remove all special day classes + .removeClass('calendar_calToday calendar_calBirthday calendar_calHoliday') + // Except this one... + .addClass("et2_clickable et2_link"); + this.title.attr('data-holiday', ''); + // Set today class - note +1 when dealing with today, as months in JS are 0-11 + var today = new Date(); + today.setUTCMinutes(today.getUTCMinutes() - today.getTimezoneOffset()); + this.title.toggleClass("calendar_calToday", this.options.date === '' + today.getUTCFullYear() + + sprintf("%02d", today.getUTCMonth() + 1) + + sprintf("%02d", today.getUTCDate())); + // Holidays and birthdays + var holidays = et2_widget_view_1.et2_calendar_view.get_holidays(this, this.options.date.substring(0, 4)); + var holiday_list = []; + var holiday_pref = (egw.preference('birthdays_as_events', 'calendar') || []); + if (typeof holiday_pref === 'string') { + holiday_pref = holiday_pref.split(','); + } + else { + holiday_pref = jQuery.extend([], holiday_pref); + } + // Show holidays as events on mobile or by preference + var holidays_as_events = egwIsMobile() || egw.preference('birthdays_as_events', 'calendar') === true || + holiday_pref.indexOf('holiday') >= 0; + var birthdays_as_events = egwIsMobile() || holiday_pref.indexOf('birthday') >= 0; + if (holidays && holidays[this.options.date]) { + holidays = holidays[this.options.date]; + for (var i = 0; i < holidays.length; i++) { + if (typeof holidays[i]['birthyear'] !== 'undefined') { + // Show birthdays as events on mobile or by preference + if (birthdays_as_events) { + // Create event + this.getParent().date_helper.set_value(this.options.date.substring(0, 4) + '-' + + (this.options.date.substring(4, 6)) + '-' + this.options.date.substring(6, 8) + + 'T00:00:00Z'); + var event = et2_createWidget('calendar-event', { + id: 'event_' + holidays[i].name, + value: { + title: holidays[i].name, + whole_day: true, + whole_day_on_top: true, + start: new Date(this.getParent().date_helper.get_value()), + end: this.options.date, + owner: this.options.owner, + participants: this.options.owner, + app: 'calendar', + class: 'calendar_calBirthday' + }, + readonly: true, + class: 'calendar_calBirthday' + }, this); + event.doLoadingFinished(); + event._update(); + } + if (!egwIsMobile()) { + //If the birthdays are already displayed as event, don't + //show them in the caption + this.title.addClass('calendar_calBirthday'); + holiday_list.push(holidays[i]['name']); + } + } + else { + // Show holidays as events on mobile + if (holidays_as_events) { + // Create event + this.getParent().date_helper.set_value(this.options.date.substring(0, 4) + '-' + + (this.options.date.substring(4, 6)) + '-' + this.options.date.substring(6, 8) + + 'T00:00:00Z'); + var event = et2_createWidget('calendar-event', { + id: 'event_' + holidays[i].name, + value: { + title: holidays[i].name, + whole_day: true, + whole_day_on_top: true, + start: new Date(this.getParent().date_helper.get_value()), + end: this.options.date, + owner: this.options.owner, + participants: this.options.owner, + app: 'calendar', + class: 'calendar_calHoliday' + }, + readonly: true, + class: 'calendar_calHoliday' + }, this); + event.doLoadingFinished(); + event._update(); + } + else { + this.title.addClass('calendar_calHoliday'); + this.title.attr('data-holiday', holidays[i]['name']); + //If the birthdays are already displayed as event, don't + //show them in the caption + if (!this.options.display_holiday_as_event) { + holiday_list.push(holidays[i]['name']); + } + } + } + } + } + this.title.attr('title', holiday_list.join(', ')); + }; + /** + * Load the event data for this day and create event widgets for each. + * + * If event information is not provided, it will be pulled from the content array. + * + * @param {Object[]} [_events] Array of event information, one per event. + */ + et2_calendar_daycol.prototype._update_events = function (_events) { + var c; + var events = _events || this.getArrayMgr('content').getEntry(this.options.date) || []; + // Remove extra events + while (this._children.length > 0) { + var node = this._children[this._children.length - 1]; + this.removeChild(node); + node.destroy(); + } + // Make sure children are in cronological order, or columns are backwards + events.sort(function (a, b) { + var start = new Date(a.start) - new Date(b.start); + var end = new Date(a.end) - new Date(b.end); + // Whole day events sorted by ID, normal events by start / end time + if (a.whole_day && b.whole_day) { + return (a.app_id - b.app_id); + } + else if (a.whole_day || b.whole_day) { + return a.whole_day ? -1 : 1; + } + return start ? start : end; + }); + for (c = 0; c < events.length; c++) { + // Create event + var event = et2_createWidget('calendar-event', { + id: 'event_' + events[c].id, + value: events[c] + }, this); + } + // Seperate loop so column sorting finds all children in the right place + for (c = 0; c < events.length && c < this._children.length; c++) { + var event_2 = this.getWidgetById('event_' + events[c].id); + if (!event_2) + continue; + if (this.isInTree()) { + event_2.doLoadingFinished(); + } + } + // Show holidays as events on mobile or by preference + if (egwIsMobile() || egw.preference('birthdays_as_events', 'calendar')) { + this.day_class_holiday(); + } + // Apply styles to hidden events + this._out_of_view(); + }; + /** + * Apply styles for out-of-view and partially hidden events + * + * There are 3 different states or modes of display: + * + * - 'Normal' - When showing events positioned by time, the indicator is just + * a bar colored by the last category color. On hover it shows either the + * title of a single event or "x event(s)" if more than one are hidden. + * Clicking adjusts the current view to show the earliest / latest hidden + * event + * + * - Fixed - When showing events positioned by time but in a fixed-height + * week (not auto-sized to fit screen) the indicator is the same as sized. + * On hover it shows the titles of the hidden events, clicking changes + * the view to the selected day. + * + * - GridList - When showing just a list, the indicator shows "x event(s)", + * and on hover shows the category color, title & time. Clicking changes + * the view to the selected day, and opens the event for editing. + */ + et2_calendar_daycol.prototype._out_of_view = function () { + // Reset + this.header.children('.hiddenEventBefore').remove(); + this.div.children('.hiddenEventAfter').remove(); + this.event_wrapper.css('overflow', 'visible'); + this.all_day.removeClass('overflown'); + jQuery('.calendar_calEventBody', this.div).css({ 'padding-top': '', 'margin-top': '' }); + var timegrid = this.getParent(); + // elem is jquery div of event + function isHidden(elem) { + // Add an extra 5px top and bottom to include events just on the + // edge of visibility + var docViewTop = timegrid.scrolling.scrollTop() + 5, docViewBottom = docViewTop + (this.display_settings.granularity === 0 ? + this.event_wrapper.height() : + timegrid.scrolling.height() - 10), elemTop = elem.position().top, elemBottom = elemTop + elem.outerHeight(true); + if ((elemBottom <= docViewBottom) && (elemTop >= docViewTop)) { + // Entirely visible + return false; + } + var visible = { + hidden: elemTop > docViewTop ? 'bottom' : 'top', + completely: false + }; + visible.completely = visible.hidden == 'top' ? elemBottom < docViewTop : elemTop > docViewBottom; + return visible; + } + // In gridlist view, we can quickly check if we need it at all + if (this.display_settings.granularity === 0 && this._children.length) { + jQuery('div.calendar_calEvent', this.div).show(0); + if (Math.ceil(this.div.height() / this._children[0].div.height()) > this._children.length) { + return; + } + } + // Check all day overflow + this.all_day.toggleClass('overflown', this.all_day[0].scrollHeight - this.all_day.innerHeight() > 5); + // Check each event + this.iterateOver(function (event) { + // Skip whole day events and events missing value + if (this.display_settings.granularity && ((!event.options || !event.options.value || event.options.value.whole_day_on_top))) { + return; + } + // Reset + event.title.css({ 'top': '', 'background-color': '' }); + event.body.css({ 'padding-top': '', 'margin-top': '' }); + var hidden = isHidden.call(this, event.div); + var day = this; + if (!hidden) { + return; + } + // Only top is hidden, move label + // Bottom hidden is fine + if (hidden.hidden === 'top' && !hidden.completely && !event.div.hasClass('calendar_calEventSmall')) { + var title_height = event.title.outerHeight(); + event.title.css({ + 'top': timegrid.scrolling.scrollTop() - event.div.position().top, + 'background-color': 'transparent' + }); + event.body.css({ + 'padding-top': timegrid.scrolling.scrollTop() - event.div.position().top + title_height, + 'margin-top': -title_height + }); + } + // Too many in gridlist view, show indicator + else if (this.display_settings.granularity === 0 && hidden) { + if (jQuery('.hiddenEventAfter', this.div).length == 0) { + this.event_wrapper.css('overflow', 'hidden'); + } + this._hidden_indicator(event, false, function () { + app.calendar.update_state({ view: 'day', date: day.date }); + }); + // Avoid partially visible events + // We need to hide all, or the next row will be visible + event.div.hide(0); + } + // Completely out of view, show indicator + else if (hidden.completely) { + this._hidden_indicator(event, hidden.hidden == 'top', false); + } + }, this, et2_widget_event_1.et2_calendar_event); + }; + /** + * Show an indicator that there are hidden events + * + * The indicator works 3 different ways, depending on if the day can be + * scrolled, is fixed, or if in gridview. + * + * @see _out_of_view() + * + * @param {et2_calendar_event} event Event we're creating the indicator for + * @param {boolean} top Events hidden at the top (true) or bottom (false) + * @param {function} [onclick] Callback for when user clicks on the indicator + */ + et2_calendar_daycol.prototype._hidden_indicator = function (event, top, onclick) { + var indicator = null; + var day = this; + var timegrid = this.getParent(); + var fixed_height = timegrid.div.hasClass('calendar_calTimeGridFixed'); + // Event is before the displayed times + if (top) { + // Create if not already there + if (jQuery('.hiddenEventBefore', this.header).length === 0) { + indicator = jQuery('
') + .appendTo(this.header) + .attr('data-hidden_count', 1); + if (!fixed_height) { + indicator + .text(event.options.value.title) + .on('click', typeof onclick === 'function' ? onclick : function () { + jQuery('.calendar_calEvent', day.div).first()[0].scrollIntoView(); + return false; + }); + } + } + else { + indicator = jQuery('.hiddenEventBefore', this.header); + indicator.attr('data-hidden_count', parseInt(indicator.attr('data-hidden_count')) + 1); + if (!fixed_height) { + indicator.text(day.egw().lang('%1 event(s) %2', indicator.attr('data-hidden_count'), '')); + } + } + } + // Event is after displayed times + else { + indicator = jQuery('.hiddenEventAfter', this.div); + // Create if not already there + if (indicator.length === 0) { + indicator = jQuery('
') + .attr('data-hidden_count', 0) + .appendTo(this.div); + if (!fixed_height) { + indicator + .on('click', typeof onclick === 'function' ? onclick : function () { + jQuery('.calendar_calEvent', day.div).last()[0].scrollIntoView(false); + // Better re-run this to clean up + day._out_of_view(); + return false; + }); + } + else { + indicator + .on('mouseover', function () { + indicator.css({ + 'height': (indicator.attr('data-hidden_count') * 1.2) + 'em', + 'margin-top': -(indicator.attr('data-hidden_count') * 1.2) + 'em' + }); + }) + .on('mouseout', function () { + indicator.css({ + 'height': '', + 'margin-top': '' + }); + }); + } + } + var count = parseInt(indicator.attr('data-hidden_count')) + 1; + indicator.attr('data-hidden_count', count); + if (this.display_settings.granularity === 0) { + indicator.append(event.div.clone()); + indicator.attr('data-hidden_label', day.egw().lang('%1 event(s) %2', indicator.attr('data-hidden_count'), '')); + } + else if (!fixed_height) { + indicator.text(day.egw().lang('%1 event(s) %2', indicator.attr('data-hidden_count'), '')); + } + indicator.css('top', timegrid.scrolling.height() + timegrid.scrolling.scrollTop() - indicator.innerHeight()); + } + // Show different stuff for fixed height + if (fixed_height) { + indicator + .append("
" + + event.options.value.title + + "
"); + } + // Match color to the event + if (indicator !== null) { + // Avoid white, which is hard to see + // Use border-bottom-color, Firefox doesn't give a value with border-color + var color = jQuery.Color(event.div.css('background-color')).toString() !== jQuery.Color('white').toString() ? + event.div.css('background-color') : event.div.css('border-bottom-color'); + if (color !== 'rgba(0, 0, 0, 0)') { + indicator.css('border-color', color); + } + } + }; + /** + * Sort a day's events into minimally overlapping columns + * + * @returns {Array[]} Events sorted into columns + */ + et2_calendar_daycol.prototype._spread_events = function () { + if (!this.date) + return []; + var day_start = this.date.valueOf() / 1000; + var dst_check = new Date(this.date); + dst_check.setUTCHours(12); + // if daylight saving is switched on or off, correct $day_start + // gives correct times after 2am, times between 0am and 2am are wrong + var daylight_diff = day_start + 12 * 60 * 60 - (dst_check.valueOf() / 1000); + if (daylight_diff) { + day_start -= daylight_diff; + } + var eventCols = [], col_ends = []; + // Make sure children are in cronological order, or columns are backwards + this._children.sort(function (a, b) { + var start = new Date(a.options.value.start) - new Date(b.options.value.start); + var end = new Date(a.options.value.end) - new Date(b.options.value.end); + // Whole day events sorted by ID, normal events by start / end time + if (a.options.value.whole_day && b.options.value.whole_day) { + // Longer duration comes first so we have nicer bars across the top + var duration = (new Date(b.options.value.end) - new Date(b.options.value.start)) - + (new Date(a.options.value.end) - new Date(a.options.value.start)); + return duration ? duration : (a.options.value.app_id - b.options.value.app_id); + } + else if (a.options.value.whole_day || b.options.value.whole_day) { + return a.options.value.whole_day ? -1 : 1; + } + return start ? start : end; + }); + for (var i = 0; i < this._children.length; i++) { + var event_3 = this._children[i].options.value || false; + if (!event_3) + continue; + if (event_3.date && event_3.date != this.options.date && + // Multi-day events date may be different + (new Date(event_3.start) >= this.date || new Date(event_3.end) < this.date)) { + // Still have a child event that has changed date (DnD) + this._children[i].destroy(); + this.removeChild(this._children[i]); + continue; + } + var c = 0; + event_3['multiday'] = false; + if (typeof event_3.start !== 'object') { + event_3.start = new Date(event_3.start); + } + if (typeof event_3.end !== 'object') { + event_3.end = new Date(event_3.end); + } + event_3['start_m'] = (event_3.start.valueOf() / 1000 - day_start) / 60; + if (event_3['start_m'] < 0) { + event_3['start_m'] = 0; + event_3['multiday'] = true; + } + event_3['end_m'] = (event_3.end.valueOf() / 1000 - day_start) / 60; + if (event_3['end_m'] >= 24 * 60) { + event_3['end_m'] = 24 * 60 - 1; + event_3['multiday'] = true; + } + if (!event_3.start.getUTCHours() && !event_3.start.getUTCMinutes() && event_3.end.getUTCHours() == 23 && event_3.end.getUTCMinutes() == 59) { + event_3.whole_day_on_top = (event_3.non_blocking && event_3.non_blocking != '0'); + } + if (!event_3['whole_day_on_top']) { + for (c = 0; event_3['start_m'] < col_ends[c]; ++c) + ; + col_ends[c] = event_3['end_m']; + } + if (typeof eventCols[c] === 'undefined') { + eventCols[c] = []; + } + eventCols[c].push(this._children[i]); + } + return eventCols; + }; + /** + * Position the event according to its time and how this widget is laid + * out. + * + * @param {et2_calendar_event} [event] - Event to be updated + * If a single event is not provided, all events are repositioned. + */ + et2_calendar_daycol.prototype.position_event = function (event) { + // If hidden, skip it - it takes too long + if (!this.div.is(':visible')) + return; + // Sort events into minimally-overlapping columns + var columns = this._spread_events(); + for (var c = 0; c < columns.length; c++) { + // Calculate horizontal positioning + var left = Math.ceil(5 + (1.5 * 100 / (parseFloat(this.options.width) || 100))); + var right = 2; + if (columns.length !== 1) { + right = !c ? 30 : 2; + left += c * (100.0 - left) / columns.length; + } + for (var i = 0; (columns[c].indexOf(event) >= 0 || !event) && i < columns[c].length; i++) { + // Calculate vertical positioning + var top_1 = 0; + var height = 0; + // Position the event + if (this.display_settings.granularity === 0) { + if (this.all_day.has(columns[c][i].div).length) { + columns[c][i].div.prependTo(this.event_wrapper); + } + columns[c][i].div.css('top', ''); + columns[c][i].div.css('height', ''); + columns[c][i].div.css('left', ''); + columns[c][i].div.css('right', ''); + // Strip out of view padding + columns[c][i].body.css('padding-top', ''); + continue; + } + if (columns[c][i].options.value.whole_day_on_top) { + if (!this.all_day.has(columns[c][i].div).length) { + columns[c][i].div.css('top', ''); + columns[c][i].div.css('height', ''); + columns[c][i].div.css('left', ''); + columns[c][i].div.css('right', ''); + columns[c][i].body.css('padding-top', ''); + columns[c][i].div + .appendTo(this.all_day); + this.getParent().resizeTimes(); + } + continue; + } + else { + if (this.all_day.has(columns[c][i].div).length) { + columns[c][i].div.appendTo(this.event_wrapper); + this.getParent().resizeTimes(); + } + top_1 = this._time_to_position(columns[c][i].options.value.start_m); + height = this._time_to_position(columns[c][i].options.value.end_m) - top_1; + } + // Position the event + if (event && columns[c].indexOf(event) >= 0 || !event) { + columns[c][i].div.css('top', top_1 + '%'); + columns[c][i].div.css('height', height + '%'); + // Remove spacing from border, but only if visible or the height will be wrong + if (columns[c][i].div.is(':visible')) { + var border_diff = columns[c][i].div.outerHeight() - columns[c][i].div.height(); + columns[c][i].div.css('height', 'calc(' + height + '% - ' + border_diff + ')'); + } + // This gives the wrong height + //columns[c][i].div.outerHeight(height+'%'); + columns[c][i].div.css('left', left.toFixed(1) + '%'); + columns[c][i].div.css('right', right.toFixed(1) + '%'); + columns[c][i].div.css('z-index', parseInt(20) + c); + columns[c][i]._small_size(); + } + } + // Only wanted to position this event, leave the other columns alone + if (event && columns[c].indexOf(event) >= 0) { + return; + } + } + }; + /** + * Calculates the vertical position based on the time + * + * This calculation is a percentage from 00:00 to 23:59 + * + * @param {int} time in minutes from midnight + * @return {float} position in percent + */ + et2_calendar_daycol.prototype._time_to_position = function (time) { + var pos = 0.0; + // 24h + pos = ((time / 60) / 24) * 100; + return pos.toFixed(1); + }; + et2_calendar_daycol.prototype.attachToDOM = function () { + var result = _super.prototype.attachToDOM.call(this); + // Remove the binding for the click handler, unless there's something + // custom here. + if (!this.onclick) { + jQuery(this.node).off("click"); + } + // But we do want to listen to certain clicks, and handle them internally + jQuery(this.node).on('click.et2_daycol', '.calendar_calDayColHeader,.calendar_calAddEvent', jQuery.proxy(this.click, this)); + return result; + }; + /** + * Click handler calling custom handler set via onclick attribute to this.onclick, + * or the default which is to open a new event at that time. + * + * Normally, you don't bind to this one, but the attribute is supported if you + * can get a reference to the widget. + * + * @param {Event} _ev + * @returns {boolean} + */ + et2_calendar_daycol.prototype.click = function (_ev) { + if (this.getParent().options.readonly) + return; + // Drag to create in progress + if (this.getParent().drag_create.start !== null) + return; + // Click on the title + if (jQuery(_ev.target).hasClass('calendar_calAddEvent')) { + if (this.header.has(_ev.target).length == 0 && !_ev.target.dataset.whole_day) { + // Default handler to open a new event at the selected time + var options = { + date: _ev.target.dataset.date || this.options.date, + hour: _ev.target.dataset.hour || this.getParent().options.day_start, + minute: _ev.target.dataset.minute || 0, + owner: this.options.owner + }; + app.calendar.add(options); + return false; + } + // Header, all day non-blocking + else if (this.header.has(_ev.target).length && !jQuery('.hiddenEventBefore', this.header).has(_ev.target).length || + this.header.is(_ev.target)) { + // Click on the header, but not the title. That's an all-day non-blocking + var end = this.date.getFullYear() + '-' + (this.date.getUTCMonth() + 1) + '-' + this.date.getUTCDate() + 'T23:59'; + var options_1 = { + start: this.date.toJSON(), + end: end, + non_blocking: true, + owner: this.options.owner + }; + app.calendar.add(options_1); + return false; + } + } + // Day label + else if (this.title.is(_ev.target) || this.title.has(_ev.target).length) { + app.calendar.update_state({ view: 'day', date: this.date.toJSON() }); + return false; + } + }; + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs array to add further attributes to + */ + et2_calendar_daycol.prototype.getDetachedAttributes = function (_attrs) { + }; + et2_calendar_daycol.prototype.getDetachedNodes = function () { + return [this.getDOMNode(this)]; + }; + et2_calendar_daycol.prototype.setDetachedAttributes = function (_nodes, _values) { + }; + // Resizable interface + /** + * Resize + * + * Parent takes care of setting proper width & height for the containing div + * here we just need to adjust the events to fit the new size. + */ + et2_calendar_daycol.prototype.resize = function () { + if (this.disabled || !this.div.is(':visible') || this.getParent().disabled) { + return; + } + if (this.display_settings.granularity !== this.getParent().options.granularity) { + // Layout has changed + this._draw(); + // Resize & position all events + this.position_event(); + } + else { + // Don't need to resize & reposition, just clear some stuff + // to reset for _out_of_view() + this.iterateOver(function (widget) { + widget._small_size(); + }, this, et2_widget_event_1.et2_calendar_event); + } + this._out_of_view(); + }; + et2_calendar_daycol._attributes = { + date: { + name: "Date", + type: "any", + description: "What date is this daycol for. YYYYMMDD or Date", + default: et2_no_init + }, + owner: { + name: "Owner", + type: "any", + default: et2_no_init, + description: "Account ID number of the calendar owner, if not the current user" + }, + display_birthday_as_event: { + name: "Birthdays", + type: "boolean", + default: false, + description: "Display birthdays as events" + }, + display_holiday_as_event: { + name: "Holidays", + type: "boolean", + default: false, + description: "Display holidays as events" + } + }; + return et2_calendar_daycol; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_calendar_daycol = et2_calendar_daycol; +et2_core_widget_1.et2_register_widget(et2_calendar_daycol, ["calendar-daycol"]); +//# sourceMappingURL=et2_widget_daycol.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_event.js b/calendar/js/et2_widget_event.js index e3ec118261..7acbab2a69 100644 --- a/calendar/js/et2_widget_event.js +++ b/calendar/js/et2_widget_event.js @@ -1,3 +1,4 @@ +"use strict"; /* * Egroupware Calendar event widget * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License @@ -7,12 +8,29 @@ * @author Nathan Gray * @version $Id$ */ - - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /etemplate/js/et2_core_valueWidget; + /etemplate/js/et2_core_valueWidget; */ - +var et2_core_widget_1 = require("../../api/js/etemplate/et2_core_widget"); +var et2_core_valueWidget_1 = require("../../api/js/etemplate/et2_core_valueWidget"); +var et2_core_inheritance_1 = require("../../api/js/etemplate/et2_core_inheritance"); +var et2_core_DOMWidget_1 = require("../../api/js/etemplate/et2_core_DOMWidget"); +var et2_widget_daycol_1 = require("./et2_widget_daycol"); +var et2_widget_planner_row_1 = require("./et2_widget_planner_row"); /** * Class for a single event, displayed in either the timegrid or planner view * @@ -37,1237 +55,1002 @@ * et2_calendar_planner_row rather than either et2_calendar_timegrid or * et2_calendar_planner directly. * - * - * @augments et2_valueWidget */ -var et2_calendar_event = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM], -{ - - attributes: { - "value": { - type: "any", - default: et2_no_init - }, - "onclick": { - "description": "JS code which is executed when the element is clicked. " + - "If no handler is provided, or the handler returns true and the event is not read-only, the " + - "event will be opened according to calendar settings." - } - }, - - /** - * Constructor - * - * @memberOf et2_calendar_daycol - */ - init: function() { - this._super.apply(this, arguments); - - var event = this; - - // Main container - this.div = jQuery(document.createElement("div")) - .addClass("calendar_calEvent") - .addClass(this.options.class) - .css('width',this.options.width) - .on('mouseenter', function() { - // Bind actions on first mouseover for faster creation - if(event._need_actions_linked) - { - event._copy_parent_actions(); - } - // Tooltip - if(!event._tooltipElem) - { - event.options.statustext_html = true; - event.set_statustext(event._tooltip()); - if(event.statustext) - { - return event.div.trigger('mouseenter'); - } - } - // Hacky to remove egw's tooltip border and let the mouse in - window.setTimeout(function() { - jQuery('body .egw_tooltip') - .css('border','none') - .on('mouseenter', function() { - event.div.off('mouseleave.tooltip'); - jQuery('body.egw_tooltip').remove(); - jQuery('body').append(this); - jQuery(this).stop(true).fadeTo(400, 1) - .on('mouseleave', function() { - jQuery(this).fadeOut('400', function() { - jQuery(this).remove(); - // Set up to work again - event.set_statustext(event._tooltip()); - }); - }); - }); - - },105); - }); - this.title = jQuery(document.createElement('div')) - .addClass("calendar_calEventHeader") - .appendTo(this.div); - this.body = jQuery(document.createElement('div')) - .addClass("calendar_calEventBody") - .appendTo(this.div); - this.icons = jQuery(document.createElement('div')) - .addClass("calendar_calEventIcons") - .appendTo(this.title); - - this.setDOMNode(this.div[0]); - - this._need_actions_linked = false; - }, - - doLoadingFinished: function() { - this._super.apply(this, arguments); - - // Already know what is needed to hook to cache - if(this.options.value && this.options.value.row_id) - { - egw.dataRegisterUID( - 'calendar::'+this.options.value.row_id, - this._UID_callback, - this, - this.getInstanceManager().execId, - this.id - ); - } - return true; - }, - - destroy: function() { - this._super.apply(this, arguments); - - if(this._actionObject) - { - this._actionObject.remove(); - this._actionObject = null; - } - - this.div.off(); - this.title.remove(); - this.title = null; - this.body.remove(); - this.body = null; - this.icons = null; - this.div.remove(); - this.div = null; - - jQuery('body.egw_tooltip').remove(); - - // Unregister, or we'll continue to be notified... - if(this.options.value) - { - var old_app_id = this.options.value.row_id; - egw.dataUnregisterUID('calendar::'+old_app_id,false,this); - } - }, - - set_value: function(_value) { - // Un-register for updates - if(this.options.value) - { - var old_id = this.options.value.row_id; - if(!_value || !_value.row_id || old_id !== _value.row_id) - { - egw.dataUnregisterUID('calendar::'+old_id,false,this); - } - } - this.options.value = _value; - - // Register for updates - var id = this.options.value.row_id; - if(!old_id || old_id !== id) - { - egw.dataRegisterUID('calendar::'+id, this._UID_callback ,this,this.getInstanceManager().execId,this.id); - } - if(_value && !egw.dataHasUID('calendar::'+id)) - { - egw.dataStoreUID('calendar::'+id, _value); - } - }, - - /** - * Callback for changes in cached data - */ - _UID_callback: function _UID_callback(event) { - // Copy to avoid changes, which may cause nm problems - var value = event === null ? null : jQuery.extend({},event); - var parent = this._parent; - - // Make sure id is a string, check values - if(value) - { - this._values_check(value); - } - - // Check for changing days in the grid view - if(!this._sameday_check(value)) - { - // May need to update parent to remove out-of-view events - parent.removeChild(this); - if(event === null && parent && parent._out_of_view) - { - parent._out_of_view(); - } - - // This should now cease to exist, as new events have been created - this.free(); - return; - } - - // Copy to avoid changes, which may cause nm problems - this.options.value = jQuery.extend({},value); - - if(this._parent.options.date) - { - this.options.value.date = this._parent.options.date; - } - - // Let parent position - this._parent.position_event(this); - - // Parent may remove this if the date isn't the same - if(this._parent) - { - this._update(); - } - }, - - /** - * Draw the event - */ - _update: function() { - - // Update to reflect new information - var event = this.options.value; - - var id = event.row_id ? event.row_id : event.id + (event.recur_type ? ':'+event.recur_date : ''); - var formatted_start = event.start.toJSON(); - - this.set_id('event_' + id); - if(this._actionObject) - { - this._actionObject.id = 'calendar::' + id; - } - - this._need_actions_linked = !this.options.readonly; - - // Make sure category stuff is there - // Fake it to use the cache / call - if already there, these will return - // immediately. - var im = this.getInstanceManager(); - et2_selectbox.cat_options({ - _type:'select-cat', - getInstanceManager: function() {return im} - }, {application:event.app||'calendar'}); - - // Need cleaning? (DnD helper removes content) - if(!this.div.has(this.title).length) - { - this.div - .empty() - .append(this.title) - .append(this.body); - } - if(!this._parent.options.readonly && !this.options.readonly && this.div.droppable('instance')) - { - this.div - // Let timegrid always get the drag - .droppable('option','greedy',false); - } - // DOM nodes - this.div - // Set full day flag - .attr('data-full_day', event.whole_day) - - // Put everything we need for basic interaction here, so it's available immediately - .attr('data-id', event.id) - .attr('data-app', event.app || 'calendar') - .attr('data-app_id', event.app_id) - .attr('data-start', formatted_start) - .attr('data-owner', event.owner) - .attr('data-recur_type', event.recur_type) - .attr('data-resize', event.whole_day ? 'WD' : '' + (event.recur_type ? 'S':'')) - .attr('data-priority', event.priority) - // Remove any category classes - .removeClass(function(index, css) { - return (css.match (/(^|\s)cat_\S+/g) || []).join(' '); - }) - // Remove any status classes - .removeClass(function(index, css) { - return (css.match(/calendar_calEvent\S+/g) || []).join(' '); - }) - .removeClass('calendar_calEventSmall') - .addClass(event.class) - .toggleClass('calendar_calEventPrivate', typeof event.private !== 'undefined' && event.private); - this.options.class = event.class; - var status_class = this._status_class(); - - // Add category classes, if real categories are set - if(event.category && event.category != '0') - { - var cats = event.category.split(','); - for(var i = 0; i < cats.length; i++) - { - this.div.addClass('cat_' + cats[i]); - } - } - - this.div.toggleClass('calendar_calEventUnknown', event.participants[egw.user('account_id')] ? event.participants[egw.user('account_id')][0] === 'U' : false); - this.div.addClass(status_class); - - this.body.toggleClass('calendar_calEventBodySmall', event.whole_day_on_top || false); - - // Header - var title = !event.is_private ? egw.htmlspecialchars(event['title']) : egw.lang('private'); - - this.title - .html(''+this._get_timespan(event) + '
') - .append(''+title+'') - - // Colors - don't make them transparent if there is no color - if(jQuery.Color("rgba(0,0,0,0)").toRgbaString() != jQuery.Color(this.div,'background-color').toRgbaString()) - { - // Most statuses use colored borders - this.div.css('border-color',this.div.css('background-color') ); - } - - this.icons.appendTo(this.title) - .html(this._icons()); - - // Body - if(event.whole_day_on_top) - { - this.body.html(title); - } - else - { - var start_time = jQuery.datepicker.formatTime( - egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", - { - hour: event.start_m / 60, - minute: event.start_m % 60, - seconds: 0, - timezone: 0 - }, - {"ampm": (egw.preference("timeformat") === "12")} - ).trim(); - - this.body - .html(''+title+'') - .append(''+start_time + ''); - if(this.options.value.description.trim()) - { - this.body - .append('

'+egw.htmlspecialchars(this.options.value.description)+'

'); - } - } - - // Clear tooltip for regeneration - this.set_statustext(''); - - // Height specific section - // This can take an unreasonable amount of time if parent is hidden - if(this._parent.div.is(':visible')) - { - this._small_size(); - } - }, - - /** - * Calculate display variants for when event is too short for full display - * - * Display is based on the number of visible lines, calculated off the header - * height: - * 1 - show just the event title, with ellipsis - * 2 - Show timespan and title, with ellipsis - * > 4 - Show description as well, truncated to fit - */ - _small_size: function() { - - if(this.options.value.whole_day_on_top) return; - - // Skip for planner view, it's always small - if(this._parent && this._parent.instanceOf(et2_calendar_planner_row)) return; - - // Pre-calculation reset - this.div.removeClass('calendar_calEventSmall'); - this.body.css('height', 'auto'); - - var line_height = parseFloat(this.div.css('line-height')); - var visible_lines = Math.floor(this.div.innerHeight() / line_height); - - if(!this.title.height()) - { - // Handle sizing while hidden, such as when calendar is not the active tab - visible_lines = Math.floor(egw.getHiddenDimensions(this.div).h / egw.getHiddenDimensions(this.title).h); - } - visible_lines = Math.max(1,visible_lines); - - if(this.getParent() && this.getParent().instanceOf(et2_calendar_daycol)) - { - this.div.toggleClass('calendar_calEventSmall',visible_lines < 4); - this.div - .attr('data-visible_lines', visible_lines); - } - else if (this.getParent() && this.getParent().instanceOf(et2_calendar_planner_row)) - { - // Less than 8 hours is small - this.div.toggleClass('calendar_calEventSmall',this.options.value.end.valueOf() - this.options.value.start.valueOf() < 28800000); - } - - - if(this.body.height() > this.div.height() - this.title.height() && visible_lines >= 4) - { - this.body.css('height', Math.floor((visible_lines-1)*line_height - this.title.height()) + 'px'); - } - else - { - this.body.css('height', ''); - } - }, - - /** - * Examines the participants & returns CSS classname for status - * - * @returns {String} - */ - _status_class: function() { - var status_class = 'calendar_calEventAllAccepted'; - for(var id in this.options.value.participants) - { - var status = this.options.value.participants[id]; - - status = et2_calendar_event.split_status(status); - - switch (status) - { - case 'A': - case '': // app without status - break; - case 'U': - status_class = 'calendar_calEventSomeUnknown'; - return status_class; // break for - default: - status_class = 'calendar_calEventAllAnswered'; - break; - } - } - return status_class; - }, - - /** - * Create tooltip shown on hover - * - * @return {String} - */ - _tooltip: function() { - if(!this.div || !this.options.value || !this.options.value.app_id) return ''; - - var border = this.div.css('borderTopColor'); - var bg_color = this.div.css('background-color'); - var header_color = this.title.css('color'); - var timespan = this._get_timespan(this.options.value); - - this._parent.date_helper.set_value(this.options.value.start.valueOf ? new Date(this.options.value.start) : this.options.value.start); - var start = this._parent.date_helper.input_date.val(); - this._parent.date_helper.set_value(this.options.value.end.valueOf ? new Date(this.options.value.end) : this.options.value.end); - var end = this._parent.date_helper.input_date.val(); - - var times = !this.options.value.multiday ? - ''+this.egw().lang('Time')+':' + timespan : - ''+this.egw().lang('Start') + ':' +start+ ' ' + - ''+this.egw().lang('End') + ':' + end; - var cat_label = ''; - if(this.options.value.category) - { - var cat = et2_createWidget('select-cat',{'readonly':true},this); - cat.set_value(this.options.value.category); - cat_label = this.options.value.category.indexOf(',') <= 0 ? cat.span.text() : []; - if(typeof cat_label != 'string') - { - cat.span.children().each(function() { - cat_label.push(jQuery(this).text()); - }); - cat_label = cat_label.join(', '); - } - cat.destroy(); - } - var participants = ''; - if(this.options.value.participant_types['']) - { - participants += this.options.value.participant_types[''].join("
"); - } - for(var type_name in this.options.value.participant_types) - { - if(type_name) - { - participants += '

'+type_name+':
'; - participants += this.options.value.participant_types[type_name].join("
"); - } - } - - return '

'+ - '
'+ - ''+timespan+''+ - this.icons[0].outerHTML+ - '
'+ - '
'+ - '

'+ - ''+egw.htmlspecialchars(this.options.value.title)+'
'+ - egw.htmlspecialchars(this.options.value.description)+'

'+ - '

'+times+'

'+ - (this.options.value.location ? '

'+this.egw().lang('Location') + ':' + - egw.htmlspecialchars(this.options.value.location)+'

' : '')+ - (cat_label ? '

'+this.egw().lang('Category') + ':' + cat_label +'

' : '')+ - '

'+this.egw().lang('Participants')+':
'+ - participants + '

'+ this._participant_summary(this.options.value.participants) + - '
'+ - '
'; - }, - - /** - * Generate participant summary line - * - * @returns {String} - */ - _participant_summary: function(participants) - { - if( Object.keys(this.options.value.participants).length < 2) - { - return ''; - } - - var participant_status = {A: 0, R: 0, T: 0, U: 0, D: 0}; - var status_label = {A: 'accepted', R: 'rejected', T: 'tentative', U: 'unknown', D: 'delegated'}; - var participant_summary = Object.keys(this.options.value.participants).length + ' ' + this.egw().lang('Participants')+': '; - var status_totals = []; - - for(var id in this.options.value.participants) - { - var status = this.options.value.participants[id].substr(0,1); - participant_status[status]++; - } - for(var status in participant_status) - { - if(participant_status[status] > 0) - { - status_totals.push(participant_status[status] + ' ' + this.egw().lang(status_label[status])); - } - } - return participant_summary + status_totals.join(', '); - }, - - /** - * Get actual icons from list - * @returns {undefined} - */ - _icons: function() { - var icons = []; - - if(this.options.value.is_private) - { - // Hide everything - icons.push(''); - } - else - { - if(this.options.value.app !== 'calendar') - { - icons.push(''); - } - if(this.options.value.priority == 3) - { - icons.push(''); - } - if(this.options.value.public == '0') - { - // Show private flag - icons.push(''); - } - if(this.options.value['recur_type']) - { - icons.push(''); - } - // icons for single user, multiple users or group(s) and resources - var single = ''; - var multiple = ''; - for(var uid in this.options.value['participants']) - { - if(Object.keys(this.options.value.participants).length == 1 && !isNaN(uid)) - { - icons.push(single); - break; - } - if(!isNaN(uid) && icons.indexOf(multiple) === -1) - { - icons.push(multiple); - } - /* - * TODO: resource icons - elseif(!isset($icons[$uid[0]]) && isset($this->bo->resources[$uid[0]]) && isset($this->bo->resources[$uid[0]]['icon'])) - { - $icons[$uid[0]] = html::image($this->bo->resources[$uid[0]]['app'], - ($this->bo->resources[$uid[0]]['icon'] ? $this->bo->resources[$uid[0]]['icon'] : 'navbar'), - lang($this->bo->resources[$uid[0]]['app']), - 'width="16px" height="16px"'); - } - */ - } - - if(this.options.value.alarm && !jQuery.isEmptyObject(this.options.value.alarm) && !this.options.value.is_private) - { - icons.push(''); - } - if(this.options.value.participants[egw.user('account_id')] && this.options.value.participants[egw.user('account_id')][0] == 'U') - { - icons.push(''); - } - } - - // Always include non-blocking, regardless of privacy - if(this.options.value.non_blocking) - { - icons.push(''); - } - return icons; - }, - - /** - * Get a text representation of the timespan of the event. Either start - * - end, or 'all day' - * - * @param {Object} event Event to get the timespan for - * @param {number} event.start_m Event start, in minutes from midnight - * @param {number} event.end_m Event end, in minutes from midnight - * - * @return {string} Timespan - */ - _get_timespan: function(event) { - var timespan = ''; - if (event['start_m'] === 0 && event['end_m'] >= 24*60-1) - { - if (event['end_m'] > 24*60) - { - timespan = jQuery.datepicker.formatTime( - egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", - { - hour: event.start_m / 60, - minute: event.start_m % 60, - seconds: 0, - timezone: 0 - }, - {"ampm": (egw.preference("timeformat") === "12")} - ).trim()+' - '+jQuery.datepicker.formatTime( - egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", - { - hour: event.end_m / 60, - minute: event.end_m % 60, - seconds: 0, - timezone: 0 - }, - {"ampm": (egw.preference("timeformat") === "12")} - ).trim(); - } - else - { - timespan = this.egw().lang('Whole day'); - } - } - else - { - var duration = event.multiday ? - (event.end - event.start) / 60000 : - (event.end_m - event.start_m); - duration = Math.floor(duration/60) + this.egw().lang('h')+(duration%60 ? duration%60 : ''); - - timespan = jQuery.datepicker.formatTime( - egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", - { - hour: event.start_m / 60, - minute: event.start_m % 60, - seconds: 0, - timezone: 0 - }, - {"ampm": (egw.preference("timeformat") === "12")} - ).trim(); - - timespan += ' - ' + jQuery.datepicker.formatTime( - egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", - { - hour: event.end_m / 60, - minute: event.end_m % 60, - seconds: 0, - timezone: 0 - }, - {"ampm": (egw.preference("timeformat") === "12")} - ).trim(); - - timespan += ': ' + duration; - } - return timespan; - }, - - /** - * Make sure event data has all proper values, and format them as expected - * @param {Object} event - */ - _values_check: function _values_check(event) - { - // Make sure ID is a string - if(event.id) - { - event.id = ''+event.id; - } - - // Use dates as objects - if(typeof event.start !== 'object') - { - this._parent.date_helper.set_value(event.start); - event.start = new Date(this._parent.date_helper.getValue()); - } - if(typeof event.end !== 'object') - { - this._parent.date_helper.set_value(event.end); - event.end = new Date(this._parent.date_helper.getValue()); - } - - // We need minutes for durations - if(typeof event.start_m === 'undefined') - { - event.start_m = event.start.getUTCHours() * 60 + event.start.getUTCMinutes(); - event.end_m = event.end.getUTCHours() * 60 + event.end.getUTCMinutes(); - } - if(typeof event.multiday === 'undefined') - { - event.multiday = (event.start.getUTCFullYear() !== event.end.getUTCFullYear() || - event.start.getUTCMonth() !== event.end.getUTCMonth() || - event.start.getUTCDate() != event.end.getUTCDate()); - } - if(!event.start.getUTCHours() && !event.start.getUTCMinutes() && event.end.getUTCHours() == 23 && event.end.getUTCMinutes() == 59) - { - event.whole_day_on_top = (event.non_blocking && event.non_blocking != '0'); - } - }, - - /** - * Check to see if the provided event information is for the same date as - * what we're currently expecting, and that it has not been changed. - * - * If the date has changed, we adjust the associated daywise caches to move - * the event's ID to where it should be. This check allows us to be more - * directly reliant on the data cache, and less on any other control logic - * elsewhere first. - * - * @param {Object} event Map of event data from cache - * @param {string} event.date For non-recurring, single day events, this is - * the date the event is on. - * @param {string} event.start Start of the event (used for multi-day events) - * @param {string} event.end End of the event (used for multi-day events) - * - * @return {Boolean} Provided event data is for the same date - */ - _sameday_check: function(event) - { - // Event somehow got orphaned, or deleted - if(!this._parent || event === null) - { - return false; - } - - // Also check participants against owner - var owner_match = et2_calendar_event.owner_check(event, this._parent); - - // Simple, same day - if(owner_match && this.options.value.date && event.date == this.options.value.date) - { - return true; - } - - // Multi-day non-recurring event spans days - date does not match - var event_start = new Date(event.start); - var event_end = new Date(event.end); - if(owner_match && this._parent.date >= event_start && this._parent.date <= event_end) - { - return true; - } - - // Delete all old actions - if(this._actionObject) - { - this._actionObject.clear(); - this._actionObject.unregisterActions(); - this._actionObject = null; - } - - // Update daywise caches - var new_cache_id = app.classes.calendar._daywise_cache_id(event.date,this._parent.options.owner); - var new_daywise = egw.dataGetUIDdata(new_cache_id); - new_daywise = new_daywise && new_daywise.data ? new_daywise.data : []; - var old_cache_id = false; - if(this.options.value && this.options.value.date) - { - old_cache_id = app.classes.calendar._daywise_cache_id(this.options.value.date,this._parent.options.owner); - } - - if(new_cache_id != old_cache_id) - { - var old_daywise = egw.dataGetUIDdata(old_cache_id); - old_daywise = old_daywise && old_daywise.data ? old_daywise.data : []; - old_daywise.splice(old_daywise.indexOf(this.options.value.row_id),1); - egw.dataStoreUID(old_cache_id,old_daywise); - - if (new_daywise.indexOf(event.row_id) < 0) - { - new_daywise.push(event.row_id); - } - if(egw.dataHasUID(new_cache_id)) - { - egw.dataStoreUID(new_cache_id,new_daywise); - } - } - - return false; - }, - - attachToDOM: function() - { - this._super.apply(this, arguments); - - // Remove the binding for the click handler, unless there's something - // custom here. - if (!this.onclick) - { - jQuery(this.node).off("click"); - } - }, - - /** - * Click handler calling custom handler set via onclick attribute to this.onclick. - * All other handling is done by the timegrid widget. - * - * @param {Event} _ev - * @returns {boolean} - */ - click: function(_ev) { - var result = true; - if(typeof this.onclick == 'function') - { - // Make sure function gets a reference to the widget, splice it in as 2. argument if not - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.splice(1, 0, this); - - result = this.onclick.apply(this, args); - } - return result; - }, - - /** - * Show the recur prompt for this event - * - * Calls et2_calendar_event.recur_prompt with this event's value. - * - * @param {et2_calendar_event~prompt_callback} callback - * @param {Object} [extra_data] - */ - recur_prompt: function(callback, extra_data) - { - et2_calendar_event.recur_prompt(this.options.value,callback,extra_data); - }, - - /** - * Show the series split prompt for this event - * - * Calls et2_calendar_event.series_split_prompt with this event's value. - * - * @param {et2_calendar_event~prompt_callback} callback - */ - series_split_prompt: function(callback) - { - et2_calendar_event.series_split_prompt(this.options.value,this.options.value.recur_date, callback); - }, - - /** - * Copy the actions set on the parent, apply them to self - * - * This can take a while to do, so we try to do it only when needed - on mouseover - */ - _copy_parent_actions: function() - { - // Copy actions set in parent - if(!this.options.readonly && !this._parent.options.readonly) - { - var action_parent = this; - while(action_parent != null && !action_parent.options.actions && - !action_parent.instanceOf(et2_container) - ) - { - action_parent = action_parent.getParent(); - } - try { - this._link_actions(action_parent.options.actions||{}); - this._need_actions_linked = false; - } catch (e) { - // something went wrong, but keep quiet about it - } - } - }, - - /** - * Link the actions to the DOM nodes / widget bits. - * - * @param {object} actions {ID: {attributes..}+} map of egw action information - */ - _link_actions: function(actions) - { - if(!this._actionObject) - { - // Get the top level element - timegrid or so - var objectManager = this.getParent()._actionObject || this.getParent().getParent()._actionObject || - egw_getAppObjectManager(true).getObjectById(this._parent._parent._parent.id) || egw_getAppObjectManager(true); - this._actionObject = objectManager.getObjectById('calendar::'+this.options.value.row_id); - } - - if (this._actionObject == null) { - // Add a new container to the object manager which will hold the widget - // objects - this._actionObject = objectManager.insertObject(false, new egwActionObject( - 'calendar::'+this.options.value.row_id, objectManager, new et2_event_action_object_impl(this,this.getDOMNode()), - this._actionManager || objectManager.manager.getActionById('calendar::'+this.options.value.row_id) || objectManager.manager - )); - } - else - { - this._actionObject.setAOI(new et2_event_action_object_impl(this, this.getDOMNode())); - } - - // Delete all old objects - this._actionObject.clear(); - this._actionObject.unregisterActions(); - - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - var action_links = this._get_action_links(actions); - action_links.push('egw_link_drag'); - action_links.push('egw_link_drop'); - if(this._actionObject.parent.getActionLink('invite')) - { - action_links.push('invite'); - } - this._actionObject.updateActionLinks(action_links); - }, - - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes: function(_attrs) { - - }, - - getDetachedNodes: function() { - return [this.getDOMNode()]; - }, - - setDetachedAttributes: function(_nodes, _values) { - - }, -});}).call(this); -et2_register_widget(et2_calendar_event, ["calendar-event"]); - -// Static class stuff -/** - * Check event owner against a parent object - * - * As an event is edited, its participants may change. Also, as the state - * changes we may change which events are displayed and show the same event - * in several places for different users. Here we check the event participants - * against an owner value (which may be an array) to see if the event should be - * displayed or included. - * - * @param {Object} event - Event information - * @param {et2_widget_daycol|et2_widget_planner_row} parent - potential parent object - * that has an owner option - * @param {boolean} owner_too - Include the event owner in consideration, or only - * event participants - * - * @return {boolean} Should the event be displayed - */ -et2_calendar_event.owner_check = function owner_check(event, parent, owner_too) -{ - var owner_match = true; - if(typeof owner_too === 'undefined' && app.calendar.state.status_filter) - { - owner_too = app.calendar.state.status_filter === 'owner'; - } - var options = false; - if(app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) - { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); - } - else - { - options = parent.getArrayMgr("sel_options").getRoot().getEntry('owner'); - } - if(event.participants && typeof parent.options.owner != 'undefined' && parent.options.owner.length > 0) - { - var parent_owner = jQuery.extend([], typeof parent.options.owner !== 'object' ? - [parent.options.owner] : - parent.options.owner); - owner_match = false; - var length = parent_owner.length; - for(var i = 0; i < length; i++ ) - { - // Handle groups & grouped resources like mailing lists, they won't match so - // we need the list - pull it from sidebox owner - if((isNaN(parent_owner[i]) || parent_owner[i] < 0) && options && options.find) - { - var resource = options.find(function(element) {return element.id == parent_owner[i];}) || {}; - if(resource && resource.resources) - { - parent_owner.splice(i,1); - parent_owner = parent_owner.concat(resource.resources); - continue; - } - } - } - var participants = jQuery.extend([],Object.keys(event.participants)); - for(var i = 0; i < participants.length; i++ ) - { - var id = participants[i]; - // Expand group invitations - if (parseInt(id) < 0) - { - // Add in groups, if we can get them from options, great - var resource; - if(options && options.find && (resource = options.find(function(element) {return element.id === id;})) && resource.resources) - { - participants = participants.concat(resource.resources); - } - else - { - // Add in groups, if we can get them (this is asynchronous) - egw.accountData(id,'account_id',true,function(members) { - participants = participants.concat(Object.keys(members)); - }); - } - } - if(parent.options.owner == id || - parent_owner.indexOf && - parent_owner.indexOf(id) >= 0) - { - owner_match = true; - break; - } - } - } - if(owner_too && !owner_match) - { - owner_match = (parent.options.owner == event.owner || - parent_owner.indexOf && - parent_owner.indexOf(event.owner) >= 0); - } - return owner_match; -}; - -/** - * @callback et2_calendar_event~prompt_callback - * @param {string} button_id - One of ok, exception, series, single or cancel - * depending on which buttons are on the prompt - * @param {Object} event_data - Event information - whatever you passed in to - * the prompt. - */ -/** - * Recur prompt - * If the event is recurring, asks the user if they want to edit the event as - * an exception, or change the whole series. Then the callback is called. - * - * If callback is not provided, egw.open() will be used to open an edit dialog. - * - * If you call this on a single (non-recurring) event, the callback will be - * executed immediately, with the passed button_id as 'single'. - * - * @param {Object} event_data - Event information - * @param {string} event_data.id - Unique ID for the event, possibly with a - * timestamp - * @param {string|Date} event_data.start - Start date/time for the event - * @param {number} event_data.recur_type - Recur type, or 0 for a non-recurring event - * @param {et2_calendar_event~prompt_callback} [callback] - Callback is - * called with the button (exception, series, single or cancel) and the event - * data. - * @param {Object} [extra_data] - Additional data passed to the callback, used - * as extra parameters for default callback - * - * @augments {et2_calendar_event} - */ -et2_calendar_event.recur_prompt = function(event_data, callback, extra_data) -{ - var edit_id = event_data.app_id; - var edit_date = event_data.start; - - // seems window.opener somehow in certian conditions could be from different origin - // we try to catch the exception and in this case retrive the egw object from current window. - try { - var egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : window.opener && typeof window.opener.egw != 'undefined' ? window.opener.egw('calendar'):window.egw('calendar'); - } - catch(e){ - var egw = window.egw('calendar'); - } - - var that = this; - - var extra_params = extra_data && typeof extra_data == 'object' ? extra_data : {}; - extra_params.date = edit_date.toJSON ? edit_date.toJSON() : edit_date; - if(typeof callback != 'function') - { - callback = function(_button_id) - { - switch(_button_id) - { - case 'exception': - extra_params.exception = '1'; - egw.open(edit_id, event_data.app||'calendar', 'edit', extra_params); - break; - case 'series': - case 'single': - egw.open(edit_id, event_data.app||'calendar', 'edit', extra_params); - break; - case 'cancel': - default: - break; - } - }; - } - if(parseInt(event_data.recur_type)) - { - var buttons = [ - {text: egw.lang("Edit exception"), id: "exception", class: "ui-priority-primary", "default": true}, - {text: egw.lang("Edit series"), id:"series"}, - {text: egw.lang("Cancel"), id:"cancel"} - ]; - et2_dialog.show_dialog( - function(button_id) {callback.call(that, button_id, event_data);}, - (!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" + - egw.lang("Do you want to edit this event as an exception or the whole series?"), - egw.lang("This event is part of a series"), {}, buttons, et2_dialog.QUESTION_MESSAGE - ); - } - else - { - callback.call(this,'single',event_data); - } -}; - -/** - * Split series prompt - * - * If the event is recurring and the user adjusts the time or duration, we may need - * to split the series, ending the current one and creating a new one with the changes. - * This prompts the user if they really want to do that. - * - * There is no default callback, and nothing happens if you call this on a - * single (non-recurring) event - * - * @param {Object} event_data - Event information - * @param {string} event_data.id - Unique ID for the event, possibly with a timestamp - * @param {string|Date} instance_date - The date of the edited instance of the event - * @param {et2_calendar_event~prompt_callback} callback - Callback is - * called with the button (ok or cancel) and the event data. - * @augments {et2_calendar_event} - */ -et2_calendar_event.series_split_prompt = function(event_data, instance_date, callback) -{ - // seems window.opener somehow in certian conditions could be from different origin - // we try to catch the exception and in this case retrive the egw object from current window. - try { - var egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : window.opener && typeof window.opener.egw != 'undefined' ? window.opener.egw('calendar'):window.egw('calendar'); - } - catch(e){ - var egw = window.egw('calendar'); - } - - var that = this; - - if(typeof instance_date == 'string') - { - instance_date = new Date(instance_date); - } - - // Check for modifying a series that started before today - var tempDate = new Date(); - var today = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(),tempDate.getHours(),-tempDate.getTimezoneOffset(),tempDate.getSeconds()); - var termination_date = instance_date < today ? egw.lang('today') : date(egw.preference('dateformat'),instance_date); - - if(parseInt(event_data.recur_type)) - { - et2_dialog.show_dialog( - function(button_id) {callback.call(that, button_id, event_data);}, - (!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" + - egw.lang("Do you really want to change the start of this series? If you do, the original series will be terminated as of %1 and a new series for the future reflecting your changes will be created.", termination_date), - egw.lang("This event is part of a series"), {}, et2_dialog.BUTTONS_OK_CANCEL , et2_dialog.WARNING_MESSAGE - ); - } -}; - -et2_calendar_event.drag_helper = function(event,ui) { - ui.helper.width(ui.width()); -}; -/** -* splits the combined status, quantity and role -* -* @param {string} status - combined value, O: status letter: U, T, A, R -* @param {int} [quantity] - quantity -* @param {string} [role] -* @return string status U, T, A or R, same as $status parameter on return -*/ -et2_calendar_event.split_status = function(status,quantity,role) -{ - quantity = 1; - role = 'REQ-PARTICIPANT'; - //error_log(__METHOD__.__LINE__.array2string($status)); - var matches = null; - if (typeof status === 'string' && status.length > 1) - { - matches = status.match(/^.([0-9]*)(.*)$/gi); - } - if(matches) - { - if (parseInt(matches[1]) > 0) quantity = parseInt(matches[1]); - if (matches[2]) role = matches[2]; - status = status[0]; - } - else if (status === true) - { - status = 'U'; - } - return status; -} - -/** - * The egw_action system requires an egwActionObjectInterface Interface implementation - * to tie actions to DOM nodes. I'm not sure if we need this. - * - * The class extension is different than the widgets - * - * @param {et2_DOMWidget} widget - * @param {Object} node - * - */ -function et2_event_action_object_impl(widget, node) -{ - var aoi = new et2_action_object_impl(widget, node); - -// _outerCall may be used to determine, whether the state change has been -// evoked from the outside and the stateChangeCallback has to be called -// or not. - aoi.doSetState = function(_state, _outerCall) { - }; - - return aoi; -}; +var et2_calendar_event = /** @class */ (function (_super) { + __extends(et2_calendar_event, _super); + /** + * Constructor + */ + function et2_calendar_event(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_calendar_event._attributes, _child || {})) || this; + _this._need_actions_linked = false; + var event = _this; + // Main container + _this.div = jQuery(document.createElement("div")) + .addClass("calendar_calEvent") + .addClass(_this.options.class) + .css('width', _this.options.width) + .on('mouseenter', function () { + // Bind actions on first mouseover for faster creation + if (event._need_actions_linked) { + event._copy_parent_actions(); + } + // Tooltip + if (!event._tooltipElem) { + event.options.statustext_html = true; + event.set_statustext(event._tooltip()); + if (event.statustext) { + return event.div.trigger('mouseenter'); + } + } + // Hacky to remove egw's tooltip border and let the mouse in + window.setTimeout(function () { + jQuery('body .egw_tooltip') + .css('border', 'none') + .on('mouseenter', function () { + event.div.off('mouseleave.tooltip'); + jQuery('body.egw_tooltip').remove(); + jQuery('body').append(this); + jQuery(this).stop(true).fadeTo(400, 1) + .on('mouseleave', function () { + jQuery(this).fadeOut('400', function () { + jQuery(this).remove(); + // Set up to work again + event.set_statustext(event._tooltip()); + }); + }); + }); + }, 105); + }); + _this.title = jQuery(document.createElement('div')) + .addClass("calendar_calEventHeader") + .appendTo(_this.div); + _this.body = jQuery(document.createElement('div')) + .addClass("calendar_calEventBody") + .appendTo(_this.div); + _this.icons = jQuery(document.createElement('div')) + .addClass("calendar_calEventIcons") + .appendTo(_this.title); + _this.setDOMNode(_this.div[0]); + return _this; + } + et2_calendar_event.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.call(this); + // Already know what is needed to hook to cache + if (this.options.value && this.options.value.row_id) { + egw.dataRegisterUID('calendar::' + this.options.value.row_id, this._UID_callback, this, this.getInstanceManager().execId, this.id); + } + return true; + }; + et2_calendar_event.prototype.destroy = function () { + _super.prototype.destroy.call(this); + if (this._actionObject) { + this._actionObject.remove(); + this._actionObject = null; + } + this.div.off(); + this.title.remove(); + this.title = null; + this.body.remove(); + this.body = null; + this.icons = null; + this.div.remove(); + this.div = null; + jQuery('body.egw_tooltip').remove(); + // Unregister, or we'll continue to be notified... + if (this.options.value) { + var old_app_id = this.options.value.row_id; + egw.dataUnregisterUID('calendar::' + old_app_id, null, this); + } + }; + et2_calendar_event.prototype.set_value = function (_value) { + // Un-register for updates + if (this.options.value) { + var old_id = this.options.value.row_id; + if (!_value || !_value.row_id || old_id !== _value.row_id) { + egw.dataUnregisterUID('calendar::' + old_id, null, this); + } + } + this.options.value = _value; + // Register for updates + var id = this.options.value.row_id; + if (!old_id || old_id !== id) { + egw.dataRegisterUID('calendar::' + id, this._UID_callback, this, this.getInstanceManager().execId, this.id); + } + if (_value && !egw.dataHasUID('calendar::' + id)) { + egw.dataStoreUID('calendar::' + id, _value); + } + }; + /** + * Callback for changes in cached data + */ + et2_calendar_event.prototype._UID_callback = function (event) { + // Copy to avoid changes, which may cause nm problems + var value = event === null ? null : jQuery.extend({}, event); + var parent = this.getParent(); + // Make sure id is a string, check values + if (value) { + this._values_check(value); + } + // Check for changing days in the grid view + if (!this._sameday_check(value)) { + // May need to update parent to remove out-of-view events + parent.removeChild(this); + if (event === null && parent && parent.instanceOf(et2_widget_daycol_1.et2_calendar_daycol)) { + parent._out_of_view(); + } + // This should now cease to exist, as new events have been created + this.destroy(); + return; + } + // Copy to avoid changes, which may cause nm problems + this.options.value = jQuery.extend({}, value); + if (this.getParent().options.date) { + this.options.value.date = this.getParent().options.date; + } + // Let parent position - could also be et2_calendar_planner_row + this.getParent().position_event(this); + // Parent may remove this if the date isn't the same + if (this.getParent()) { + this._update(); + } + }; + /** + * Draw the event + */ + et2_calendar_event.prototype._update = function () { + // Update to reflect new information + var event = this.options.value; + var id = event.row_id ? event.row_id : event.id + (event.recur_type ? ':' + event.recur_date : ''); + var formatted_start = event.start.toJSON(); + this.set_id('event_' + id); + if (this._actionObject) { + this._actionObject.id = 'calendar::' + id; + } + this._need_actions_linked = !this.options.readonly; + // Make sure category stuff is there + // Fake it to use the cache / call - if already there, these will return + // immediately. + var im = this.getInstanceManager(); + et2_selectbox.cat_options({ + _type: 'select-cat', + getInstanceManager: function () { return im; } + }, { application: event.app || 'calendar' }); + // Need cleaning? (DnD helper removes content) + // @ts-ignore + if (!this.div.has(this.title).length) { + this.div + .empty() + .append(this.title) + .append(this.body); + } + if (!this.getParent().options.readonly && !this.options.readonly && this.div.droppable('instance')) { + this.div + // Let timegrid always get the drag + .droppable('option', 'greedy', false); + } + // DOM nodes + this.div + // Set full day flag + .attr('data-full_day', event.whole_day) + // Put everything we need for basic interaction here, so it's available immediately + .attr('data-id', event.id) + .attr('data-app', event.app || 'calendar') + .attr('data-app_id', event.app_id) + .attr('data-start', formatted_start) + .attr('data-owner', event.owner) + .attr('data-recur_type', event.recur_type) + .attr('data-resize', event.whole_day ? 'WD' : '' + (event.recur_type ? 'S' : '')) + .attr('data-priority', event.priority) + // Remove any category classes + .removeClass(function (index, css) { + return (css.match(/(^|\s)cat_\S+/g) || []).join(' '); + }) + // Remove any status classes + .removeClass(function (index, css) { + return (css.match(/calendar_calEvent\S+/g) || []).join(' '); + }) + .removeClass('calendar_calEventSmall') + .addClass(event.class) + .toggleClass('calendar_calEventPrivate', typeof event.private !== 'undefined' && event.private); + this.options.class = event.class; + var status_class = this._status_class(); + // Add category classes, if real categories are set + if (event.category && event.category != '0') { + var cats = event.category.split(','); + for (var i = 0; i < cats.length; i++) { + this.div.addClass('cat_' + cats[i]); + } + } + this.div.toggleClass('calendar_calEventUnknown', event.participants[egw.user('account_id')] ? event.participants[egw.user('account_id')][0] === 'U' : false); + this.div.addClass(status_class); + this.body.toggleClass('calendar_calEventBodySmall', event.whole_day_on_top || false); + // Header + var title = !event.is_private ? egw.htmlspecialchars(event['title']) : egw.lang('private'); + this.title + .html('' + this._get_timespan(event) + '
') + .append('' + title + ''); + // Colors - don't make them transparent if there is no color + // @ts-ignore + if (jQuery.Color("rgba(0,0,0,0)").toRgbaString() != jQuery.Color(this.div, 'background-color').toRgbaString()) { + // Most statuses use colored borders + this.div.css('border-color', this.div.css('background-color')); + } + this.icons.appendTo(this.title) + .html(this._icons().join('')); + // Body + if (event.whole_day_on_top) { + this.body.html(title); + } + else { + // @ts-ignore + var start_time = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { + hour: event.start_m / 60, + minute: event.start_m % 60, + seconds: 0, + timezone: 0 + }, { "ampm": (egw.preference("timeformat") === "12") }).trim(); + this.body + .html('' + title + '') + .append('' + start_time + ''); + if (this.options.value.description.trim()) { + this.body + .append('

' + egw.htmlspecialchars(this.options.value.description) + '

'); + } + } + // Clear tooltip for regeneration + this.set_statustext(''); + // Height specific section + // This can take an unreasonable amount of time if parent is hidden + if (jQuery(this.getParent().getDOMNode(this)).is(':visible')) { + this._small_size(); + } + }; + /** + * Calculate display variants for when event is too short for full display + * + * Display is based on the number of visible lines, calculated off the header + * height: + * 1 - show just the event title, with ellipsis + * 2 - Show timespan and title, with ellipsis + * > 4 - Show description as well, truncated to fit + */ + et2_calendar_event.prototype._small_size = function () { + if (this.options.value.whole_day_on_top) + return; + // Skip for planner view, it's always small + if (this.getParent() && this.getParent().instanceOf(et2_widget_planner_row_1.et2_calendar_planner_row)) + return; + // Pre-calculation reset + this.div.removeClass('calendar_calEventSmall'); + this.body.css('height', 'auto'); + var line_height = parseFloat(this.div.css('line-height')); + var visible_lines = Math.floor(this.div.innerHeight() / line_height); + if (!this.title.height()) { + // Handle sizing while hidden, such as when calendar is not the active tab + visible_lines = Math.floor(egw.getHiddenDimensions(this.div).h / egw.getHiddenDimensions(this.title).h); + } + visible_lines = Math.max(1, visible_lines); + if (this.getParent() && this.getParent().instanceOf(et2_widget_daycol_1.et2_calendar_daycol)) { + this.div.toggleClass('calendar_calEventSmall', visible_lines < 4); + this.div + .attr('data-visible_lines', visible_lines); + } + else if (this.getParent() && this.getParent().instanceOf(et2_widget_planner_row_1.et2_calendar_planner_row)) { + // Less than 8 hours is small + this.div.toggleClass('calendar_calEventSmall', this.options.value.end.valueOf() - this.options.value.start.valueOf() < 28800000); + } + if (this.body.height() > this.div.height() - this.title.height() && visible_lines >= 4) { + this.body.css('height', Math.floor((visible_lines - 1) * line_height - this.title.height()) + 'px'); + } + else { + this.body.css('height', ''); + } + }; + /** + * Examines the participants & returns CSS classname for status + * + * @returns {String} + */ + et2_calendar_event.prototype._status_class = function () { + var status_class = 'calendar_calEventAllAccepted'; + for (var id in this.options.value.participants) { + var status_1 = this.options.value.participants[id]; + status_1 = et2_calendar_event.split_status(status_1); + switch (status_1) { + case 'A': + case '': // app without status + break; + case 'U': + status_class = 'calendar_calEventSomeUnknown'; + return status_class; // break for + default: + status_class = 'calendar_calEventAllAnswered'; + break; + } + } + return status_class; + }; + /** + * Create tooltip shown on hover + * + * @return {String} + */ + et2_calendar_event.prototype._tooltip = function () { + if (!this.div || !this.options.value || !this.options.value.app_id) + return ''; + var border = this.div.css('borderTopColor'); + var bg_color = this.div.css('background-color'); + var header_color = this.title.css('color'); + var timespan = this._get_timespan(this.options.value); + var parent = this.getParent() instanceof et2_widget_daycol_1.et2_calendar_daycol ? this.getParent() : this.getParent(); + parent.date_helper.set_value(this.options.value.start.valueOf ? new Date(this.options.value.start) : this.options.value.start); + var start = parent.date_helper.input_date.val(); + parent.date_helper.set_value(this.options.value.end.valueOf ? new Date(this.options.value.end) : this.options.value.end); + var end = parent.date_helper.input_date.val(); + var times = !this.options.value.multiday ? + '' + this.egw().lang('Time') + ':' + timespan : + '' + this.egw().lang('Start') + ':' + start + ' ' + + '' + this.egw().lang('End') + ':' + end; + var cat_label = ''; + if (this.options.value.category) { + var cat = et2_createWidget('select-cat', { 'readonly': true }, this); + cat.set_value(this.options.value.category); + var cat_label_1 = this.options.value.category.indexOf(',') <= 0 ? cat.span.text() : []; + if (typeof cat_label_1 != 'string') { + cat.span.children().each(function () { + cat_label_1.push(jQuery(this).text()); + }); + cat_label_1 = cat_label_1.join(', '); + } + cat.destroy(); + } + var participants = ''; + if (this.options.value.participant_types['']) { + participants += this.options.value.participant_types[''].join("
"); + } + for (var type_name in this.options.value.participant_types) { + if (type_name) { + participants += '

' + type_name + ':
'; + participants += this.options.value.participant_types[type_name].join("
"); + } + } + return '

' + + '
' + + '' + timespan + '' + + this.icons[0].outerHTML + + '
' + + '
' + + '

' + + '' + egw.htmlspecialchars(this.options.value.title) + '
' + + egw.htmlspecialchars(this.options.value.description) + '

' + + '

' + times + '

' + + (this.options.value.location ? '

' + this.egw().lang('Location') + ':' + + egw.htmlspecialchars(this.options.value.location) + '

' : '') + + (cat_label ? '

' + this.egw().lang('Category') + ':' + cat_label + '

' : '') + + '

' + this.egw().lang('Participants') + ':
' + + participants + '

' + this._participant_summary(this.options.value.participants) + + '
' + + '
'; + }; + /** + * Generate participant summary line + * + * @returns {String} + */ + et2_calendar_event.prototype._participant_summary = function (participants) { + if (Object.keys(this.options.value.participants).length < 2) { + return ''; + } + var participant_status = { A: 0, R: 0, T: 0, U: 0, D: 0 }; + var status_label = { A: 'accepted', R: 'rejected', T: 'tentative', U: 'unknown', D: 'delegated' }; + var participant_summary = Object.keys(this.options.value.participants).length + ' ' + this.egw().lang('Participants') + ': '; + var status_totals = []; + for (var id in this.options.value.participants) { + var status = this.options.value.participants[id].substr(0, 1); + participant_status[status]++; + } + for (var status_2 in participant_status) { + if (participant_status[status_2] > 0) { + status_totals.push(participant_status[status_2] + ' ' + this.egw().lang(status_label[status_2])); + } + } + return participant_summary + status_totals.join(', '); + }; + /** + * Get actual icons from list + */ + et2_calendar_event.prototype._icons = function () { + var icons = []; + if (this.options.value.is_private) { + // Hide everything + icons.push(''); + } + else { + if (this.options.value.app !== 'calendar') { + icons.push(''); + } + if (this.options.value.priority == 3) { + icons.push(''); + } + if (this.options.value.public == '0') { + // Show private flag + icons.push(''); + } + if (this.options.value['recur_type']) { + icons.push(''); + } + // icons for single user, multiple users or group(s) and resources + var single = ''; + var multiple = ''; + for (var uid in this.options.value['participants']) { + // @ts-ignore + if (Object.keys(this.options.value.participants).length == 1 && !isNaN(uid)) { + icons.push(single); + break; + } + // @ts-ignore + if (!isNaN(uid) && icons.indexOf(multiple) === -1) { + icons.push(multiple); + } + /* + * TODO: resource icons + elseif(!isset($icons[$uid[0]]) && isset($this->bo->resources[$uid[0]]) && isset($this->bo->resources[$uid[0]]['icon'])) + { + $icons[$uid[0]] = html::image($this->bo->resources[$uid[0]]['app'], + ($this->bo->resources[$uid[0]]['icon'] ? $this->bo->resources[$uid[0]]['icon'] : 'navbar'), + lang($this->bo->resources[$uid[0]]['app']), + 'width="16px" height="16px"'); + } + */ + } + if (this.options.value.alarm && !jQuery.isEmptyObject(this.options.value.alarm) && !this.options.value.is_private) { + icons.push(''); + } + if (this.options.value.participants[egw.user('account_id')] && this.options.value.participants[egw.user('account_id')][0] == 'U') { + icons.push(''); + } + } + // Always include non-blocking, regardless of privacy + if (this.options.value.non_blocking) { + icons.push(''); + } + return icons; + }; + /** + * Get a text representation of the timespan of the event. Either start + * - end, or 'all day' + * + * @param {Object} event Event to get the timespan for + * @param {number} event.start_m Event start, in minutes from midnight + * @param {number} event.end_m Event end, in minutes from midnight + * + * @return {string} Timespan + */ + et2_calendar_event.prototype._get_timespan = function (event) { + var timespan = ''; + if (event['start_m'] === 0 && event['end_m'] >= 24 * 60 - 1) { + if (event['end_m'] > 24 * 60) { + // @ts-ignore + timespan = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { + hour: event.start_m / 60, + minute: event.start_m % 60, + seconds: 0, + timezone: 0 + }, { "ampm": (egw.preference("timeformat") === "12") } + // @ts-ignore + ).trim() + ' - ' + jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { + hour: event.end_m / 60, + minute: event.end_m % 60, + seconds: 0, + timezone: 0 + }, { "ampm": (egw.preference("timeformat") === "12") }).trim(); + } + else { + timespan = this.egw().lang('Whole day'); + } + } + else { + var duration = event.multiday ? + (event.end - event.start) / 60000 : + (event.end_m - event.start_m); + duration = Math.floor(duration / 60) + this.egw().lang('h') + (duration % 60 ? duration % 60 : ''); + // @ts-ignore + timespan = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { + hour: event.start_m / 60, + minute: event.start_m % 60, + seconds: 0, + timezone: 0 + }, { "ampm": (egw.preference("timeformat") === "12") }).trim(); + // @ts-ignore + timespan += ' - ' + jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { + hour: event.end_m / 60, + minute: event.end_m % 60, + seconds: 0, + timezone: 0 + }, { "ampm": (egw.preference("timeformat") === "12") }).trim(); + timespan += ': ' + duration; + } + return timespan; + }; + /** + * Make sure event data has all proper values, and format them as expected + * @param {Object} event + */ + et2_calendar_event.prototype._values_check = function (event) { + // Make sure ID is a string + if (event.id) { + event.id = '' + event.id; + } + // Parent might be a daycol or a planner_row + var parent = this.getParent(); + // Use dates as objects + if (typeof event.start !== 'object') { + parent.date_helper.set_value(event.start); + event.start = new Date(parent.date_helper.getValue()); + } + if (typeof event.end !== 'object') { + parent.date_helper.set_value(event.end); + event.end = new Date(parent.date_helper.getValue()); + } + // We need minutes for durations + if (typeof event.start_m === 'undefined') { + event.start_m = event.start.getUTCHours() * 60 + event.start.getUTCMinutes(); + event.end_m = event.end.getUTCHours() * 60 + event.end.getUTCMinutes(); + } + if (typeof event.multiday === 'undefined') { + event.multiday = (event.start.getUTCFullYear() !== event.end.getUTCFullYear() || + event.start.getUTCMonth() !== event.end.getUTCMonth() || + event.start.getUTCDate() != event.end.getUTCDate()); + } + if (!event.start.getUTCHours() && !event.start.getUTCMinutes() && event.end.getUTCHours() == 23 && event.end.getUTCMinutes() == 59) { + event.whole_day_on_top = (event.non_blocking && event.non_blocking != '0'); + } + }; + /** + * Check to see if the provided event information is for the same date as + * what we're currently expecting, and that it has not been changed. + * + * If the date has changed, we adjust the associated daywise caches to move + * the event's ID to where it should be. This check allows us to be more + * directly reliant on the data cache, and less on any other control logic + * elsewhere first. + * + * @param {Object} event Map of event data from cache + * @param {string} event.date For non-recurring, single day events, this is + * the date the event is on. + * @param {string} event.start Start of the event (used for multi-day events) + * @param {string} event.end End of the event (used for multi-day events) + * + * @return {Boolean} Provided event data is for the same date + */ + et2_calendar_event.prototype._sameday_check = function (event) { + // Event somehow got orphaned, or deleted + if (!this.getParent() || event === null) { + return false; + } + // Also check participants against owner + var owner_match = et2_calendar_event.owner_check(event, this.getParent()); + // Simple, same day + if (owner_match && this.options.value.date && event.date == this.options.value.date) { + return true; + } + // Multi-day non-recurring event spans days - date does not match + var event_start = new Date(event.start); + var event_end = new Date(event.end); + var parent = this.getParent(); + if (owner_match && (parent instanceof et2_widget_daycol_1.et2_calendar_daycol) && parent.getDate() >= event_start && parent.getDate() <= event_end) { + return true; + } + // Delete all old actions + if (this._actionObject) { + this._actionObject.clear(); + this._actionObject.unregisterActions(); + this._actionObject = null; + } + // Update daywise caches + var new_cache_id = CalendarApp._daywise_cache_id(event.date, this.getParent().options.owner); + var new_daywise = egw.dataGetUIDdata(new_cache_id); + new_daywise = new_daywise && new_daywise.data ? new_daywise.data : []; + var old_cache_id = ''; + if (this.options.value && this.options.value.date) { + old_cache_id = CalendarApp._daywise_cache_id(this.options.value.date, parent.options.owner); + } + if (new_cache_id != old_cache_id) { + var old_daywise = egw.dataGetUIDdata(old_cache_id); + old_daywise = old_daywise && old_daywise.data ? old_daywise.data : []; + old_daywise.splice(old_daywise.indexOf(this.options.value.row_id), 1); + egw.dataStoreUID(old_cache_id, old_daywise); + if (new_daywise.indexOf(event.row_id) < 0) { + new_daywise.push(event.row_id); + } + if (egw.dataHasUID(new_cache_id)) { + egw.dataStoreUID(new_cache_id, new_daywise); + } + } + return false; + }; + et2_calendar_event.prototype.attachToDOM = function () { + var result = _super.prototype.attachToDOM.call(this); + // Remove the binding for the click handler, unless there's something + // custom here. + if (!this.onclick) { + jQuery(this.node).off("click"); + } + return result; + }; + /** + * Click handler calling custom handler set via onclick attribute to this.onclick. + * All other handling is done by the timegrid widget. + * + * @param {Event} _ev + * @returns {boolean} + */ + et2_calendar_event.prototype.click = function (_ev) { + var result = true; + if (typeof this.onclick == 'function') { + // Make sure function gets a reference to the widget, splice it in as 2. argument if not + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.splice(1, 0, this); + result = this.onclick.apply(this, args); + } + return result; + }; + /** + * Show the recur prompt for this event + * + * Calls et2_calendar_event.recur_prompt with this event's value. + * + * @param {et2_calendar_event~prompt_callback} callback + * @param {Object} [extra_data] + */ + et2_calendar_event.prototype.recur_prompt = function (callback, extra_data) { + et2_calendar_event.recur_prompt(this.options.value, callback, extra_data); + }; + /** + * Show the series split prompt for this event + * + * Calls et2_calendar_event.series_split_prompt with this event's value. + * + * @param {et2_calendar_event~prompt_callback} callback + */ + et2_calendar_event.prototype.series_split_prompt = function (callback) { + et2_calendar_event.series_split_prompt(this.options.value, this.options.value.recur_date, callback); + }; + /** + * Copy the actions set on the parent, apply them to self + * + * This can take a while to do, so we try to do it only when needed - on mouseover + */ + et2_calendar_event.prototype._copy_parent_actions = function () { + // Copy actions set in parent + if (!this.options.readonly && !this.getParent().options.readonly) { + var action_parent = this; + while (action_parent != null && !action_parent.options.actions && + !(action_parent instanceof et2_container)) { + action_parent = action_parent.getParent(); + } + try { + this._link_actions(action_parent.options.actions || {}); + this._need_actions_linked = false; + } + catch (e) { + // something went wrong, but keep quiet about it + } + } + }; + /** + * Link the actions to the DOM nodes / widget bits. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + */ + et2_calendar_event.prototype._link_actions = function (actions) { + if (!this._actionObject) { + // Get the top level element - timegrid or so + var objectManager = this.getParent()._actionObject || this.getParent().getParent()._actionObject || + egw_getAppObjectManager(true).getObjectById(this.getParent().getParent().getParent().id) || egw_getAppObjectManager(true); + this._actionObject = objectManager.getObjectById('calendar::' + this.options.value.row_id); + } + if (this._actionObject == null) { + // Add a new container to the object manager which will hold the widget + // objects + this._actionObject = objectManager.insertObject(false, new egwActionObject('calendar::' + this.options.value.row_id, objectManager, et2_calendar_event.et2_event_action_object_impl(this, this.getDOMNode()), this._actionManager || objectManager.manager.getActionById('calendar::' + this.options.value.row_id) || objectManager.manager)); + } + else { + this._actionObject.setAOI(et2_calendar_event.et2_event_action_object_impl(this, this.getDOMNode(this))); + } + // Delete all old objects + this._actionObject.clear(); + this._actionObject.unregisterActions(); + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + var action_links = this._get_action_links(actions); + action_links.push('egw_link_drag'); + action_links.push('egw_link_drop'); + if (this._actionObject.parent.getActionLink('invite')) { + action_links.push('invite'); + } + this._actionObject.updateActionLinks(action_links); + }; + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs array to add further attributes to + */ + et2_calendar_event.prototype.getDetachedAttributes = function (_attrs) { + }; + et2_calendar_event.prototype.getDetachedNodes = function () { + return [this.getDOMNode()]; + }; + et2_calendar_event.prototype.setDetachedAttributes = function (_nodes, _values) { + }; + // Static class stuff + /** + * Check event owner against a parent object + * + * As an event is edited, its participants may change. Also, as the state + * changes we may change which events are displayed and show the same event + * in several places for different users. Here we check the event participants + * against an owner value (which may be an array) to see if the event should be + * displayed or included. + * + * @param {Object} event - Event information + * @param {et2_widget_daycol|et2_widget_planner_row} parent - potential parent object + * that has an owner option + * @param {boolean} [owner_too] - Include the event owner in consideration, or only + * event participants + * + * @return {boolean} Should the event be displayed + */ + et2_calendar_event.owner_check = function (event, parent, owner_too) { + var owner_match = true; + if (typeof owner_too === 'undefined' && app.calendar.state.status_filter) { + owner_too = app.calendar.state.status_filter === 'owner'; + } + var options = null; + if (app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { + options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); + } + else { + options = parent.getArrayMgr("sel_options").getRoot().getEntry('owner'); + } + if (event.participants && typeof parent.options.owner != 'undefined' && parent.options.owner.length > 0) { + var parent_owner = jQuery.extend([], typeof parent.options.owner !== 'object' ? + [parent.options.owner] : + parent.options.owner); + owner_match = false; + var length_1 = parent_owner.length; + for (var i = 0; i < length_1; i++) { + // Handle groups & grouped resources like mailing lists, they won't match so + // we need the list - pull it from sidebox owner + if ((isNaN(parent_owner[i]) || parent_owner[i] < 0) && options && typeof options.find == "function") { + var resource = options.find(function (element) { return element.id == parent_owner[i]; }) || {}; + if (resource && resource.resources) { + parent_owner.splice(i, 1); + parent_owner = parent_owner.concat(resource.resources); + } + } + } + var participants_1 = jQuery.extend([], Object.keys(event.participants)); + var _loop_1 = function () { + var id = participants_1[i]; + // Expand group invitations + if (parseInt(id) < 0) { + if (options && options.find && (resource = options.find(function (element) { return element.id === id; })) && resource.resources) { + participants_1 = participants_1.concat(resource.resources); + } + else { + // Add in groups, if we can get them (this is asynchronous) + egw.accountData(id, 'account_id', true, function (members) { + participants_1 = participants_1.concat(Object.keys(members)); + }, this_1); + } + } + if (parent.options.owner == id || + parent_owner.indexOf && + parent_owner.indexOf(id) >= 0) { + owner_match = true; + return "break"; + } + }; + var this_1 = this, resource; + for (var i = 0; i < participants_1.length; i++) { + var state_1 = _loop_1(); + if (state_1 === "break") + break; + } + } + if (owner_too && !owner_match) { + owner_match = (parent.options.owner == event.owner || + parent_owner.indexOf && + parent_owner.indexOf(event.owner) >= 0); + } + return owner_match; + }; + /** + * @callback et2_calendar_event~prompt_callback + * @param {string} button_id - One of ok, exception, series, single or cancel + * depending on which buttons are on the prompt + * @param {Object} event_data - Event information - whatever you passed in to + * the prompt. + */ + /** + * Recur prompt + * If the event is recurring, asks the user if they want to edit the event as + * an exception, or change the whole series. Then the callback is called. + * + * If callback is not provided, egw.open() will be used to open an edit dialog. + * + * If you call this on a single (non-recurring) event, the callback will be + * executed immediately, with the passed button_id as 'single'. + * + * @param {Object} event_data - Event information + * @param {string} event_data.id - Unique ID for the event, possibly with a + * timestamp + * @param {string|Date} event_data.start - Start date/time for the event + * @param {number} event_data.recur_type - Recur type, or 0 for a non-recurring event + * @param {et2_calendar_event~prompt_callback} [callback] - Callback is + * called with the button (exception, series, single or cancel) and the event + * data. + * @param {Object} [extra_data] - Additional data passed to the callback, used + * as extra parameters for default callback + * + * @augments {et2_calendar_event} + */ + et2_calendar_event.recur_prompt = function (event_data, callback, extra_data) { + var egw; + var edit_id = event_data.app_id; + var edit_date = event_data.start; + // seems window.opener somehow in certain conditions could be from different origin + // we try to catch the exception and in this case retrieve the egw object from current window. + try { + egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : window.opener && typeof window.opener.egw != 'undefined' ? window.opener.egw('calendar') : window.egw('calendar'); + } + catch (e) { + egw = window.egw('calendar'); + } + var that = this; + var extra_params = extra_data && typeof extra_data == 'object' ? extra_data : {}; + extra_params.date = edit_date.toJSON ? edit_date.toJSON() : edit_date; + if (typeof callback != 'function') { + callback = function (_button_id) { + switch (_button_id) { + case 'exception': + extra_params.exception = '1'; + egw.open(edit_id, event_data.app || 'calendar', 'edit', extra_params); + break; + case 'series': + case 'single': + egw.open(edit_id, event_data.app || 'calendar', 'edit', extra_params); + break; + case 'cancel': + default: + break; + } + }; + } + if (parseInt(event_data.recur_type)) { + var buttons = [ + { text: egw.lang("Edit exception"), id: "exception", class: "ui-priority-primary", "default": true }, + { text: egw.lang("Edit series"), id: "series" }, + { text: egw.lang("Cancel"), id: "cancel" } + ]; + et2_dialog.show_dialog(function (button_id) { callback.call(that, button_id, event_data); }, (!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" + + egw.lang("Do you want to edit this event as an exception or the whole series?"), egw.lang("This event is part of a series"), {}, buttons, et2_dialog.QUESTION_MESSAGE); + } + else { + callback.call(this, 'single', event_data); + } + }; + /** + * Split series prompt + * + * If the event is recurring and the user adjusts the time or duration, we may need + * to split the series, ending the current one and creating a new one with the changes. + * This prompts the user if they really want to do that. + * + * There is no default callback, and nothing happens if you call this on a + * single (non-recurring) event + * + * @param {Object} event_data - Event information + * @param {string} event_data.id - Unique ID for the event, possibly with a timestamp + * @param {string|Date} instance_date - The date of the edited instance of the event + * @param {et2_calendar_event~prompt_callback} callback - Callback is + * called with the button (ok or cancel) and the event data. + * @augments {et2_calendar_event} + */ + et2_calendar_event.series_split_prompt = function (event_data, instance_date, callback) { + var egw; + // seems window.opener somehow in certian conditions could be from different origin + // we try to catch the exception and in this case retrieve the egw object from current window. + try { + egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : window.opener && typeof window.opener.egw != 'undefined' ? window.opener.egw('calendar') : window.egw('calendar'); + } + catch (e) { + egw = window.egw('calendar'); + } + var that = this; + if (typeof instance_date == 'string') { + instance_date = new Date(instance_date); + } + // Check for modifying a series that started before today + var tempDate = new Date(); + var today = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(), tempDate.getHours(), -tempDate.getTimezoneOffset(), tempDate.getSeconds()); + var termination_date = instance_date < today ? egw.lang('today') : date(egw.preference('dateformat'), instance_date); + if (parseInt(event_data.recur_type)) { + et2_dialog.show_dialog(function (button_id) { callback.call(that, button_id, event_data); }, (!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" + + egw.lang("Do you really want to change the start of this series? If you do, the original series will be terminated as of %1 and a new series for the future reflecting your changes will be created.", termination_date), egw.lang("This event is part of a series"), {}, et2_dialog.BUTTONS_OK_CANCEL, et2_dialog.WARNING_MESSAGE); + } + }; + et2_calendar_event.drag_helper = function (event, ui) { + ui.helper.width(ui.width()); + }; + ; + /** + * splits the combined status, quantity and role + * + * @param {string} status - combined value, O: status letter: U, T, A, R + * @param {int} [quantity] - quantity + * @param {string} [role] + * @return string status U, T, A or R, same as $status parameter on return + */ + et2_calendar_event.split_status = function (status, quantity, role) { + quantity = 1; + role = 'REQ-PARTICIPANT'; + //error_log(__METHOD__.__LINE__.array2string($status)); + var matches = null; + if (typeof status === 'string' && status.length > 1) { + matches = status.match(/^.([0-9]*)(.*)$/gi); + } + if (matches) { + if (parseInt(matches[1]) > 0) + quantity = parseInt(matches[1]); + if (matches[2]) + role = matches[2]; + status = status[0]; + } + else if (status === true) { + status = 'U'; + } + return status; + }; + /** + * The egw_action system requires an egwActionObjectInterface Interface implementation + * to tie actions to DOM nodes. I'm not sure if we need this. + * + * The class extension is different than the widgets + * + * @param {et2_DOMWidget} widget + * @param {Object} node + * + */ + et2_calendar_event.et2_event_action_object_impl = function (widget, node) { + var aoi = new et2_core_DOMWidget_1.et2_action_object_impl(widget, node).getAOI(); + // _outerCall may be used to determine, whether the state change has been + // evoked from the outside and the stateChangeCallback has to be called + // or not. + aoi.doSetState = function (_state, _outerCall) { + }; + return aoi; + }; + + et2_calendar_event._attributes = { + "value": { + type: "any", + default: et2_no_init + }, + "onclick": { + "description": "JS code which is executed when the element is clicked. " + + "If no handler is provided, or the handler returns true and the event is not read-only, the " + + "event will be opened according to calendar settings." + } + }; + return et2_calendar_event; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_calendar_event = et2_calendar_event; +et2_core_widget_1.et2_register_widget(et2_calendar_event, ["calendar-event"]); +//# sourceMappingURL=et2_widget_event.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_owner.js b/calendar/js/et2_widget_owner.js index 6f82eff507..f0f9f234ec 100644 --- a/calendar/js/et2_widget_owner.js +++ b/calendar/js/et2_widget_owner.js @@ -1,3 +1,4 @@ +"use strict"; /* * Egroupware * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License @@ -7,12 +8,24 @@ * @author Nathan Gray * @version $Id$ */ - - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - et2_widget_taglist; + et2_widget_taglist; */ - +var et2_core_widget_1 = require("../../api/js/etemplate/et2_core_widget"); /** * Tag list widget customised for calendar owner, which can be a user * account or group, or an entry from almost any app, or an email address @@ -23,117 +36,109 @@ * @see http://nicolasbize.github.io/magicsuggest/ * @augments et2_selectbox */ -var et2_calendar_owner = (function(){ "use strict"; return et2_taglist_email.extend( -{ - attributes: { - "autocomplete_url": { - "default": "calendar_owner_etemplate_widget::ajax_owner" - }, - "autocomplete_params": { - "name": "Autocomplete parameters", - "type": "any", - "default": {}, - "description": "Extra parameters passed to autocomplete URL. It should be a stringified JSON object." - }, - allowFreeEntries: { - "default": false, - ignore: true - }, - select_options: { - "type": "any", - "name": "Select options", - // Set to empty object to use selectbox's option finding - "default": {}, - "description": "Internally used to hold the select options." - } - }, - - // Allows sub-widgets to override options to the library - lib_options: { - autoSelect: false, - groupBy: 'app', - minChars: 2, - selectFirst: true, - // This option will also expand when the selection is changed - // via code, which we do not want - //expandOnFocus: true - toggleOnClick: true - }, - - - doLoadingFinished: function() { - this._super.apply(this, arguments); - - var widget = this; - // onChange fired when losing focus, which is different from normal - this._oldValue = this.taglist.getValue(); - - return true; - }, - - selectionRenderer: function(item) - { - if(this && this.options && this.options.allowFreeEntries) - { - return this._super.apply(this,arguments); - } - else - { - var label = jQuery('').text(item.label); - if (item.class) label.addClass(item.class); - if (typeof item.title != 'undefined') label.attr('title', item.title); - if (typeof item.data != 'undefined') label.attr('data', item.data); - if (typeof item.icon != 'undefined') - { - var wrapper = jQuery('
').addClass('et2_taglist_tags_icon_wrapper'); - jQuery('') - .addClass('et2_taglist_tags_icon') - .css({"background-image": "url("+(item.icon.match(/^(http|https|\/)/) ? item.icon : egw.image(item.icon, item.app))+")"}) - .appendTo(wrapper); - label.appendTo(wrapper); - return wrapper; - } - return label; - } - }, - - getValue: function() - { - if(this.taglist == null) return null; - return this.taglist.getValue(); - }, - - /** - * Override parent to handle our special additional data types (c#,r#,etc.) when they - * are not available client side. - * - * @param {string|string[]} _value array of selected owners, which can be a number, - * or a number prefixed with one character indicating the resource type. - */ - set_value: function(_value) - { - this._super.apply(this, arguments); - - // If parent didn't find a label, label will be the same as ID so we - // can find them that way - for(var i = 0; i < this.options.value.length; i++) - { - var value = this.options.value[i]; - if(value.id == value.label) - { - // Proper label was not fount by parent - ask directly - egw.json('calendar_owner_etemplate_widget::ajax_owner',value.id,function(data) { - this.widget.options.value[this.i].label = data; - this.widget.set_value(this.widget.options.value); - }, this,true,{widget: this, i: i}).sendRequest(); - } - } - - if(this.taglist) - { - this.taglist.clear(true); - this.taglist.addToSelection(this.options.value,true); - } - } -});}).call(this); -et2_register_widget(et2_calendar_owner, ["calendar-owner"]); \ No newline at end of file +var et2_calendar_owner = /** @class */ (function (_super) { + __extends(et2_calendar_owner, _super); + function et2_calendar_owner() { + var _this = _super !== null && _super.apply(this, arguments) || this; + // Allows sub-widgets to override options to the library + _this.lib_options = { + autoSelect: false, + groupBy: 'app', + minChars: 2, + selectFirst: true, + // This option will also expand when the selection is changed + // via code, which we do not want + //expandOnFocus: true + toggleOnClick: true + }; + return _this; + } + et2_calendar_owner.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.call(this); + var widget = this; + // onChange fired when losing focus, which is different from normal + this._oldValue = this.taglist.getValue(); + return true; + }; + et2_calendar_owner.prototype.selectionRenderer = function (item) { + if (this && this.options && this.options.allowFreeEntries) { + return _super.prototype.selectionRenderer.call(this, item); + } + else { + var label = jQuery('').text(item.label); + if (item.class) + label.addClass(item.class); + if (typeof item.title != 'undefined') + label.attr('title', item.title); + if (typeof item.data != 'undefined') + label.attr('data', item.data); + if (typeof item.icon != 'undefined') { + var wrapper = jQuery('
').addClass('et2_taglist_tags_icon_wrapper'); + jQuery('') + .addClass('et2_taglist_tags_icon') + .css({ "background-image": "url(" + (item.icon.match(/^(http|https|\/)/) ? item.icon : egw.image(item.icon, item.app)) + ")" }) + .appendTo(wrapper); + label.appendTo(wrapper); + return wrapper; + } + return label; + } + }; + et2_calendar_owner.prototype.getValue = function () { + if (this.taglist == null) + return null; + return this.taglist.getValue(); + }; + /** + * Override parent to handle our special additional data types (c#,r#,etc.) when they + * are not available client side. + * + * @param {string|string[]} _value array of selected owners, which can be a number, + * or a number prefixed with one character indicating the resource type. + */ + et2_calendar_owner.prototype.set_value = function (_value) { + _super.prototype.set_value.call(this, _value); + // If parent didn't find a label, label will be the same as ID so we + // can find them that way + for (var i = 0; i < this.options.value.length; i++) { + var value = this.options.value[i]; + if (value.id == value.label) { + // Proper label was not fount by parent - ask directly + egw.json('calendar_owner_etemplate_widget::ajax_owner', value.id, function (data) { + this.widget.options.value[this.i].label = data; + this.widget.set_value(this.widget.options.value); + }, this, true, { widget: this, i: i }).sendRequest(); + } + } + if (this.taglist) { + this.taglist.clear(true); + this.taglist.addToSelection(this.options.value, true); + } + }; + et2_calendar_owner._attributes = { + "autocomplete_url": { + "default": "calendar_owner_etemplate_widget::ajax_owner" + }, + "autocomplete_params": { + "name": "Autocomplete parameters", + "type": "any", + "default": {}, + "description": "Extra parameters passed to autocomplete URL. It should be a stringified JSON object." + }, + allowFreeEntries: { + "default": false, + ignore: true + }, + select_options: { + "type": "any", + "name": "Select options", + // Set to empty object to use selectbox's option finding + "default": {}, + "description": "Internally used to hold the select options." + } + }; + return et2_calendar_owner; +}(et2_taglist_email)); +exports.et2_calendar_owner = et2_calendar_owner; +et2_core_widget_1.et2_register_widget(et2_calendar_owner, ["calendar-owner"]); +//# sourceMappingURL=et2_widget_owner.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_planner.js b/calendar/js/et2_widget_planner.js index 7b716af221..5473623a2d 100644 --- a/calendar/js/et2_widget_planner.js +++ b/calendar/js/et2_widget_planner.js @@ -1,3 +1,4 @@ +"use strict"; /* * Egroupware Calendar timegrid * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License @@ -7,14 +8,31 @@ * @author Nathan Gray * @version $Id$ */ - - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /calendar/js/et2_widget_view.js; - /calendar/js/et2_widget_planner_row.js; - /calendar/js/et2_widget_event.js; + /calendar/js/et2_widget_view.js; + /calendar/js/et2_widget_planner_row.js; + /calendar/js/et2_widget_event.js; */ - +var et2_core_widget_1 = require("../../api/js/etemplate/et2_core_widget"); +var et2_core_inheritance_1 = require("../../api/js/etemplate/et2_core_inheritance"); +var et2_widget_view_1 = require("./et2_widget_view"); +var et2_core_DOMWidget_1 = require("../../api/js/etemplate/et2_core_DOMWidget"); +var et2_widget_event_1 = require("./et2_widget_event"); +var et2_widget_planner_row_1 = require("./et2_widget_planner_row"); /** * Class which implements the "calendar-planner" XET-Tag for displaying a longer * ( > 10 days) span of time. Events can be grouped into rows by either user, @@ -23,2430 +41,1956 @@ * * @augments et2_calendar_view */ -var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.extend([et2_IDetachedDOM, et2_IResizeable, et2_IPrint], -{ - createNamespace: true, - - attributes: { - group_by: { - name: "Group by", - type: "string", // or category ID - default: "0", - description: "Display planner by 'user', 'month', or the given category" - }, - filter: { - name: "Filter", - type: "string", - default: '', - description: 'A filter that is used to select events. It is passed along when events are queried.' - }, - show_weekend: { - name: "Weekends", - type: "boolean", - default: egw.preference('days_in_weekview','calendar') != 5, - description: "Display weekends. The date range should still include them for proper scrolling, but they just won't be shown." - }, - hide_empty: { - name: "Hide empty rows", - type: "boolean", - default: false, - description: "Hide rows with no events." - }, - value: { - type: "any", - description: "A list of events, optionally you can set start_date, end_date and group_by as keys and events will be fetched" - }, - "onchange": { - "name": "onchange", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when the date range changes." - }, - "onevent_change": { - "name": "onevent_change", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when an event changes." - } - }, - - DEFERRED_ROW_TIME: 100, - - /** - * Constructor - * - * @memberOf et2_calendar_planner - * @constructor - */ - init: function() { - this._super.apply(this, arguments); - - // Main container - this.div = jQuery(document.createElement("div")) - .addClass("calendar_plannerWidget"); - - // Header - this.gridHeader = jQuery(document.createElement("div")) - .addClass("calendar_plannerHeader") - .appendTo(this.div); - this.headerTitle = jQuery(document.createElement("div")) - .addClass("calendar_plannerHeaderTitle") - .appendTo(this.gridHeader); - this.headers = jQuery(document.createElement("div")) - .addClass("calendar_plannerHeaderRows") - .appendTo(this.gridHeader); - - this.rows = jQuery(document.createElement("div")) - .addClass("calendar_plannerRows") - .appendTo(this.div); - this.grid = jQuery(document.createElement("div")) - .addClass("calendar_plannerGrid") - .appendTo(this.div); - - this.vertical_bar = jQuery(document.createElement("div")) - .addClass('verticalBar') - .appendTo(this.div); - - this.value = []; - - // Update timer, to avoid redrawing twice when changing start & end date - this.update_timer = null; - this.doInvalidate = true; - - this.setDOMNode(this.div[0]); - - this.registeredCallbacks = []; - this.cache = {}; - this._deferred_row_updates = {}; - }, - - destroy: function() { - this._super.apply(this, arguments); - this.div.off(); - - for(var i = 0; i < this.registeredCallbacks.length; i++) - { - egw.dataUnregisterUID(this.registeredCallbacks[i],false,this); - } - }, - - doLoadingFinished: function() { - this._super.apply(this, arguments); - - // Don't bother to draw anything if there's no date yet - if(this.options.start_date) - { - this._drawGrid(); - } - - // Automatically bind drag and resize for every event using jQuery directly - // - no action system - - var planner = this; - - this.cache = {}; - this._deferred_row_updates = {}; - - /** - * If user puts the mouse over an event, then we'll set up resizing so - * they can adjust the length. Should be a little better on resources - * than binding it for every calendar event. - */ - this.div.on('mouseover', '.calendar_calEvent:not(.ui-resizable):not(.rowNoEdit)', function() { - // Load the event - planner._get_event_info(this); - var that = this; - - //Resizable event handler - jQuery(this).resizable - ({ - distance: 10, - grid: [5, 10000], - autoHide: false, - handles: 'e', - containment:'parent', - - /** - * Triggered when the resizable is created. - * - * @param {event} event - * @param {Object} ui - */ - create:function(event, ui) - { - var resizeHelper = event.target.getAttribute('data-resize'); - if (resizeHelper == 'WD' || resizeHelper == 'WDS') - { - jQuery(this).resizable('destroy'); - } - }, - - /** - * If dragging to resize an event, abort drag to create - * - * @param {jQuery.Event} event - * @param {Object} ui - */ - start: function(event, ui) - { - if(planner.drag_create.start) - { - // Abort drag to create, we're dragging to resize - planner._drag_create_end({}); - } - }, - - /** - * Triggered at the end of resizing the calEvent. - * - * @param {event} event - * @param {Object} ui - */ - stop:function(event, ui) - { - var e = new jQuery.Event('change'); - e.originalEvent = event; - e.data = {duration: 0}; - var event_data = planner._get_event_info(this); - var event_widget = planner.getWidgetById(event_data.widget_id); - var sT = event_widget.options.value.start_m; - if (typeof this.dropEnd != 'undefined') - { - var eT = parseInt(this.dropEnd.getUTCHours() * 60) + parseInt(this.dropEnd.getUTCMinutes()); - e.data.duration = ((eT - sT)/60) * 3600; - - if(event_widget) - { - event_widget.options.value.end_m = eT; - event_widget.options.value.duration = e.data.duration; - } - - // Leave the helper there until the update is done - var loading = ui.helper.clone().appendTo(ui.helper.parent()); - - // and add a loading icon so user knows something is happening - jQuery('.calendar_timeDemo',loading).after('
'); - - jQuery(this).trigger(e); - - // That cleared the resize handles, so remove for re-creation... - jQuery(this).resizable('destroy'); - - // Remove loading, done or not - loading.remove(); - } - // Clear the helper, re-draw - if(event_widget) - { - event_widget._parent.position_event(event_widget); - } - }, - - /** - * Triggered during the resize, on the drag of the resize handler - * - * @param {event} event - * @param {Object} ui - */ - resize:function(event, ui) - { - if(planner.options.group_by == 'month') - { - var position = {left: event.clientX, top: event.clientY}; - } - else - { - var position = {top:ui.position.top, left: ui.position.left + ui.helper.width()}; - } - planner._drag_helper(this,position,ui.helper.outerHeight()); - } - }); - }) - .on('mousemove', function(event) { - // Ignore headers - if(planner.headers.has(event.target).length !== 0) - { - planner.vertical_bar.hide(); - return; - } - // Position bar by mouse - planner.vertical_bar.position({ - my: 'right-1', - of: event, - collision: 'fit' - }); - planner.vertical_bar.css('top','0px'); - - // Get time at mouse - if(jQuery(event.target).closest('.calendar_eventRows').length == 0) - { - // "Invalid" times, from space after the last planner row, or header - var time = planner._get_time_from_position(event.pageX - planner.grid.offset().left, 10); - } - else if(planner.options.group_by == 'month') - { - var time = planner._get_time_from_position(event.clientX, event.clientY); - } - else - { - var time = planner._get_time_from_position(event.offsetX, event.offsetY); - } - // Passing to formatter, cancel out timezone - if(time) - { - var formatDate = new Date(time.valueOf() + time.getTimezoneOffset() * 60 * 1000); - planner.vertical_bar - .html(''+date(egw.preference('timeformat','calendar') == 12 ? 'h:ia' : 'H:i',formatDate)+'') - .show(); - - if(planner.drag_create.event && planner.drag_create.parent && planner.drag_create.end) - { - - planner.drag_create.end.date = time.toJSON() - planner._drag_update_event(); - } - } - else - { - // No (valid) time, just hide - planner.vertical_bar.hide(); - } - }) - .on('mousedown', jQuery.proxy(this._mouse_down, this)) - .on('mouseup', jQuery.proxy(this._mouse_up, this)); - - // Actions may be set on a parent, so we need to explicitly get in here - // and get ours - this._link_actions(this.options.actions || this._parent.options.actions || []); - - // Customize and override some draggable settings - this.div.on('dragcreate','.calendar_calEvent', function(event, ui) { - jQuery(this).draggable('option','cancel','.rowNoEdit'); - // Act like you clicked the header, makes it easier to position - jQuery(this).draggable('option','cursorAt', {top: 5, left: 5}); - }) - .on('dragstart', '.calendar_calEvent', function(event,ui) { - jQuery('.calendar_calEvent',ui.helper).width(jQuery(this).width()) - .height(jQuery(this).outerHeight()) - .css('top', '').css('left','') - .appendTo(ui.helper); - ui.helper.width(jQuery(this).width()); - - // Cancel drag to create, we're dragging an existing event - planner._drag_create_end(); - }); - return true; - }, - - /** - * These handle the differences between the different group types. - * They provide the different titles, labels and grouping - */ - groupers: { - // Group by user has one row for each user - user: - { - // Title in top left corner - title: function() { return this.egw().lang('User');}, - // Column headers - headers: function() { - var start = new Date(this.options.start_date); - var end = new Date(this.options.end_date); - var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(),start.getUTCDate()); - var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(),end.getUTCDate()); - var day_count = Math.round((end_date - start_date) /(1000*3600*24))+1; - if(day_count >= 6) - { - this.headers.append(this._header_months(start, day_count)); - } - if(day_count < 120) - { - var weeks = this._header_weeks(start, day_count); - this.headers.append(weeks); - this.grid.append(weeks); - } - if(day_count < 60) - { - var days = this._header_days(start, day_count); - this.headers.append(days); - this.grid.append(days); - } - if(day_count <= 7) - { - var hours = this._header_hours(start, day_count); - this.headers.append(hours); - this.grid.append(hours); - } - }, - // Labels for the rows - row_labels: function() { - var labels = []; - var already_added = []; - var options = false; - var resource = null; - if(app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) - { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); - } - else - { - options = this.getArrayMgr("sel_options").getRoot().getEntry('owner'); - } - for(var i = 0; i < this.options.owner.length; i++) - { - var user = this.options.owner[i]; - // Handle grouped resources like mailing lists - pull it from sidebox owner - // and expand to their contents - if(options && options.find && - ((resource = options.find(function(element) {return element.id == user;}) || {}) || isNaN(user))) - { - if(resource && resource.resources) - { - for(var j = 0; j < resource.resources.length; j++) - { - var id = resource.resources[j]; - if(already_added.indexOf(''+id) < 0) - { - labels.push({ - id: id, - label: this._get_owner_name(id)||'', - data: {participants:id,owner:id} - }); - already_added.push(''+id); - } - } - } - else if(already_added.indexOf(''+user) < 0) - { - labels.push({ - id: user, - label: this._get_owner_name(user), - data: {participants:user,owner:user} - }); - already_added.push(''+user); - } - } - else if (user < 0) // groups - { - egw.accountData(parseInt(user),'account_fullname',true,function(result) { - for(var id in result) - { - if(already_added.indexOf(''+id) < 0) - { - this.push({id: id, label: result[id]||'', data: {participants:id,owner:id}}); - already_added.push(''+id); - } - } - },labels); - } - else // users - { - if(already_added.indexOf(user) < 0) - { - var label = this._get_owner_name(user)||''; - labels.push({id: user, label: label, data: {participants:user,owner:''}}); - already_added.push(''+user); - } - } - } - - return labels.sort(function(a,b) { - return a.label.localeCompare(b.label); - }); - }, - // Group the events into the rows - group: function(labels, rows, event) { - // convert filter to allowed status - var status_to_show = ['U','A','T','D','G']; - switch(this.options.filter) - { - case 'unknown': - status_to_show = ['U','G']; break; - case 'accepted': - status_to_show = ['A']; break; - case 'tentative': - status_to_show = ['T']; break; - case 'rejected': - status_to_show = ['R']; break; - case 'delegated': - status_to_show = ['D']; break; - case 'all': - status_to_show = ['U','A','T','D','G','R']; break; - default: - status_to_show = ['U','A','T','D','G']; break; - } - var participants = event.participants; - var add_row = function(user, participant) { - var label_index = false; - for(var i = 0; i < labels.length; i++) - { - if(labels[i].id == user) - { - label_index = i; - break; - } - } - if(participant && label_index !== false && status_to_show.indexOf(participant.substr(0,1)) >= 0 || - !participant && label_index !== false || - this.options.filter === 'owner' && event.owner === user) - { - if(typeof rows[label_index] === 'undefined') - { - rows[label_index] = []; - } - rows[label_index].push(event); - } - }; - for(var user in participants) - { - var participant = participants[user]; - if (parseInt(user) < 0) // groups - { - var planner = this; - egw.accountData(user,'account_fullname',true,function(result) { - for(var id in result) - { - if(!participants[id]) add_row.call(planner,id,participant); - } - },labels); - continue; - } - add_row.call(this, user, participant); - } - }, - // Draw a single row - draw_row: function(sort_key, label, events) { - var row = this._drawRow(sort_key, label,events,this.options.start_date, this.options.end_date); - if(this.options.hide_empty && !events.length) - { - row.set_disabled(true); - } - // Highlight current user, sort_key is account_id - if(sort_key === egw.user('account_id')) - { - row.set_class('current_user') - } - // Since the daywise cache is by user, we can tap in here - var t = new Date(this.options.start_date); - var end = new Date(this.options.end_date); - do - { - var cache_id = app.classes.calendar._daywise_cache_id(t, sort_key); - egw.dataRegisterUID(cache_id, row._data_callback, row); - - t.setUTCDate(t.getUTCDate() + 1); - } - while(t < end); - return row; - } - }, - - // Group by month has one row for each month - month: - { - title: function() { return this.egw().lang('Month');}, - headers: function() { - this.headers.append(this._header_day_of_month()); - }, - row_labels: function() { - var labels = []; - var d = new Date(this.options.start_date); - d = new Date(d.valueOf() + d.getTimezoneOffset() * 60 * 1000); - for(var i = 0; i < 12; i++) - { - // Not using UTC because we corrected for timezone offset - labels.push({id:sprintf('%04d-%02d', d.getFullYear(), d.getMonth()), label:this.egw().lang(date('F',d))+' '+d.getFullYear()}); - d.setMonth(d.getMonth()+1); - } - return labels; - }, - group: function(labels, rows,event) { - // Yearly planner does not show infologs - if(event && event.app && event.app == 'infolog') return; - - var start = new Date(event.start); - start = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000); - var key = sprintf('%04d-%02d', start.getFullYear(), start.getMonth()); - var label_index = false; - for(var i = 0; i < labels.length; i++) - { - if(labels[i].id == key) - { - label_index = i; - break; - } - } - if(typeof rows[label_index] === 'undefined') - { - rows[label_index] = []; - } - rows[label_index].push(event); - - // end in a different month? - var end = new Date(event.end); - end = new Date(end.valueOf() + end.getTimezoneOffset() * 60 * 1000); - var end_key = sprintf('%04d-%02d',end.getFullYear(),end.getMonth()); - var year = start.getFullYear(); - var month = start.getMonth(); - key = sprintf('%04d-%02d',year,month); - - do - { - var end_label_index = label_index; - - for(var i = end_label_index; i < labels.length; i++) - { - if(labels[i].id == key) - { - end_label_index = i; - if(typeof rows[end_label_index] === 'undefined') - { - rows[end_label_index] = []; - } - break; - } - } - if(end_label_index != label_index) - { - rows[label_index].push(event); - } - if (++month > 11) - { - ++year; - month = 0; - } - key = sprintf('%04d-%02d',year,month); - } while(key <= end_key) - }, - // Draw a single row, but split up the dates - draw_row: function(sort_key, label, events) - { - var key = sort_key.split('-'); - var start = new Date(key[0]+"-"+sprintf("%02d",parseInt(key[1])+1)+"-01T00:00:00Z"); - // Use some care to avoid issues with timezones and daylight savings - var end = new Date(start); - end.setUTCMonth(start.getUTCMonth() + 1); - end.setUTCDate(1); - end.setUTCHours(0); - end.setUTCMinutes(0); - end = new Date(end.valueOf() - 1000); - end.setUTCMonth(start.getUTCMonth()) - this._drawRow(sort_key, label, events, start, end); - } - }, - // Group by category has one row for each [sub]category - category: - { - title: function() { return this.egw().lang('Category');}, - headers: function() { - var start = new Date(this.options.start_date); - var end = new Date(this.options.end_date); - var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(),start.getUTCDate()); - var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(),end.getUTCDate()); - var day_count = Math.round((end_date - start_date) /(1000*3600*24))+1; - - if(day_count >= 6) - { - this.headers.append(this._header_months(start, day_count)); - } - if(day_count < 120) - { - var weeks = this._header_weeks(start, day_count); - this.headers.append(weeks); - this.grid.append(weeks); - } - if(day_count < 60) - { - var days = this._header_days(start, day_count); - this.headers.append(days); - this.grid.append(days); - } - if(day_count <= 7) - { - var hours = this._header_hours(start, day_count); - this.headers.append(hours); - this.grid.append(hours); - } - }, - row_labels: function() { - var im = this.getInstanceManager(); - var categories = et2_selectbox.cat_options({ - _type:'select-cat', - getInstanceManager: function() {return im;} - },{application: 'calendar'}); - - var labels = []; - if(!app.calendar.state.cat_id || - app.calendar.state.cat_id.toString() === '' || - app.calendar.state.cat_id.toString() == '0' - ) - { - app.calendar.state.cat_id = ''; - labels.push({id:'',value:'',label: egw.lang('none'), main: '', data: {}}); - labels = labels.concat(categories); - } - else - { - var cat_id = app.calendar.state.cat_id; - if(typeof cat_id == 'string') - { - cat_id = cat_id.split(','); - } - for(var i = 0; i < cat_id.length; i++) - { - // Find label for that category - for(var j = 0; j < categories.length; j++) - { - if(categories[j].value == cat_id[i]) - { - categories[j].id = categories[j].value; - labels.push(categories[j]); - break; - } - } - - // Get its children immediately - egw.json( - 'EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options', - ['select-cat',',,,calendar,'+cat_id[i]], - function(data) { - labels = labels.concat(data); - } - ).sendRequest(false); - } - } - - for(var i = labels.length -1; i >= 0; i--) - { - labels[i].id = labels[i].value; - labels[i].data = { - cat_id: labels[i].id, - main: labels[i].value==labels[i].main - }; - if(labels[i].children && labels[i].children.length) - { - labels[i].data.has_children = true; - } - } - return labels; - }, - group: function(labels, rows, event) { - var cats = event.category; - if(typeof event.category === 'string') - { - cats = cats.split(','); - } - for(var cat = 0; cat < cats.length; cat++) - { - var label_index = false; - var category = cats[cat] ? parseInt(cats[cat],10) : false; - if(category == 0 || !category) category = ''; - for(var i = 0; i < labels.length; i++) - { - if(labels[i].id == category) - { - // If there's no cat filter, only show the top level - if(!app.calendar.state.cat_id) - { - for(var j = 0; j < labels.length; j++) - { - if(labels[j].id == labels[i].main) - { - label_index = j; - break; - } - } - break; - } - label_index = i; - break; - } - } - if(label_index !== false && typeof rows[label_index] === 'undefined') - { - rows[label_index] = []; - } - if(label_index !== false && rows[label_index].indexOf(event) === -1) - { - rows[label_index].push(event); - } - } - }, - draw_row: function(sort_key, label, events) { - var row = this._drawRow(sort_key, label,events,this.options.start_date, this.options.end_date); - if(this.options.hide_empty && !events.length) - { - row.set_disabled(true); - } - return row; - } - } - }, - - /** - * Something changed, and the planner needs to be re-drawn. We wait a bit to - * avoid re-drawing twice if start and end date both changed, then recreate. - * - * @param {boolean} trigger =false Trigger an event once things are done. - * Waiting until invalidate completes prevents 2 updates when changing the date range. - * @returns {undefined} - */ - invalidate: function(trigger) { - - // Busy - if(!this.doInvalidate) return; - - // Not yet ready - if(!this.options.start_date || !this.options.end_date) return; - - // Wait a bit to see if anything else changes, then re-draw the days - if(this.update_timer !== null) - { - window.clearTimeout(this.update_timer); - } - this.update_timer = window.setTimeout(jQuery.proxy(function() { - this.widget.doInvalidate = false; - - // Show AJAX loader - this.widget.loader.show(); - - this.widget.cache = {}; - this._deferred_row_updates = {}; - - this.widget._fetch_data(); - - this.widget._drawGrid(); - - if(this.trigger) - { - this.widget.change(); - } - this.widget.update_timer = null; - this.widget.doInvalidate = true; - - window.setTimeout(jQuery.proxy(function() {if(this.loader) this.loader.hide();},this.widget),500); - },{widget:this,"trigger":trigger}),ET2_GRID_INVALIDATE_TIMEOUT); - }, - - detachFromDOM: function() { - // Remove the binding to the change handler - jQuery(this.div).off("change.et2_calendar_timegrid"); - - this._super.apply(this, arguments); - }, - - attachToDOM: function() { - this._super.apply(this, arguments); - - // Add the binding for the event change handler - jQuery(this.div).on("change.et2_calendar_timegrid", '.calendar_calEvent', this, function(e) { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.push(this); - - return e.data.event_change.apply(e.data, args); - }); - - // Add the binding for the change handler - jQuery(this.div).on("change.et2_calendar_timegrid", '*:not(.calendar_calEvent)', this, function(e) { - return e.data.change.call(e.data, e, this); - }); - - }, - - getDOMNode: function(_sender) - { - if(_sender === this || !_sender) - { - return this.div[0]; - } - if(_sender._parent === this) - { - return this.rows[0]; - } - }, - - /** - * Creates all the DOM nodes for the planner grid - * - * Any existing nodes (& children) are removed, the headers & labels are - * determined according to the current group_by value, and then the rows - * are created. - * - * @method - * @private - * - */ - _drawGrid: function() - { - - this.div.css('height', this.options.height); - - // Clear old events - var delete_index = this._children.length - 1; - while(this._children.length > 0 && delete_index >= 0) - { - this._children[delete_index].free(); - this.removeChild(this._children[delete_index--]); - } - - // Clear old rows - this.rows.empty() - .append(this.grid); - this.grid.empty(); - - var grouper = this.grouper; - if(!grouper) return; - - // Headers - this.headers.empty(); - this.headerTitle.text(grouper.title.apply(this)); - grouper.headers.apply(this); - this.grid.find('*').contents().filter(function(){ - return this.nodeType === 3; - }).remove(); - - // Get the rows / labels - var labels = grouper.row_labels.call(this); - - // Group the events - var events = {}; - for(var i = 0; i < this.value.length; i++) - { - grouper.group.call(this, labels, events, this.value[i]); - } - - // Set height for rows - this.rows.height(this.div.height() - this.headers.outerHeight()); - - // Draw the rows - for(var key in labels) - { - if (!labels.hasOwnProperty(key)) continue; - - // Skip sub-categories (events are merged into top level) - if(this.options.group_by == 'category' && - (!app.calendar.state.cat_id || app.calendar.state.cat_id == '') && - labels[key].id != labels[key].main - ) - { - continue; - } - var row = grouper.draw_row.call(this,labels[key].id, labels[key].label, events[key] || []); - - // Add extra data for clicking on row - if(row) - { - for(var extra in labels[key].data) - { - row.getDOMNode().dataset[extra] = labels[key].data[extra]; - } - } - } - - // Adjust header if there's a scrollbar - if(this.rows.children().last().length) - { - this.gridHeader.css('margin-right', (this.rows.width() - this.rows.children().last().width()) + 'px'); - } - // Add actual events - for(var key in this._deferred_row_updates) - { - window.clearTimeout(key); - } - window.setTimeout(jQuery.proxy(function() { - this._deferred_row_update(); - }, this ),this.DEFERRED_ROW_TIME) - this.value = []; - }, - - /** - * Draw a single row of the planner - * - * @param {string} key Index into the grouped labels & events - * @param {string} label - * @param {Array} events - * @param {Date} start - * @param {Date} end - */ - _drawRow: function(key, label, events, start, end) - { - var row = et2_createWidget('calendar-planner_row',{ - id: 'planner_row_'+key, - label: label, - start_date: start, - end_date: end, - value: events, - readonly: this.options.readonly - },this); - - - if(this.isInTree()) - { - row.doLoadingFinished(); - } - - return row; - }, - - - _header_day_of_month: function() - { - var day_width = 3.23; // 100.0 / 31; - - // month scale with navigation - var content = '
'; - var start = new Date(this.options.start_date); - start = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000); - var end = new Date(this.options.end_date); - end = new Date(end.valueOf() + end.getTimezoneOffset() * 60 * 1000); - - var title = this.egw().lang(date('F',start))+' '+date('Y',start)+' - '+ - this.egw().lang(date('F',end))+' '+date('Y',end); - - content += '"; - content += "
"; // end of plannerScale - - // day of month scale - content +='
'; - - for(var left = 0, i = 0; i < 31; left += day_width,++i) - { - content += '
'+ - (1+i)+"
\n"; - } - content += "
\n"; - - return content; - }, - - /** - * Make a header showing the months - * @param {Date} start - * @param {number} days - * @returns {string} HTML snippet - */ - _header_months: function(start, days) - { - var content = '
'; - var days_in_month = 0; - var day_width = 100 / days; - var end = new Date(start); - end.setUTCDate(end.getUTCDate()+days); - var t = new Date(start.valueOf()); - for(var left = 0,i = 0; i < days;t.setUTCDate(1),t.setUTCMonth(t.getUTCMonth()+1),left += days_in_month*day_width,i += days_in_month) - { - var u = new Date(t.getUTCFullYear(),t.getUTCMonth()+1,0,-t.getTimezoneOffset()/60); - days_in_month = 1+ ((u-t) / (24*3600*1000)); - - var first = new Date(t.getUTCFullYear(),t.getUTCMonth(),1,-t.getTimezoneOffset()/60); - if(days_in_month <= 0) break; - - if (i + days_in_month > days) - { - days_in_month = days - i; - } - var title = this.egw().lang(date('F',new Date(t.valueOf() + t.getTimezoneOffset() * 60 * 1000))); - if (days_in_month > 10) - { - title += ' '+t.getUTCFullYear(); - } - else if (days_in_month < 5) - { - title = ' '; - } - content += '
'+ - title+"
"; - } - content += "
"; // end of plannerScale - - return content; - }, - - /** - * Make a header showing the week numbers - * - * @param {Date} start - * @param {number} days - * @returns {string} HTML snippet - */ - _header_weeks: function(start, days) - { - - var content = '
'; - var state = ''; - - // we're not using UTC so date() formatting function works - var t = new Date(start.valueOf()); - - // Make sure we're lining up on the week - var week_end = app.calendar.date.end_of_week(start); - var days_in_week = Math.floor(((week_end-start ) / (24*3600*1000))+1); - var week_width = 100 / days * (days <= 7 ? days : days_in_week); - for(var left = 0,i = 0; i < days; t.setUTCDate(t.getUTCDate() + 7),left += week_width) - { - // Avoid overflow at the end - if(days - i < 7) - { - days_in_week = days-i; - } - var usertime = new Date(t.valueOf()); - if(start.getTimezoneOffset() < 0) - { - // Gets the right week # east of GMT. West does not need it(?) - usertime.setUTCMinutes(usertime.getUTCMinutes() - start.getTimezoneOffset()); - } - - week_width = 100 / days * Math.min(days, days_in_week); - - var title = this.egw().lang('Week')+' '+app.calendar.date.week_number(usertime); - - if(start.getTimezoneOffset() > 0) - { - // Gets the right week start west of GMT - usertime.setUTCMinutes(usertime.getUTCMinutes() +start.getTimezoneOffset()); - } - state = app.calendar.date.start_of_week(usertime); - state.setUTCHours(0); - state.setUTCMinutes(0); - state = state.toJSON(); - - if(days_in_week > 1 || days == 1) - { - content += '"; - } - i+= days_in_week; - if(days_in_week != 7) - { - t.setUTCDate(t.getUTCDate() - (7 - days_in_week)); - days_in_week = 7; - } - } - content += "
"; // end of plannerScale - - return content; - }, - - /** - * Make a header for some days - * - * @param {Date} start - * @param {number} days - * @returns {string} HTML snippet - */ - _header_days: function(start, days) - { - var day_width = 100 / days; - var content = '
'; - - // we're not using UTC so date() formatting function works - var t = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000); - for(var left = 0,i = 0; i < days; t.setDate(t.getDate()+1),left += day_width,++i) - { - if(!this.options.show_weekend && [0,6].indexOf(t.getDay()) !== -1 ) continue; - var holidays = []; - var tempDate = new Date(t); - tempDate.setMinutes(tempDate.getMinutes()-tempDate.getTimezoneOffset()); - var title = ''; - var state = ''; - state = new Date(t.valueOf() - t.getTimezoneOffset() * 60 * 1000); - var day_class = this.day_class_holiday(state,holidays, days); - - if (days <= 3) - { - title = this.egw().lang(date('l',t))+', '+date('j',t)+'. '+this.egw().lang(date('F',t)); - } - else if (days <= 7) - { - title = this.egw().lang(date('l',t))+' '+date('j',t); - } - else - { - title = this.egw().lang(date('D',t)).substr(0,2)+'
'+date('j',t); - } - - content += '\n"; - } - content += "
"; // end of plannerScale - - return content; - }, - - /** - * Create a header with hours - * - * @param {Date} start - * @param {number} days - * @returns {string} HTML snippet for the header - */ - _header_hours: function(start,days) - { - var divisors = [1,2,3,4,6,8,12]; - var decr = 1; - for(var i = 0; i < divisors.length; i++) // numbers dividing 24 without rest - { - if (divisors[i] > days) break; - decr = divisors[i]; - } - var hours = days * 24; - if (days === 1) // for a single day we calculate the hours of a days, to take into account daylight saving changes (23 or 25 hours) - { - var t = new Date(start.getUTCFullYear(),start.getUTCMonth(),start.getUTCDate(),-start.getTimezoneOffset()/60); - var s = new Date(start); - s.setUTCHours(23); - s.setUTCMinutes(59); - s.setUTCSeconds(59); - hours = Math.ceil((s.getTime() - t.getTime()) / 3600000); - } - var cell_width = 100 / hours * decr; - - var content = '
'; - - // we're not using UTC so date() formatting function works - var t = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000); - for(var left = 0,i = 0; i < hours; left += cell_width,i += decr) - { - if(!this.options.show_weekend && [0,6].indexOf(t.getDay()) !== -1 ) continue; - var title = date(egw.preference('timeformat','calendar') == 12 ? 'ha' : 'H',t); - - content += '"; - t.setHours(t.getHours()+decr); - } - content += "
"; // end of plannerScale - - return content; - }, - - /** - * Applies class for today, and any holidays for current day - * - * @param {Date} date - * @param {string[]} holiday_list Filled with a list of holidays for that day - * @param {integer} days Number of days shown in the day header - * - * @return {string} CSS Classes for the day. calendar_calBirthday, calendar_calHoliday, calendar_calToday and calendar_weekend as appropriate - */ - day_class_holiday: function(date,holiday_list, days) { - - if(!date) return ''; - - var day_class = ''; - - // Holidays and birthdays - var holidays = et2_calendar_view.get_holidays(this,date.getUTCFullYear()); - - // Pass a string rather than the date object, to make sure it doesn't get changed - this.date_helper.set_value(date.toJSON()); - var date_key = ''+this.date_helper.get_year() + sprintf('%02d',this.date_helper.get_month()) + sprintf('%02d',this.date_helper.get_date()); - if(holidays && holidays[date_key]) - { - holidays = holidays[date_key]; - for(var i = 0; i < holidays.length; i++) - { - if (typeof holidays[i]['birthyear'] !== 'undefined') - { - day_class += ' calendar_calBirthday '; - if(typeof days == 'undefined' || days <= 21) - { - day_class += ' calendar_calBirthdayIcon '; - } - - holiday_list.push(holidays[i]['name']); - } - else - { - day_class += 'calendar_calHoliday '; - - holiday_list.push(holidays[i]['name']); - } - } - } - holidays = holiday_list.join(','); - var today = new Date(); - if(date_key === ''+today.getFullYear()+ - sprintf("%02d",today.getMonth()+1)+ - sprintf("%02d",today.getDate()) - ) - { - day_class += "calendar_calToday "; - } - if(date.getUTCDay() == 0 || date.getUTCDay() == 6) - { - day_class += "calendar_weekend "; - } - return day_class; - }, - - /** - * Link the actions to the DOM nodes / widget bits. - * - * @todo This currently does nothing - * @param {object} actions {ID: {attributes..}+} map of egw action information - */ - _link_actions: function(actions) - { - if(!this._actionObject) - { - // Get the parent? Might be a grid row, might not. Either way, it is - // just a container with no valid actions - var objectManager = egw_getObjectManager(this.getInstanceManager().app,true,1); - objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId,2) || objectManager; - var parent = objectManager.getObjectById(this.id,3) || objectManager.getObjectById(this._parent.id,3) || objectManager; - if(!parent) - { - debugger; - egw.debug('error','No parent objectManager found'); - return; - } - - for(var i = 0; i < parent.children.length; i++) - { - var parent_finder = jQuery('#'+this.div.id, parent.children[i].iface.doGetDOMNode()); - if(parent_finder.length > 0) - { - parent = parent.children[i]; - break; - } - } - } - - // This binds into the egw action system. Most user interactions (drag to move, resize) - // are handled internally using jQuery directly. - var widget_object = this._actionObject || parent.getObjectById(this.id); - - var aoi = new et2_action_object_impl(this,this.getDOMNode()); - - /** - * Determine if we allow a dropped event to use the invite/change actions, - * and enable or disable them appropriately - * - * @param {egwAction} action - * @param {et2_calendar_event} event The event widget being dragged - * @param {egwActionObject} target Planner action object - */ - var _invite_enabled = function(action, event, target) - { - var event = event.iface.getWidget(); - var planner = target.iface.getWidget() || false; - //debugger; - if(event === planner || !event || !planner || - !event.options || !event.options.value.participants || !planner.options.owner - ) - { - return false; - } - var owner_match = false; - var own_row = false; - - for(var id in event.options.value.participants) - { - planner.iterateOver(function(row) { - // Check scroll section or header section - if(row.div.hasClass('drop-hover') || row.div.has(':hover')) - { - owner_match = owner_match || row.node.dataset[planner.options.group_by] === ''+id; - own_row = (row === event.getParent()); - } - }, this, et2_calendar_planner_row); - - } - var enabled = !owner_match && - // Not inside its own row - !own_row; - - widget_object.getActionLink('invite').enabled = enabled; - widget_object.getActionLink('change_participant').enabled = enabled; - - // If invite or change participant are enabled, drag is not - widget_object.getActionLink('egw_link_drop').enabled = !enabled; - }; - - aoi.doTriggerEvent = function(_event, _data) { - - // Determine target node - var event = _data.event || false; - if(!event) return; - if(_data.ui.draggable.hasClass('rowNoEdit')) return; - - /* - We have to handle the drop in the normal event stream instead of waiting - for the egwAction system so we can get the helper, and destination - */ - if(event.type === 'drop') - { - this.getWidget()._event_drop.call(jQuery('.calendar_d-n-d_timeCounter',_data.ui.helper)[0],this.getWidget(),event, _data.ui); - } - var drag_listener = function(event, ui) { - aoi.getWidget()._drag_helper(jQuery('.calendar_d-n-d_timeCounter',ui.helper)[0],{ - top:ui.position.top, - left: ui.position.left - jQuery(this).parent().offset().left - },0); - }; - var time = jQuery('.calendar_d-n-d_timeCounter',_data.ui.helper); - switch(_event) - { - // Triggered once, when something is dragged into the timegrid's div - case EGW_AI_DRAG_OVER: - // Listen to the drag and update the helper with the time - // This part lets us drag between different timegrids - _data.ui.draggable.on('drag.et2_timegrid'+widget_object.id, drag_listener); - _data.ui.draggable.on('dragend.et2_timegrid'+widget_object.id, function() { - _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); - }); - if(time.length) - { - // The out will trigger after the over, so we count - time.data('count',time.data('count')+1); - } - else - { - _data.ui.helper.prepend('
'); - } - - break; - - // Triggered once, when something is dragged out of the timegrid - case EGW_AI_DRAG_OUT: - // Stop listening - _data.ui.draggable.off('drag.et2_timegrid'+widget_object.id); - // Remove any highlighted time squares - jQuery('[data-date]',this.doGetDOMNode()).removeClass("ui-state-active"); - - // Out triggers after the over, count to not accidentally remove - time.data('count',time.data('count')-1); - if(time.length && time.data('count') <= 0) - { - time.remove(); - } - break; - } - }; - - if (widget_object == null) { - // Add a new container to the object manager which will hold the widget - // objects - widget_object = parent.insertObject(false, new egwActionObject( - this.id, parent, aoi, - this._actionManager || parent.manager.getActionById(this.id) || parent.manager - ),EGW_AO_FLAG_IS_CONTAINER); - } - else - { - widget_object.setAOI(aoi); - } - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - var action_links = this._get_action_links(actions); - - this._init_links_dnd(widget_object.manager, action_links); - - widget_object.updateActionLinks(action_links); - this._actionObject = widget_object; - }, - - /** - * Automatically add dnd support for linking - * - * @param {type} mgr - * @param {type} actionLinks - */ - _init_links_dnd: function(mgr,actionLinks) { - - if (this.options.readonly) return; - - var self = this; - - var drop_action = mgr.getActionById('egw_link_drop'); - var drop_change_participant = mgr.getActionById('change_participant'); - var drop_invite = mgr.getActionById('invite'); - var drag_action = mgr.getActionById('egw_link_drag'); - var paste_action = mgr.getActionById('egw_paste'); - - // Disable paste action - if(paste_action == null) - { - paste_action = mgr.addAction('popup', 'egw_paste', egw.lang('Paste'), egw.image('editpaste'), function(){},true); - } - paste_action.set_enabled(false); - - // Check if this app supports linking - if(!egw.link_get_registry(this.dataStorePrefix || 'calendar', 'query') || - egw.link_get_registry(this.dataStorePrefix || 'calendar', 'title')) - { - if(drop_action) - { - drop_action.remove(); - if(actionLinks.indexOf(drop_action.id) >= 0) - { - actionLinks.splice(actionLinks.indexOf(drop_action.id),1); - } - } - if(drag_action) - { - drag_action.remove(); - if(actionLinks.indexOf(drag_action.id) >= 0) - { - actionLinks.splice(actionLinks.indexOf(drag_action.id),1); - } - } - return; - } - - // Don't re-add - if(drop_action == null) - { - // Create the drop action that links entries - drop_action = mgr.addAction('drop', 'egw_link_drop', egw.lang('Create link'), egw.image('link'), function(action, source, dropped) { - // Extract link IDs - var links = []; - var id = ''; - for(var i = 0; i < source.length; i++) - { - if(!source[i].id) continue; - id = source[i].id.split('::'); - links.push({app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1]}); - } - if(!links.length) - { - return; - } - if(links.length && dropped && dropped.iface.getWidget() && dropped.iface.getWidget().instanceOf(et2_calendar_event)) - { - // Link the entries - egw.json(self.egw().getAppName()+".etemplate_widget_link.ajax_link.etemplate", - dropped.id.split('::').concat([links]), - function(result) { - if(result) - { - this.egw().message('Linked'); - } - }, - self, - true, - self - ).sendRequest(); - } - },true); - - drop_action.acceptedTypes = ['default','link']; - drop_action.hideOnDisabled = true; - - // Create the drop action for moving events between planner rows - var invite_action = function(action, source, target) { - - // Extract link IDs - var links = []; - var id = ''; - for(var i = 0; i < source.length; i++) - { - // Check for no ID (invalid) or same manager (dragging an event) - if(!source[i].id) continue; - if(source[i].manager === target.manager) - { - - // Find the row, could have dropped on an event - var row = target.iface.getWidget(); - while(target.parent && row.instanceOf && !row.instanceOf(et2_calendar_planner_row)) - { - target = target.parent; - row = target.iface.getWidget(); - } - - // Leave the helper there until the update is done - var loading = action.ui.helper.clone(true).appendTo(jQuery('body')); - - // and add a loading icon so user knows something is happening - if(jQuery('.calendar_timeDemo',loading).length == 0) - { - jQuery('.calendar_calEventHeader',loading).addClass('loading'); - } - else - { - jQuery('.calendar_timeDemo',loading).after('
'); - } - - var event_data = egw.dataGetUIDdata(source[i].id).data; - et2_calendar_event.recur_prompt(event_data, function(button_id) { - if(button_id === 'cancel' || !button_id) - { - return; - } - var add_owner = [row.node.dataset.participants]; - - egw().json('calendar.calendar_uiforms.ajax_invite', [ - button_id==='series' ? event_data.id : event_data.app_id, - add_owner, - action.id === 'change_participant' ? - [source[i].iface.getWidget().getParent().node.dataset.participants] : - [] - ], - function() { loading.remove();} - ).sendRequest(true); - }); - // Ok, stop. - return false; - } - } - }; - - drop_change_participant = mgr.addAction('drop', 'change_participant', egw.lang('Move to'), egw.image('participant'), invite_action,true); - drop_change_participant.acceptedTypes = ['calendar']; - drop_change_participant.hideOnDisabled = true; - - drop_invite = mgr.addAction('drop', 'invite', egw.lang('Invite'), egw.image('participant'), invite_action,true); - drop_invite.acceptedTypes = ['calendar']; - drop_invite.hideOnDisabled = true; - } - if(actionLinks.indexOf(drop_action.id) < 0) - { - actionLinks.push(drop_action.id); - } - actionLinks.push(drop_invite.id); - actionLinks.push(drop_change_participant.id); - - // Accept other links, and files dragged from the filemanager - // This does not handle files dragged from the desktop. They are - // handled by et2_nextmatch, since it needs DOM stuff - if(drop_action.acceptedTypes.indexOf('link') == -1) - { - drop_action.acceptedTypes.push('link'); - } - - // Don't re-add - if(drag_action == null) - { - // Create drag action that allows linking - drag_action = mgr.addAction('drag', 'egw_link_drag', egw.lang('link'), 'link', function(action, selected) { - // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links - // TODO: Need to decide if we need to create a customized helper interface for links anyway - //return helper; - return null; - },true); - } - // The planner itself is not draggable, the action is there for the children - if(false && actionLinks.indexOf(drag_action.id) < 0) - { - actionLinks.push(drag_action.id); - } - drag_action.set_dragType(['link','calendar']); - }, - - /** - * Get all action-links / id's of 1.-level actions from a given action object - * - * Here we are only interested in drop events. - * - * @param actions - * @returns {Array} - */ - _get_action_links: function(actions) - { - var action_links = []; - - // Only these actions are allowed without a selection (empty actions) - var empty_actions = ['add']; - - for(var i in actions) - { - var action = actions[i]; - if(empty_actions.indexOf(action.id) !== -1 || action.type === 'drop') - { - action_links.push(typeof action.id !== 'undefined' ? action.id : i); - } - } - // Disable automatic paste action, it doesn't have what is needed to work - action_links.push({ - "actionObj": 'egw_paste', - "enabled": false, - "visible": false - }); - return action_links; - }, - - /** - * Show the current time while dragging - * Used for resizing as well as drag & drop - * - * @param {type} element - * @param {type} position - * @param {type} height - */ - _drag_helper: function(element, position ,height) - { - var time = this._get_time_from_position(position.left, position.top); - element.dropEnd = time; - var formatted_time = jQuery.datepicker.formatTime( - egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", - { - hour: time.getUTCHours(), - minute: time.getUTCMinutes(), - seconds: 0, - timezone: 0 - }, - {"ampm": (egw.preference("timeformat") === "12")} - ); - - element.innerHTML = '
'+formatted_time+'
'; - - //jQuery(element).width(jQuery(helper).width()); - }, - - /** - * Handler for dropping an event on the timegrid - * - * @param {type} planner - * @param {type} event - * @param {type} ui - */ - _event_drop: function(planner, event,ui) { - var e = new jQuery.Event('change'); - e.originalEvent = event; - e.data = {start: 0}; - if (typeof this.dropEnd != 'undefined') - { - var drop_date = this.dropEnd.toJSON() ||false; - - var event_data = planner._get_event_info(ui.draggable); - var event_widget = planner.getWidgetById(event_data.widget_id); - if(event_widget) - { - event_widget._parent.date_helper.set_value(drop_date); - event_widget.options.value.start = new Date(event_widget._parent.date_helper.getValue()); - - // Leave the helper there until the update is done - var loading = ui.helper.clone().appendTo(ui.helper.parent()); - // and add a loading icon so user knows something is happening - jQuery('.calendar_timeDemo',loading).after('
'); - - event_widget.recur_prompt(function(button_id) { - if(button_id === 'cancel' || !button_id) return; - //Get infologID if in case if it's an integrated infolog event - if (event_data.app === 'infolog') - { - // If it is an integrated infolog event we need to edit infolog entry - egw().json('stylite_infolog_calendar_integration::ajax_moveInfologEvent', - [event_data.id, event_widget.options.value.start||false], - function() {loading.remove();} - ).sendRequest(true); - } - else - { - //Edit calendar event - egw().json('calendar.calendar_uiforms.ajax_moveEvent', [ - button_id==='series' ? event_data.id : event_data.app_id,event_data.owner, - event_widget.options.value.start, - planner.options.owner||egw.user('account_id') - ], - function() { loading.remove();} - ).sendRequest(true); - } - }); - } - } - }, - - /** - * Use the egw.data system to get data from the calendar list for the - * selected time span. - * - */ - _fetch_data: function() - { - var value = []; - var fetch = false; - this.doInvalidate = false; - - for(var i = 0; i < this.registeredCallbacks.length; i++) - { - egw.dataUnregisterUID(this.registeredCallbacks[i],false,this); - } - this.registeredCallbacks.splice(0,this.registeredCallbacks.length); - - // Remember previous day to avoid multi-days duplicating - var last_data = []; - - var t = new Date(this.options.start_date); - var end = new Date(this.options.end_date); - do - { - value = value.concat(this._cache_register(t, this.options.owner, last_data)); - - t.setUTCDate(t.getUTCDate() + 1); - } - while(t < end); - - this.doInvalidate = true; - return value; - }, - - /** - * Deal with registering for data cache - * - * @param Date t - * @param String owner Calendar owner - */ - _cache_register: function _cache_register(t, owner, last_data) - { - // Cache is by date (and owner, if seperate) - var date = t.getUTCFullYear() + sprintf('%02d',t.getUTCMonth()+1) + sprintf('%02d',t.getUTCDate()); - var cache_id = app.classes.calendar._daywise_cache_id(date, owner); - var value = []; - - if(egw.dataHasUID(cache_id)) - { - var c = egw.dataGetUIDdata(cache_id); - if(c.data && c.data !== null) - { - // There is data, pass it along now - for(var j = 0; j < c.data.length; j++) - { - if(last_data.indexOf(c.data[j]) === -1 && egw.dataHasUID('calendar::'+c.data[j])) - { - value.push(egw.dataGetUIDdata('calendar::'+c.data[j]).data); - } - } - last_data = c.data; - } - } - else - { - fetch = true; - // Assume it's empty, if there is data it will be filled later - egw.dataStoreUID(cache_id, []); - } - this.registeredCallbacks.push(cache_id); - - egw.dataRegisterUID(cache_id, function(data) { - - if(data && data.length) - { - var invalidate = true; - - // Try to determine rows interested - var labels = []; - var events = {}; - if(this.grouper) - { - labels = this.grouper.row_labels.call(this); - invalidate = false; - } - - var im = this.getInstanceManager(); - for(var i = 0; i < data.length; i++) - { - var event = egw.dataGetUIDdata('calendar::'+data[i]); - - if(!event) continue; - events = {}; - - // Try to determine rows interested - if(event.data && this.grouper) - { - this.grouper.group.call(this, labels, events, event.data); - } - if(Object.keys(events).length > 0 ) - { - for(var label_id in events) - { - var id = ""+labels[label_id].id; - if(typeof this.cache[id] === 'undefined') - { - this.cache[id] = []; - } - if(this.cache[id].indexOf(event.data.row_id) === -1) - { - this.cache[id].push(event.data.row_id); - } - if (this._deferred_row_updates[id]) - { - window.clearTimeout(this._deferred_row_updates[id]); - } - this._deferred_row_updates[id] = window.setTimeout(jQuery.proxy(this._deferred_row_update,this,id),this.DEFERRED_ROW_TIME); - } - } - else - { - // Could be an event no row is interested in, could be a problem. - // Just redraw everything - invalidate = true; - continue; - } - - // If displaying by category, we need the infolog (or other app) categories too - if(event && event.data && event.data.app && this.options.group_by == 'category') - { - // Fake it to use the cache / call - et2_selectbox.cat_options({ - _type:'select-cat', - getInstanceManager: function() {return im;} - }, {application:event.data.app||'calendar'}); - } - } - - if(invalidate) - { - this.invalidate(false); - } - } - }, this, this.getInstanceManager().execId,this.id); - - return value; - }, - - /** - * Because users may be participants in various events and the time it takes - * to create many events, we don't want to update a row too soon - we may have - * to re-draw it if we find the user / category in another event. Pagination - * makes this worse. We wait a bit before updating the row to avoid - * having to re-draw it multiple times. - * - * @param {type} id - * @returns {undefined} - */ - _deferred_row_update: function(id) { - // Something's in progress, skip - if(!this.doInvalidate) return; - - this.grid.height(0); - - var id_list = typeof id === 'undefined' ? Object.keys(this.cache) : [id]; - for(var i = 0; i < id_list.length; i++) - { - var cache_id = id_list[i]; - var row = this.getWidgetById('planner_row_'+cache_id); - - window.clearTimeout(this._deferred_row_updates[cache_id]); - delete this._deferred_row_updates[cache_id]; - - if(row) - { - row._data_callback(this.cache[cache_id]); - row.set_disabled(this.options.hide_empty && this.cache[cache_id].length === 0); - } - else - { - break; - } - } - - // Updating the row may push things longer, update length - // Add 1 to keep the scrollbar, otherwise we need to recalculate the - // header widths too. - this.grid.height(this.rows[0].scrollHeight+1); - - // Adjust header if there's a scrollbar - Firefox needs this re-calculated, - // otherwise the header will be missing the margin space for the scrollbar - // in some cases - if(this.rows.children().last().length) - { - this.gridHeader.css('margin-right', (this.rows.width() - this.rows.children().last().width()) + 'px'); - } - }, - - /** - * Provide specific data to be displayed. - * This is a way to set start and end dates, owner and event data in once call. - * - * @param {Object[]} events Array of events, indexed by date in Ymd format: - * { - * 20150501: [...], - * 20150502: [...] - * } - * Days should be in order. - * - */ - set_value: function set_value(events) - { - if(typeof events !== 'object') return false; - - this._super.apply(this, arguments); - - // Planner uses an array, not map - var val = this.value; - var array = []; - Object.keys(this.value).forEach(function (key) { - array.push(val[key]); - }); - this.value = array; - }, - - /** - * Change the start date - * Planner view uses a date object internally - * - * @param {string|number|Date} new_date New starting date - * @returns {undefined} - */ - set_start_date: function set_start_date(new_date) - { - this._super.apply(this, arguments); - this.options.start_date = new Date(this.options.start_date); - }, - - /** - * Change the end date - * Planner view uses a date object internally - * - * @param {string|number|Date} new_date New end date - * @returns {undefined} - */ - set_end_date: function set_end_date(new_date) - { - this._super.apply(this, arguments); - this.options.end_date = new Date(this.options.end_date); - }, - - /** - * Change how the planner is grouped - * - * @param {string|number} group_by 'user', 'month', or an integer category ID - * @returns {undefined} - */ - set_group_by: function set_group_by(group_by) - { - if(isNaN(group_by) && typeof this.groupers[group_by] === 'undefined') - { - throw new Error('Invalid group_by "'+group_by+'"'); - } - var old = this.options.group_by; - this.options.group_by = ''+group_by; - - this.grouper = this.groupers[isNaN(this.options.group_by) ? this.options.group_by : 'category']; - - if(old !== this.options.group_by && this.isAttached()) - { - this.invalidate(true); - } - }, - - /** - * Turn on or off the visibility of weekends - * - * @param {boolean} weekends - */ - set_show_weekend: function set_show_weekend(weekends) - { - weekends = weekends ? true : false; - if(this.options.show_weekend !== weekends) - { - this.options.show_weekend = weekends; - if(this.isAttached()) - { - this.invalidate(); - } - } - }, - - /** - * Turn on or off the visibility of hidden (empty) rows - * - * @param {boolean} hidden - */ - set_hide_empty: function set_hide_empty(hidden) - { - this.options.hide_empty = hidden; - }, - - /** - * Call change handler, if set - * - * @param {type} event - */ - change: function(event) { - if (this.onchange) - { - if(typeof this.onchange == 'function') - { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.push(this); - - return this.onchange.apply(this, args); - } else { - return (et2_compileLegacyJS(this.options.onchange, this, _node))(); - } - } - }, - - /** - * Call event change handler, if set - * - * @param {type} event - * @param {type} dom_node - */ - event_change: function(event, dom_node) { - if (this.onevent_change) - { - var event_data = this._get_event_info(dom_node); - var event_widget = this.getWidgetById(event_data.widget_id); - et2_calendar_event.recur_prompt(event_data, jQuery.proxy(function(button_id, event_data) { - // No need to continue - if(button_id === 'cancel') return false; - - if(typeof this.onevent_change == 'function') - { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - - if(args.indexOf(event_widget) == -1) args.push(event_widget); - - // Put button ID in event - event.button_id = button_id; - - return this.onevent_change.apply(this, [event, event_widget, button_id]); - } else { - return (et2_compileLegacyJS(this.options.onevent_change, event_widget, dom_node))(); - } - },this)); - } - return false; - }, - - /** - * Click handler calling custom handler set via onclick attribute to this.onclick - * - * This also handles all its own actions, including navigation. If there is - * an event associated with the click, it will be found and passed to the - * onclick function. - * - * @param {Event} _ev - * @returns {boolean} Continue processing event (true) or stop (false) - */ - click: function(_ev) - { - var result = true; - - // Drag to create in progress - if(this.drag_create.start !== null) return; - - // Is this click in the event stuff, or in the header? - if(!this.options.readonly && this.gridHeader.has(_ev.target).length === 0 && !jQuery(_ev.target).hasClass('calendar_plannerRowHeader')) - { - // Event came from inside, maybe a calendar event - var event = this._get_event_info(_ev.originalEvent.target); - if(typeof this.onclick == 'function') - { - // Make sure function gets a reference to the widget, splice it in as 2. argument if not - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.splice(1, 0, this); - - result = this.onclick.apply(this, args); - } - - if(event.id && result && !this.options.disabled && !this.options.readonly) - { - et2_calendar_event.recur_prompt(event); - - return false; - } - else if (!event.id) - { - // Clicked in row, but not on an event - // Default handler to open a new event at the selected time - if(jQuery(event.target).closest('.calendar_eventRows').length == 0) - { - // "Invalid" times, from space after the last planner row, or header - var date = this._get_time_from_position(_ev.pageX - this.grid.offset().left, _ev.pageY - this.grid.offset().top); - } - else if(this.options.group_by == 'month') - { - var date = this._get_time_from_position(_ev.clientX, _ev.clientY); - } - else - { - var date = this._get_time_from_position(_ev.offsetX, _ev.offsetY); - } - var row = jQuery(_ev.target).closest('.calendar_plannerRowWidget'); - var data = row.length ? row[0].dataset : {}; - app.calendar.add(jQuery.extend({ - start: date.toJSON(), - hour: date.getUTCHours(), - minute: date.getUTCMinutes() - },data)); - return false; - } - return result; - } - else if (this.gridHeader.has(_ev.target).length > 0 && !jQuery.isEmptyObject(_ev.target.dataset) || - jQuery(_ev.target).hasClass('calendar_plannerRowHeader') && !jQuery.isEmptyObject(_ev.target.dataset)) - { - // Click on a header, we can go there - _ev.data = jQuery.extend({},_ev.target.parentNode.dataset, _ev.target.dataset); - for(var key in _ev.data) - { - if(!_ev.data[key]) - { - delete _ev.data[key]; - } - } - app.calendar.update_state(_ev.data); - } - else if (!this.options.readonly) - { - // Default handler to open a new event at the selected time - // TODO: Determine date / time more accurately from position - app.calendar.add({ - date: _ev.target.dataset.date || this.options.start_date.toJSON(), - hour: _ev.target.dataset.hour || this.options.day_start, - minute: _ev.target.dataset.minute || 0 - }); - return false; - } - }, - - /** - * Get time from position - * - * @param {number} x - * @param {number} y - * @returns {Date|Boolean} A time for the given position, or false if one - * could not be determined. - */ - _get_time_from_position: function(x,y) { - - x = Math.round(x); - y = Math.round(y); - - // Round to user's preferred event interval - var interval = egw.preference('interval','calendar') || 30; - - // Relative horizontal position, as a percentage - var width = 0; - jQuery('.calendar_eventRows',this.div).each(function() {width = Math.max(width,jQuery(this).width());}); - var rel_x = Math.min(x / width,1); - - // Relative time, in minutes from start - var rel_time = 0; - - var day_header = jQuery('.calendar_plannerScaleDay',this.headers); - - // Simple math, the x is offset from start date - if(this.options.group_by !== 'month' && ( - // Either all days are visible, or only 1 day (no day header) - this.options.show_weekend || day_header.length === 0 - )) - { - rel_time = (new Date(this.options.end_date) - new Date(this.options.start_date))*rel_x/1000; - this.date_helper.set_value(this.options.start_date.toJSON()); - } - // Not so simple math, need to account for missing days - else if(this.options.group_by !== 'month' && !this.options.show_weekend) - { - // Find which day - if(day_header.length === 0) return false; - var day = document.elementFromPoint( - day_header.offset().left + rel_x * this.headers.innerWidth(), - day_header.offset().top - ); - - // Use day, and find time in that day - if(day && day.dataset && day.dataset.date) - { - this.date_helper.set_value(day.dataset.date); - rel_time = ((x - jQuery(day).position().left) / jQuery(day).outerWidth(true)) * 24*60; - this.date_helper.set_minutes(Math.round(rel_time/interval) * interval); - return new Date(this.date_helper.getValue()); - } - return false; - } - else - { - // Find the correct row so we know which month, then get the offset - var hidden_nodes = []; - var row = null; - // Hide any drag or tooltips that may interfere - do - { - row = document.elementFromPoint(x, y); - if(this.div.has(row).length == 0) - { - hidden_nodes.push(jQuery(row).hide()); - } - else - { - break; - } - } while(row && row.nodeName !== 'BODY'); - if(!row) return false; - - // Restore hidden nodes - for(var i = 0; i < hidden_nodes.length; i++) - { - hidden_nodes[i].show(); - } - row = jQuery(row).closest('.calendar_plannerRowWidget'); - - - var row_widget = null; - for(var i = 0; i < this._children.length && row.length > 0; i++) - { - if(this._children[i].div[0] == row[0]) - { - row_widget = this._children[i]; - break; - } - } - if(row_widget) - { - // Not sure where the extra -1 and +2 are coming from, but it makes it work out - // in FF & Chrome - rel_x = Math.min((x-row_widget.rows.offset().left-1)/(row_widget.rows.width()+2),1); - - // 2678400 is the number of seconds in 31 days - rel_time = (2678400)*rel_x; - this.date_helper.set_value(row_widget.options.start_date.toJSON()); - } - else - { - return false; - } - } - if(rel_time < 0) return false; - - this.date_helper.set_minutes(Math.round(rel_time / (60 * interval))*interval); - - return new Date(this.date_helper.getValue()); - }, - - /** - * Mousedown handler to support drag to create - * - * @param {jQuery.Event} event - */ - _mouse_down: function(event) - { - // Only left mouse button - if(event.which !== 1) return; - - // Ignore headers - if(this.headers.has(event.target).length !== 0) return false; - - // Get time at mouse - if(this.options.group_by === 'month') - { - var time = this._get_time_from_position(event.clientX, event.clientY); - } - else - { - var time = this._get_time_from_position(event.offsetX, event.offsetY); - } - if(!time) return false; - - // Find the correct row so we know the parent - var row = event.target.closest('.calendar_plannerRowWidget'); - for(var i = 0; i < this._children.length && row; i++) - { - if(this._children[i].div[0] === row) - { - this.drag_create.parent = this._children[i]; - // Clear cached events for re-layout - this._children[i]._cached_rows = []; - break; - } - } - if(!this.drag_create.parent) return false; - - this.div.css('cursor', 'ew-resize'); - - return this._drag_create_start(jQuery.extend({},this.drag_create.parent.node.dataset,{date: time.toJSON()})); - }, - - /** - * Mouseup handler to support drag to create - * - * @param {jQuery.Event} event - */ - _mouse_up: function(event) - { - // Get time at mouse - if(this.options.group_by === 'month') - { - var time = this._get_time_from_position(event.clientX, event.clientY); - } - else - { - var time = this._get_time_from_position(event.offsetX, event.offsetY); - } - - return this._drag_create_end(time ? {date: time.toJSON()} : false); - }, - - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes: function(_attrs) { - _attrs.push('start_date','end_date'); - }, - - getDetachedNodes: function() { - return [this.getDOMNode()]; - }, - - setDetachedAttributes: function(_nodes, _values) { - this.div = jQuery(_nodes[0]); - - if(_values.start_date) - { - this.set_start_date(_values.start_date); - } - if(_values.end_date) - { - this.set_end_date(_values.end_date); - } - }, - - // Resizable interface - resize: function () - { - // Take the whole tab height - var height = Math.min(jQuery(this.getInstanceManager().DOMContainer).height(),jQuery(this.getInstanceManager().DOMContainer).parent().innerHeight()); - - // Allow for toolbar - height -= jQuery('#calendar-toolbar',this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true); - - this.options.height = height; - this.div.css('height', this.options.height); - // Set height for rows - this.rows.height(this.div.height() - this.headers.outerHeight()); - - this.grid.height(this.rows[0].scrollHeight); - }, - - /** - * Set up for printing - * - * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up - * (waiting for data) - */ - beforePrint: function() { - - if(this.disabled || !this.div.is(':visible')) - { - return; - } - this.rows.css('overflow-y', 'visible'); - - var rows = jQuery('.calendar_eventRows'); - var width = rows.width(); - var events = jQuery('.calendar_calEvent', rows) - .each(function() { - var event = jQuery(this); - event.width((event.width() / width) * 100 + '%') - }); - - }, - - /** - * Reset after printing - */ - afterPrint: function() { - this.rows.css('overflow-y', 'auto'); - } -});}).call(this); -et2_register_widget(et2_calendar_planner, ["calendar-planner"]); +var et2_calendar_planner = /** @class */ (function (_super) { + __extends(et2_calendar_planner, _super); + /** + * Constructor + */ + function et2_calendar_planner(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_calendar_planner._attributes, _child || {})) || this; + /** + * These handle the differences between the different group types. + * They provide the different titles, labels and grouping + */ + _this.groupers = { + // Group by user has one row for each user + user: { + // Title in top left corner + title: function () { + return this.egw().lang('User'); + }, + // Column headers + headers: function () { + var start = new Date(this.options.start_date); + var end = new Date(this.options.end_date); + var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate()); + var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate()); + var day_count = Math.round((end_date - start_date) / (1000 * 3600 * 24)) + 1; + if (day_count >= 6) { + this.headers.append(this._header_months(start, day_count)); + } + if (day_count < 120) { + var weeks = this._header_weeks(start, day_count); + this.headers.append(weeks); + this.grid.append(weeks); + } + if (day_count < 60) { + var days = this._header_days(start, day_count); + this.headers.append(days); + this.grid.append(days); + } + if (day_count <= 7) { + var hours = this._header_hours(start, day_count); + this.headers.append(hours); + this.grid.append(hours); + } + }, + // Labels for the rows + row_labels: function () { + var labels = []; + var already_added = []; + var options = false; + var resource = null; + if (app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { + options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); + } + else { + options = this.getArrayMgr("sel_options").getRoot().getEntry('owner'); + } + for (var i = 0; i < this.options.owner.length; i++) { + var user = this.options.owner[i]; + // Handle grouped resources like mailing lists - pull it from sidebox owner + // and expand to their contents + if (options && options.find && + ((resource = options.find(function (element) { return element.id == user; }) || {}) || isNaN(user))) { + if (resource && resource.resources) { + for (var j = 0; j < resource.resources.length; j++) { + var id = resource.resources[j]; + if (already_added.indexOf('' + id) < 0) { + labels.push({ + id: id, + label: this._get_owner_name(id) || '', + data: { participants: id, owner: id } + }); + already_added.push('' + id); + } + } + } + else if (already_added.indexOf('' + user) < 0) { + labels.push({ + id: user, + label: this._get_owner_name(user), + data: { participants: user, owner: user } + }); + already_added.push('' + user); + } + } + else if (user < 0) // groups + { + egw.accountData(parseInt(user), 'account_fullname', true, function (result) { + for (var id in result) { + if (already_added.indexOf('' + id) < 0) { + this.push({ id: id, label: result[id] || '', data: { participants: id, owner: id } }); + already_added.push('' + id); + } + } + }, labels); + } + else // users + { + if (already_added.indexOf(user) < 0) { + var label = this._get_owner_name(user) || ''; + labels.push({ id: user, label: label, data: { participants: user, owner: '' } }); + already_added.push('' + user); + } + } + } + return labels.sort(function (a, b) { + return a.label.localeCompare(b.label); + }); + }, + // Group the events into the rows + group: function (labels, rows, event) { + // convert filter to allowed status + var status_to_show = ['U', 'A', 'T', 'D', 'G']; + switch (this.options.filter) { + case 'unknown': + status_to_show = ['U', 'G']; + break; + case 'accepted': + status_to_show = ['A']; + break; + case 'tentative': + status_to_show = ['T']; + break; + case 'rejected': + status_to_show = ['R']; + break; + case 'delegated': + status_to_show = ['D']; + break; + case 'all': + status_to_show = ['U', 'A', 'T', 'D', 'G', 'R']; + break; + default: + status_to_show = ['U', 'A', 'T', 'D', 'G']; + break; + } + var participants = event.participants; + var add_row = function (user, participant) { + var label_index = false; + for (var i = 0; i < labels.length; i++) { + if (labels[i].id == user) { + label_index = i; + break; + } + } + if (participant && label_index !== false && status_to_show.indexOf(participant.substr(0, 1)) >= 0 || + !participant && label_index !== false || + this.options.filter === 'owner' && event.owner === user) { + if (typeof rows[label_index] === 'undefined') { + rows[label_index] = []; + } + rows[label_index].push(event); + } + }; + for (var user in participants) { + var participant = participants[user]; + if (parseInt(user) < 0) // groups + { + var planner = this; + egw.accountData(user, 'account_fullname', true, function (result) { + for (var id in result) { + if (!participants[id]) + add_row.call(planner, id, participant); + } + }, labels); + continue; + } + add_row.call(this, user, participant); + } + }, + // Draw a single row + draw_row: function (sort_key, label, events) { + var row = this._drawRow(sort_key, label, events, this.options.start_date, this.options.end_date); + if (this.options.hide_empty && !events.length) { + row.set_disabled(true); + } + // Highlight current user, sort_key is account_id + if (sort_key === egw.user('account_id')) { + row.set_class('current_user'); + } + // Since the daywise cache is by user, we can tap in here + var t = new Date(this.options.start_date); + var end = new Date(this.options.end_date); + do { + var cache_id = CalendarApp._daywise_cache_id(t, sort_key); + egw.dataRegisterUID(cache_id, row._data_callback, row); + t.setUTCDate(t.getUTCDate() + 1); + } while (t < end); + return row; + } + }, + // Group by month has one row for each month + month: { + title: function () { return this.egw().lang('Month'); }, + headers: function () { + this.headers.append(this._header_day_of_month()); + }, + row_labels: function () { + var labels = []; + var d = new Date(this.options.start_date); + d = new Date(d.valueOf() + d.getTimezoneOffset() * 60 * 1000); + for (var i = 0; i < 12; i++) { + // Not using UTC because we corrected for timezone offset + labels.push({ id: sprintf('%04d-%02d', d.getFullYear(), d.getMonth()), label: this.egw().lang(date('F', d)) + ' ' + d.getFullYear() }); + d.setMonth(d.getMonth() + 1); + } + return labels; + }, + group: function (labels, rows, event) { + // Yearly planner does not show infologs + if (event && event.app && event.app == 'infolog') + return; + var start = new Date(event.start); + start = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000); + var key = sprintf('%04d-%02d', start.getFullYear(), start.getMonth()); + var label_index = false; + for (var i = 0; i < labels.length; i++) { + if (labels[i].id == key) { + label_index = i; + break; + } + } + if (typeof rows[label_index] === 'undefined') { + rows[label_index] = []; + } + rows[label_index].push(event); + // end in a different month? + var end = new Date(event.end); + end = new Date(end.valueOf() + end.getTimezoneOffset() * 60 * 1000); + var end_key = sprintf('%04d-%02d', end.getFullYear(), end.getMonth()); + var year = start.getFullYear(); + var month = start.getMonth(); + key = sprintf('%04d-%02d', year, month); + do { + var end_label_index = label_index; + for (var i = end_label_index; i < labels.length; i++) { + if (labels[i].id == key) { + end_label_index = i; + if (typeof rows[end_label_index] === 'undefined') { + rows[end_label_index] = []; + } + break; + } + } + if (end_label_index != label_index) { + rows[label_index].push(event); + } + if (++month > 11) { + ++year; + month = 0; + } + key = sprintf('%04d-%02d', year, month); + } while (key <= end_key); + }, + // Draw a single row, but split up the dates + draw_row: function (sort_key, label, events) { + var key = sort_key.split('-'); + var start = new Date(key[0] + "-" + sprintf("%02d", parseInt(key[1]) + 1) + "-01T00:00:00Z"); + // Use some care to avoid issues with timezones and daylight savings + var end = new Date(start); + end.setUTCMonth(start.getUTCMonth() + 1); + end.setUTCDate(1); + end.setUTCHours(0); + end.setUTCMinutes(0); + end = new Date(end.valueOf() - 1000); + end.setUTCMonth(start.getUTCMonth()); + this._drawRow(sort_key, label, events, start, end); + } + }, + // Group by category has one row for each [sub]category + category: { + title: function () { return this.egw().lang('Category'); }, + headers: function () { + var start = new Date(this.options.start_date); + var end = new Date(this.options.end_date); + var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate()); + var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate()); + var day_count = Math.round((end_date - start_date) / (1000 * 3600 * 24)) + 1; + if (day_count >= 6) { + this.headers.append(this._header_months(start, day_count)); + } + if (day_count < 120) { + var weeks = this._header_weeks(start, day_count); + this.headers.append(weeks); + this.grid.append(weeks); + } + if (day_count < 60) { + var days = this._header_days(start, day_count); + this.headers.append(days); + this.grid.append(days); + } + if (day_count <= 7) { + var hours = this._header_hours(start, day_count); + this.headers.append(hours); + this.grid.append(hours); + } + }, + row_labels: function () { + var im = this.getInstanceManager(); + var categories = et2_selectbox.cat_options({ + _type: 'select-cat', + getInstanceManager: function () { return im; } + }, { application: 'calendar' }); + var labels = []; + if (!app.calendar.state.cat_id || + app.calendar.state.cat_id.toString() === '' || + app.calendar.state.cat_id.toString() == '0') { + app.calendar.state.cat_id = ''; + labels.push({ id: '', value: '', label: egw.lang('none'), main: '', data: {} }); + labels = labels.concat(categories); + } + else { + var cat_id = app.calendar.state.cat_id; + if (typeof cat_id == 'string') { + cat_id = cat_id.split(','); + } + for (var i = 0; i < cat_id.length; i++) { + // Find label for that category + for (var j = 0; j < categories.length; j++) { + if (categories[j].value == cat_id[i]) { + categories[j].id = categories[j].value; + labels.push(categories[j]); + break; + } + } + // Get its children immediately + egw.json('EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options', ['select-cat', ',,,calendar,' + cat_id[i]], function (data) { + labels = labels.concat(data); + }).sendRequest(false); + } + } + for (var i = labels.length - 1; i >= 0; i--) { + labels[i].id = labels[i].value; + labels[i].data = { + cat_id: labels[i].id, + main: labels[i].value == labels[i].main + }; + if (labels[i].children && labels[i].children.length) { + labels[i].data.has_children = true; + } + } + return labels; + }, + group: function (labels, rows, event) { + var cats = event.category; + if (typeof event.category === 'string') { + cats = cats.split(','); + } + for (var cat = 0; cat < cats.length; cat++) { + var label_index = false; + var category = cats[cat] ? parseInt(cats[cat], 10) : false; + if (category == 0 || !category) + category = ''; + for (var i = 0; i < labels.length; i++) { + if (labels[i].id == category) { + // If there's no cat filter, only show the top level + if (!app.calendar.state.cat_id) { + for (var j = 0; j < labels.length; j++) { + if (labels[j].id == labels[i].main) { + label_index = j; + break; + } + } + break; + } + label_index = i; + break; + } + } + if (label_index !== false && typeof rows[label_index] === 'undefined') { + rows[label_index] = []; + } + if (label_index !== false && rows[label_index].indexOf(event) === -1) { + rows[label_index].push(event); + } + } + }, + draw_row: function (sort_key, label, events) { + var row = this._drawRow(sort_key, label, events, this.options.start_date, this.options.end_date); + if (this.options.hide_empty && !events.length) { + row.set_disabled(true); + } + return row; + } + } + }; + // Main container + _this.div = jQuery(document.createElement("div")) + .addClass("calendar_plannerWidget"); + // Header + _this.gridHeader = jQuery(document.createElement("div")) + .addClass("calendar_plannerHeader") + .appendTo(_this.div); + _this.headerTitle = jQuery(document.createElement("div")) + .addClass("calendar_plannerHeaderTitle") + .appendTo(_this.gridHeader); + _this.headers = jQuery(document.createElement("div")) + .addClass("calendar_plannerHeaderRows") + .appendTo(_this.gridHeader); + _this.rows = jQuery(document.createElement("div")) + .addClass("calendar_plannerRows") + .appendTo(_this.div); + _this.grid = jQuery(document.createElement("div")) + .addClass("calendar_plannerGrid") + .appendTo(_this.div); + _this.vertical_bar = jQuery(document.createElement("div")) + .addClass('verticalBar') + .appendTo(_this.div); + _this.value = []; + // Update timer, to avoid redrawing twice when changing start & end date + _this.update_timer = null; + _this.doInvalidate = true; + _this.setDOMNode(_this.div[0]); + _this.registeredCallbacks = []; + _this.cache = {}; + _this._deferred_row_updates = {}; + return _this; + } + et2_calendar_planner.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.div.off(); + for (var i = 0; i < this.registeredCallbacks.length; i++) { + egw.dataUnregisterUID(this.registeredCallbacks[i], null, this); + } + }; + et2_calendar_planner.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.call(this); + // Don't bother to draw anything if there's no date yet + if (this.options.start_date) { + this._drawGrid(); + } + // Automatically bind drag and resize for every event using jQuery directly + // - no action system - + var planner = this; + this.cache = {}; + this._deferred_row_updates = {}; + /** + * If user puts the mouse over an event, then we'll set up resizing so + * they can adjust the length. Should be a little better on resources + * than binding it for every calendar event. + */ + this.div.on('mouseover', '.calendar_calEvent:not(.ui-resizable):not(.rowNoEdit)', function () { + // Load the event + planner._get_event_info(this); + var that = this; + //Resizable event handler + jQuery(this).resizable({ + distance: 10, + grid: [5, 10000], + autoHide: false, + handles: 'e', + containment: 'parent', + /** + * Triggered when the resizable is created. + * + * @param {event} event + * @param {Object} ui + */ + create: function (event, ui) { + var resizeHelper = event.target.getAttribute('data-resize'); + if (resizeHelper == 'WD' || resizeHelper == 'WDS') { + jQuery(this).resizable('destroy'); + } + }, + /** + * If dragging to resize an event, abort drag to create + * + * @param {jQuery.Event} event + * @param {Object} ui + */ + start: function (event, ui) { + if (planner.drag_create.start) { + // Abort drag to create, we're dragging to resize + planner._drag_create_end({}); + } + }, + /** + * Triggered at the end of resizing the calEvent. + * + * @param {event} event + * @param {Object} ui + */ + stop: function (event, ui) { + var e = new jQuery.Event('change'); + e.originalEvent = event; + e.data = { duration: 0 }; + var event_data = planner._get_event_info(this); + var event_widget = planner.getWidgetById(event_data.widget_id); + var sT = event_widget.options.value.start_m; + if (typeof this.dropEnd != 'undefined') { + var eT = parseInt(this.dropEnd.getUTCHours() * 60) + parseInt(this.dropEnd.getUTCMinutes()); + e.data.duration = ((eT - sT) / 60) * 3600; + if (event_widget) { + event_widget.options.value.end_m = eT; + event_widget.options.value.duration = e.data.duration; + } + // Leave the helper there until the update is done + var loading = ui.helper.clone().appendTo(ui.helper.parent()); + // and add a loading icon so user knows something is happening + jQuery('.calendar_timeDemo', loading).after('
'); + jQuery(this).trigger(e); + // That cleared the resize handles, so remove for re-creation... + jQuery(this).resizable('destroy'); + // Remove loading, done or not + loading.remove(); + } + // Clear the helper, re-draw + if (event_widget) { + event_widget.getParent().position_event(event_widget); + } + }, + /** + * Triggered during the resize, on the drag of the resize handler + * + * @param {event} event + * @param {Object} ui + */ + resize: function (event, ui) { + var position; + if (planner.options.group_by == 'month') { + position = { left: event.clientX, top: event.clientY }; + } + else { + position = { top: ui.position.top, left: ui.position.left + ui.helper.width() }; + } + planner._drag_helper(this, position, ui.helper.outerHeight()); + } + }); + }) + .on('mousemove', function (event) { + // Ignore headers + if (planner.headers.has(event.target).length !== 0) { + planner.vertical_bar.hide(); + return; + } + // Position bar by mouse + planner.vertical_bar.position({ + my: 'right-1', + of: event, + collision: 'fit' + }); + planner.vertical_bar.css('top', '0px'); + // Get time at mouse + if (jQuery(event.target).closest('.calendar_eventRows').length == 0) { + // "Invalid" times, from space after the last planner row, or header + var time = planner._get_time_from_position(event.pageX - planner.grid.offset().left, 10); + } + else if (planner.options.group_by == 'month') { + var time = planner._get_time_from_position(event.clientX, event.clientY); + } + else { + var time = planner._get_time_from_position(event.offsetX, event.offsetY); + } + // Passing to formatter, cancel out timezone + if (time) { + var formatDate = new Date(time.valueOf() + time.getTimezoneOffset() * 60 * 1000); + planner.vertical_bar + .html('' + date(egw.preference('timeformat', 'calendar') == 12 ? 'h:ia' : 'H:i', formatDate) + '') + .show(); + if (planner.drag_create.event && planner.drag_create.parent && planner.drag_create.end) { + planner.drag_create.end.date = time.toJSON(); + planner._drag_update_event(); + } + } + else { + // No (valid) time, just hide + planner.vertical_bar.hide(); + } + }) + .on('mousedown', jQuery.proxy(this._mouse_down, this)) + .on('mouseup', jQuery.proxy(this._mouse_up, this)); + // Actions may be set on a parent, so we need to explicitly get in here + // and get ours + this._link_actions(this.options.actions || this.getParent().options.actions || []); + // Customize and override some draggable settings + this.div.on('dragcreate', '.calendar_calEvent', function (event, ui) { + jQuery(this).draggable('option', 'cancel', '.rowNoEdit'); + // Act like you clicked the header, makes it easier to position + jQuery(this).draggable('option', 'cursorAt', { top: 5, left: 5 }); + }) + .on('dragstart', '.calendar_calEvent', function (event, ui) { + jQuery('.calendar_calEvent', ui.helper).width(jQuery(this).width()) + .height(jQuery(this).outerHeight()) + .css('top', '').css('left', '') + .appendTo(ui.helper); + ui.helper.width(jQuery(this).width()); + // Cancel drag to create, we're dragging an existing event + planner._drag_create_end(); + }); + return true; + }; + et2_calendar_planner.prototype._createNamespace = function () { + return true; + }; + /** + * Something changed, and the planner needs to be re-drawn. We wait a bit to + * avoid re-drawing twice if start and end date both changed, then recreate. + * + * @param {boolean} trigger =false Trigger an event once things are done. + * Waiting until invalidate completes prevents 2 updates when changing the date range. + * @returns {undefined} + */ + et2_calendar_planner.prototype.invalidate = function (trigger) { + // Busy + if (!this.doInvalidate) + return; + // Not yet ready + if (!this.options.start_date || !this.options.end_date) + return; + // Wait a bit to see if anything else changes, then re-draw the days + if (this.update_timer !== null) { + window.clearTimeout(this.update_timer); + } + this.update_timer = window.setTimeout(jQuery.proxy(function () { + this.widget.doInvalidate = false; + // Show AJAX loader + this.widget.loader.show(); + this.widget.cache = {}; + this._deferred_row_updates = {}; + this.widget._fetch_data(); + this.widget._drawGrid(); + if (this.trigger) { + this.widget.change(); + } + this.widget.update_timer = null; + this.widget.doInvalidate = true; + window.setTimeout(jQuery.proxy(function () { if (this.loader) + this.loader.hide(); }, this.widget), 500); + }, { widget: this, "trigger": trigger }), et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); + }; + et2_calendar_planner.prototype.detachFromDOM = function () { + // Remove the binding to the change handler + jQuery(this.div).off("change.et2_calendar_timegrid"); + return _super.prototype.detachFromDOM.call(this); + }; + et2_calendar_planner.prototype.attachToDOM = function () { + var result = _super.prototype.attachToDOM.call(this); + // Add the binding for the event change handler + jQuery(this.div).on("change.et2_calendar_timegrid", '.calendar_calEvent', this, function (e) { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.push(this); + return e.data.event_change.apply(e.data, args); + }); + // Add the binding for the change handler + jQuery(this.div).on("change.et2_calendar_timegrid", '*:not(.calendar_calEvent)', this, function (e) { + return e.data.change.call(e.data, e, this); + }); + return result; + }; + et2_calendar_planner.prototype.getDOMNode = function (_sender) { + if (_sender === this || !_sender) { + return this.div[0]; + } + if (_sender._parent === this) { + return this.rows[0]; + } + }; + /** + * Creates all the DOM nodes for the planner grid + * + * Any existing nodes (& children) are removed, the headers & labels are + * determined according to the current group_by value, and then the rows + * are created. + * + * @method + * @private + * + */ + et2_calendar_planner.prototype._drawGrid = function () { + this.div.css('height', this.options.height); + // Clear old events + var delete_index = this._children.length - 1; + while (this._children.length > 0 && delete_index >= 0) { + this._children[delete_index].destroy(); + this.removeChild(this._children[delete_index--]); + } + // Clear old rows + this.rows.empty() + .append(this.grid); + this.grid.empty(); + var grouper = this.grouper; + if (!grouper) + return; + // Headers + this.headers.empty(); + this.headerTitle.text(grouper.title.apply(this)); + grouper.headers.apply(this); + this.grid.find('*').contents().filter(function () { + return this.nodeType === 3; + }).remove(); + // Get the rows / labels + var labels = grouper.row_labels.call(this); + // Group the events + var events = {}; + for (var i = 0; i < this.value.length; i++) { + grouper.group.call(this, labels, events, this.value[i]); + } + // Set height for rows + this.rows.height(this.div.height() - this.headers.outerHeight()); + // Draw the rows + for (var key in labels) { + if (!labels.hasOwnProperty(key)) + continue; + // Skip sub-categories (events are merged into top level) + if (this.options.group_by == 'category' && + (!app.calendar.state.cat_id || app.calendar.state.cat_id == '') && + labels[key].id != labels[key].main) { + continue; + } + var row = grouper.draw_row.call(this, labels[key].id, labels[key].label, events[key] || []); + // Add extra data for clicking on row + if (row) { + for (var extra in labels[key].data) { + row.getDOMNode().dataset[extra] = labels[key].data[extra]; + } + } + } + // Adjust header if there's a scrollbar + if (this.rows.children().last().length) { + this.gridHeader.css('margin-right', (this.rows.width() - this.rows.children().last().width()) + 'px'); + } + // Add actual events + for (var key in this._deferred_row_updates) { + window.clearTimeout(key); + } + window.setTimeout(jQuery.proxy(function () { + this._deferred_row_update(); + }, this), et2_calendar_planner.DEFERRED_ROW_TIME); + this.value = []; + }; + /** + * Draw a single row of the planner + * + * @param {string} key Index into the grouped labels & events + * @param {string} label + * @param {Array} events + * @param {Date} start + * @param {Date} end + */ + et2_calendar_planner.prototype._drawRow = function (key, label, events, start, end) { + var row = et2_createWidget('calendar-planner_row', { + id: 'planner_row_' + key, + label: label, + start_date: start, + end_date: end, + value: events, + readonly: this.options.readonly + }, this); + if (this.isInTree()) { + row.doLoadingFinished(); + } + return row; + }; + et2_calendar_planner.prototype._header_day_of_month = function () { + var day_width = 3.23; // 100.0 / 31; + // month scale with navigation + var content = '
'; + var start = new Date(this.options.start_date); + start = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000); + var end = new Date(this.options.end_date); + end = new Date(end.valueOf() + end.getTimezoneOffset() * 60 * 1000); + var title = this.egw().lang(date('F', start)) + ' ' + date('Y', start) + ' - ' + + this.egw().lang(date('F', end)) + ' ' + date('Y', end); + content += '"; + content += "
"; // end of plannerScale + // day of month scale + content += '
'; + for (var left = 0, i = 0; i < 31; left += day_width, ++i) { + content += '
' + + (1 + i) + "
\n"; + } + content += "
\n"; + return content; + }; + /** + * Make a header showing the months + * @param {Date} start + * @param {number} days + * @returns {string} HTML snippet + */ + et2_calendar_planner.prototype._header_months = function (start, days) { + var content = '
'; + var days_in_month = 0; + var day_width = 100 / days; + var end = new Date(start); + end.setUTCDate(end.getUTCDate() + days); + var t = new Date(start.valueOf()); + for (var left = 0, i = 0; i < days; t.setUTCDate(1), t.setUTCMonth(t.getUTCMonth() + 1), left += days_in_month * day_width, i += days_in_month) { + var u = new Date(t.getUTCFullYear(), t.getUTCMonth() + 1, 0, -t.getTimezoneOffset() / 60); + days_in_month = 1 + ((u - t) / (24 * 3600 * 1000)); + var first = new Date(t.getUTCFullYear(), t.getUTCMonth(), 1, -t.getTimezoneOffset() / 60); + if (days_in_month <= 0) + break; + if (i + days_in_month > days) { + days_in_month = days - i; + } + var title = this.egw().lang(date('F', new Date(t.valueOf() + t.getTimezoneOffset() * 60 * 1000))); + if (days_in_month > 10) { + title += ' ' + t.getUTCFullYear(); + } + else if (days_in_month < 5) { + title = ' '; + } + content += '
' + + title + "
"; + } + content += "
"; // end of plannerScale + return content; + }; + /** + * Make a header showing the week numbers + * + * @param {Date} start + * @param {number} days + * @returns {string} HTML snippet + */ + et2_calendar_planner.prototype._header_weeks = function (start, days) { + var content = '
'; + var state = ''; + // we're not using UTC so date() formatting function works + var t = new Date(start.valueOf()); + // Make sure we're lining up on the week + var week_end = app.calendar.date.end_of_week(start); + var days_in_week = Math.floor(((week_end - start) / (24 * 3600 * 1000)) + 1); + var week_width = 100 / days * (days <= 7 ? days : days_in_week); + for (var left = 0, i = 0; i < days; t.setUTCDate(t.getUTCDate() + 7), left += week_width) { + // Avoid overflow at the end + if (days - i < 7) { + days_in_week = days - i; + } + var usertime = new Date(t.valueOf()); + if (start.getTimezoneOffset() < 0) { + // Gets the right week # east of GMT. West does not need it(?) + usertime.setUTCMinutes(usertime.getUTCMinutes() - start.getTimezoneOffset()); + } + week_width = 100 / days * Math.min(days, days_in_week); + var title = this.egw().lang('Week') + ' ' + app.calendar.date.week_number(usertime); + if (start.getTimezoneOffset() > 0) { + // Gets the right week start west of GMT + usertime.setUTCMinutes(usertime.getUTCMinutes() + start.getTimezoneOffset()); + } + state = app.calendar.date.start_of_week(usertime); + state.setUTCHours(0); + state.setUTCMinutes(0); + state = state.toJSON(); + if (days_in_week > 1 || days == 1) { + content += '"; + } + i += days_in_week; + if (days_in_week != 7) { + t.setUTCDate(t.getUTCDate() - (7 - days_in_week)); + days_in_week = 7; + } + } + content += "
"; // end of plannerScale + return content; + }; + /** + * Make a header for some days + * + * @param {Date} start + * @param {number} days + * @returns {string} HTML snippet + */ + et2_calendar_planner.prototype._header_days = function (start, days) { + var day_width = 100 / days; + var content = '
'; + // we're not using UTC so date() formatting function works + var t = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000); + for (var left = 0, i = 0; i < days; t.setDate(t.getDate() + 1), left += day_width, ++i) { + if (!this.options.show_weekend && [0, 6].indexOf(t.getDay()) !== -1) + continue; + var holidays = []; + var tempDate = new Date(t); + tempDate.setMinutes(tempDate.getMinutes() - tempDate.getTimezoneOffset()); + var title = ''; + var state = new Date(t.valueOf() - t.getTimezoneOffset() * 60 * 1000); + var day_class = this.day_class_holiday(state, holidays, days); + if (days <= 3) { + title = this.egw().lang(date('l', t)) + ', ' + date('j', t) + '. ' + this.egw().lang(date('F', t)); + } + else if (days <= 7) { + title = this.egw().lang(date('l', t)) + ' ' + date('j', t); + } + else { + title = this.egw().lang(date('D', t)).substr(0, 2) + '
' + date('j', t); + } + content += '\n"; + } + content += "
"; // end of plannerScale + return content; + }; + /** + * Create a header with hours + * + * @param {Date} start + * @param {number} days + * @returns {string} HTML snippet for the header + */ + et2_calendar_planner.prototype._header_hours = function (start, days) { + var divisors = [1, 2, 3, 4, 6, 8, 12]; + var decr = 1; + for (var i = 0; i < divisors.length; i++) // numbers dividing 24 without rest + { + if (divisors[i] > days) + break; + decr = divisors[i]; + } + var hours = days * 24; + if (days === 1) // for a single day we calculate the hours of a days, to take into account daylight saving changes (23 or 25 hours) + { + var t = new Date(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate(), -start.getTimezoneOffset() / 60); + var s = new Date(start); + s.setUTCHours(23); + s.setUTCMinutes(59); + s.setUTCSeconds(59); + hours = Math.ceil((s.getTime() - t.getTime()) / 3600000); + } + var cell_width = 100 / hours * decr; + var content = '
'; + // we're not using UTC so date() formatting function works + var t = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000); + for (var left = 0, i = 0; i < hours; left += cell_width, i += decr) { + if (!this.options.show_weekend && [0, 6].indexOf(t.getDay()) !== -1) + continue; + var title = date(egw.preference('timeformat', 'calendar') == 12 ? 'ha' : 'H', t); + content += '"; + t.setHours(t.getHours() + decr); + } + content += "
"; // end of plannerScale + return content; + }; + /** + * Applies class for today, and any holidays for current day + * + * @param {Date} date + * @param {string[]} holiday_list Filled with a list of holidays for that day + * @param {integer} days Number of days shown in the day header + * + * @return {string} CSS Classes for the day. calendar_calBirthday, calendar_calHoliday, calendar_calToday and calendar_weekend as appropriate + */ + et2_calendar_planner.prototype.day_class_holiday = function (date, holiday_list, days) { + if (!date) + return ''; + var day_class = ''; + // Holidays and birthdays + var holidays = et2_widget_view_1.et2_calendar_view.get_holidays(this, date.getUTCFullYear()); + // Pass a string rather than the date object, to make sure it doesn't get changed + this.date_helper.set_value(date.toJSON()); + var date_key = '' + this.date_helper.get_year() + sprintf('%02d', this.date_helper.get_month()) + sprintf('%02d', this.date_helper.get_date()); + if (holidays && holidays[date_key]) { + holidays = holidays[date_key]; + for (var i = 0; i < holidays.length; i++) { + if (typeof holidays[i]['birthyear'] !== 'undefined') { + day_class += ' calendar_calBirthday '; + if (typeof days == 'undefined' || days <= 21) { + day_class += ' calendar_calBirthdayIcon '; + } + holiday_list.push(holidays[i]['name']); + } + else { + day_class += 'calendar_calHoliday '; + holiday_list.push(holidays[i]['name']); + } + } + } + holidays = holiday_list.join(','); + var today = new Date(); + if (date_key === '' + today.getFullYear() + + sprintf("%02d", today.getMonth() + 1) + + sprintf("%02d", today.getDate())) { + day_class += "calendar_calToday "; + } + if (date.getUTCDay() == 0 || date.getUTCDay() == 6) { + day_class += "calendar_weekend "; + } + return day_class; + }; + /** + * Link the actions to the DOM nodes / widget bits. + * + * @todo This currently does nothing + * @param {object} actions {ID: {attributes..}+} map of egw action information + */ + et2_calendar_planner.prototype._link_actions = function (actions) { + if (!this._actionObject) { + // Get the parent? Might be a grid row, might not. Either way, it is + // just a container with no valid actions + var objectManager = egw_getObjectManager(this.getInstanceManager().app, true, 1); + objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId, 2) || objectManager; + var parent = objectManager.getObjectById(this.id, 3) || objectManager.getObjectById(this._parent.id, 3) || objectManager; + if (!parent) { + debugger; + egw.debug('error', 'No parent objectManager found'); + return; + } + for (var i = 0; i < parent.children.length; i++) { + var parent_finder = jQuery('#' + this.div.id, parent.children[i].iface.doGetDOMNode()); + if (parent_finder.length > 0) { + parent = parent.children[i]; + break; + } + } + } + // This binds into the egw action system. Most user interactions (drag to move, resize) + // are handled internally using jQuery directly. + var widget_object = this._actionObject || parent.getObjectById(this.id); + var aoi = new et2_core_DOMWidget_1.et2_action_object_impl(this, this.getDOMNode(this)).getAOI(); + /** + * Determine if we allow a dropped event to use the invite/change actions, + * and enable or disable them appropriately + * + * @param {egwAction} action + * @param {et2_calendar_event} event The event widget being dragged + * @param {egwActionObject} target Planner action object + */ + var _invite_enabled = function (action, event, target) { + var event = event.iface.getWidget(); + var planner = target.iface.getWidget() || false; + //debugger; + if (event === planner || !event || !planner || + !event.options || !event.options.value.participants || !planner.options.owner) { + return false; + } + var owner_match = false; + var own_row = false; + for (var id in event.options.value.participants) { + planner.iterateOver(function (row) { + // Check scroll section or header section + if (row.div.hasClass('drop-hover') || row.div.has(':hover')) { + owner_match = owner_match || row.node.dataset[planner.options.group_by] === '' + id; + own_row = (row === event.getParent()); + } + }, this, et2_widget_planner_row_1.et2_calendar_planner_row); + } + var enabled = !owner_match && + // Not inside its own row + !own_row; + widget_object.getActionLink('invite').enabled = enabled; + widget_object.getActionLink('change_participant').enabled = enabled; + // If invite or change participant are enabled, drag is not + widget_object.getActionLink('egw_link_drop').enabled = !enabled; + }; + aoi.doTriggerEvent = function (_event, _data) { + // Determine target node + var event = _data.event || false; + if (!event) + return; + if (_data.ui.draggable.hasClass('rowNoEdit')) + return; + /* + We have to handle the drop in the normal event stream instead of waiting + for the egwAction system so we can get the helper, and destination + */ + if (event.type === 'drop') { + this.getWidget()._event_drop.call(jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper)[0], this.getWidget(), event, _data.ui); + } + var drag_listener = function (event, ui) { + aoi.getWidget()._drag_helper(jQuery('.calendar_d-n-d_timeCounter', ui.helper)[0], { + top: ui.position.top, + left: ui.position.left - jQuery(this).parent().offset().left + }, 0); + }; + var time = jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper); + switch (_event) { + // Triggered once, when something is dragged into the timegrid's div + case EGW_AI_DRAG_OVER: + // Listen to the drag and update the helper with the time + // This part lets us drag between different timegrids + _data.ui.draggable.on('drag.et2_timegrid' + widget_object.id, drag_listener); + _data.ui.draggable.on('dragend.et2_timegrid' + widget_object.id, function () { + _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); + }); + if (time.length) { + // The out will trigger after the over, so we count + time.data('count', time.data('count') + 1); + } + else { + _data.ui.helper.prepend('
'); + } + break; + // Triggered once, when something is dragged out of the timegrid + case EGW_AI_DRAG_OUT: + // Stop listening + _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); + // Remove any highlighted time squares + jQuery('[data-date]', this.doGetDOMNode()).removeClass("ui-state-active"); + // Out triggers after the over, count to not accidentally remove + time.data('count', time.data('count') - 1); + if (time.length && time.data('count') <= 0) { + time.remove(); + } + break; + } + }; + if (widget_object == null) { + // Add a new container to the object manager which will hold the widget + // objects + widget_object = parent.insertObject(false, new egwActionObject(this.id, parent, aoi, this._actionManager || parent.manager.getActionById(this.id) || parent.manager), EGW_AO_FLAG_IS_CONTAINER); + } + else { + widget_object.setAOI(aoi); + } + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + var action_links = this._get_action_links(actions); + this._init_links_dnd(widget_object.manager, action_links); + widget_object.updateActionLinks(action_links); + this._actionObject = widget_object; + }; + /** + * Automatically add dnd support for linking + * + * @param {type} mgr + * @param {type} actionLinks + */ + et2_calendar_planner.prototype._init_links_dnd = function (mgr, actionLinks) { + if (this.options.readonly) + return; + var self = this; + var drop_action = mgr.getActionById('egw_link_drop'); + var drop_change_participant = mgr.getActionById('change_participant'); + var drop_invite = mgr.getActionById('invite'); + var drag_action = mgr.getActionById('egw_link_drag'); + var paste_action = mgr.getActionById('egw_paste'); + // Disable paste action + if (paste_action == null) { + paste_action = mgr.addAction('popup', 'egw_paste', egw.lang('Paste'), egw.image('editpaste'), function () { }, true); + } + paste_action.set_enabled(false); + // Check if this app supports linking + if (!egw.link_get_registry(this.dataStorePrefix || 'calendar', 'query') || + egw.link_get_registry(this.dataStorePrefix || 'calendar', 'title')) { + if (drop_action) { + drop_action.remove(); + if (actionLinks.indexOf(drop_action.id) >= 0) { + actionLinks.splice(actionLinks.indexOf(drop_action.id), 1); + } + } + if (drag_action) { + drag_action.remove(); + if (actionLinks.indexOf(drag_action.id) >= 0) { + actionLinks.splice(actionLinks.indexOf(drag_action.id), 1); + } + } + return; + } + // Don't re-add + if (drop_action == null) { + // Create the drop action that links entries + drop_action = mgr.addAction('drop', 'egw_link_drop', egw.lang('Create link'), egw.image('link'), function (action, source, dropped) { + // Extract link IDs + var links = []; + var id = ''; + for (var i = 0; i < source.length; i++) { + if (!source[i].id) + continue; + id = source[i].id.split('::'); + links.push({ app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1] }); + } + if (!links.length) { + return; + } + if (links.length && dropped && dropped.iface.getWidget() && dropped.iface.getWidget().instanceOf(et2_widget_event_1.et2_calendar_event)) { + // Link the entries + egw.json(self.egw().getAppName() + ".etemplate_widget_link.ajax_link.etemplate", dropped.id.split('::').concat([links]), function (result) { + if (result) { + this.egw().message('Linked'); + } + }, self, true, self).sendRequest(); + } + }, true); + drop_action.acceptedTypes = ['default', 'link']; + drop_action.hideOnDisabled = true; + // Create the drop action for moving events between planner rows + var invite_action = function (action, source, target) { + // Extract link IDs + var links = []; + var id = ''; + for (var i = 0; i < source.length; i++) { + // Check for no ID (invalid) or same manager (dragging an event) + if (!source[i].id) + continue; + if (source[i].manager === target.manager) { + // Find the row, could have dropped on an event + var row = target.iface.getWidget(); + while (target.parent && row.instanceOf && !row.instanceOf(et2_widget_planner_row_1.et2_calendar_planner_row)) { + target = target.parent; + row = target.iface.getWidget(); + } + // Leave the helper there until the update is done + var loading = action.ui.helper.clone(true).appendTo(jQuery('body')); + // and add a loading icon so user knows something is happening + if (jQuery('.calendar_timeDemo', loading).length == 0) { + jQuery('.calendar_calEventHeader', loading).addClass('loading'); + } + else { + jQuery('.calendar_timeDemo', loading).after('
'); + } + var event_data = egw.dataGetUIDdata(source[i].id).data; + et2_widget_event_1.et2_calendar_event.recur_prompt(event_data, function (button_id) { + if (button_id === 'cancel' || !button_id) { + return; + } + var add_owner = [row.node.dataset.participants]; + egw().json('calendar.calendar_uiforms.ajax_invite', [ + button_id === 'series' ? event_data.id : event_data.app_id, + add_owner, + action.id === 'change_participant' ? + [source[i].iface.getWidget().getParent().node.dataset.participants] : + [] + ], function () { loading.remove(); }).sendRequest(true); + }); + // Ok, stop. + return false; + } + } + }; + drop_change_participant = mgr.addAction('drop', 'change_participant', egw.lang('Move to'), egw.image('participant'), invite_action, true); + drop_change_participant.acceptedTypes = ['calendar']; + drop_change_participant.hideOnDisabled = true; + drop_invite = mgr.addAction('drop', 'invite', egw.lang('Invite'), egw.image('participant'), invite_action, true); + drop_invite.acceptedTypes = ['calendar']; + drop_invite.hideOnDisabled = true; + } + if (actionLinks.indexOf(drop_action.id) < 0) { + actionLinks.push(drop_action.id); + } + actionLinks.push(drop_invite.id); + actionLinks.push(drop_change_participant.id); + // Accept other links, and files dragged from the filemanager + // This does not handle files dragged from the desktop. They are + // handled by et2_nextmatch, since it needs DOM stuff + if (drop_action.acceptedTypes.indexOf('link') == -1) { + drop_action.acceptedTypes.push('link'); + } + // Don't re-add + if (drag_action == null) { + // Create drag action that allows linking + drag_action = mgr.addAction('drag', 'egw_link_drag', egw.lang('link'), 'link', function (action, selected) { + // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links + // TODO: Need to decide if we need to create a customized helper interface for links anyway + //return helper; + return null; + }, true); + } + // The planner itself is not draggable, the action is there for the children + if (false && actionLinks.indexOf(drag_action.id) < 0) { + actionLinks.push(drag_action.id); + } + drag_action.set_dragType(['link', 'calendar']); + }; + /** + * Get all action-links / id's of 1.-level actions from a given action object + * + * Here we are only interested in drop events. + * + * @param actions + * @returns {Array} + */ + et2_calendar_planner.prototype._get_action_links = function (actions) { + var action_links = []; + // Only these actions are allowed without a selection (empty actions) + var empty_actions = ['add']; + for (var i in actions) { + var action = actions[i]; + if (empty_actions.indexOf(action.id) !== -1 || action.type === 'drop') { + action_links.push(typeof action.id !== 'undefined' ? action.id : i); + } + } + // Disable automatic paste action, it doesn't have what is needed to work + action_links.push({ + "actionObj": 'egw_paste', + "enabled": false, + "visible": false + }); + return action_links; + }; + /** + * Show the current time while dragging + * Used for resizing as well as drag & drop + * + * @param {type} element + * @param {type} position + * @param {type} height + */ + et2_calendar_planner.prototype._drag_helper = function (element, position, height) { + var time = this._get_time_from_position(position.left, position.top); + element.dropEnd = time; + var formatted_time = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { + hour: time.getUTCHours(), + minute: time.getUTCMinutes(), + seconds: 0, + timezone: 0 + }, { "ampm": (egw.preference("timeformat") === "12") }); + element.innerHTML = '
' + formatted_time + '
'; + //jQuery(element).width(jQuery(helper).width()); + }; + /** + * Handler for dropping an event on the timegrid + * + * @param {type} planner + * @param {type} event + * @param {type} ui + */ + et2_calendar_planner.prototype._event_drop = function (planner, event, ui) { + var e = new jQuery.Event('change'); + e.originalEvent = event; + e.data = { start: 0 }; + if (typeof this.dropEnd != 'undefined') { + var drop_date = this.dropEnd.toJSON() || false; + var event_data = planner._get_event_info(ui.draggable); + var event_widget = planner.getWidgetById(event_data.widget_id); + if (event_widget) { + event_widget._parent.date_helper.set_value(drop_date); + event_widget.options.value.start = new Date(event_widget._parent.date_helper.getValue()); + // Leave the helper there until the update is done + var loading = ui.helper.clone().appendTo(ui.helper.parent()); + // and add a loading icon so user knows something is happening + jQuery('.calendar_timeDemo', loading).after('
'); + event_widget.recur_prompt(function (button_id) { + if (button_id === 'cancel' || !button_id) + return; + //Get infologID if in case if it's an integrated infolog event + if (event_data.app === 'infolog') { + // If it is an integrated infolog event we need to edit infolog entry + egw().json('stylite_infolog_calendar_integration::ajax_moveInfologEvent', [event_data.id, event_widget.options.value.start || false], function () { loading.remove(); }).sendRequest(true); + } + else { + //Edit calendar event + egw().json('calendar.calendar_uiforms.ajax_moveEvent', [ + button_id === 'series' ? event_data.id : event_data.app_id, event_data.owner, + event_widget.options.value.start, + planner.options.owner || egw.user('account_id') + ], function () { loading.remove(); }).sendRequest(true); + } + }); + } + } + }; + /** + * Use the egw.data system to get data from the calendar list for the + * selected time span. + * + */ + et2_calendar_planner.prototype._fetch_data = function () { + var value = []; + var fetch = false; + this.doInvalidate = false; + for (var i = 0; i < this.registeredCallbacks.length; i++) { + egw.dataUnregisterUID(this.registeredCallbacks[i], false, this); + } + this.registeredCallbacks.splice(0, this.registeredCallbacks.length); + // Remember previous day to avoid multi-days duplicating + var last_data = []; + var t = new Date(this.options.start_date); + var end = new Date(this.options.end_date); + do { + value = value.concat(this._cache_register(t, this.options.owner, last_data)); + t.setUTCDate(t.getUTCDate() + 1); + } while (t < end); + this.doInvalidate = true; + return value; + }; + /** + * Deal with registering for data cache + * + * @param Date t + * @param String owner Calendar owner + */ + et2_calendar_planner.prototype._cache_register = function (t, owner, last_data) { + // Cache is by date (and owner, if seperate) + var date = t.getUTCFullYear() + sprintf('%02d', t.getUTCMonth() + 1) + sprintf('%02d', t.getUTCDate()); + var cache_id = CalendarApp._daywise_cache_id(date, owner); + var value = []; + if (egw.dataHasUID(cache_id)) { + var c = egw.dataGetUIDdata(cache_id); + if (c.data && c.data !== null) { + // There is data, pass it along now + for (var j = 0; j < c.data.length; j++) { + if (last_data.indexOf(c.data[j]) === -1 && egw.dataHasUID('calendar::' + c.data[j])) { + value.push(egw.dataGetUIDdata('calendar::' + c.data[j]).data); + } + } + last_data = c.data; + } + } + else { + fetch = true; + // Assume it's empty, if there is data it will be filled later + egw.dataStoreUID(cache_id, []); + } + this.registeredCallbacks.push(cache_id); + egw.dataRegisterUID(cache_id, function (data) { + if (data && data.length) { + var invalidate = true; + // Try to determine rows interested + var labels = []; + var events = {}; + if (this.grouper) { + labels = this.grouper.row_labels.call(this); + invalidate = false; + } + var im = this.getInstanceManager(); + for (var i = 0; i < data.length; i++) { + var event = egw.dataGetUIDdata('calendar::' + data[i]); + if (!event) + continue; + events = {}; + // Try to determine rows interested + if (event.data && this.grouper) { + this.grouper.group.call(this, labels, events, event.data); + } + if (Object.keys(events).length > 0) { + for (var label_id in events) { + var id = "" + labels[label_id].id; + if (typeof this.cache[id] === 'undefined') { + this.cache[id] = []; + } + if (this.cache[id].indexOf(event.data.row_id) === -1) { + this.cache[id].push(event.data.row_id); + } + if (this._deferred_row_updates[id]) { + window.clearTimeout(this._deferred_row_updates[id]); + } + this._deferred_row_updates[id] = window.setTimeout(jQuery.proxy(this._deferred_row_update, this, id), this.DEFERRED_ROW_TIME); + } + } + else { + // Could be an event no row is interested in, could be a problem. + // Just redraw everything + invalidate = true; + continue; + } + // If displaying by category, we need the infolog (or other app) categories too + if (event && event.data && event.data.app && this.options.group_by == 'category') { + // Fake it to use the cache / call + et2_selectbox.cat_options({ + _type: 'select-cat', + getInstanceManager: function () { return im; } + }, { application: event.data.app || 'calendar' }); + } + } + if (invalidate) { + this.invalidate(false); + } + } + }, this, this.getInstanceManager().execId, this.id); + return value; + }; + /** + * Because users may be participants in various events and the time it takes + * to create many events, we don't want to update a row too soon - we may have + * to re-draw it if we find the user / category in another event. Pagination + * makes this worse. We wait a bit before updating the row to avoid + * having to re-draw it multiple times. + * + * @param {type} id + * @returns {undefined} + */ + et2_calendar_planner.prototype._deferred_row_update = function (id) { + // Something's in progress, skip + if (!this.doInvalidate) + return; + this.grid.height(0); + var id_list = typeof id === 'undefined' ? Object.keys(this.cache) : [id]; + for (var i = 0; i < id_list.length; i++) { + var cache_id = id_list[i]; + var row = this.getWidgetById('planner_row_' + cache_id); + window.clearTimeout(this._deferred_row_updates[cache_id]); + delete this._deferred_row_updates[cache_id]; + if (row) { + row._data_callback(this.cache[cache_id]); + row.set_disabled(this.options.hide_empty && this.cache[cache_id].length === 0); + } + else { + break; + } + } + // Updating the row may push things longer, update length + // Add 1 to keep the scrollbar, otherwise we need to recalculate the + // header widths too. + this.grid.height(this.rows[0].scrollHeight + 1); + // Adjust header if there's a scrollbar - Firefox needs this re-calculated, + // otherwise the header will be missing the margin space for the scrollbar + // in some cases + if (this.rows.children().last().length) { + this.gridHeader.css('margin-right', (this.rows.width() - this.rows.children().last().width()) + 'px'); + } + }; + /** + * Provide specific data to be displayed. + * This is a way to set start and end dates, owner and event data in once call. + * + * @param {Object[]} events Array of events, indexed by date in Ymd format: + * { + * 20150501: [...], + * 20150502: [...] + * } + * Days should be in order. + * + */ + et2_calendar_planner.prototype.set_value = function (events) { + if (typeof events !== 'object') + return false; + _super.prototype.set_value.call(this, events); + // Planner uses an array, not map + var val = this.value; + var array = []; + Object.keys(this.value).forEach(function (key) { + array.push(val[key]); + }); + this.value = array; + }; + /** + * Change the start date + * Planner view uses a date object internally + * + * @param {string|number|Date} new_date New starting date + * @returns {undefined} + */ + et2_calendar_planner.prototype.set_start_date = function (new_date) { + _super.prototype.set_start_date.call(this, new_date); + this.options.start_date = new Date(this.options.start_date); + }; + /** + * Change the end date + * Planner view uses a date object internally + * + * @param {string|number|Date} new_date New end date + * @returns {undefined} + */ + et2_calendar_planner.prototype.set_end_date = function (new_date) { + _super.prototype.set_end_date.call(this, new_date); + this.options.end_date = new Date(this.options.end_date); + }; + /** + * Change how the planner is grouped + * + * @param {string|number} group_by 'user', 'month', or an integer category ID + * @returns {undefined} + */ + et2_calendar_planner.prototype.set_group_by = function (group_by) { + if (isNaN(group_by) && typeof this.groupers[group_by] === 'undefined') { + throw new Error('Invalid group_by "' + group_by + '"'); + } + var old = this.options.group_by; + this.options.group_by = '' + group_by; + this.grouper = this.groupers[isNaN(this.options.group_by) ? this.options.group_by : 'category']; + if (old !== this.options.group_by && this.isAttached()) { + this.invalidate(true); + } + }; + /** + * Turn on or off the visibility of weekends + * + * @param {boolean} weekends + */ + et2_calendar_planner.prototype.set_show_weekend = function (weekends) { + weekends = weekends ? true : false; + if (this.options.show_weekend !== weekends) { + this.options.show_weekend = weekends; + if (this.isAttached()) { + this.invalidate(); + } + } + }; + /** + * Turn on or off the visibility of hidden (empty) rows + * + * @param {boolean} hidden + */ + et2_calendar_planner.prototype.set_hide_empty = function (hidden) { + this.options.hide_empty = hidden; + }; + /** + * Call change handler, if set + * + * @param {type} event + */ + et2_calendar_planner.prototype.change = function (event) { + if (this.onchange) { + if (typeof this.onchange == 'function') { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.push(this); + return this.onchange.apply(this, args); + } + else { + return (et2_compileLegacyJS(this.options.onchange, this, _node))(); + } + } + }; + /** + * Call event change handler, if set + * + * @param {type} event + * @param {type} dom_node + */ + et2_calendar_planner.prototype.event_change = function (event, dom_node) { + if (this.onevent_change) { + var event_data = this._get_event_info(dom_node); + var event_widget = this.getWidgetById(event_data.widget_id); + et2_widget_event_1.et2_calendar_event.recur_prompt(event_data, jQuery.proxy(function (button_id, event_data) { + // No need to continue + if (button_id === 'cancel') + return false; + if (typeof this.onevent_change == 'function') { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(event_widget) == -1) + args.push(event_widget); + // Put button ID in event + event.button_id = button_id; + return this.onevent_change.apply(this, [event, event_widget, button_id]); + } + else { + return (et2_compileLegacyJS(this.options.onevent_change, event_widget, dom_node))(); + } + }, this)); + } + return false; + }; + /** + * Click handler calling custom handler set via onclick attribute to this.onclick + * + * This also handles all its own actions, including navigation. If there is + * an event associated with the click, it will be found and passed to the + * onclick function. + * + * @param {Event} _ev + * @returns {boolean} Continue processing event (true) or stop (false) + */ + et2_calendar_planner.prototype.click = function (_ev) { + var result = true; + // Drag to create in progress + if (this.drag_create.start !== null) + return; + // Is this click in the event stuff, or in the header? + if (!this.options.readonly && this.gridHeader.has(_ev.target).length === 0 && !jQuery(_ev.target).hasClass('calendar_plannerRowHeader')) { + // Event came from inside, maybe a calendar event + var event = this._get_event_info(_ev.originalEvent.target); + if (typeof this.onclick == 'function') { + // Make sure function gets a reference to the widget, splice it in as 2. argument if not + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.splice(1, 0, this); + result = this.onclick.apply(this, args); + } + if (event.id && result && !this.options.disabled && !this.options.readonly) { + et2_widget_event_1.et2_calendar_event.recur_prompt(event); + return false; + } + else if (!event.id) { + // Clicked in row, but not on an event + // Default handler to open a new event at the selected time + if (jQuery(event.target).closest('.calendar_eventRows').length == 0) { + // "Invalid" times, from space after the last planner row, or header + var date = this._get_time_from_position(_ev.pageX - this.grid.offset().left, _ev.pageY - this.grid.offset().top); + } + else if (this.options.group_by == 'month') { + var date = this._get_time_from_position(_ev.clientX, _ev.clientY); + } + else { + var date = this._get_time_from_position(_ev.offsetX, _ev.offsetY); + } + var row = jQuery(_ev.target).closest('.calendar_plannerRowWidget'); + var data = row.length ? row[0].dataset : {}; + if (date) { + app.calendar.add(jQuery.extend({ + start: date.toJSON(), + hour: date.getUTCHours(), + minute: date.getUTCMinutes() + }, data)); + return false; + } + } + return result; + } + else if (this.gridHeader.has(_ev.target).length > 0 && !jQuery.isEmptyObject(_ev.target.dataset) || + jQuery(_ev.target).hasClass('calendar_plannerRowHeader') && !jQuery.isEmptyObject(_ev.target.dataset)) { + // Click on a header, we can go there + _ev.data = jQuery.extend({}, _ev.target.parentNode.dataset, _ev.target.dataset); + for (var key in _ev.data) { + if (!_ev.data[key]) { + delete _ev.data[key]; + } + } + app.calendar.update_state(_ev.data); + } + else if (!this.options.readonly) { + // Default handler to open a new event at the selected time + // TODO: Determine date / time more accurately from position + app.calendar.add({ + date: _ev.target.dataset.date || this.options.start_date.toJSON(), + hour: _ev.target.dataset.hour || this.options.day_start, + minute: _ev.target.dataset.minute || 0 + }); + return false; + } + }; + /** + * Get time from position + * + * @param {number} x + * @param {number} y + * @returns {Date|Boolean} A time for the given position, or false if one + * could not be determined. + */ + et2_calendar_planner.prototype._get_time_from_position = function (x, y) { + x = Math.round(x); + y = Math.round(y); + // Round to user's preferred event interval + var interval = egw.preference('interval', 'calendar') || 30; + // Relative horizontal position, as a percentage + var width = 0; + jQuery('.calendar_eventRows', this.div).each(function () { width = Math.max(width, jQuery(this).width()); }); + var rel_x = Math.min(x / width, 1); + // Relative time, in minutes from start + var rel_time = 0; + var day_header = jQuery('.calendar_plannerScaleDay', this.headers); + // Simple math, the x is offset from start date + if (this.options.group_by !== 'month' && ( + // Either all days are visible, or only 1 day (no day header) + this.options.show_weekend || day_header.length === 0)) { + rel_time = (new Date(this.options.end_date) - new Date(this.options.start_date)) * rel_x / 1000; + this.date_helper.set_value(this.options.start_date.toJSON()); + } + // Not so simple math, need to account for missing days + else if (this.options.group_by !== 'month' && !this.options.show_weekend) { + // Find which day + if (day_header.length === 0) + return false; + var day = document.elementFromPoint(day_header.offset().left + rel_x * this.headers.innerWidth(), day_header.offset().top); + // Use day, and find time in that day + if (day && day.dataset && day.dataset.date) { + this.date_helper.set_value(day.dataset.date); + rel_time = ((x - jQuery(day).position().left) / jQuery(day).outerWidth(true)) * 24 * 60; + this.date_helper.set_minutes(Math.round(rel_time / interval) * interval); + return new Date(this.date_helper.getValue()); + } + return false; + } + else { + // Find the correct row so we know which month, then get the offset + var hidden_nodes = []; + var row = null; + // Hide any drag or tooltips that may interfere + do { + row = document.elementFromPoint(x, y); + if (this.div.has(row).length == 0) { + hidden_nodes.push(jQuery(row).hide()); + } + else { + break; + } + } while (row && row.nodeName !== 'BODY'); + if (!row) + return false; + // Restore hidden nodes + for (var i = 0; i < hidden_nodes.length; i++) { + hidden_nodes[i].show(); + } + row = jQuery(row).closest('.calendar_plannerRowWidget'); + var row_widget = null; + for (var i = 0; i < this._children.length && row.length > 0; i++) { + if (this._children[i].div[0] == row[0]) { + row_widget = this._children[i]; + break; + } + } + if (row_widget) { + // Not sure where the extra -1 and +2 are coming from, but it makes it work out + // in FF & Chrome + rel_x = Math.min((x - row_widget.rows.offset().left - 1) / (row_widget.rows.width() + 2), 1); + // 2678400 is the number of seconds in 31 days + rel_time = (2678400) * rel_x; + this.date_helper.set_value(row_widget.options.start_date.toJSON()); + } + else { + return false; + } + } + if (rel_time < 0) + return false; + this.date_helper.set_minutes(Math.round(rel_time / (60 * interval)) * interval); + return new Date(this.date_helper.getValue()); + }; + /** + * Mousedown handler to support drag to create + * + * @param {jQuery.Event} event + */ + et2_calendar_planner.prototype._mouse_down = function (event) { + // Only left mouse button + if (event.which !== 1) + return; + // Ignore headers + if (this.headers.has(event.target).length !== 0) + return false; + // Get time at mouse + if (this.options.group_by === 'month') { + var time = this._get_time_from_position(event.clientX, event.clientY); + } + else { + var time = this._get_time_from_position(event.offsetX, event.offsetY); + } + if (!time) + return false; + // Find the correct row so we know the parent + var row = event.target.closest('.calendar_plannerRowWidget'); + for (var i = 0; i < this._children.length && row; i++) { + if (this._children[i].div[0] === row) { + this.drag_create.parent = this._children[i]; + // Clear cached events for re-layout + this._children[i]._cached_rows = []; + break; + } + } + if (!this.drag_create.parent) + return false; + this.div.css('cursor', 'ew-resize'); + return this._drag_create_start(jQuery.extend({}, this.drag_create.parent.node.dataset, { date: time.toJSON() })); + }; + /** + * Mouseup handler to support drag to create + * + * @param {jQuery.Event} event + */ + et2_calendar_planner.prototype._mouse_up = function (event) { + // Get time at mouse + if (this.options.group_by === 'month') { + var time = this._get_time_from_position(event.clientX, event.clientY); + } + else { + var time = this._get_time_from_position(event.offsetX, event.offsetY); + } + return this._drag_create_end(time ? { date: time.toJSON() } : false); + }; + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs array to add further attributes to + */ + et2_calendar_planner.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push('start_date', 'end_date'); + }; + et2_calendar_planner.prototype.getDetachedNodes = function () { + return [this.getDOMNode()]; + }; + et2_calendar_planner.prototype.setDetachedAttributes = function (_nodes, _values) { + this.div = jQuery(_nodes[0]); + if (_values.start_date) { + this.set_start_date(_values.start_date); + } + if (_values.end_date) { + this.set_end_date(_values.end_date); + } + }; + // Resizable interface + et2_calendar_planner.prototype.resize = function () { + // Take the whole tab height + var height = Math.min(jQuery(this.getInstanceManager().DOMContainer).height(), jQuery(this.getInstanceManager().DOMContainer).parent().innerHeight()); + // Allow for toolbar + height -= jQuery('#calendar-toolbar', this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true); + this.options.height = height; + this.div.css('height', this.options.height); + // Set height for rows + this.rows.height(this.div.height() - this.headers.outerHeight()); + this.grid.height(this.rows[0].scrollHeight); + }; + /** + * Set up for printing + * + * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up + * (waiting for data) + */ + et2_calendar_planner.prototype.beforePrint = function () { + if (this.disabled || !this.div.is(':visible')) { + return; + } + this.rows.css('overflow-y', 'visible'); + var rows = jQuery('.calendar_eventRows'); + var width = rows.width(); + var events = jQuery('.calendar_calEvent', rows) + .each(function () { + var event = jQuery(this); + event.width((event.width() / width) * 100 + '%'); + }); + }; + /** + * Reset after printing + */ + et2_calendar_planner.prototype.afterPrint = function () { + this.rows.css('overflow-y', 'auto'); + }; + et2_calendar_planner._attributes = { + group_by: { + name: "Group by", + type: "string", + default: "0", + description: "Display planner by 'user', 'month', or the given category" + }, + filter: { + name: "Filter", + type: "string", + default: '', + description: 'A filter that is used to select events. It is passed along when events are queried.' + }, + show_weekend: { + name: "Weekends", + type: "boolean", + default: egw.preference('days_in_weekview', 'calendar') != 5, + description: "Display weekends. The date range should still include them for proper scrolling, but they just won't be shown." + }, + hide_empty: { + name: "Hide empty rows", + type: "boolean", + default: false, + description: "Hide rows with no events." + }, + value: { + type: "any", + description: "A list of events, optionally you can set start_date, end_date and group_by as keys and events will be fetched" + }, + "onchange": { + "name": "onchange", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the date range changes." + }, + "onevent_change": { + "name": "onevent_change", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when an event changes." + } + }; + et2_calendar_planner.DEFERRED_ROW_TIME = 100; + return et2_calendar_planner; +}(et2_widget_view_1.et2_calendar_view)); +exports.et2_calendar_planner = et2_calendar_planner; +et2_core_widget_1.et2_register_widget(et2_calendar_planner, ["calendar-planner"]); +//# sourceMappingURL=et2_widget_planner.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_planner_row.js b/calendar/js/et2_widget_planner_row.js index 7e713bb8b1..fa978f58ec 100644 --- a/calendar/js/et2_widget_planner_row.js +++ b/calendar/js/et2_widget_planner_row.js @@ -1,3 +1,4 @@ +"use strict"; /* * Egroupware * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License @@ -7,812 +8,663 @@ * @author Nathan Gray * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /calendar/js/et2_widget_view.js; - /calendar/js/et2_widget_daycol.js; - /calendar/js/et2_widget_event.js; + /calendar/js/et2_widget_view.js; + /calendar/js/et2_widget_daycol.js; + /calendar/js/et2_widget_event.js; */ - - +var et2_core_widget_1 = require("../../api/js/etemplate/et2_core_widget"); +var et2_core_valueWidget_1 = require("../../api/js/etemplate/et2_core_valueWidget"); +var et2_core_inheritance_1 = require("../../api/js/etemplate/et2_core_inheritance"); +var et2_core_DOMWidget_1 = require("../../api/js/etemplate/et2_core_DOMWidget"); /** * Class for one row of a planner * * This widget is responsible for the label on the side * - * @augments et2_valueWidget */ -var et2_calendar_planner_row = (function(){ "use strict"; return et2_valueWidget.extend([et2_IResizeable], -{ - attributes: { - start_date: { - name: "Start date", - type: "any" - }, - end_date: { - name: "End date", - type: "any" - }, - value: { - type: "any" - } - }, - - /** - * Constructor - * - * @memberOf et2_calendar_daycol - */ - init: function() { - this._super.apply(this, arguments); - - // Main container - this.div = jQuery(document.createElement("div")) - .addClass("calendar_plannerRowWidget") - .css('width',this.options.width); - this.title = jQuery(document.createElement('div')) - .addClass("calendar_plannerRowHeader") - .appendTo(this.div); - this.rows = jQuery(document.createElement('div')) - .addClass("calendar_eventRows") - .appendTo(this.div); - - this.setDOMNode(this.div[0]); - - // Used for its date calculations - this.date_helper = et2_createWidget('date-time',{},null); - this.date_helper.loadingFinished(); - - this.set_start_date(this.options.start_date); - this.set_end_date(this.options.end_date); - - this._cached_rows = []; - this._row_height = 20; - }, - - doLoadingFinished: function() { - this._super.apply(this, arguments); - - this.set_label(this.options.label); - this._draw(); - - // Actions are set on the parent, so we need to explicitly get in here - // and get ours - this._link_actions(this._parent.options.actions || []); - return true; - }, - - destroy: function() { - this._super.apply(this, arguments); - - // date_helper has no parent, so we must explicitly remove it - this.date_helper.destroy(); - this.date_helper = null; - }, - - getDOMNode: function(_sender) - { - if(_sender === this || !_sender) - { - return this.div[0]; - } - if(_sender._parent === this) - { - return this.rows[0]; - } - }, - - /** - * Link the actions to the DOM nodes / widget bits. - * - * @param {object} actions {ID: {attributes..}+} map of egw action information - */ - _link_actions: function(actions) - { - // Get the parent? Might be a grid row, might not. Either way, it is - // just a container with no valid actions - var objectManager = egw_getObjectManager(this.getInstanceManager().app,true,1); - objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId,2) || objectManager; - var parent = objectManager.getObjectById(this.id,1) || objectManager.getObjectById(this._parent.id,1) || objectManager; - if(!parent) - { - egw.debug('error','No parent objectManager found'); - return; - } - - // This binds into the egw action system. Most user interactions (drag to move, resize) - // are handled internally using jQuery directly. - var widget_object = this._actionObject || parent.getObjectById(this.id); - var aoi = new et2_action_object_impl(this,this.getDOMNode()); - var planner = this.getParent(); - - for(var i = 0; i < parent.children.length; i++) - { - var parent_finder = jQuery(parent.children[i].iface.doGetDOMNode()).find(this.div); - if(parent_finder.length > 0) - { - parent = parent.children[i]; - break; - } - } - - // Determine if we allow a dropped event to use the invite/change actions - var _invite_enabled = function(action, event, target) - { - var event = event.iface.getWidget(); - var row = target.iface.getWidget() || false; - if(event === row || !event || !row || - !event.options || !event.options.value.participants - ) - { - return false; - } - - var owner_match = false; - var own_row = event.getParent() === row; - - for(var id in event.options.value.participants) - { - owner_match = owner_match || row.node.dataset.participants === ''+id; - } - - var enabled = !owner_match && - // Not inside its own timegrid - !own_row; - - widget_object.getActionLink('invite').enabled = enabled; - widget_object.getActionLink('change_participant').enabled = enabled; - - // If invite or change participant are enabled, drag is not - widget_object.getActionLink('egw_link_drop').enabled = !enabled; - }; - - aoi.doTriggerEvent = function(_event, _data) { - - // Determine target node - var event = _data.event || false; - if(!event) return; - if(_data.ui.draggable.hasClass('rowNoEdit')) return; - /* - We have to handle the drop in the normal event stream instead of waiting - for the egwAction system so we can get the helper, and destination - */ - if(event.type === 'drop' && widget_object.getActionLink('egw_link_drop').enabled) - { - this.getWidget().getParent()._event_drop.call( - jQuery('.calendar_d-n-d_timeCounter',_data.ui.helper)[0], - this.getWidget().getParent(), event, _data.ui, - this.getWidget() - ); - } - var drag_listener = function(_event, ui) { - if(planner.options.group_by === 'month') - { - var position = {left: _event.clientX, top: _event.clientY}; - } - else - { - var position = {top:ui.position.top, left: ui.position.left - jQuery(this).parent().offset().left}; - } - aoi.getWidget().getParent()._drag_helper( - jQuery('.calendar_d-n-d_timeCounter',ui.helper)[0], - position,0 - ); - - var event = _data.ui.draggable.data('selected')[0]; - if(!event || event.id && event.id.indexOf('calendar') !== 0) - { - event = false; - } - if(event) - { - _invite_enabled( - widget_object.getActionLink('invite').actionObj, - event, - widget_object - ); - } - }; - var time = jQuery('.calendar_d-n-d_timeCounter',_data.ui.helper); - switch(_event) - { - // Triggered once, when something is dragged into the timegrid's div - case EGW_AI_DRAG_OVER: - // Listen to the drag and update the helper with the time - // This part lets us drag between different timegrids - _data.ui.draggable.on('drag.et2_timegrid_row'+widget_object.id, drag_listener); - _data.ui.draggable.on('dragend.et2_timegrid_row'+widget_object.id, function() { - _data.ui.draggable.off('drag.et2_timegrid_row' + widget_object.id); - }); - widget_object.iface.getWidget().div.addClass('drop-hover'); - - // Disable invite / change actions for same calendar or already participant - var event = _data.ui.draggable.data('selected')[0]; - if(!event || event.id && event.id.indexOf('calendar') !== 0) - { - event = false; - } - if(event) - { - _invite_enabled( - widget_object.getActionLink('invite').actionObj, - event, - widget_object - ); - } - if(time.length) - { - // The out will trigger after the over, so we count - time.data('count',time.data('count')+1); - } - else - { - _data.ui.helper.prepend('
'); - } - - - break; - - // Triggered once, when something is dragged out of the timegrid - case EGW_AI_DRAG_OUT: - // Stop listening - _data.ui.draggable.off('drag.et2_timegrid_row'+widget_object.id); - // Remove highlight - widget_object.iface.getWidget().div.removeClass('drop-hover'); - - // Out triggers after the over, count to not accidentally remove - time.data('count',time.data('count')-1); - if(time.length && time.data('count') <= 0) - { - time.remove(); - } - break; - } - }; - - if (widget_object == null) { - // Add a new container to the object manager which will hold the widget - // objects - widget_object = parent.insertObject(false, new egwActionObject( - this.id, parent, aoi, - this._actionManager|| parent.manager.getActionById(this.id) || parent.manager - )); - } - else - { - widget_object.setAOI(aoi); - } - this._actionObject = widget_object; - - // Delete all old objects - widget_object.clear(); - widget_object.unregisterActions(); - - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - var action_links = this._get_action_links(actions); - - this.getParent()._init_links_dnd(widget_object.manager, action_links); - - widget_object.updateActionLinks(action_links); - }, - - /** - * Get all action-links / id's of 1.-level actions from a given action object - * - * Here we are only interested in drop events. - * - * @param actions - * @returns {Array} - */ - _get_action_links: function(actions) - { - var action_links = []; - - // Only these actions are allowed without a selection (empty actions) - var empty_actions = ['add']; - - for(var i in actions) - { - var action = actions[i]; - if(empty_actions.indexOf(action.id) !== -1 || action.type == 'drop') - { - action_links.push(typeof action.id != 'undefined' ? action.id : i); - } - } - return action_links; - }, - - /** - * Draw the individual divs for weekends and events - */ - _draw: function() { - // Remove any existing - this.rows.remove('.calendar_eventRowsMarkedDay,.calendar_eventRowsFiller').nextAll().remove(); - - var days = 31; - var width = 100; - if (this._parent.options.group_by === 'month') - { - days = this.options.end_date.getUTCDate(); - - if(days < 31) - { - var diff = 31 - days; - width = 'calc('+(diff * 3.23) + '% - ' + (diff * 7) + 'px)'; - } - } - - // mark weekends and other special days in yearly planner - if (this._parent.options.group_by == 'month') - { - this.rows.append(this._yearlyPlannerMarkDays(this.options.start_date, days)); - } - - if (this._parent.options.group_by === 'month' && days < 31) - { - // add a filler for non existing days in that month - this.rows.after('
'); - } - }, - - set_label: function(label) - { - this.options.label = label; - this.title.text(label); - if(this._parent.options.group_by === 'month') - { - this.title.attr('data-date', this.options.start_date.toJSON()); - this.title.attr('data-sortby', 'user'); - this.title.addClass('et2_clickable et2_link'); - } - else - { - this.title.attr('data-date',''); - this.title.removeClass('et2_clickable'); - } - }, - - /** - * Change the start date - * - * @param {Date} new_date New end date - * @returns {undefined} - */ - set_start_date: function(new_date) - { - if(!new_date || new_date === null) - { - throw exception('Invalid end date. ' + new_date.toString()); - } - - this.options.start_date = new Date(typeof new_date == 'string' ? new_date : new_date.toJSON()); - this.options.start_date.setUTCHours(0); - this.options.start_date.setUTCMinutes(0); - this.options.start_date.setUTCSeconds(0); - }, - /** - * Change the end date - * - * @param {string|number|Date} new_date New end date - * @returns {undefined} - */ - set_end_date: function(new_date) - { - if(!new_date || new_date === null) - { - throw exception('Invalid end date. ' + new_date.toString()); - } - - this.options.end_date = new Date(typeof new_date == 'string' ? new_date : new_date.toJSON()); - this.options.end_date.setUTCHours(23); - this.options.end_date.setUTCMinutes(59); - this.options.end_date.setUTCSeconds(59); - }, - - /** - * Mark special days (birthdays, holidays) on the planner - * - * @param {Date} start Start of the month - * @param {number} days How many days in the month - */ - _yearlyPlannerMarkDays: function(start,days) - { - var day_width = 3.23; - var t = new Date(start); - var content = ''; - for(var i = 0; i < days;i++) - { - var holidays = []; - // TODO: implement this, pull / copy data from et2_widget_timegrid - var day_class = this._parent.day_class_holiday(t,holidays); - - if (day_class) // no regular weekday - { - content += '
'; - } - t.setUTCDate(t.getUTCDate()+1); - } - return content; - }, - - /** - * Callback used when the daywise data changes - * - * Events should update themselves when their data changes, here we are - * dealing with a change in which events are displayed on this row. - * - * @param {String[]} event_ids - * @returns {undefined} - */ - _data_callback: function(event_ids) { - var events = []; - if(event_ids == null || typeof event_ids.length == 'undefined') event_ids = []; - for(var i = 0; i < event_ids.length; i++) - { - var event = egw.dataGetUIDdata('calendar::'+event_ids[i]); - event = event && event.data || false; - if(event && event.date) - { - events.push(event); - } - else if (event) - { - // Got an ID that doesn't belong - event_ids.splice(i--,1); - } - } - if(!this._parent.disabled && event_ids.length > 0) - { - this.resize(); - this._update_events(events); - } - }, - - /** - * Load the event data for this day and create event widgets for each. - * - * If event information is not provided, it will be pulled from the content array. - * - * @param {Object[]} [events] Array of event information, one per event. - */ - _update_events: function(events) - { - // Remove all events - while(this._children.length > 0) - { - var node = this._children[this._children.length-1]; - this.removeChild(node); - node.free(); - } - this._cached_rows = []; - - for(var c = 0; c < events.length; c++) - { - // Create event - var event = et2_createWidget('calendar-event',{ - id:'event_'+events[c].row_id, - value: events[c] - },this); - } - - // Seperate loop so column sorting finds all children in the right place - for(var c = 0; c < events.length; c++) - { - var event = this.getWidgetById('event_'+events[c].row_id); - if(!event) continue; - if(this.isInTree()) - { - event.doLoadingFinished(); - } - } - }, - - /** - * Position the event according to it's time and how this widget is laid - * out. - * - * @param {undefined|Object|et2_calendar_event} event - */ - position_event: function(event) - { - var rows = this._spread_events(); - var height = rows.length * this._row_height; - var row_width = this.rows.width(); - if(row_width == 0) - { - // Not rendered yet or something - row_width = this.getParent().gridHeader.width() - this.title.width() - } - row_width -= 15; - - for(var c = 0; c < rows.length; c++) - { - // Calculate vertical positioning - var top = c * (100.0 / rows.length); - - for(var i = 0; (rows[c].indexOf(event) >=0 || !event) && i < rows[c].length; i++) - { - // Calculate horizontal positioning - var left = this._time_to_position(rows[c][i].options.value.start); - var width = this._time_to_position(rows[c][i].options.value.end)-left; - - // Position the event - rows[c][i].div.css('top', top+'%'); - rows[c][i].div.css('height', (100/rows.length)+'%'); - rows[c][i].div.css('left', left.toFixed(1)+'%'); - rows[c][i].div.outerWidth((width/100 * row_width) +'px'); - } - } - if(height) - { - this.div.height(height+'px'); - } - }, - - /** - * Sort a day's events into non-overlapping rows - * - * @returns {Array[]} Events sorted into rows - */ - _spread_events: function() - { - // Keep it so we don't have to re-do it when the next event asks - var cached_length = 0; - this._cached_rows.map(function(row) {cached_length+=row.length;}); - if(cached_length === this._children.length) - { - return this._cached_rows; - } - - // sorting the events in non-overlapping rows - var rows = []; - var row_end = [0]; - - // Sort in chronological order, so earliest ones are at the top - this._children.sort(function(a,b) { - var start = new Date(a.options.value.start) - new Date(b.options.value.start); - var end = new Date(a.options.value.end) - new Date(b.options.value.end); - // Whole day events sorted by ID, normal events by start / end time - if(a.options.value.whole_day && b.options.value.whole_day) - { - // Longer duration comes first so we have nicer bars across the top - var duration = - (new Date(b.options.value.end) - new Date(b.options.value.start)) - - (new Date(a.options.value.end) - new Date(a.options.value.start)); - - return duration ? duration : (a.options.value.app_id - b.options.value.app_id); - } - else if (a.options.value.whole_day || b.options.value.whole_day) - { - return a.options.value.whole_day ? -1 : 1; - } - return start ? start : end; - }); - - for(var n = 0; n < this._children.length; n++) - { - var event = this._children[n].options.value || false; - if(typeof event.start !== 'object') - { - this.date_helper.set_value(event.start); - event.start = new Date(this.date_helper.getValue()); - } - if(typeof event.end !== 'object') - { - this.date_helper.set_value(event.end); - event.end = new Date(this.date_helper.getValue()); - } - if(typeof event['start_m'] === 'undefined') - { - var day_start = event.start.valueOf() / 1000; - var dst_check = new Date(event.start); - dst_check.setUTCHours(12); - - // if daylight saving is switched on or off, correct $day_start - // gives correct times after 2am, times between 0am and 2am are wrong - var daylight_diff = day_start + 12*60*60 - (dst_check.valueOf()/1000); - if(daylight_diff) - { - day_start -= daylight_diff; - } - - event['start_m'] = event.start.getUTCHours() * 60 + event.start.getUTCMinutes(); - if (event['start_m'] < 0) - { - event['start_m'] = 0; - event['multiday'] = true; - } - event['end_m'] = event.end.getUTCHours() * 60 + event.end.getUTCMinutes(); - if (event['end_m'] >= 24*60) - { - event['end_m'] = 24*60-1; - event['multiday'] = true; - } - if(!event.start.getUTCHours() && !event.start.getUTCMinutes() && event.end.getUTCHours() == 23 && event.end.getUTCMinutes() == 59) - { - event.whole_day_on_top = (event.non_blocking && event.non_blocking != '0'); - } - } - - // Skip events entirely on hidden weekends - if(this._hidden_weekend_event(event)) - { - var node = this._children[n]; - this.removeChild(n--); - node.free(); - continue; - } - - var event_start = new Date(event.start).valueOf(); - for(var row = 0; row_end[row] > event_start; ++row); // find a "free" row (no other event) - if(typeof rows[row] === 'undefined') rows[row] = []; - rows[row].push(this._children[n]); - row_end[row] = new Date(event['end']).valueOf(); - } - this._cached_rows = rows; - return rows; - }, - - /** - * Check to see if the event is entirely on a hidden weekend - * - * @param values Array of event values, not an et2_widget_event - */ - _hidden_weekend_event: function(values) - { - if(!this._parent || this._parent.options.group_by == 'month' || this._parent.options.show_weekend) - { - return false; - } - // Starts on Saturday or Sunday, ends Sat or Sun, less than 2 days long - else if([0,6].indexOf(values.start.getUTCDay()) !== -1 && [0,6].indexOf(values.end.getUTCDay()) !== -1 - && values.end - values.start < 2 * 24 * 3600 * 1000) - { - return true; - } - return false; - }, - - /** - * Calculates the horizontal position based on the time given, as a percentage - * between the start and end times - * - * @param {int|Date|string} time in minutes from midnight, or a Date in string or object form - * @param {int|Date|string} start Earliest possible time (0%) - * @param {int|Date|string} end Latest possible time (100%) - * @return {float} position in percent - */ - _time_to_position: function(time, start, end) - { - var pos = 0.0; - - // Handle the different value types - start = this.options.start_date; - end = this.options.end_date; - - if(typeof start === 'string') - { - start = new Date(start); - end = new Date(end); - } - var wd_start = 60 * (parseInt(egw.preference('workdaystarts','calendar')) || 9); - var wd_end = 60 * (parseInt(egw.preference('workdayends','calendar')) || 17); - - var t = time; - if(typeof time === 'number' && time < 3600) - { - t = new Date(start.valueOf() + wd_start * 3600*1000); - } - else - { - t = new Date(time); - } - - // Limits - if(t <= start) return 0; // We are left of our scale - if(t >= end) return 100; // We are right of our scale - - // Remove space for weekends, if hidden - var weekend_count = 0; - var weekend_before = 0; - var partial_weekend = 0; - if(this._parent.options.group_by !== 'month' && this._parent && !this._parent.options.show_weekend) - { - - var counter_date = new Date(start); - do - { - if([0,6].indexOf(counter_date.getUTCDay()) !== -1) - { - if(counter_date.getUTCDate() === t.getUTCDate() && counter_date.getUTCMonth() === t.getUTCMonth()) - { - // Event is partially on a weekend - partial_weekend += (t.getUTCHours() *60 + t.getUTCMinutes())*60*1000; - } - else if(counter_date < t) - { - weekend_before++; - } - weekend_count++; - } - counter_date.setUTCDate(counter_date.getUTCDate() + 1); - } while(counter_date < end) - // Put it in ms - weekend_before *= 24 * 3600 * 1000; - weekend_count *= 24 * 3600 * 1000; - } - - // Basic scaling, doesn't consider working times - pos = (t - start - weekend_before-partial_weekend) / (end - start - weekend_count); - - // Month view - if(this._parent.options.group_by !== 'month') - { - // Daywise scaling - /* Needs hourly scales that consider working hours - var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(),start.getUTCDate()); - var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(),end.getUTCDate()); - var t_date = new Date(t.getUTCFullYear(), t.getUTCMonth(),t.getUTCDate()); - - var days = Math.round((end_date - start_date) / (24 * 3600 * 1000))+1; - pos = 1 / days * Math.round((t_date - start_date) / (24*3600 * 1000)); - - var time_of_day = typeof t === 'object' ? 60 * t.getUTCHours() + t.getUTCMinutes() : t; - - if (time_of_day >= wd_start) - { - var day_percentage = 0.1; - if (time_of_day > wd_end) - { - day_percentage = 1; - } - else - { - var wd_length = wd_end - wd_start; - if (wd_length <= 0) wd_length = 24*60; - day_percentage = (time_of_day-wd_start) / wd_length; // between 0 and 1 - } - pos += day_percentage / days; - } - */ - } - else - { - // 2678400 is the number of seconds in 31 days - pos = (t - start) / 2678400000; - } - pos = 100 * pos; - - return pos; - }, - - // Resizable interface - /** - * Resize - * - * Parent takes care of setting proper width & height for the containing div - * here we just need to adjust the events to fit the new size. - */ - resize: function () - { - if(this.disabled || !this.div.is(':visible') || this._parent.disabled) - { - return; - } - - var row = jQuery('
').appendTo(this.rows); - this._row_height = (parseInt(window.getComputedStyle(row[0]).getPropertyValue("height")) || 20); - row.remove(); - - // Resize & position all events - this.position_event(); - } - -});}).call(this); - -et2_register_widget(et2_calendar_planner_row, ["calendar-planner_row"]); \ No newline at end of file +var et2_calendar_planner_row = /** @class */ (function (_super) { + __extends(et2_calendar_planner_row, _super); + /** + * Constructor + */ + function et2_calendar_planner_row(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_calendar_planner_row._attributes, _child || {})) || this; + _this._row_height = 20; + // Main container + _this.div = jQuery(document.createElement("div")) + .addClass("calendar_plannerRowWidget") + .css('width', _this.options.width); + _this.title = jQuery(document.createElement('div')) + .addClass("calendar_plannerRowHeader") + .appendTo(_this.div); + _this.rows = jQuery(document.createElement('div')) + .addClass("calendar_eventRows") + .appendTo(_this.div); + _this.setDOMNode(_this.div[0]); + // Used for its date calculations + _this._date_helper = et2_createWidget('date-time', {}, null); + _this._date_helper.loadingFinished(); + _this.set_start_date(_this.options.start_date); + _this.set_end_date(_this.options.end_date); + _this._cached_rows = []; + return _this; + } + et2_calendar_planner_row.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.call(this); + this.set_label(this.options.label); + this._draw(); + // Actions are set on the parent, so we need to explicitly get in here + // and get ours + this._link_actions(this.getParent().options.actions || []); + return true; + }; + et2_calendar_planner_row.prototype.destroy = function () { + _super.prototype.destroy.call(this); + // date_helper has no parent, so we must explicitly remove it + this._date_helper.destroy(); + this._date_helper = null; + }; + et2_calendar_planner_row.prototype.getDOMNode = function (_sender) { + if (_sender === this || !_sender) { + return this.div[0]; + } + if (_sender._parent === this) { + return this.rows[0]; + } + }; + /** + * Link the actions to the DOM nodes / widget bits. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + */ + et2_calendar_planner_row.prototype._link_actions = function (actions) { + // Get the parent? Might be a grid row, might not. Either way, it is + // just a container with no valid actions + var objectManager = egw_getObjectManager(this.getInstanceManager().app, true, 1); + objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId, 2) || objectManager; + var parent = objectManager.getObjectById(this.id, 1) || objectManager.getObjectById(this.getParent().id, 1) || objectManager; + if (!parent) { + egw.debug('error', 'No parent objectManager found'); + return; + } + // This binds into the egw action system. Most user interactions (drag to move, resize) + // are handled internally using jQuery directly. + var widget_object = this._actionObject || parent.getObjectById(this.id); + var aoi = new et2_core_DOMWidget_1.et2_action_object_impl(this, this.getDOMNode(this)).getAOI(); + var planner = this.getParent(); + for (var i = 0; i < parent.children.length; i++) { + var parent_finder = jQuery(parent.children[i].iface.doGetDOMNode()).find(this.div); + if (parent_finder.length > 0) { + parent = parent.children[i]; + break; + } + } + // Determine if we allow a dropped event to use the invite/change actions + var _invite_enabled = function (action, event, target) { + var event = event.iface.getWidget(); + var row = target.iface.getWidget() || false; + if (event === row || !event || !row || + !event.options || !event.options.value.participants) { + return false; + } + var owner_match = false; + var own_row = event.getParent() === row; + for (var id in event.options.value.participants) { + owner_match = owner_match || row.node.dataset.participants === '' + id; + } + var enabled = !owner_match && + // Not inside its own timegrid + !own_row; + widget_object.getActionLink('invite').enabled = enabled; + widget_object.getActionLink('change_participant').enabled = enabled; + // If invite or change participant are enabled, drag is not + widget_object.getActionLink('egw_link_drop').enabled = !enabled; + }; + aoi.doTriggerEvent = function (_event, _data) { + // Determine target node + var event = _data.event || false; + if (!event) + return; + if (_data.ui.draggable.hasClass('rowNoEdit')) + return; + /* + We have to handle the drop in the normal event stream instead of waiting + for the egwAction system so we can get the helper, and destination + */ + if (event.type === 'drop' && widget_object.getActionLink('egw_link_drop').enabled) { + this.getWidget().getParent()._event_drop.call(jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper)[0], this.getWidget().getParent(), event, _data.ui, this.getWidget()); + } + var drag_listener = function (_event, ui) { + if (planner.options.group_by === 'month') { + var position = { left: _event.clientX, top: _event.clientY }; + } + else { + var position = { top: ui.position.top, left: ui.position.left - jQuery(this).parent().offset().left }; + } + aoi.getWidget().getParent()._drag_helper(jQuery('.calendar_d-n-d_timeCounter', ui.helper)[0], position, 0); + var event = _data.ui.draggable.data('selected')[0]; + if (!event || event.id && event.id.indexOf('calendar') !== 0) { + event = false; + } + if (event) { + _invite_enabled(widget_object.getActionLink('invite').actionObj, event, widget_object); + } + }; + var time = jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper); + switch (_event) { + // Triggered once, when something is dragged into the timegrid's div + case EGW_AI_DRAG_OVER: + // Listen to the drag and update the helper with the time + // This part lets us drag between different timegrids + _data.ui.draggable.on('drag.et2_timegrid_row' + widget_object.id, drag_listener); + _data.ui.draggable.on('dragend.et2_timegrid_row' + widget_object.id, function () { + _data.ui.draggable.off('drag.et2_timegrid_row' + widget_object.id); + }); + widget_object.iface.getWidget().div.addClass('drop-hover'); + // Disable invite / change actions for same calendar or already participant + var event = _data.ui.draggable.data('selected')[0]; + if (!event || event.id && event.id.indexOf('calendar') !== 0) { + event = false; + } + if (event) { + _invite_enabled(widget_object.getActionLink('invite').actionObj, event, widget_object); + } + if (time.length) { + // The out will trigger after the over, so we count + time.data('count', time.data('count') + 1); + } + else { + _data.ui.helper.prepend('
'); + } + break; + // Triggered once, when something is dragged out of the timegrid + case EGW_AI_DRAG_OUT: + // Stop listening + _data.ui.draggable.off('drag.et2_timegrid_row' + widget_object.id); + // Remove highlight + widget_object.iface.getWidget().div.removeClass('drop-hover'); + // Out triggers after the over, count to not accidentally remove + time.data('count', time.data('count') - 1); + if (time.length && time.data('count') <= 0) { + time.remove(); + } + break; + } + }; + if (widget_object == null) { + // Add a new container to the object manager which will hold the widget + // objects + widget_object = parent.insertObject(false, new egwActionObject(this.id, parent, aoi, this._actionManager || parent.manager.getActionById(this.id) || parent.manager)); + } + else { + widget_object.setAOI(aoi); + } + this._actionObject = widget_object; + // Delete all old objects + widget_object.clear(); + widget_object.unregisterActions(); + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + var action_links = this._get_action_links(actions); + this.getParent()._init_links_dnd(widget_object.manager, action_links); + widget_object.updateActionLinks(action_links); + }; + /** + * Get all action-links / id's of 1.-level actions from a given action object + * + * Here we are only interested in drop events. + * + * @param actions + * @returns {Array} + */ + et2_calendar_planner_row.prototype._get_action_links = function (actions) { + var action_links = []; + // Only these actions are allowed without a selection (empty actions) + var empty_actions = ['add']; + for (var i in actions) { + var action = actions[i]; + if (empty_actions.indexOf(action.id) !== -1 || action.type == 'drop') { + action_links.push(typeof action.id != 'undefined' ? action.id : i); + } + } + return action_links; + }; + /** + * Draw the individual divs for weekends and events + */ + et2_calendar_planner_row.prototype._draw = function () { + // Remove any existing + this.rows.remove('.calendar_eventRowsMarkedDay,.calendar_eventRowsFiller').nextAll().remove(); + var days = 31; + var width = '100'; + if (this.getParent().options.group_by === 'month') { + days = this.options.end_date.getUTCDate(); + if (days < 31) { + var diff = 31 - days; + width = 'calc(' + (diff * 3.23) + '% - ' + (diff * 7) + 'px)'; + } + } + // mark weekends and other special days in yearly planner + if (this.getParent().options.group_by == 'month') { + this.rows.append(this._yearlyPlannerMarkDays(this.options.start_date, days)); + } + if (this.getParent().options.group_by === 'month' && days < 31) { + // add a filler for non existing days in that month + this.rows.after('
'); + } + }; + et2_calendar_planner_row.prototype.set_label = function (label) { + this.options.label = label; + this.title.text(label); + if (this.getParent().options.group_by === 'month') { + this.title.attr('data-date', this.options.start_date.toJSON()); + this.title.attr('data-sortby', 'user'); + this.title.addClass('et2_clickable et2_link'); + } + else { + this.title.attr('data-date', ''); + this.title.removeClass('et2_clickable'); + } + }; + /** + * Change the start date + * + * @param {Date} new_date New end date + * @returns {undefined} + */ + et2_calendar_planner_row.prototype.set_start_date = function (new_date) { + if (!new_date || new_date === null) { + throw new TypeError('Invalid end date. ' + new_date.toString()); + } + this.options.start_date = new Date(typeof new_date == 'string' ? new_date : new_date.toJSON()); + this.options.start_date.setUTCHours(0); + this.options.start_date.setUTCMinutes(0); + this.options.start_date.setUTCSeconds(0); + }; + /** + * Change the end date + * + * @param {string|number|Date} new_date New end date + * @returns {undefined} + */ + et2_calendar_planner_row.prototype.set_end_date = function (new_date) { + if (!new_date || new_date === null) { + throw new TypeError('Invalid end date. ' + new_date.toString()); + } + this.options.end_date = new Date(typeof new_date == 'string' ? new_date : new_date.toJSON()); + this.options.end_date.setUTCHours(23); + this.options.end_date.setUTCMinutes(59); + this.options.end_date.setUTCSeconds(59); + }; + /** + * Mark special days (birthdays, holidays) on the planner + * + * @param {Date} start Start of the month + * @param {number} days How many days in the month + */ + et2_calendar_planner_row.prototype._yearlyPlannerMarkDays = function (start, days) { + var day_width = 3.23; + var t = new Date(start); + var content = ''; + for (var i = 0; i < days; i++) { + var holidays = []; + // TODO: implement this, pull / copy data from et2_widget_timegrid + var day_class = this.getParent().day_class_holiday(t, holidays); + if (day_class) // no regular weekday + { + content += '
'; + } + t.setUTCDate(t.getUTCDate() + 1); + } + return content; + }; + /** + * Callback used when the daywise data changes + * + * Events should update themselves when their data changes, here we are + * dealing with a change in which events are displayed on this row. + * + * @param {String[]} event_ids + * @returns {undefined} + */ + et2_calendar_planner_row.prototype._data_callback = function (event_ids) { + var events = []; + if (event_ids == null || typeof event_ids.length == 'undefined') + event_ids = []; + for (var i = 0; i < event_ids.length; i++) { + var event_1 = egw.dataGetUIDdata('calendar::' + event_ids[i]); + event_1 = event_1 && event_1.data || false; + if (event_1 && event_1.date) { + events.push(event_1); + } + else if (event_1) { + // Got an ID that doesn't belong + event_ids.splice(i--, 1); + } + } + if (!this.getParent().disabled && event_ids.length > 0) { + this.resize(); + this._update_events(events); + } + }; + Object.defineProperty(et2_calendar_planner_row.prototype, "date_helper", { + get: function () { + return this._date_helper; + }, + enumerable: true, + configurable: true + }); + /** + * Load the event data for this day and create event widgets for each. + * + * If event information is not provided, it will be pulled from the content array. + * + * @param {Object[]} [events] Array of event information, one per event. + */ + et2_calendar_planner_row.prototype._update_events = function (events) { + // Remove all events + while (this._children.length > 0) { + var node = this._children[this._children.length - 1]; + this.removeChild(node); + node.destroy(); + } + this._cached_rows = []; + for (var c = 0; c < events.length; c++) { + // Create event + var event = et2_createWidget('calendar-event', { + id: 'event_' + events[c].row_id, + value: events[c] + }, this); + } + // Seperate loop so column sorting finds all children in the right place + for (var c = 0; c < events.length; c++) { + var event_2 = this.getWidgetById('event_' + events[c].row_id); + if (!event_2) + continue; + if (this.isInTree()) { + event_2.doLoadingFinished(); + } + } + }; + /** + * Position the event according to it's time and how this widget is laid + * out. + * + * @param {undefined|Object|et2_calendar_event} event + */ + et2_calendar_planner_row.prototype.position_event = function (event) { + var rows = this._spread_events(); + var height = rows.length * this._row_height; + var row_width = this.rows.width(); + if (row_width == 0) { + // Not rendered yet or something + row_width = this.getParent().gridHeader.width() - this.title.width(); + } + row_width -= 15; + for (var c = 0; c < rows.length; c++) { + // Calculate vertical positioning + var top_1 = c * (100.0 / rows.length); + for (var i = 0; (rows[c].indexOf(event) >= 0 || !event) && i < rows[c].length; i++) { + // Calculate horizontal positioning + var left = this._time_to_position(rows[c][i].options.value.start); + var width = this._time_to_position(rows[c][i].options.value.end) - left; + // Position the event + rows[c][i].div.css('top', top_1 + '%'); + rows[c][i].div.css('height', (100 / rows.length) + '%'); + rows[c][i].div.css('left', left.toFixed(1) + '%'); + rows[c][i].div.outerWidth((width / 100 * row_width) + 'px'); + } + } + if (height) { + this.div.height(height + 'px'); + } + }; + /** + * Sort a day's events into non-overlapping rows + * + * @returns {Array[]} Events sorted into rows + */ + et2_calendar_planner_row.prototype._spread_events = function () { + // Keep it so we don't have to re-do it when the next event asks + var cached_length = 0; + this._cached_rows.map(function (row) { cached_length += row.length; }); + if (cached_length === this._children.length) { + return this._cached_rows; + } + // sorting the events in non-overlapping rows + var rows = []; + var row_end = [0]; + // Sort in chronological order, so earliest ones are at the top + this._children.sort(function (a, b) { + var start = new Date(a.options.value.start) - new Date(b.options.value.start); + var end = new Date(a.options.value.end) - new Date(b.options.value.end); + // Whole day events sorted by ID, normal events by start / end time + if (a.options.value.whole_day && b.options.value.whole_day) { + // Longer duration comes first so we have nicer bars across the top + var duration = (new Date(b.options.value.end) - new Date(b.options.value.start)) - + (new Date(a.options.value.end) - new Date(a.options.value.start)); + return duration ? duration : (a.options.value.app_id - b.options.value.app_id); + } + else if (a.options.value.whole_day || b.options.value.whole_day) { + return a.options.value.whole_day ? -1 : 1; + } + return start ? start : end; + }); + for (var n = 0; n < this._children.length; n++) { + var event_3 = this._children[n].options.value || false; + if (typeof event_3.start !== 'object') { + this._date_helper.set_value(event_3.start); + event_3.start = new Date(this._date_helper.getValue()); + } + if (typeof event_3.end !== 'object') { + this._date_helper.set_value(event_3.end); + event_3.end = new Date(this._date_helper.getValue()); + } + if (typeof event_3['start_m'] === 'undefined') { + var day_start = event_3.start.valueOf() / 1000; + var dst_check = new Date(event_3.start); + dst_check.setUTCHours(12); + // if daylight saving is switched on or off, correct $day_start + // gives correct times after 2am, times between 0am and 2am are wrong + var daylight_diff = day_start + 12 * 60 * 60 - (dst_check.valueOf() / 1000); + if (daylight_diff) { + day_start -= daylight_diff; + } + event_3['start_m'] = event_3.start.getUTCHours() * 60 + event_3.start.getUTCMinutes(); + if (event_3['start_m'] < 0) { + event_3['start_m'] = 0; + event_3['multiday'] = true; + } + event_3['end_m'] = event_3.end.getUTCHours() * 60 + event_3.end.getUTCMinutes(); + if (event_3['end_m'] >= 24 * 60) { + event_3['end_m'] = 24 * 60 - 1; + event_3['multiday'] = true; + } + if (!event_3.start.getUTCHours() && !event_3.start.getUTCMinutes() && event_3.end.getUTCHours() == 23 && event_3.end.getUTCMinutes() == 59) { + event_3.whole_day_on_top = (event_3.non_blocking && event_3.non_blocking != '0'); + } + } + // Skip events entirely on hidden weekends + if (this._hidden_weekend_event(event_3)) { + var node = this._children[n]; + this.removeChild(n--); + node.destroy(); + continue; + } + var event_start = new Date(event_3.start).valueOf(); + for (var row = 0; row_end[row] > event_start; ++row) + ; // find a "free" row (no other event) + if (typeof rows[row] === 'undefined') + rows[row] = []; + rows[row].push(this._children[n]); + row_end[row] = new Date(event_3['end']).valueOf(); + } + this._cached_rows = rows; + return rows; + }; + /** + * Check to see if the event is entirely on a hidden weekend + * + * @param values Array of event values, not an et2_widget_event + */ + et2_calendar_planner_row.prototype._hidden_weekend_event = function (values) { + if (!this.getParent() || this.getParent().options.group_by == 'month' || this.getParent().options.show_weekend) { + return false; + } + // Starts on Saturday or Sunday, ends Sat or Sun, less than 2 days long + else if ([0, 6].indexOf(values.start.getUTCDay()) !== -1 && [0, 6].indexOf(values.end.getUTCDay()) !== -1 + && values.end - values.start < 2 * 24 * 3600 * 1000) { + return true; + } + return false; + }; + /** + * Calculates the horizontal position based on the time given, as a percentage + * between the start and end times + * + * @param {int|Date|string} time in minutes from midnight, or a Date in string or object form + * @param {int|Date|string} start Earliest possible time (0%) + * @param {int|Date|string} end Latest possible time (100%) + * @return {float} position in percent + */ + et2_calendar_planner_row.prototype._time_to_position = function (time, start, end) { + var pos = 0.0; + // Handle the different value types + start = this.options.start_date; + end = this.options.end_date; + if (typeof start === 'string') { + start = new Date(start); + end = new Date(end); + } + var wd_start = 60 * (parseInt('' + egw.preference('workdaystarts', 'calendar')) || 9); + var wd_end = 60 * (parseInt('' + egw.preference('workdayends', 'calendar')) || 17); + var t = time; + if (typeof time === 'number' && time < 3600) { + t = new Date(start.valueOf() + wd_start * 3600 * 1000); + } + else { + t = new Date(time); + } + // Limits + if (t <= start) + return 0; // We are left of our scale + if (t >= end) + return 100; // We are right of our scale + // Remove space for weekends, if hidden + var weekend_count = 0; + var weekend_before = 0; + var partial_weekend = 0; + if (this.getParent().options.group_by !== 'month' && this.getParent() && !this.getParent().options.show_weekend) { + var counter_date = new Date(start); + do { + if ([0, 6].indexOf(counter_date.getUTCDay()) !== -1) { + if (counter_date.getUTCDate() === t.getUTCDate() && counter_date.getUTCMonth() === t.getUTCMonth()) { + // Event is partially on a weekend + partial_weekend += (t.getUTCHours() * 60 + t.getUTCMinutes()) * 60 * 1000; + } + else if (counter_date < t) { + weekend_before++; + } + weekend_count++; + } + counter_date.setUTCDate(counter_date.getUTCDate() + 1); + } while (counter_date < end); + // Put it in ms + weekend_before *= 24 * 3600 * 1000; + weekend_count *= 24 * 3600 * 1000; + } + // Basic scaling, doesn't consider working times + pos = (t - start - weekend_before - partial_weekend) / (end - start - weekend_count); + // Month view + if (this.getParent().options.group_by !== 'month') { + // Daywise scaling + /* Needs hourly scales that consider working hours + var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(),start.getUTCDate()); + var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(),end.getUTCDate()); + var t_date = new Date(t.getUTCFullYear(), t.getUTCMonth(),t.getUTCDate()); + + var days = Math.round((end_date - start_date) / (24 * 3600 * 1000))+1; + pos = 1 / days * Math.round((t_date - start_date) / (24*3600 * 1000)); + + var time_of_day = typeof t === 'object' ? 60 * t.getUTCHours() + t.getUTCMinutes() : t; + + if (time_of_day >= wd_start) + { + var day_percentage = 0.1; + if (time_of_day > wd_end) + { + day_percentage = 1; + } + else + { + var wd_length = wd_end - wd_start; + if (wd_length <= 0) wd_length = 24*60; + day_percentage = (time_of_day-wd_start) / wd_length; // between 0 and 1 + } + pos += day_percentage / days; + } + */ + } + else { + // 2678400 is the number of seconds in 31 days + pos = (t - start) / 2678400000; + } + pos = 100 * pos; + return pos; + }; + // Resizable interface + /** + * Resize + * + * Parent takes care of setting proper width & height for the containing div + * here we just need to adjust the events to fit the new size. + */ + et2_calendar_planner_row.prototype.resize = function () { + if (this.disabled || !this.div.is(':visible') || this.getParent().disabled) { + return; + } + var row = jQuery('
').appendTo(this.rows); + this._row_height = (parseInt(window.getComputedStyle(row[0]).getPropertyValue("height")) || 20); + row.remove(); + // Resize & position all events + this.position_event(); + }; + et2_calendar_planner_row._attributes = { + start_date: { + name: "Start date", + type: "any" + }, + end_date: { + name: "End date", + type: "any" + }, + value: { + type: "any" + } + }; + return et2_calendar_planner_row; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_calendar_planner_row = et2_calendar_planner_row; +et2_core_widget_1.et2_register_widget(et2_calendar_planner_row, ["calendar-planner_row"]); +//# sourceMappingURL=et2_widget_planner_row.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_timegrid.js b/calendar/js/et2_widget_timegrid.js index 2fc678385e..889126f976 100644 --- a/calendar/js/et2_widget_timegrid.js +++ b/calendar/js/et2_widget_timegrid.js @@ -1,3 +1,4 @@ +"use strict"; /* * Egroupware Calendar timegrid * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License @@ -7,12 +8,28 @@ * @author Nathan Gray * @version $Id$ */ - - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /calendar/js/et2_widget_view.js; + /calendar/js/et2_widget_view.js; */ - +var et2_core_widget_1 = require("../../api/js/etemplate/et2_core_widget"); +var et2_core_inheritance_1 = require("../../api/js/etemplate/et2_core_inheritance"); +var et2_widget_view_1 = require("./et2_widget_view"); +var et2_core_DOMWidget_1 = require("../../api/js/etemplate/et2_core_DOMWidget"); +var et2_dataview_view_grid_1 = require("../../api/js/etemplate/et2_dataview_view_grid"); /** * Class which implements the "calendar-timegrid" XET-Tag for displaying a span of days * @@ -27,2300 +44,1840 @@ * * @augments et2_calendar_view */ -var et2_calendar_timegrid = (function(){ "use strict"; return et2_calendar_view.extend([et2_IDetachedDOM, et2_IResizeable,et2_IPrint], -{ - createNamespace: true, - - attributes: { - value: { - type: "any", - description: "An array of events, indexed by date (Ymd format)." - }, - day_start: { - name: "Day start time", - type: "string", - default: parseInt(egw.preference('workdaystarts','calendar')) || 9, - description: "Work day start time. If unset, this will default to the current user's preference" - }, - day_end: { - name: "Day end time", - type: "string", - default: parseInt(egw.preference('workdayends','calendar')) || 17, - description: "Work day end time. If unset, this will default to the current user's preference" - }, - show_weekend: { - name: "Weekends", - type: "boolean", - default: egw.preference('days_in_weekview','calendar') != 5, - description: "Display weekends. The date range should still include them for proper scrolling, but they just won't be shown." - }, - granularity: { - name: "Granularity", - type: "integer", - default: parseInt(egw.preference('interval','calendar')) || 30, - description: "How many minutes per row, or 0 to display events as a list" - }, - "onchange": { - "name": "onchange", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when the date range changes." - }, - "onevent_change": { - "name": "onevent_change", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when an event changes." - }, - height: { - "default": '100%' - } - }, - /** - * Constructor - * - * @memberOf et2_calendar_timegrid - */ - init: function() { - this._super.apply(this, arguments); - - // Main container - this.div = jQuery(document.createElement("div")) - .addClass("calendar_calTimeGrid") - .addClass("calendar_TimeGridNoLabel"); - - // Headers - this.gridHeader = jQuery(document.createElement("div")) - .addClass("calendar_calGridHeader") - .appendTo(this.div); - this.dayHeader = jQuery(document.createElement("div")) - .appendTo(this.gridHeader); - - // Contains times / rows - this.scrolling = jQuery(document.createElement('div')) - .addClass("calendar_calTimeGridScroll") - .appendTo(this.div) - .append('
'); - - // Contains days / columns - this.days = jQuery(document.createElement("div")) - .addClass("calendar_calDayCols") - .appendTo(this.scrolling); - - // Used for owners - this.owner = et2_createWidget('description',{},this); - - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label et2_link") - .appendTo(this.gridHeader); - - this.gridHover = jQuery('
'); - - // List of dates in Ymd - // The first one should be start_date, last should be end_date - this.day_list = []; - this.day_widgets = []; - - // Update timer, to avoid redrawing twice when changing start & end date - this.update_timer = null; - - // Timer to re-scale time to fit - this.resize_timer = null; - - this.setDOMNode(this.div[0]); - }, - destroy: function() { - - // Stop listening to tab changes - if(typeof framework !== 'undefined' && framework.getApplicationByName('calendar').tab) - { - jQuery(framework.getApplicationByName('calendar').tab.contentDiv).off('show.' + this.id); - } - - this._super.apply(this, arguments); - - // Delete all old objects - this._actionObject.clear(); - this._actionObject.unregisterActions(); - this._actionObject.remove(); - this._actionObject = null; - - this.div.off(); - this.div = null; - this.gridHeader = null; - this.dayHeader = null; - this.days = null; - this.scrolling = null; - this._labelContainer = null; - - // Stop the resize timer - if(this.resize_timer) - { - window.clearTimeout(this.resize_timer); - } - }, - - doLoadingFinished: function() { - this._super.apply(this, arguments); - - // Listen to tab show to make sure we scroll to the day start, not top - if(typeof framework !== 'undefined' && framework.getApplicationByName('calendar').tab) - { - jQuery(framework.getApplicationByName('calendar').tab.contentDiv) - .on('show.' + this.id, jQuery.proxy( - function() - { - if(this.scrolling) - { - this.scrolling.scrollTop(this._top_time); - } - },this) - ); - } - - // Need to get the correct internal sizing - this.resize(); - - this._drawGrid(); - - // Actions may be set on a parent, so we need to explicitly get in here - // and get ours - this._link_actions(this.options.actions || this._parent.options.actions || []); - - // Automatically bind drag and resize for every event using jQuery directly - // - no action system - - var timegrid = this; - - /** - * If user puts the mouse over an event, then we'll set up resizing so - * they can adjust the length. Should be a little better on resources - * than binding it for every calendar event, and we won't need exceptions - * for planner view to resize horizontally. - */ - this.div.on('mouseover', '.calendar_calEvent:not(.ui-resizable):not(.rowNoEdit)', function() { - // Only resize in timegrid - if(timegrid.options.granularity === 0) return; - - // Load the event - timegrid._get_event_info(this); - var that = this; - - //Resizable event handler - jQuery(this).resizable - ({ - distance: 10, - // Grid matching preference - grid: [10000,timegrid.rowHeight], - autoHide: false, - handles: 's,se', - containment:'parent', - - /** - * Triggered when the resizable is created. - * - * @param {event} event - * @param {Object} ui - */ - create:function(event, ui) - { - var resizeHelper = event.target.getAttribute('data-resize'); - if (resizeHelper == 'WD' || resizeHelper == 'WDS') - { - jQuery(this).resizable('destroy'); - } - }, - - /** - * If dragging to resize an event, abort drag to create - * - * @param {jQuery.Event} event - * @param {Object} ui - */ - start: function(event, ui) - { - if(timegrid.drag_create.start) - { - // Abort drag to create, we're dragging to resize - timegrid._drag_create_end({}); - } - }, - - /** - * Triggered at the end of resizing the calEvent. - * - * @param {event} event - * @param {Object} ui - */ - stop:function(event, ui) - { - var e = new jQuery.Event('change'); - e.originalEvent = event; - e.data = {duration: 0}; - var event_data = timegrid._get_event_info(this); - var event_widget = timegrid.getWidgetById(event_data.widget_id); - var sT = event_widget.options.value.start_m; - if (typeof this.dropEnd != 'undefined' && this.dropEnd.length == 1) - { - var eT = (parseInt(timegrid._drop_data.hour) * 60) + parseInt(timegrid._drop_data.minute); - e.data.duration = ((eT - sT)/60) * 3600; - - if(event_widget) - { - event_widget.options.value.end_m = eT; - event_widget.options.value.duration = e.data.duration; - } - jQuery(this).trigger(e); - event_widget._update(event_widget.options.value); - - // That cleared the resize handles, so remove for re-creation... - if(jQuery(this).resizable('instance')) - { - jQuery(this).resizable('destroy'); - } - } - // Clear the helper, re-draw - if(event_widget && event_widget._parent) - { - event_widget._parent.position_event(event_widget); - } - timegrid.div.children('.drop-hover').removeClass('.drop-hover'); - }, - - /** - * Triggered during the resize, on the drag of the resize handler - * - * @param {event} event - * @param {Object} ui - */ - resize:function(event, ui) - { - // Add a bit for better understanding - it will show _to_ the start, - // covering the 'actual' target - timegrid._get_time_from_position(ui.helper[0].getBoundingClientRect().left, ui.helper[0].getBoundingClientRect().bottom+5); - timegrid.gridHover.hide(); - var drop = timegrid._drag_helper(this,ui.element[0]); - if(drop && !drop.is(':visible')) - { - drop.get(0).scrollIntoView(false); - } - } - }); - }); - - // Customize and override some draggable settings - this.div - .on('dragcreate','.calendar_calEvent', function(event, ui) { - jQuery(this).draggable('option','cancel','.rowNoEdit'); - // Act like you clicked the header, makes it easier to position - // but put it to the side (-5) so we can still do the hover - jQuery(this).draggable('option','cursorAt', {top: 5, left: -5}); - }) - .on('dragstart', '.calendar_calEvent', function(event,ui) { - jQuery('.calendar_calEvent',ui.helper).width(jQuery(this).width()) - .height(jQuery(this).outerHeight()) - .css('top', '').css('left','') - .appendTo(ui.helper); - ui.helper.width(jQuery(this).width()); - - // Cancel drag to create, we're dragging an existing event - timegrid.drag_create.start = null; - timegrid._drag_create_end(); - }) - .on('mousemove', function(event) { - timegrid._get_time_from_position(event.clientX, event.clientY); - }) - .on('mouseout', function(event) { - if(timegrid.div.has(event.relatedTarget).length === 0) - { - timegrid.gridHover.hide(); - } - }) - .on('mousedown', jQuery.proxy(this._mouse_down, this)) - .on('mouseup', jQuery.proxy(this._mouse_up, this)); - - return true; - }, - - /** - * Show the current time while dragging - * Used for resizing as well as drag & drop - * - * @param {type} element - * @param {type} helper - * @param {type} height - */ - _drag_helper: function(element, helper,height) - { - if(!element) return; - - element.dropEnd = this.gridHover; - - if(element.dropEnd.length) - { - this._drop_data = jQuery.extend({},element.dropEnd[0].dataset || {}); - } - - if (typeof element.dropEnd != 'undefined' && element.dropEnd.length) - { - // Make sure the target is visible in the scrollable day - if(this.gridHover.is(':visible')) - { - if(this.scrolling.scrollTop() > 0 && this.scrolling.scrollTop() >= this.gridHover.position().top - this.rowHeight) - { - this.scrolling.scrollTop(this.gridHover.position().top-this.rowHeight); - } - else if (this.scrolling.scrollTop() + this.scrolling.height() <= this.gridHover.position().top + (2*this.rowHeight)) - { - this.scrolling.scrollTop(this.scrolling.scrollTop() + this.rowHeight); - } - } - var time = ''; - if(this._drop_data.whole_day) - { - time = this.egw().lang('Whole day'); - } - else if (this.options.granularity === 0) - { - // No times, keep what's in the event - // Add class to helper to keep formatting - jQuery(helper).addClass('calendar_calTimeGridList'); - } - else - { - time = jQuery.datepicker.formatTime( - egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", - { - hour: element.dropEnd.attr('data-hour'), - minute: element.dropEnd.attr('data-minute'), - seconds: 0, - timezone: 0 - }, - {"ampm": (egw.preference("timeformat") == "12")} - ); - } - element.innerHTML = '
'+time+'
'; - } - else - { - element.innerHTML = '
'; - } - jQuery(element).width(jQuery(helper).width()); - return element.dropEnd; - }, - - /** - * Handler for dropping an event on the timegrid - * - * @param {type} timegrid - * @param {type} event - * @param {type} ui - * @param {type} dropEnd - */ - _event_drop: function(timegrid, event,ui, dropEnd) { - var e = new jQuery.Event('change'); - e.originalEvent = event; - e.data = {start: 0}; - - if(typeof dropEnd != 'undefined' && dropEnd) - { - var drop_date = dropEnd.date||false; - - var event_data = timegrid._get_event_info(ui.draggable); - var event_widget = timegrid.getWidgetById(event_data.widget_id); - if(!event_widget) - { - // Widget was moved across weeks / owners - event_widget = timegrid.getParent().getWidgetById(event_data.widget_id); - } - if(event_widget) - { - // Send full string to avoid rollover between months using set_month() - event_widget._parent.date_helper.set_value( - drop_date.substring(0,4)+'-'+drop_date.substring(4,6)+'-'+drop_date.substring(6,8)+ - 'T00:00:00Z' - ); - - - // Make sure whole day events stay as whole day events by ignoring drop time - if(event_data.app == 'calendar' && event_widget.options.value.whole_day) - { - event_widget._parent.date_helper.set_hours(0); - event_widget._parent.date_helper.set_minutes(0); - } - else if (timegrid.options.granularity === 0) - { - // List, not time grid - keep time - event_widget._parent.date_helper.set_hours(event_widget.options.value.start.getUTCHours()); - event_widget._parent.date_helper.set_minutes(event_widget.options.value.start.getUTCMinutes()); - } - else - { - // Non-whole day events, and integrated apps, can change - event_widget._parent.date_helper.set_hours(dropEnd.whole_day ? 0 : dropEnd.hour||0); - event_widget._parent.date_helper.set_minutes(dropEnd.whole_day ? 0 : dropEnd.minute||0); - } - - // Leave the helper there until the update is done - var loading = ui.helper.clone(true).appendTo(jQuery('body')); - // and add a loading icon so user knows something is happening - if(jQuery('.calendar_timeDemo',loading).length == 0) - { - jQuery('.calendar_calEventHeader',loading).addClass('loading'); - } - else - { - jQuery('.calendar_timeDemo',loading).after('
'); - } - - event_widget.recur_prompt(function(button_id) { - if(button_id === 'cancel' || !button_id) - { - // Need to refresh the event with original info to clean up - var app_id = event_widget.options.value.app_id ? event_widget.options.value.app_id : event_widget.options.value.id + (event_widget.options.value.recur_type ? ':'+event_widget.options.value.recur_date : ''); - egw().dataStoreUID('calendar::'+app_id,egw.dataGetUIDdata('calendar::'+app_id).data); - loading.remove(); - return; - } - //Get infologID if in case if it's an integrated infolog event - if (event_data.app === 'infolog') - { - // Duration - infologs are always non-blocking - var duration = dropEnd.whole_day ? 86400-1 : ( - event_widget.options.value.whole_day ? (egw().preference('defaultlength','calendar')*60) : false); - - // If it is an integrated infolog event we need to edit infolog entry - egw().json('stylite_infolog_calendar_integration::ajax_moveInfologEvent', - [event_data.app_id, event_widget._parent.date_helper.getValue()||false,duration], - function() {loading.remove();} - ).sendRequest(true); - } - else - { - //Edit calendar event - - // Duration - check for whole day dropped on a time, change it to full days - var duration = event_widget.options.value.whole_day && dropEnd.hour ? - // Make duration whole days, less 1 second - (Math.round((event_widget.options.value.end - event_widget.options.value.start) / (1000 * 86400)) * 86400) -1 : - false; - // Event (whole day or not) dropped on whole day section, change to whole day non blocking - if(dropEnd.whole_day) duration = 'whole_day'; - - // Send the update - var _send = function(series_instance) - { - var start = new Date(event_widget._parent.date_helper.getValue()); - - egw().json('calendar.calendar_uiforms.ajax_moveEvent', [ - button_id==='series' ? event_data.id : event_data.app_id,event_data.owner, - start, - timegrid.options.owner||egw.user('account_id'), - duration, - series_instance - ], - function() { loading.remove();} - ).sendRequest(true); - }; - - // Check for modifying a series that started before today - if (event_widget.options.value.recur_type && button_id === 'series') - { - event_widget.series_split_prompt(function(_button_id) { - if (_button_id === et2_dialog.OK_BUTTON) - { - _send(event_widget.options.value.recur_date); - } - else - { - loading.remove(); - } - }); - } - else - { - _send(event_widget.options.value.recur_date); - } - } - }); - } - } - }, - - /** - * Something changed, and the days need to be re-drawn. We wait a bit to - * avoid re-drawing twice if start and end date both changed, then recreate - * the days. - * The whole grid is not regenerated because times aren't expected to change, - * just the days. - * - * @param {boolean} [trigger=false] Trigger an event once things are done. - * Waiting until invalidate completes prevents 2 updates when changing the date range. - * @returns {undefined} - */ - invalidate: function(trigger) { - - // Reset the list of days - this.day_list = []; - - // Wait a bit to see if anything else changes, then re-draw the days - if(this.update_timer) - { - window.clearTimeout(this.update_timer); - } - this.update_timer = window.setTimeout(jQuery.proxy(function() { - this.widget.update_timer = null; - window.clearTimeout(this.resize_timer); - this.widget.loader.hide().show(); - - // Update actions - if(this.widget._actionManager) - { - this.widget._link_actions(this.widget._actionManager.children); - } - - this.widget._drawDays(); - // We have to completely re-do times, as they may have changed in - // scale to the point where more labels are needed / need to be removed - this.widget._drawTimes(); - if(this.trigger) - { - this.widget.change(); - } - // Hide loader - window.setTimeout(jQuery.proxy(function() {this.loader.hide();},this.widget),200); - },{widget:this,"trigger":trigger}),ET2_GRID_INVALIDATE_TIMEOUT); - }, - - detachFromDOM: function() { - // Remove the binding to the change handler - jQuery(this.div).off(".et2_calendar_timegrid"); - - this._super.apply(this, arguments); - }, - - attachToDOM: function() { - this._super.apply(this, arguments); - - // Add the binding for the event change handler - jQuery(this.div).on("change.et2_calendar_timegrid", '.calendar_calEvent', this, function(e) { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.push(this); - - return e.data.event_change.apply(e.data, args); - }); - - // Add the binding for the change handler - jQuery(this.div).on("change.et2_calendar_timegrid", '*:not(.calendar_calEvent)', this, function(e) { - return e.data.change.call(e.data, e, this); - }); - - // Catch resize and prevent it from bubbling further, triggering - // etemplate's resize - this.div.on('resize', this, function(e) { - e.stopPropagation(); - }); - }, - - getDOMNode: function(_sender) { - if(_sender === this || !_sender) - { - return this.div ? this.div[0] : null; - } - else if (_sender.instanceOf(et2_calendar_daycol)) - { - return this.days ? this.days[0] : null; - } - else if (_sender) - { - return this.gridHeader ? this.gridHeader[0] : null; - } - }, - - set_disabled: function(disabled) { - var old_value = this.options.disabled; - this._super.apply(this, arguments); - if(disabled) - { - this.loader.show(); - } - else if (old_value !== disabled) - { - // Scroll to start of day - stops jumping in FF - // For some reason on Chrome & FF this doesn't quite get the day start - // to the top, so add 2px; - this.scrolling.scrollTop(this._top_time+2); - } - - }, - - /** - * Clear everything, and redraw the whole grid - */ - _drawGrid: function() { - - this.div.css('height', this.options.height) - .empty(); - this.loader.prependTo(this.div).show(); - - // Draw in the horizontal - the times - this._drawTimes(); - - // Draw in the vertical - the days - this.invalidate(); - }, - - /** - * Creates the DOM nodes for the times in the left column, and the horizontal - * lines (mostly via CSS) that span the whole date range. - */ - _drawTimes: function() { - jQuery('.calendar_calTimeRow',this.div).remove(); - - this.div.toggleClass('calendar_calTimeGridList', this.options.granularity === 0); - - this.gridHeader - .attr('data-date', this.options.start_date) - .attr('data-owner', this.options.owner) - .append(this._labelContainer) - .append(this.owner.getDOMNode()) - .append(this.dayHeader) - .appendTo(this.div); - - // Max with 18 avoids problems when it's not shown - var header_height = Math.max(this.gridHeader.outerHeight(true), 18); - - this.scrolling - .appendTo(this.div) - .off(); - - // No time grid - list - if(this.options.granularity === 0) - { - this.scrolling.css('height','100%'); - this.days.css('height', '100%'); - this.iterateOver(function(day) { - day.resize(); - },this,et2_calendar_daycol); - return; - } - - var wd_start = 60*this.options.day_start; - var wd_end = 60*this.options.day_end; - var granularity = this.options.granularity; - var totalDisplayMinutes = wd_end - wd_start; - var rowsToDisplay = Math.ceil((totalDisplayMinutes+60)/granularity); - var row_count = (1440 / this.options.granularity); - - - this.scrolling - .on('scroll', jQuery.proxy(this._scroll, this)); - - // Percent - var rowHeight = (100/rowsToDisplay).toFixed(1); - // Pixels - this.rowHeight = this.scrolling.height() / rowsToDisplay; - - // We need a reasonable bottom limit here, but resize will handle it - // if we get too small - if(this.rowHeight < 5 && this.div.is(':visible')) - { - if(this.rowHeight === 0) - { - // Something is not right... - this.rowHeight = 5; - } - } - - // the hour rows - var show = { - 5 : [0,15,30,45], - 10 : [0,30], - 15 : [0,30], - 45 : [0,15,30,45] - }; - var html = ''; - var line_height = parseInt(this.div.css('line-height')); - this._top_time = 0; - for(var t = 0,i = 0; t < 1440; t += granularity,++i) - { - if(t <= wd_start && t + granularity > wd_start) - { - this._top_time = this.rowHeight * (i+1+(wd_start - (t+granularity))/granularity); - } - var working_hours = (t >= wd_start && t < wd_end) ? ' calendar_calWorkHours' : ''; - html += '
'; - // show time for full hours, always for 45min interval and at least on every 3 row - var time = jQuery.datepicker.formatTime( - egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", - { - hour: t / 60, - minute: t % 60, - seconds: 0, - timezone: 0 - }, - {"ampm": (egw.preference("timeformat") === "12")} - ); - - var time_label = (typeof show[granularity] === 'undefined' ? t % 60 === 0 : show[granularity].indexOf(t % 60) !== -1) ? time : ''; - if(time_label && egw.preference("timeformat") == "12" && time_label.split(':')[0] < 10) - { - time_label ='  ' + time_label; - } - html += '
'+time_label+"
\n"; - } - - // Set heights in pixels for scrolling - jQuery('.calendar_calTimeLabels',this.scrolling) - .empty() - .height(this.rowHeight*i) - .append(html); - this.days.css('height', (this.rowHeight*i)+'px'); - this.gridHover.css('height', this.rowHeight); - - // Scroll to start of day - this.scrolling.scrollTop(this._top_time); - }, - - /** - * As window size and number of all day non-blocking events change, we need - * to re-scale the time grid to make sure the full working day is shown. - * - * We use a timeout to avoid doing it multiple times if redrawing or resizing. - */ - resizeTimes: function() { - - // Hide resizing from user - this.loader.show(); - - // Wait a bit to see if anything else changes, then re-draw the times - if(this.resize_timer) - { - window.clearTimeout(this.resize_timer); - } - // No point if it is just going to be redone completely - if(this.update_timer) return; - - this.resize_timer = window.setTimeout(jQuery.proxy(function() { - if(this._resizeTimes) - { - this.resize_timer = null; - - this._resizeTimes(); - } - },this),1); - }, - - /** - * Re-scale the time grid to make sure the full working day is shown. - * This is the timeout callback that does the actual re-size immediately. - */ - _resizeTimes: function() { - - if(!this.div.is(':visible')) - { - return; - } - var wd_start = 60*this.options.day_start; - var wd_end = 60*this.options.day_end; - var totalDisplayMinutes = wd_end - wd_start; - var rowsToDisplay = Math.ceil((totalDisplayMinutes+60)/this.options.granularity); - var row_count = (1440 / this.options.granularity); - - var new_height = this.scrolling.height() / rowsToDisplay; - var old_height = this.rowHeight; - this.rowHeight = new_height; - - jQuery('.calendar_calTimeLabels', this.scrolling).height(this.rowHeight*row_count); - this.days.css('height', this.options.granularity === 0 ? - '100%' : - (this.rowHeight*row_count)+'px' - ); - - // Scroll to start of day - this._top_time = (wd_start * this.rowHeight) / this.options.granularity; - // For some reason on Chrome & FF this doesn't quite get the day start - // to the top, so add 2px; - this.scrolling.scrollTop(this._top_time+2); - - if(this.rowHeight != old_height) - { - this.iterateOver(function(child) { - if(child === this) return; - child.resize(); - },this, et2_IResizeable); - } - - this.loader.hide(); - }, - - /** - * Set up the needed day widgets to correctly display the selected date - * range. First we calculate the needed dates, then we create any needed - * widgets. Existing widgets are recycled rather than discarded. - */ - _drawDays: function() { - this.scrolling.append(this.days); - - // If day list is still empty, recalculate it from start & end date - if(this.day_list.length === 0 && this.options.start_date && this.options.end_date) - { - this.day_list = this._calculate_day_list(this.options.start_date, this.options.end_date, this.options.show_weekend); - } - // For a single day, we show each owner in their own daycol - this.daily_owner = this.day_list.length === 1 && - this.options.owner.length > 1 && - this.options.owner.length < (parseInt(egw.preference('day_consolidate','calendar')) || 6); - var daycols_needed = this.daily_owner ? this.options.owner.length : this.day_list.length; - var day_width = ( Math.min( jQuery(this.getInstanceManager().DOMContainer).width(),this.days.width())/daycols_needed); - if(!day_width || !this.day_list) - { - // Hidden on another tab, or no days for some reason - var dim = egw.getHiddenDimensions(this.days, false); - day_width = ( dim.w /Math.max(daycols_needed,1)); - } - - // Create any needed widgets - otherwise, we'll just recycle - // Add any needed day widgets (now showing more days) - var add_index = 0; - var before = true; - - while(daycols_needed > this.day_widgets.length) - { - var existing_index = this.day_widgets[add_index] && !this.daily_owner ? - this.day_list.indexOf(this.day_widgets[add_index].options.date) : - -1; - before = existing_index > add_index; - - var day = et2_createWidget('calendar-daycol',{ - owner: this.options.owner, - width: (before ? 0 : day_width) + "px" - },this); - if(this.isInTree()) - { - day.doLoadingFinished(); - } - if(existing_index != -1 && parseInt(this.day_list[add_index]) < parseInt(this.day_list[existing_index])) - { - this.day_widgets.unshift(day); - jQuery(this.getDOMNode(day)).prepend(day.getDOMNode(day)); - } - else - { - this.day_widgets.push(day); - } - add_index++; - } - // Remove any extra day widgets (now showing less) - var delete_index = this.day_widgets.length - 1; - before = false; - while(this.day_widgets.length > daycols_needed) - { - // If we're going down to an existing one, just keep it for cool CSS animation - while(delete_index > 1 && this.day_list.indexOf(this.day_widgets[delete_index].options.date) > -1) - { - delete_index--; - before = true; - } - if(delete_index < 0) delete_index = 0; - - // Widgets that are before our date shrink, after just get pushed out - if(before) - { - this.day_widgets[delete_index].set_width('0px'); - } - this.day_widgets[delete_index].div.hide(); - this.day_widgets[delete_index].header.hide(); - this.day_widgets[delete_index].destroy(); - this.day_widgets.splice(delete_index--,1); - } - - this.set_header_classes(); - - // Create / update day widgets with dates and data - for(var i = 0; i < this.day_widgets.length; i++) - { - day = this.day_widgets[i]; - - // Position - day.set_left((day_width * i) + 'px'); - day.title.removeClass('blue_title'); - if(this.daily_owner) - { - // Each 'day' is the same date, different user - day.set_id(this.day_list[0]+'-'+this.options.owner[i]); - day.set_date(this.day_list[0], false); - day.set_owner(this.options.owner[i]); - day.set_label(this._get_owner_name(this.options.owner[i])); - day.title.addClass('blue_title'); - } - else - { - // Show user name in day header even if only one - if(this.day_list.length === 1) - { - day.set_label(this._get_owner_name(this.options.owner)); - day.title.addClass('blue_title'); - } - else - { - // Go back to self-calculated date by clearing the label - day.set_label(''); - } - day.set_id(this.day_list[i]); - day.set_date(this.day_list[i], this.value[this.day_list[i]] || false); - day.set_owner(this.options.owner); - } - day.set_width(day_width + 'px'); - } - - // Adjust and scroll to start of day - this.resizeTimes(); - - // Don't hold on to value any longer, use the data cache for best info - this.value = {}; - - if(this.daily_owner) - { - this.set_label(''); - } - - // Handle not fully visible elements - this._scroll(); - - // TODO: Figure out how to do this with detached nodes - /* - var nodes = this.day_col.getDetachedNodes(); - var supportedAttrs = []; - this.day_col.getDetachedAttributes(supportedAttrs); - supportedAttrs.push("id"); - - for(var i = 0; i < day_count; i++) - { - this.day_col.setDetachedAttributes(nodes.clone(),) - } - */ - }, - - /** - * Set header classes - * - */ - set_header_classes: function() - { - var day; - for(var i = 0; i < this.day_widgets.length; i++) - { - day = this.day_widgets[i]; - - // Classes - if(app.calendar && app.calendar.state && - this.day_list[i] && parseInt(this.day_list[i].substr(4,2)) !== new Date(app.calendar.state.date).getUTCMonth()+1) - { - day.set_class('calendar_differentMonth'); - } - else - { - day.set_class(''); - } - } - }, - - /** - * Update UI while scrolling within the selected time - * - * Toggles out of view indicators and adjusts not visible headers - * @param {Event} event Scroll event - */ - _scroll: function(event) - { - if(!this.day_widgets) return; - - // Loop through days, let them deal with it - for(var day = 0; day < this.day_widgets.length; day++) - { - this.day_widgets[day]._out_of_view(); - } - }, - - /** - * Calculate a list of days between start and end date, skipping weekends if - * desired. - * - * @param {Date|string} start_date Date that et2_date widget can understand - * @param {Date|string} end_date Date that et2_date widget can understand - * @param {boolean} show_weekend If not showing weekend, Saturday and Sunday - * will not be in the returned list. - * - * @returns {string[]} List of days in Ymd format - */ - _calculate_day_list: function(start_date, end_date, show_weekend) { - - var day_list = []; - - this.date_helper.set_value(end_date); - var end = this.date_helper.date.getTime(); - var i = 1; - this.date_helper.set_value(new Date(start_date)); - - do - { - if(show_weekend || !show_weekend && [0,6].indexOf(this.date_helper.date.getUTCDay()) === -1 || end_date === start_date) - { - day_list.push(''+this.date_helper.get_year() + sprintf('%02d',this.date_helper.get_month()) + sprintf('%02d',this.date_helper.get_date())); - } - this.date_helper.set_date(this.date_helper.get_date()+1); - } - // Limit it to 14 days to avoid infinite loops in case something is mis-set, - // though the limit is more based on how wide the screen is - while(end >= this.date_helper.date.getTime() && i++ <= 14) - - return day_list; - }, - - /** - * Link the actions to the DOM nodes / widget bits. - * - * @param {object} actions {ID: {attributes..}+} map of egw action information - */ - _link_actions: function(actions) - { - // Get the parent? Might be a grid row, might not. Either way, it is - // just a container with no valid actions - var objectManager = egw_getObjectManager(this.getInstanceManager().app,true,1); - objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId,2) || objectManager; - var parent = objectManager.getObjectById(this.id,1) || objectManager.getObjectById(this._parent.id,1) || objectManager; - if(!parent) - { - debugger; - egw.debug('error','No parent objectManager found'); - return; - } - - // This binds into the egw action system. Most user interactions (drag to move, resize) - // are handled internally using jQuery directly. - var widget_object = this._actionObject || parent.getObjectById(this.id); - var aoi = new et2_action_object_impl(this,this.getDOMNode()); - - for(var i = 0; i < parent.children.length; i++) - { - var parent_finder = jQuery(parent.children[i].iface.doGetDOMNode()).find(this.div); - if(parent_finder.length > 0) - { - parent = parent.children[i]; - break; - } - } - // Determine if we allow a dropped event to use the invite/change actions - var _invite_enabled = function(action, event, target) - { - var event = event.iface.getWidget(); - var timegrid = target.iface.getWidget() || false; - if(event === timegrid || !event || !timegrid || - !event.options || !event.options.value.participants || !timegrid.options.owner - ) - { - return false; - } - var owner_match = false; - var own_timegrid = event.getParent().getParent() === timegrid && !timegrid.daily_owner; - - for(var id in event.options.value.participants) - { - if(!timegrid.daily_owner) - { - if(timegrid.options.owner === id || - timegrid.options.owner.indexOf && - timegrid.options.owner.indexOf(id) >= 0) - { - owner_match = true; - } - } - else - { - timegrid.iterateOver(function(col) { - // Check scroll section or header section - if(col.div.has(timegrid.gridHover).length || col.header.has(timegrid.gridHover).length) - { - owner_match = owner_match || col.options.owner.indexOf(id) !== -1; - own_timegrid = (col === event.getParent()); - } - }, this, et2_calendar_daycol); - } - } - var enabled = !owner_match && - // Not inside its own timegrid - !own_timegrid; - - widget_object.getActionLink('invite').enabled = enabled; - widget_object.getActionLink('change_participant').enabled = enabled; - - // If invite or change participant are enabled, drag is not - widget_object.getActionLink('egw_link_drop').enabled = !enabled; - }; - - aoi.doTriggerEvent = function(_event, _data) { - // Determine target node - var event = _data.event || false; - if(!event) return; - if(_data.ui.draggable.hasClass('rowNoEdit')) return; - - /* - We have to handle the drop in the normal event stream instead of waiting - for the egwAction system so we can get the helper, and destination - */ - if(event.type === 'drop') - { - var dropEnd = false; - var helper = jQuery('.calendar_d-n-d_timeCounter',_data.ui.helper)[0]; - if(helper && helper.dropEnd && helper.dropEnd.length >= 1) - if (typeof this.dropEnd !== 'undefined' && this.dropEnd.length >= 1) - { - dropEnd = helper.dropEnd[0].dataset || false; - } - this.getWidget()._event_drop.call(jQuery('.calendar_d-n-d_timeCounter',_data.ui.helper)[0],this.getWidget(),event, _data.ui, dropEnd); - } - var drag_listener = function(_event, ui) { - aoi.getWidget()._drag_helper(jQuery('.calendar_d-n-d_timeCounter',ui.helper)[0],ui.helper[0],0); - if(aoi.getWidget().daily_owner) - { - _invite_enabled( - widget_object.getActionLink('invite').actionObj, - event, - widget_object - ); - } - }; - var time = jQuery('.calendar_d-n-d_timeCounter',_data.ui.helper); - switch(_event) - { - // Triggered once, when something is dragged into the timegrid's div - case EGW_AI_DRAG_OVER: - // Listen to the drag and update the helper with the time - // This part lets us drag between different timegrids - _data.ui.draggable.on('drag.et2_timegrid'+widget_object.id, drag_listener); - _data.ui.draggable.on('dragend.et2_timegrid'+widget_object.id, function() { - _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); - }); - - // Remove formatting for out-of-view events (full day non-blocking) - jQuery('.calendar_calEventHeader',_data.ui.helper).css('top',''); - jQuery('.calendar_calEventBody',_data.ui.helper).css('padding-top',''); - - // Disable invite / change actions for same calendar or already participant - var event = _data.ui.draggable.data('selected')[0]; - if(!event || event.id && event.id.indexOf('calendar') !== 0) - { - event = false; - } - if(event) - { - _invite_enabled( - widget_object.getActionLink('invite').actionObj, - event, - widget_object - ); - } - - if(time.length) - { - // The out will trigger after the over, so we count - time.data('count',time.data('count')+1); - } - else - { - _data.ui.helper.prepend('
'); - } - - break; - - // Triggered once, when something is dragged out of the timegrid - case EGW_AI_DRAG_OUT: - // Stop listening - _data.ui.draggable.off('drag.et2_timegrid'+widget_object.id); - // Remove highlighted time square - var timegrid = aoi.getWidget(); - timegrid.gridHover.hide(); - timegrid.scrolling.scrollTop(timegrid._top_time); - - // Out triggers after the over, count to not accidentally remove - time.data('count',time.data('count')-1); - if(time.length && time.data('count') <= 0) - { - time.remove(); - } - break; - } - }; - - if (widget_object == null) { - // Add a new container to the object manager which will hold the widget - // objects - widget_object = parent.insertObject(false, new egwActionObject( - this.id, parent, aoi, - this._actionManager|| parent.manager.getActionById(this.id) || parent.manager - )); - } - else - { - widget_object.setAOI(aoi); - } - this._actionObject = widget_object; - - // Delete all old objects - widget_object.clear(); - widget_object.unregisterActions(); - - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - var action_links = this._get_action_links(actions); - - this._init_links_dnd(widget_object.manager, action_links); - - widget_object.updateActionLinks(action_links); - }, - - /** - * Automatically add dnd support for linking - * - * @param {type} mgr - * @param {type} actionLinks - */ - _init_links_dnd: function(mgr,actionLinks) { - - if (this.options.readonly) return; - - var self = this; - - var drop_link = mgr.getActionById('egw_link_drop'); - var drop_change_participant = mgr.getActionById('change_participant'); - var drop_invite = mgr.getActionById('invite'); - var drag_action = mgr.getActionById('egw_link_drag'); - - // Check if this app supports linking - if(!egw.link_get_registry(this.dataStorePrefix || 'calendar', 'query') || - egw.link_get_registry(this.dataStorePrefix || 'calendar', 'title')) - { - if(drop_link) - { - drop_link.remove(); - if(actionLinks.indexOf(drop_link.id) >= 0) - { - actionLinks.splice(actionLinks.indexOf(drop_link.id),1); - } - } - if(drag_action) - { - drag_action.remove(); - if(actionLinks.indexOf(drag_action.id) >= 0) - { - actionLinks.splice(actionLinks.indexOf(drag_action.id),1); - } - } - return; - } - // Don't re-add - if(drop_link == null) - { - // Create the drop action that links entries - drop_link = mgr.addAction('drop', 'egw_link_drop', egw.lang('Create link'), egw.image('link'), function(action, source, target) { - - // Extract link IDs - var links = []; - var id = ''; - for(var i = 0; i < source.length; i++) - { - // Check for no ID (invalid) or same manager (dragging an event) - if(!source[i].id) continue; - if(source[i].manager === target.manager) - { - // Find the timegrid, could have dropped on an event - var timegrid = target.iface.getWidget(); - while(target.parent && timegrid.instanceOf && !timegrid.instanceOf(et2_calendar_timegrid)) - { - target = target.parent; - timegrid = target.iface.getWidget(); - } - - - if (timegrid && timegrid._drop_data) - { - timegrid._event_drop.call(source[i].iface.getDOMNode(),timegrid,null, action.ui,timegrid._drop_data); - } - timegrid._drop_data = false; - // Ok, stop. - return false; - } - - id = source[i].id.split('::'); - links.push({app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1]}); - } - if(links.length && target && target.iface.getWidget() && target.iface.getWidget().instanceOf(et2_calendar_event)) - { - // Link the entries - egw.json(self.egw().getAppName()+".etemplate_widget_link.ajax_link.etemplate", - target.id.split('::').concat([links]), - function(result) { - if(result) - { - this.egw().message('Linked'); - } - }, - self, - true, - self - ).sendRequest(); - } - else if (links.length) - { - // Get date and time - var params = jQuery.extend({},jQuery('.drop-hover[data-date]',target.iface.getDOMNode())[0].dataset || {}); - - // Add link IDs - var app_registry = egw.link_get_registry('calendar'); - params[app_registry.add_app] = []; - params[app_registry.add_id] = []; - for(var n in links) - { - params[app_registry.add_app].push( links[n].app); - params[app_registry.add_id].push( links[n].id); - } - app.calendar.add(params); - } - - },true); - - drop_link.acceptedTypes = ['default','link']; - drop_link.hideOnDisabled = true; - - // Create the drop action for moving events between calendars - var invite_action = function(action, source, target) { - - // Extract link IDs - var links = []; - var id = ''; - for(var i = 0; i < source.length; i++) - { - // Check for no ID (invalid) or same manager (dragging an event) - if(!source[i].id) continue; - if(source[i].manager === target.manager) - { - - // Find the timegrid, could have dropped on an event - var timegrid = target.iface.getWidget(); - while(target.parent && timegrid.instanceOf && !timegrid.instanceOf(et2_calendar_timegrid)) - { - target = target.parent; - timegrid = target.iface.getWidget(); - } - - // Leave the helper there until the update is done - var loading = action.ui.helper.clone(true).appendTo(jQuery('body')); - - // and add a loading icon so user knows something is happening - if(jQuery('.calendar_timeDemo',loading).length == 0) - { - jQuery('.calendar_calEventHeader',loading).addClass('loading'); - } - else - { - jQuery('.calendar_timeDemo',loading).after('
'); - } - - var event_data = egw.dataGetUIDdata(source[i].id).data; - et2_calendar_event.recur_prompt(event_data, function(button_id) { - if(button_id === 'cancel' || !button_id) - { - return; - } - var add_owner = jQuery.extend([],timegrid.options.owner); - if(timegrid.daily_owner) - { - timegrid.iterateOver(function(col) { - if(col.div.has(timegrid.gridHover).length || col.header.has(timegrid.gridHover).length) - { - add_owner = col.options.owner; - } - }, this, et2_calendar_daycol); - } - egw().json('calendar.calendar_uiforms.ajax_invite', [ - button_id==='series' ? event_data.id : event_data.app_id, - add_owner, - action.id === 'change_participant' ? - jQuery.extend([],source[i].iface.getWidget().getParent().options.owner) : - [] - ], - function() { loading.remove();} - ).sendRequest(true); - }); - // Ok, stop. - return false; - } - } - }; - - drop_change_participant = mgr.addAction('drop', 'change_participant', egw.lang('Move to'), egw.image('participant'), invite_action,true); - drop_change_participant.acceptedTypes = ['calendar']; - drop_change_participant.hideOnDisabled = true; - - drop_invite = mgr.addAction('drop', 'invite', egw.lang('Invite'), egw.image('participant'), invite_action,true); - drop_invite.acceptedTypes = ['calendar']; - drop_invite.hideOnDisabled = true; - } - if(actionLinks.indexOf(drop_link.id) < 0) - { - actionLinks.push(drop_link.id); - } - - actionLinks.push(drop_invite.id); - actionLinks.push(drop_change_participant.id); - - // Don't re-add - if(drag_action == null) - { - // Create drag action that allows linking - drag_action = mgr.addAction('drag', 'egw_link_drag', egw.lang('link'), 'link', function(action, selected) { - // Drag helper - list titles. - // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links - // TODO: Need to decide if we need to create a customized helper interface for links anyway - //return helper; - return null; - },true); - } - // The timegrid itself is not draggable, so don't add a link. - // The action is there for the children (events) to use - if(false && actionLinks.indexOf(drag_action.id) < 0) - { - actionLinks.push(drag_action.id); - } - drag_action.set_dragType(['link','calendar']); - }, - - /** - * Get all action-links / id's of 1.-level actions from a given action object - * - * Here we are only interested in drop events. - * - * @param actions - * @returns {Array} - */ - _get_action_links: function(actions) - { - var action_links = []; - // TODO: determine which actions are allowed without an action (empty actions) - for(var i in actions) - { - var action = actions[i]; - if(action.type == 'drop') - { - action_links.push(typeof action.id != 'undefined' ? action.id : i); - } - } - return action_links; - }, - - /** - * Provide specific data to be displayed. - * This is a way to set start and end dates, owner and event data in one call. - * - * Events will be retrieved automatically from the egw.data cache, so there - * is no great need to provide them. - * - * @param {Object[]} events Array of events, indexed by date in Ymd format: - * { - * 20150501: [...], - * 20150502: [...] - * } - * Days should be in order. - * {string|number|Date} events.start_date - New start date - * {string|number|Date} events.end_date - New end date - * {number|number[]|string|string[]} event.owner - Owner ID, which can - * be an account ID, a resource ID (as defined in calendar_bo, not - * necessarily an entry from the resource app), or a list containing a - * combination of both. - */ - set_value: function set_value(events) - { - if(typeof events !== 'object') return false; - - var use_days_sent = true; - - if(events.start_date) - { - use_days_sent = false; - } - if(events.end_date) - { - use_days_sent = false; - } - - this._super.apply(this,arguments); - - if(use_days_sent) - { - var day_list = Object.keys(events); - if(day_list.length) - { - this.set_start_date(day_list[0]); - this.set_end_date(day_list[day_list.length-1]); - } - - - // Sub widgets actually get their own data from egw.data, so we'll - // stick it there - var consolidated = et2_calendar_view.is_consolidated(this.options.owner, this.day_list.length == 1 ? 'day' : 'week'); - for(var day in events) - { - var day_list = []; - for(var i = 0; i < events[day].length; i++) - { - day_list.push(events[day][i].row_id); - egw.dataStoreUID('calendar::'+events[day][i].row_id, events[day][i]); - } - // Might be split by user, so we have to check that too - for(var i = 0; i < this.options.owner.length; i++) - { - var owner = consolidated ? this.options.owner : this.options.owner[i]; - var day_id = app.classes.calendar._daywise_cache_id(day,owner); - egw.dataStoreUID(day_id, day_list); - if(consolidated) break; - } - } - } - - // Reset and calculate instead of just use the keys so we can get the weekend preference - this.day_list = []; - - // None of the above changed anything, hide the loader - if(!this.update_timer) - { - window.setTimeout(jQuery.proxy(function() {this.loader.hide();},this),200); - } - }, - - /** - * Set which user owns this. Owner is passed along to the individual - * days. - * - * @param {number|number[]} _owner Account ID - * @returns {undefined} - */ - set_owner: function(_owner) - { - var old = this.options.owner || 0; - this._super.apply(this, arguments); - - this.owner.set_label(''); - this.div.removeClass('calendar_TimeGridNoLabel'); - - // Check to see if it's our own calendar, with just us showing - if(typeof _owner == 'object' && _owner.length == 1) - { - var rowCount = 0; - this._parent.iterateOver(function(widget) { - if(!widget.disabled) rowCount++; - },this, et2_calendar_timegrid); - // Just us, show week number - if(rowCount == 1 && _owner.length == 1 && _owner[0] == egw.user('account_id') || rowCount != 1) _owner = false; - } - - var day_count = this.day_list.length ? this.day_list.length : - this._calculate_day_list(this.options.start_date, this.options.end_date, this.options.show_weekend).length; - if(typeof _owner == 'string' && isNaN(_owner)) - { - this.set_label(''); - this.owner.set_value(this._get_owner_name(_owner)); - - // Label is empty, but give extra space for the owner name - this.div.removeClass('calendar_TimeGridNoLabel'); - } - else if (!_owner || typeof _owner == 'object' && _owner.length > 1 || - // Single owner, single day - _owner.length === 1 && day_count === 1 - ) - { - // Don't show owners if more than one, show week number - this.owner.set_value(''); - if(this.options.start_date) - { - this.set_label(egw.lang('wk') + ' ' + - (app.calendar ? app.calendar.date.week_number(this.options.start_date) : '') - ); - } - } - else - { - this.owner.options.application = 'api-accounts'; - this.owner.set_value(this._get_owner_name(_owner)); - this.set_label(''); - jQuery(this.getDOMNode(this.owner)).prepend(this.owner.getDOMNode()); - } - - if(this.isAttached() && ( - typeof old === "number" && typeof _owner === "number" && old !== this.options.owner || - // Array of ids will not compare as equal - ((typeof old === 'object' || typeof _owner === 'object') && old.toString() !== _owner.toString()) || - // Strings - typeof old === 'string' && ''+old !== ''+this.options.owner - )) - { - this.invalidate(true); - } - }, - - /** - * Set a label for this week - * - * May conflict with owner, which is displayed when there's only one owner. - * - * @param {string} label - */ - set_label: function(label) - { - this.options.label = label; - this._labelContainer.html(label); - this.gridHeader.prepend(this._labelContainer); - - // If it's a short label (eg week number), don't give it an extra line - // but is empty, but give extra space for a single owner name - this.div.toggleClass( - 'calendar_TimeGridNoLabel', - label.trim().length > 0 && label.trim().length <= 6 || - this.options.owner.length > 1 - ); - }, - - /** - * Set how big the time divisions are - * - * Setting granularity to 0 will remove the time divisions and display - * each days events in a list style. This 'gridlist' is not to be confused - * with the list view, which uses a nextmatch. - * - * @param {number} minutes - */ - set_granularity: function(minutes) - { - // Avoid < 0 - minutes = Math.max(0,minutes); - - if(this.options.granularity !== minutes) - { - if(this.options.granularity === 0 || minutes === 0) - { - this.options.granularity = minutes; - // Need to re-do a bunch to make sure this is propagated - this.invalidate(); - } - else - { - this.options.granularity = minutes; - this._drawTimes(); - } - } - else if (!this.update_timer) - { - this.resizeTimes(); - } - }, - - /** - * Turn on or off the visibility of weekends - * - * @param {boolean} weekends - */ - set_show_weekend: function(weekends) - { - weekends = weekends ? true : false; - if(this.options.show_weekend !== weekends) - { - this.options.show_weekend = weekends; - if(this.isAttached()) - { - this.invalidate(); - } - } - }, - - /** - * Call change handler, if set - */ - change: function() { - if (this.onchange) - { - if(typeof this.onchange == 'function') - { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.push(this); - - return this.onchange.apply(this, args); - } else { - return (et2_compileLegacyJS(this.options.onchange, this, _node))(); - } - } - }, - - /** - * Call event change handler, if set - * - * @param {type} event - * @param {type} dom_node - */ - event_change: function(event, dom_node) { - if (this.onevent_change) - { - var event_data = this._get_event_info(dom_node); - var event_widget = this.getWidgetById(event_data.widget_id); - et2_calendar_event.recur_prompt(event_data, jQuery.proxy(function(button_id, event_data) { - // No need to continue - if(button_id === 'cancel') return false; - - if(typeof this.onevent_change == 'function') - { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - - if(args.indexOf(event_widget) == -1) args.push(event_widget); - - // Put button ID in event - event.button_id = button_id; - - return this.onevent_change.apply(this, [event, event_widget, button_id]); - } else { - return (et2_compileLegacyJS(this.options.onevent_change, event_widget, dom_node))(); - } - },this)); - } - return false; - }, - - get_granularity: function() - { - // get option, or user's preference - if(typeof this.options.granularity === 'undefined') - { - this.options.granularity = egw.preference('interval','calendar') || 30; - } - return parseInt(this.options.granularity); - }, - - /** - * Click handler calling custom handler set via onclick attribute to this.onclick - * - * This also handles all its own actions, including navigation. If there is - * an event associated with the click, it will be found and passed to the - * onclick function. - * - * @param {Event} _ev - * @returns {boolean} Continue processing event (true) or stop (false) - */ - click: function(_ev) - { - var result = true; - if(this.options.readonly ) return; - - // Drag to create in progress - if(this.drag_create.start !== null) return; - - // Is this click in the event stuff, or in the header? - if(_ev.target.dataset.id || jQuery(_ev.target).parents('.calendar_calEvent').length) - { - // Event came from inside, maybe a calendar event - var event = this._get_event_info(_ev.originalEvent.target); - if(typeof this.onclick == 'function') - { - // Make sure function gets a reference to the widget, splice it in as 2. argument if not - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.splice(1, 0, this); - - result = this.onclick.apply(this, args); - } - - var event_node = jQuery(event.event_node); - if(event.id && result && !this.disabled && !this.options.readonly && - // Permissions - opening will fail if we try - event_node && !(event_node.hasClass('rowNoView')) - ) - { - if(event.widget_id && this.getWidgetById(event.widget_id)) - { - this.getWidgetById(event.widget_id).recur_prompt(); - } - else - { - et2_calendar_event.recur_prompt(event); - } - - return false; - } - return result; - } - else if (this.gridHeader.is(_ev.target) && _ev.target.dataset || - this._labelContainer.is(_ev.target) && this.gridHeader[0].dataset) - { - app.calendar.update_state(jQuery.extend( - {view: 'week'}, - this._labelContainer.is(_ev.target) ? - this.gridHeader[0].dataset : - _ev.target.dataset - )); - } - else if (this.options.owner.length === 1 && jQuery(this.owner.getDOMNode()).is(_ev.target)) - { - // Click on the owner in header, show just that owner - app.calendar.update_state({owner: this.options.owner}); - } - else if (this.dayHeader.has(_ev.target).length) - { - // Click on a day header - let day deal with it - // First child is a selectAccount - for(var i = 1; i < this._children.length; i++) - { - if(this._children[i].header && ( - this._children[i].header.has(_ev.target).length || this._children[i].header.is(_ev.target)) - ) - { - return this._children[i].click(_ev); - } - } - } - // No time grid, click on a day - else if (this.options.granularity === 0 && - (jQuery(_ev.target).hasClass('event_wrapper') || jQuery(_ev.target).hasClass('.calendar_calDayCol')) - ) - { - // Default handler to open a new event at the selected time - var target = jQuery(_ev.target).hasClass('event_wrapper') ? _ev.target.parentNode : _ev.target; - var options = { - date: target.dataset.date || this.options.date, - hour: target.dataset.hour || this._parent.options.day_start, - minute: target.dataset.minute || 0, - owner: this.options.owner - }; - app.calendar.add(options); - return false; - } - }, - - /** - * Mousedown handler to support drag to create - * - * @param {jQuery.Event} event - */ - _mouse_down: function(event) - { - if(event.which !== 1) return; - - if (this.options.readonly) return; - - var start = jQuery.extend({},this.gridHover[0].dataset); - if(start.date) - { - // Set parent for event - if(this.daily_owner) - { - // Each 'day' is the same date, different user - // Find the correct row so we know the parent - var col = event.target.closest('.calendar_calDayCol'); - for(var i = 0; i < this._children.length && col; i++) - { - if(this._children[i].node === col) - { - this.drag_create.parent = this._children[i]; - break; - } - } - } - else - { - this.drag_create.parent = this.getWidgetById(start.date); - } - - // Format date - this.date_helper.set_year(start.date.substring(0,4)); - this.date_helper.set_month(start.date.substring(4,6)); - this.date_helper.set_date(start.date.substring(6,8)); - if(start.hour) - { - this.date_helper.set_hours(start.hour); - } - if(start.minute) - { - this.date_helper.set_minutes(start.minute); - } - start.date = this.date_helper.get_value(); - - this.gridHover.css('cursor', 'ns-resize'); - - // Start update - var timegrid = this; - this.div.on('mousemove.dragcreate', function() - { - if(timegrid.drag_create.event && timegrid.drag_create.parent && timegrid.drag_create.end) - { - var end = jQuery.extend({}, timegrid.gridHover[0].dataset); - if(end.date) - { - timegrid.date_helper.set_year(end.date.substring(0,4)); - timegrid.date_helper.set_month(end.date.substring(4,6)); - timegrid.date_helper.set_date(end.date.substring(6,8)); - if(end.hour) - { - timegrid.date_helper.set_hours(end.hour); - } - if(end.minute) - { - timegrid.date_helper.set_minutes(end.minute); - } - timegrid.drag_create.end.date = timegrid.date_helper.get_value(); - } - timegrid._drag_update_event(); - } - }); - } - return this._drag_create_start(start); - }, - - /** - * Mouseup handler to support drag to create - * - * @param {jQuery.Event} event - */ - _mouse_up: function(event) - { - if (this.options.readonly) return; - var end = jQuery.extend({}, this.gridHover[0].dataset); - if(end.date) - { - this.date_helper.set_year(end.date.substring(0,4)); - this.date_helper.set_month(end.date.substring(4,6)); - this.date_helper.set_date(end.date.substring(6,8)); - if(end.hour) - { - this.date_helper.set_hours(end.hour); - } - if(end.minute) - { - this.date_helper.set_minutes(end.minute); - } - end.date = this.date_helper.get_value(); - } - this.div.off('mousemove.dragcreate'); - this.gridHover.css('cursor', ''); - - return this._drag_create_end(end); - }, - - /** - * Get time from position for drag and drop - * - * This does not return an actual time on a clock, but finds the closest - * time node (.calendar_calAddEvent or day column) to the given position. - * - * @param {number} x - * @param {number} y - * @returns {DOMNode[]} time node(s) for the given position - */ - _get_time_from_position: function(x,y) { - - x = Math.round(x); - y = Math.round(y); - - var path = []; - var day = null; - var time = null; - - var node = document.elementFromPoint(x,y); - var $node = jQuery(node); - - // Ignore high level & non-time (grid itself, header parent & week label) - if([this.node, this.gridHeader[0], this._labelContainer[0]].indexOf(node) !== -1 || - // Day labels - this.gridHeader.has(node).length && !$node.hasClass("calendar_calDayColAllDay") && !$node.hasClass('calendar_calDayColHeader')) - { - return []; - } - for(var id in this.gridHover[0].dataset) { - delete this.gridHover[0].dataset[id]; - } - if(this.options.granularity == 0) - { - this.gridHover.css('height',''); - } - while(node && node != this.node && node.tagName != 'BODY' && path.length < 10) - { - path.push(node); - node.style.display = 'none'; - $node = jQuery(node); - if($node.hasClass('calendar_calDayColHeader')) { - for(var id in node.dataset) { - this.gridHover[0].dataset[id] = node.dataset[id]; - } - this.gridHover.css({ - position: 'absolute', - top: '', - bottom: '0px', - // Use 100% height if we're hiding the day labels to avoid - // any remaining space from the hidden labels - height: $node.height() > parseInt($node.css('line-height')) ? - $node.css('padding-bottom') : '100%' - }); - day = node; - this.gridHover - .attr('data-non_blocking','true'); - break; - } - if($node.hasClass('calendar_calDayCol')) - { - day = node; - this.gridHover - .attr('data-date',day.dataset.date); - } - if($node.hasClass('calendar_calTimeRowTime')) - { - time = node; - this.gridHover - .attr('data-hour',time.dataset.hour) - .attr('data-minute',time.dataset.minute); - break; - } - node = document.elementFromPoint(x,y); - } - for(var i = 0; i < path.length; i++) - { - path[i].style.display = ''; - } - - if(!day) - { - return []; - } - this.gridHover - .show() - .appendTo(day); - if(time) - { - this.gridHover - .height(this.rowHeight) - .position({my:'left top', at: 'left top', of: time}); - } - this.gridHover.css('left',''); - return this.gridHover; - }, - - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes: function(_attrs) { - _attrs.push('start_date','end_date'); - }, - - getDetachedNodes: function() { - return [this.getDOMNode()]; - }, - - setDetachedAttributes: function(_nodes, _values) { - this.div = jQuery(_nodes[0]); - - if(_values.start_date) - { - this.set_start_date(_values.start_date); - } - if(_values.end_date) - { - this.set_end_date(_values.end_date); - } - }, - - // Resizable interface - /** - * @param {boolean} [_too_small=null] Force the widget to act as if it was too small - */ - resize: function (_too_small) - { - if(this.disabled || !this.div.is(':visible')) - { - return; - } - - /* - We expect the timegrid to be in a table with 0 or more other timegrids, - 1 per row. We want each timegrid to be as large as possible, but space - shared equally. Height can't be set to a percentage on the rows, because - that doesn't work. However, if any timegrid is too small (1/2 hour < 1 line - height), we change to showing only the working hours with no vertical - scrollbar. Each week gets as much space as it needs, and all scroll together. - */ - // How many rows? - var rowCount = 0; - this._parent.iterateOver(function(widget) { - if(!widget.disabled) rowCount++; - },this, et2_calendar_timegrid); - - // Take the whole tab height, or home portlet - if(this.getInstanceManager().app === 'home') - { - var height = jQuery(this.getParent().getDOMNode()).parentsUntil('.et2_portlet').last().parent().innerHeight(); - - // Allow for portlet header - height -= jQuery('.ui-widget-header',this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true); - } - else - { - var height = jQuery(this.getInstanceManager().DOMContainer).parent().innerHeight(); - - // Allow for toolbar - height -= jQuery('#calendar-toolbar',this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true); - } - - this.options.height = Math.floor(height / rowCount); - - // Allow for borders & padding - this.options.height -= 2*((this.div.outerWidth(true) - this.div.innerWidth()) + parseInt(this.div.parent().css('padding-top'))); - - // Calculate how much space is needed, and - // if too small be bigger - var needed = ((this.day_end - this.day_start) / - (this.options.granularity / 60) * parseInt(this.div.css('line-height'))) + - this.gridHeader.outerHeight(); - var too_small = needed > this.options.height && this.options.granularity != 0; - - - if(this.getInstanceManager().app === 'home') - { - var modify_node = jQuery(this.getParent().getDOMNode()).parentsUntil('.et2_portlet').last(); - } - else - { - var modify_node = jQuery(this.getInstanceManager().DOMContainer); - } - modify_node - .css({ - 'overflow-y': too_small || _too_small ? 'auto' : 'hidden', - 'overflow-x': 'hidden', - 'height': too_small || _too_small ? height : '100%' - }); - if(too_small || _too_small) - { - this.options.height = Math.max(this.options.height, needed); - // Set all others to match - if(!_too_small && rowCount > 1 && this._parent) - { - window.setTimeout(jQuery.proxy(function() { - if(!this._parent) return; - this._parent.iterateOver(function(widget) { - if(!widget.disabled) widget.resize(true); - },this, et2_calendar_timegrid); - },this),1); - return; - } - this.div.addClass('calendar_calTimeGridFixed'); - } - else - { - this.div.removeClass('calendar_calTimeGridFixed'); - } - this.div.css('height', this.options.height); - - // Re-do time grid - if(!this.update_timer) - { - this.resizeTimes(); - } - - // Try to resize width, though animations cause problems - var total_width = modify_node.parent().innerWidth() - this.days.position().left; - // Space for todos, if there - total_width -= jQuery(this.getInstanceManager().DOMContainer).siblings().has(':visible').not('#calendar-toolbar').outerWidth() - - var day_width = (total_width > 0 ? total_width : modify_node.width())/this.day_widgets.length; - // update day widgets - for(var i = 0; i < this.day_widgets.length; i++) - { - var day = this.day_widgets[i]; - - // Position - day.set_left((day_width * i) + 'px'); - day.set_width(day_width + 'px'); - } - }, - - /** - * Set up for printing - * - * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up - * (waiting for data) - */ - beforePrint: function() { - - if(this.disabled || !this.div.is(':visible')) - { - return; - } - - var height_check = this.div.height(); - this.div.css('max-height','17cm'); - if(this.div.height() != height_check) - { - this.div.height('17cm'); - this._resizeTimes(); - } - - // update day widgets, if not on single day view - // - // TODO: Find out why don't we update single day view - // Let the single day view participate in print calculation. - if(this.day_widgets.length > 0) - { - var day_width = (100 / this.day_widgets.length); - for(var i = 0; i < this.day_widgets.length; i++) - { - var day = this.day_widgets[i]; - - // Position - day.set_left((i*day_width) + '%'); - day.set_width(day_width + '%'); - // For some reason the column's method does not set it correctly in Chrome - day.header[0].style.width = day_width + '%'; - } - } - - // Stop Firefox from scrolling the day to the top - this would break printing in Chrome - if (navigator.userAgent.match(/(firefox|safari|iceweasel)/i) && !navigator.userAgent.match(/chrome/i)) - { - var height = this.scrolling.scrollTop() + this.scrolling.height(); - this.scrolling - // Disable scroll event, or it will recalculate out of view events - .off('scroll') - // Explicitly transform to the correct place - .css({ - 'transform': 'translateY(-'+this.scrolling.scrollTop()+'px)', - 'margin-bottom': '-'+this.scrolling.scrollTop()+'px', - 'height': height+'px' - }); - this.div.css({'height':'','max-height':''}); - } - }, - - /** - * Reset after printing - */ - afterPrint: function() { - this.div.css('maxHeight',''); - this.scrolling.children().css({'transform':'', 'overflow':''}); - this.div.height(this.options.height); - if (navigator.userAgent.match(/(firefox|safari|iceweasel)/i) && !navigator.userAgent.match(/chrome/i)) - { - this._resizeTimes(); - this.scrolling - // Re-enable out-of-view formatting on scroll - .on('scroll', jQuery.proxy(this._scroll, this)) - // Remove translation - .css({'transform':'', 'margin-bottom':''}); - } - } -});}).call(this); -et2_register_widget(et2_calendar_timegrid, ["calendar-timegrid"]); \ No newline at end of file +var et2_calendar_timegrid = /** @class */ (function (_super) { + __extends(et2_calendar_timegrid, _super); + /** + * Constructor + * + * @memberOf et2_calendar_timegrid + */ + function et2_calendar_timegrid(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_calendar_timegrid._attributes, _child || {})) || this; + _this.daily_owner = false; + // Main container + _this.div = jQuery(document.createElement("div")) + .addClass("calendar_calTimeGrid") + .addClass("calendar_TimeGridNoLabel"); + // Headers + _this.gridHeader = jQuery(document.createElement("div")) + .addClass("calendar_calGridHeader") + .appendTo(_this.div); + _this.dayHeader = jQuery(document.createElement("div")) + .appendTo(_this.gridHeader); + // Contains times / rows + _this.scrolling = jQuery(document.createElement('div')) + .addClass("calendar_calTimeGridScroll") + .appendTo(_this.div) + .append('
'); + // Contains days / columns + _this.days = jQuery(document.createElement("div")) + .addClass("calendar_calDayCols") + .appendTo(_this.scrolling); + // Used for owners + _this.owner = et2_createWidget('description', {}, _this); + _this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label et2_link") + .appendTo(_this.gridHeader); + _this.gridHover = jQuery('
'); + // List of dates in Ymd + // The first one should be start_date, last should be end_date + _this.day_list = []; + _this.day_widgets = []; + // Timer to re-scale time to fit + _this.resize_timer = null; + _this.setDOMNode(_this.div[0]); + return _this; + } + et2_calendar_timegrid.prototype.destroy = function () { + // Stop listening to tab changes + if (typeof framework !== 'undefined' && framework.getApplicationByName('calendar').tab) { + jQuery(framework.getApplicationByName('calendar').tab.contentDiv).off('show.' + this.id); + } + _super.prototype.destroy.call(this); + // Delete all old objects + this._actionObject.clear(); + this._actionObject.unregisterActions(); + this._actionObject.remove(); + this._actionObject = null; + this.div.off(); + this.div = null; + this.gridHeader = null; + this.dayHeader = null; + this.days = null; + this.scrolling = null; + this._labelContainer = null; + // Stop the resize timer + if (this.resize_timer) { + window.clearTimeout(this.resize_timer); + } + }; + et2_calendar_timegrid.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.call(this); + // Listen to tab show to make sure we scroll to the day start, not top + if (typeof framework !== 'undefined' && framework.getApplicationByName('calendar').tab) { + jQuery(framework.getApplicationByName('calendar').tab.contentDiv) + .on('show.' + this.id, jQuery.proxy(function () { + if (this.scrolling) { + this.scrolling.scrollTop(this._top_time); + } + }, this)); + } + // Need to get the correct internal sizing + this.resize(); + this._drawGrid(); + // Actions may be set on a parent, so we need to explicitly get in here + // and get ours + this._link_actions(this.options.actions || this.getParent().options.actions || []); + // Automatically bind drag and resize for every event using jQuery directly + // - no action system - + var timegrid = this; + /** + * If user puts the mouse over an event, then we'll set up resizing so + * they can adjust the length. Should be a little better on resources + * than binding it for every calendar event, and we won't need exceptions + * for planner view to resize horizontally. + */ + this.div.on('mouseover', '.calendar_calEvent:not(.ui-resizable):not(.rowNoEdit)', function () { + // Only resize in timegrid + if (timegrid.options.granularity === 0) + return; + // Load the event + timegrid._get_event_info(this); + var that = this; + //Resizable event handler + jQuery(this).resizable({ + distance: 10, + // Grid matching preference + grid: [10000, timegrid.rowHeight], + autoHide: false, + handles: 's,se', + containment: 'parent', + /** + * Triggered when the resizable is created. + * + * @param {event} event + * @param {Object} ui + */ + create: function (event, ui) { + var resizeHelper = event.target.getAttribute('data-resize'); + if (resizeHelper == 'WD' || resizeHelper == 'WDS') { + jQuery(this).resizable('destroy'); + } + }, + /** + * If dragging to resize an event, abort drag to create + * + * @param {jQuery.Event} event + * @param {Object} ui + */ + start: function (event, ui) { + if (timegrid.drag_create.start) { + // Abort drag to create, we're dragging to resize + timegrid._drag_create_end({}); + } + }, + /** + * Triggered at the end of resizing the calEvent. + * + * @param {event} event + * @param {Object} ui + */ + stop: function (event, ui) { + var e = new jQuery.Event('change'); + e.originalEvent = event; + e.data = { duration: 0 }; + var event_data = timegrid._get_event_info(this); + var event_widget = timegrid.getWidgetById(event_data.widget_id); + var sT = event_widget.options.value.start_m; + if (typeof this.dropEnd != 'undefined' && this.dropEnd.length == 1) { + var eT = (parseInt(timegrid._drop_data.hour) * 60) + parseInt(timegrid._drop_data.minute); + e.data.duration = ((eT - sT) / 60) * 3600; + if (event_widget) { + event_widget.options.value.end_m = eT; + event_widget.options.value.duration = e.data.duration; + } + jQuery(this).trigger(e); + event_widget._update(event_widget.options.value); + // That cleared the resize handles, so remove for re-creation... + if (jQuery(this).resizable('instance')) { + jQuery(this).resizable('destroy'); + } + } + // Clear the helper, re-draw + if (event_widget && event_widget._parent) { + event_widget._parent.position_event(event_widget); + } + timegrid.div.children('.drop-hover').removeClass('.drop-hover'); + }, + /** + * Triggered during the resize, on the drag of the resize handler + * + * @param {event} event + * @param {Object} ui + */ + resize: function (event, ui) { + // Add a bit for better understanding - it will show _to_ the start, + // covering the 'actual' target + timegrid._get_time_from_position(ui.helper[0].getBoundingClientRect().left, ui.helper[0].getBoundingClientRect().bottom + 5); + timegrid.gridHover.hide(); + var drop = timegrid._drag_helper(this, ui.element[0]); + if (drop && !drop.is(':visible')) { + drop.get(0).scrollIntoView(false); + } + } + }); + }); + // Customize and override some draggable settings + this.div + .on('dragcreate', '.calendar_calEvent', function (event, ui) { + jQuery(this).draggable('option', 'cancel', '.rowNoEdit'); + // Act like you clicked the header, makes it easier to position + // but put it to the side (-5) so we can still do the hover + jQuery(this).draggable('option', 'cursorAt', { top: 5, left: -5 }); + }) + .on('dragstart', '.calendar_calEvent', function (event, ui) { + jQuery('.calendar_calEvent', ui.helper).width(jQuery(this).width()) + .height(jQuery(this).outerHeight()) + .css('top', '').css('left', '') + .appendTo(ui.helper); + ui.helper.width(jQuery(this).width()); + // Cancel drag to create, we're dragging an existing event + timegrid.drag_create.start = null; + timegrid._drag_create_end(); + }) + .on('mousemove', function (event) { + timegrid._get_time_from_position(event.clientX, event.clientY); + }) + .on('mouseout', function (event) { + if (timegrid.div.has(event.relatedTarget).length === 0) { + timegrid.gridHover.hide(); + } + }) + .on('mousedown', jQuery.proxy(this._mouse_down, this)) + .on('mouseup', jQuery.proxy(this._mouse_up, this)); + return true; + }; + et2_calendar_timegrid.prototype._createNamespace = function () { + return true; + }; + /** + * Show the current time while dragging + * Used for resizing as well as drag & drop + * + * @param {type} element + * @param {type} helper + * @param {type} height + */ + et2_calendar_timegrid.prototype._drag_helper = function (element, helper, height) { + if (!element) + return; + element.dropEnd = this.gridHover; + if (element.dropEnd.length) { + this._drop_data = jQuery.extend({}, element.dropEnd[0].dataset || {}); + } + if (typeof element.dropEnd != 'undefined' && element.dropEnd.length) { + // Make sure the target is visible in the scrollable day + if (this.gridHover.is(':visible')) { + if (this.scrolling.scrollTop() > 0 && this.scrolling.scrollTop() >= this.gridHover.position().top - this.rowHeight) { + this.scrolling.scrollTop(this.gridHover.position().top - this.rowHeight); + } + else if (this.scrolling.scrollTop() + this.scrolling.height() <= this.gridHover.position().top + (2 * this.rowHeight)) { + this.scrolling.scrollTop(this.scrolling.scrollTop() + this.rowHeight); + } + } + var time = ''; + if (this._drop_data.whole_day) { + time = this.egw().lang('Whole day'); + } + else if (this.options.granularity === 0) { + // No times, keep what's in the event + // Add class to helper to keep formatting + jQuery(helper).addClass('calendar_calTimeGridList'); + } + else { + // @ts-ignore + time = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { + hour: element.dropEnd.attr('data-hour'), + minute: element.dropEnd.attr('data-minute'), + seconds: 0, + timezone: 0 + }, { "ampm": (egw.preference("timeformat") == "12") }); + } + element.innerHTML = '
' + time + '
'; + } + else { + element.innerHTML = '
'; + } + jQuery(element).width(jQuery(helper).width()); + return element.dropEnd; + }; + /** + * Handler for dropping an event on the timegrid + * + * @param {type} timegrid + * @param {type} event + * @param {type} ui + * @param {type} dropEnd + */ + et2_calendar_timegrid.prototype._event_drop = function (timegrid, event, ui, dropEnd) { + var e = new jQuery.Event('change'); + e.originalEvent = event; + e.data = { start: 0 }; + if (typeof dropEnd != 'undefined' && dropEnd) { + var drop_date = dropEnd.date || false; + var event_data = timegrid._get_event_info(ui.draggable); + var event_widget = timegrid.getWidgetById(event_data.widget_id); + if (!event_widget) { + // Widget was moved across weeks / owners + event_widget = timegrid.getParent().getWidgetById(event_data.widget_id); + } + if (event_widget) { + // Send full string to avoid rollover between months using set_month() + event_widget._parent.date_helper.set_value(drop_date.substring(0, 4) + '-' + drop_date.substring(4, 6) + '-' + drop_date.substring(6, 8) + + 'T00:00:00Z'); + // Make sure whole day events stay as whole day events by ignoring drop time + if (event_data.app == 'calendar' && event_widget.options.value.whole_day) { + event_widget._parent.date_helper.set_hours(0); + event_widget._parent.date_helper.set_minutes(0); + } + else if (timegrid.options.granularity === 0) { + // List, not time grid - keep time + event_widget._parent.date_helper.set_hours(event_widget.options.value.start.getUTCHours()); + event_widget._parent.date_helper.set_minutes(event_widget.options.value.start.getUTCMinutes()); + } + else { + // Non-whole day events, and integrated apps, can change + event_widget._parent.date_helper.set_hours(dropEnd.whole_day ? 0 : dropEnd.hour || 0); + event_widget._parent.date_helper.set_minutes(dropEnd.whole_day ? 0 : dropEnd.minute || 0); + } + // Leave the helper there until the update is done + var loading = ui.helper.clone(true).appendTo(jQuery('body')); + // and add a loading icon so user knows something is happening + if (jQuery('.calendar_timeDemo', loading).length == 0) { + jQuery('.calendar_calEventHeader', loading).addClass('loading'); + } + else { + jQuery('.calendar_timeDemo', loading).after('
'); + } + event_widget.recur_prompt(function (button_id) { + if (button_id === 'cancel' || !button_id) { + // Need to refresh the event with original info to clean up + var app_id = event_widget.options.value.app_id ? event_widget.options.value.app_id : event_widget.options.value.id + (event_widget.options.value.recur_type ? ':' + event_widget.options.value.recur_date : ''); + egw().dataStoreUID('calendar::' + app_id, egw.dataGetUIDdata('calendar::' + app_id).data); + loading.remove(); + return; + } + var duration; + //Get infologID if in case if it's an integrated infolog event + if (event_data.app === 'infolog') { + // Duration - infologs are always non-blocking + duration = dropEnd.whole_day ? 86400 - 1 : (event_widget.options.value.whole_day ? (egw().preference('defaultlength', 'calendar') * 60) : false); + // If it is an integrated infolog event we need to edit infolog entry + egw().json('stylite_infolog_calendar_integration::ajax_moveInfologEvent', [event_data.app_id, event_widget._parent.date_helper.getValue() || false, duration], function () { loading.remove(); }).sendRequest(true); + } + else { + //Edit calendar event + // Duration - check for whole day dropped on a time, change it to full days + duration = event_widget.options.value.whole_day && dropEnd.hour ? + // Make duration whole days, less 1 second + (Math.round((event_widget.options.value.end - event_widget.options.value.start) / (1000 * 86400)) * 86400) - 1 : + false; + // Event (whole day or not) dropped on whole day section, change to whole day non blocking + if (dropEnd.whole_day) + duration = 'whole_day'; + // Send the update + var _send = function (series_instance) { + var start = new Date(event_widget._parent.date_helper.getValue()); + egw().json('calendar.calendar_uiforms.ajax_moveEvent', [ + button_id === 'series' ? event_data.id : event_data.app_id, event_data.owner, + start, + timegrid.options.owner || egw.user('account_id'), + duration, + series_instance + ], function () { loading.remove(); }).sendRequest(true); + }; + // Check for modifying a series that started before today + if (event_widget.options.value.recur_type && button_id === 'series') { + event_widget.series_split_prompt(function (_button_id) { + if (_button_id === et2_dialog.OK_BUTTON) { + _send(event_widget.options.value.recur_date); + } + else { + loading.remove(); + } + }); + } + else { + _send(event_widget.options.value.recur_date); + } + } + }); + } + } + }; + /** + * Something changed, and the days need to be re-drawn. We wait a bit to + * avoid re-drawing twice if start and end date both changed, then recreate + * the days. + * The whole grid is not regenerated because times aren't expected to change, + * just the days. + * + * @param {boolean} [trigger=false] Trigger an event once things are done. + * Waiting until invalidate completes prevents 2 updates when changing the date range. + * @returns {undefined} + */ + et2_calendar_timegrid.prototype.invalidate = function (trigger) { + // Reset the list of days + this.day_list = []; + // Wait a bit to see if anything else changes, then re-draw the days + if (this.update_timer) { + window.clearTimeout(this.update_timer); + } + this.update_timer = window.setTimeout(jQuery.proxy(function () { + this.widget.update_timer = null; + window.clearTimeout(this.resize_timer); + this.widget.loader.hide().show(); + // Update actions + if (this.widget._actionManager) { + this.widget._link_actions(this.widget._actionManager.children); + } + this.widget._drawDays(); + // We have to completely re-do times, as they may have changed in + // scale to the point where more labels are needed / need to be removed + this.widget._drawTimes(); + if (this.trigger) { + this.widget.change(); + } + // Hide loader + window.setTimeout(jQuery.proxy(function () { this.loader.hide(); }, this.widget), 200); + }, { widget: this, "trigger": trigger }), et2_dataview_view_grid_1.et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); + }; + et2_calendar_timegrid.prototype.detachFromDOM = function () { + // Remove the binding to the change handler + jQuery(this.div).off(".et2_calendar_timegrid"); + return _super.prototype.detachFromDOM.call(this); + }; + et2_calendar_timegrid.prototype.attachToDOM = function () { + var result = _super.prototype.attachToDOM.call(this); + // Add the binding for the event change handler + jQuery(this.div).on("change.et2_calendar_timegrid", '.calendar_calEvent', this, function (e) { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.push(this); + return e.data.event_change.apply(e.data, args); + }); + // Add the binding for the change handler + jQuery(this.div).on("change.et2_calendar_timegrid", '*:not(.calendar_calEvent)', this, function (e) { + return e.data.change.call(e.data, e, this); + }); + // Catch resize and prevent it from bubbling further, triggering + // etemplate's resize + this.div.on('resize', this, function (e) { + e.stopPropagation(); + }); + return result; + }; + et2_calendar_timegrid.prototype.getDOMNode = function (_sender) { + if (_sender === this || !_sender) { + return this.div ? this.div[0] : null; + } + else if (_sender.instanceOf(et2_calendar_daycol)) { + return this.days ? this.days[0] : null; + } + else if (_sender) { + return this.gridHeader ? this.gridHeader[0] : null; + } + }; + et2_calendar_timegrid.prototype.set_disabled = function (disabled) { + var old_value = this.options.disabled; + _super.prototype.set_disabled.call(this, disabled); + if (disabled) { + this.loader.show(); + } + else if (old_value !== disabled) { + // Scroll to start of day - stops jumping in FF + // For some reason on Chrome & FF this doesn't quite get the day start + // to the top, so add 2px; + this.scrolling.scrollTop(this._top_time + 2); + } + }; + /** + * Clear everything, and redraw the whole grid + */ + et2_calendar_timegrid.prototype._drawGrid = function () { + this.div.css('height', this.options.height) + .empty(); + this.loader.prependTo(this.div).show(); + // Draw in the horizontal - the times + this._drawTimes(); + // Draw in the vertical - the days + this.invalidate(); + }; + /** + * Creates the DOM nodes for the times in the left column, and the horizontal + * lines (mostly via CSS) that span the whole date range. + */ + et2_calendar_timegrid.prototype._drawTimes = function () { + jQuery('.calendar_calTimeRow', this.div).remove(); + this.div.toggleClass('calendar_calTimeGridList', this.options.granularity === 0); + this.gridHeader + .attr('data-date', this.options.start_date) + .attr('data-owner', this.options.owner) + .append(this._labelContainer) + .append(this.owner.getDOMNode()) + .append(this.dayHeader) + .appendTo(this.div); + // Max with 18 avoids problems when it's not shown + var header_height = Math.max(this.gridHeader.outerHeight(true), 18); + this.scrolling + .appendTo(this.div) + .off(); + // No time grid - list + if (this.options.granularity === 0) { + this.scrolling.css('height', '100%'); + this.days.css('height', '100%'); + this.iterateOver(function (day) { + day.resize(); + }, this, et2_calendar_daycol); + return; + } + var wd_start = 60 * this.options.day_start; + var wd_end = 60 * this.options.day_end; + var granularity = this.options.granularity; + var totalDisplayMinutes = wd_end - wd_start; + var rowsToDisplay = Math.ceil((totalDisplayMinutes + 60) / granularity); + var row_count = (1440 / this.options.granularity); + this.scrolling + .on('scroll', jQuery.proxy(this._scroll, this)); + // Percent + var rowHeight = (100 / rowsToDisplay).toFixed(1); + // Pixels + this.rowHeight = this.scrolling.height() / rowsToDisplay; + // We need a reasonable bottom limit here, but resize will handle it + // if we get too small + if (this.rowHeight < 5 && this.div.is(':visible')) { + if (this.rowHeight === 0) { + // Something is not right... + this.rowHeight = 5; + } + } + // the hour rows + var show = { + 5: [0, 15, 30, 45], + 10: [0, 30], + 15: [0, 30], + 45: [0, 15, 30, 45] + }; + var html = ''; + var line_height = parseInt(this.div.css('line-height')); + this._top_time = 0; + for (var t = 0, i = 0; t < 1440; t += granularity, ++i) { + if (t <= wd_start && t + granularity > wd_start) { + this._top_time = this.rowHeight * (i + 1 + (wd_start - (t + granularity)) / granularity); + } + var working_hours = (t >= wd_start && t < wd_end) ? ' calendar_calWorkHours' : ''; + html += '
'; + // show time for full hours, always for 45min interval and at least on every 3 row + // @ts-ignore + var time = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { + hour: t / 60, + minute: t % 60, + seconds: 0, + timezone: 0 + }, { "ampm": (egw.preference("timeformat") === "12") }); + var time_label = (typeof show[granularity] === 'undefined' ? t % 60 === 0 : show[granularity].indexOf(t % 60) !== -1) ? time : ''; + if (time_label && egw.preference("timeformat") == "12" && time_label.split(':')[0] < 10) { + time_label = '  ' + time_label; + } + html += '
' + time_label + "
\n"; + } + // Set heights in pixels for scrolling + jQuery('.calendar_calTimeLabels', this.scrolling) + .empty() + .height(this.rowHeight * i) + .append(html); + this.days.css('height', (this.rowHeight * i) + 'px'); + this.gridHover.css('height', this.rowHeight); + // Scroll to start of day + this.scrolling.scrollTop(this._top_time); + }; + /** + * As window size and number of all day non-blocking events change, we need + * to re-scale the time grid to make sure the full working day is shown. + * + * We use a timeout to avoid doing it multiple times if redrawing or resizing. + */ + et2_calendar_timegrid.prototype.resizeTimes = function () { + // Hide resizing from user + this.loader.show(); + // Wait a bit to see if anything else changes, then re-draw the times + if (this.resize_timer) { + window.clearTimeout(this.resize_timer); + } + // No point if it is just going to be redone completely + if (this.update_timer) + return; + this.resize_timer = window.setTimeout(jQuery.proxy(function () { + if (this._resizeTimes) { + this.resize_timer = null; + this._resizeTimes(); + } + }, this), 1); + }; + /** + * Re-scale the time grid to make sure the full working day is shown. + * This is the timeout callback that does the actual re-size immediately. + */ + et2_calendar_timegrid.prototype._resizeTimes = function () { + if (!this.div.is(':visible')) { + return; + } + var wd_start = 60 * this.options.day_start; + var wd_end = 60 * this.options.day_end; + var totalDisplayMinutes = wd_end - wd_start; + var rowsToDisplay = Math.ceil((totalDisplayMinutes + 60) / this.options.granularity); + var row_count = (1440 / this.options.granularity); + var new_height = this.scrolling.height() / rowsToDisplay; + var old_height = this.rowHeight; + this.rowHeight = new_height; + jQuery('.calendar_calTimeLabels', this.scrolling).height(this.rowHeight * row_count); + this.days.css('height', this.options.granularity === 0 ? + '100%' : + (this.rowHeight * row_count) + 'px'); + // Scroll to start of day + this._top_time = (wd_start * this.rowHeight) / this.options.granularity; + // For some reason on Chrome & FF this doesn't quite get the day start + // to the top, so add 2px; + this.scrolling.scrollTop(this._top_time + 2); + if (this.rowHeight != old_height) { + this.iterateOver(function (child) { + if (child === this) + return; + child.resize(); + }, this, et2_IResizeable); + } + this.loader.hide(); + }; + /** + * Set up the needed day widgets to correctly display the selected date + * range. First we calculate the needed dates, then we create any needed + * widgets. Existing widgets are recycled rather than discarded. + */ + et2_calendar_timegrid.prototype._drawDays = function () { + this.scrolling.append(this.days); + // If day list is still empty, recalculate it from start & end date + if (this.day_list.length === 0 && this.options.start_date && this.options.end_date) { + this.day_list = this._calculate_day_list(this.options.start_date, this.options.end_date, this.options.show_weekend); + } + // For a single day, we show each owner in their own daycol + this.daily_owner = this.day_list.length === 1 && + this.options.owner.length > 1 && + this.options.owner.length < (parseInt('' + egw.preference('day_consolidate', 'calendar')) || 6); + var daycols_needed = this.daily_owner ? this.options.owner.length : this.day_list.length; + var day_width = (Math.min(jQuery(this.getInstanceManager().DOMContainer).width(), this.days.width()) / daycols_needed); + if (!day_width || !this.day_list) { + // Hidden on another tab, or no days for some reason + var dim = egw.getHiddenDimensions(this.days, false); + day_width = (dim.w / Math.max(daycols_needed, 1)); + } + // Create any needed widgets - otherwise, we'll just recycle + // Add any needed day widgets (now showing more days) + var add_index = 0; + var before = true; + while (daycols_needed > this.day_widgets.length) { + var existing_index = this.day_widgets[add_index] && !this.daily_owner ? + this.day_list.indexOf(this.day_widgets[add_index].options.date) : + -1; + before = existing_index > add_index; + var day = et2_createWidget('calendar-daycol', { + owner: this.options.owner, + width: (before ? 0 : day_width) + "px" + }, this); + if (this.isInTree()) { + day.doLoadingFinished(); + } + if (existing_index != -1 && parseInt(this.day_list[add_index]) < parseInt(this.day_list[existing_index])) { + this.day_widgets.unshift(day); + jQuery(this.getDOMNode(day)).prepend(day.getDOMNode(day)); + } + else { + this.day_widgets.push(day); + } + add_index++; + } + // Remove any extra day widgets (now showing less) + var delete_index = this.day_widgets.length - 1; + before = false; + while (this.day_widgets.length > daycols_needed) { + // If we're going down to an existing one, just keep it for cool CSS animation + while (delete_index > 1 && this.day_list.indexOf(this.day_widgets[delete_index].options.date) > -1) { + delete_index--; + before = true; + } + if (delete_index < 0) + delete_index = 0; + // Widgets that are before our date shrink, after just get pushed out + if (before) { + this.day_widgets[delete_index].set_width('0px'); + } + this.day_widgets[delete_index].div.hide(); + this.day_widgets[delete_index].header.hide(); + this.day_widgets[delete_index].destroy(); + this.day_widgets.splice(delete_index--, 1); + } + this.set_header_classes(); + // Create / update day widgets with dates and data + for (var i = 0; i < this.day_widgets.length; i++) { + day = this.day_widgets[i]; + // Position + day.set_left((day_width * i) + 'px'); + day.title.removeClass('blue_title'); + if (this.daily_owner) { + // Each 'day' is the same date, different user + day.set_id(this.day_list[0] + '-' + this.options.owner[i]); + day.set_date(this.day_list[0], false); + day.set_owner(this.options.owner[i]); + day.set_label(this._get_owner_name(this.options.owner[i])); + day.title.addClass('blue_title'); + } + else { + // Show user name in day header even if only one + if (this.day_list.length === 1) { + day.set_label(this._get_owner_name(this.options.owner)); + day.title.addClass('blue_title'); + } + else { + // Go back to self-calculated date by clearing the label + day.set_label(''); + } + day.set_id(this.day_list[i]); + day.set_date(this.day_list[i], this.value[this.day_list[i]] || false); + day.set_owner(this.options.owner); + } + day.set_width(day_width + 'px'); + } + // Adjust and scroll to start of day + this.resizeTimes(); + // Don't hold on to value any longer, use the data cache for best info + this.value = {}; + if (this.daily_owner) { + this.set_label(''); + } + // Handle not fully visible elements + this._scroll(); + // TODO: Figure out how to do this with detached nodes + /* + var nodes = this.day_col.getDetachedNodes(); + var supportedAttrs = []; + this.day_col.getDetachedAttributes(supportedAttrs); + supportedAttrs.push("id"); + + for(var i = 0; i < day_count; i++) + { + this.day_col.setDetachedAttributes(nodes.clone(),) + } + */ + }; + /** + * Set header classes + * + */ + et2_calendar_timegrid.prototype.set_header_classes = function () { + var day; + for (var i = 0; i < this.day_widgets.length; i++) { + day = this.day_widgets[i]; + // Classes + if (app.calendar && app.calendar.state && + this.day_list[i] && parseInt(this.day_list[i].substr(4, 2)) !== new Date(app.calendar.state.date).getUTCMonth() + 1) { + day.set_class('calendar_differentMonth'); + } + else { + day.set_class(''); + } + } + }; + /** + * Update UI while scrolling within the selected time + * + * Toggles out of view indicators and adjusts not visible headers + * @param {Event} event Scroll event + */ + et2_calendar_timegrid.prototype._scroll = function (event) { + if (!this.day_widgets) + return; + // Loop through days, let them deal with it + for (var day = 0; day < this.day_widgets.length; day++) { + this.day_widgets[day]._out_of_view(); + } + }; + /** + * Calculate a list of days between start and end date, skipping weekends if + * desired. + * + * @param {Date|string} start_date Date that et2_date widget can understand + * @param {Date|string} end_date Date that et2_date widget can understand + * @param {boolean} show_weekend If not showing weekend, Saturday and Sunday + * will not be in the returned list. + * + * @returns {string[]} List of days in Ymd format + */ + et2_calendar_timegrid.prototype._calculate_day_list = function (start_date, end_date, show_weekend) { + var day_list = []; + this.date_helper.set_value(end_date); + var end = this.date_helper.date.getTime(); + var i = 1; + this.date_helper.set_value(new Date(start_date)); + do { + if (show_weekend || !show_weekend && [0, 6].indexOf(this.date_helper.date.getUTCDay()) === -1 || end_date === start_date) { + day_list.push('' + this.date_helper.get_year() + sprintf('%02d', this.date_helper.get_month()) + sprintf('%02d', this.date_helper.get_date())); + } + this.date_helper.set_date(this.date_helper.get_date() + 1); + } + // Limit it to 14 days to avoid infinite loops in case something is mis-set, + // though the limit is more based on how wide the screen is + while (end >= this.date_helper.date.getTime() && i++ <= 14); + return day_list; + }; + /** + * Link the actions to the DOM nodes / widget bits. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + */ + et2_calendar_timegrid.prototype._link_actions = function (actions) { + // Get the parent? Might be a grid row, might not. Either way, it is + // just a container with no valid actions + var objectManager = egw_getObjectManager(this.getInstanceManager().app, true, 1); + objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId, 2) || objectManager; + var parent = objectManager.getObjectById(this.id, 1) || objectManager.getObjectById(this.getParent().id, 1) || objectManager; + if (!parent) { + debugger; + egw.debug('error', 'No parent objectManager found'); + return; + } + // This binds into the egw action system. Most user interactions (drag to move, resize) + // are handled internally using jQuery directly. + var widget_object = this._actionObject || parent.getObjectById(this.id); + var aoi = new et2_core_DOMWidget_1.et2_action_object_impl(this, this.getDOMNode(this)).getAOI(); + for (var i = 0; i < parent.children.length; i++) { + var parent_finder = jQuery(parent.children[i].iface.doGetDOMNode()).find(this.div); + if (parent_finder.length > 0) { + parent = parent.children[i]; + break; + } + } + // Determine if we allow a dropped event to use the invite/change actions + var _invite_enabled = function (action, event, target) { + var event = event.iface.getWidget(); + var timegrid = target.iface.getWidget() || false; + if (event === timegrid || !event || !timegrid || + !event.options || !event.options.value.participants || !timegrid.options.owner) { + return false; + } + var owner_match = false; + var own_timegrid = event.getParent().getParent() === timegrid && !timegrid.daily_owner; + for (var id in event.options.value.participants) { + if (!timegrid.daily_owner) { + if (timegrid.options.owner === id || + timegrid.options.owner.indexOf && + timegrid.options.owner.indexOf(id) >= 0) { + owner_match = true; + } + } + else { + timegrid.iterateOver(function (col) { + // Check scroll section or header section + if (col.div.has(timegrid.gridHover).length || col.header.has(timegrid.gridHover).length) { + owner_match = owner_match || col.options.owner.indexOf(id) !== -1; + own_timegrid = (col === event.getParent()); + } + }, this, et2_calendar_daycol); + } + } + var enabled = !owner_match && + // Not inside its own timegrid + !own_timegrid; + widget_object.getActionLink('invite').enabled = enabled; + widget_object.getActionLink('change_participant').enabled = enabled; + // If invite or change participant are enabled, drag is not + widget_object.getActionLink('egw_link_drop').enabled = !enabled; + }; + aoi.doTriggerEvent = function (_event, _data) { + // Determine target node + var event = _data.event || false; + if (!event) + return; + if (_data.ui.draggable.hasClass('rowNoEdit')) + return; + /* + We have to handle the drop in the normal event stream instead of waiting + for the egwAction system so we can get the helper, and destination + */ + if (event.type === 'drop') { + var dropEnd = false; + var helper = jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper)[0]; + if (helper && helper.dropEnd && helper.dropEnd.length >= 1) + if (typeof this.dropEnd !== 'undefined' && this.dropEnd.length >= 1) { + dropEnd = helper.dropEnd[0].dataset || false; + } + this.getWidget()._event_drop.call(jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper)[0], this.getWidget(), event, _data.ui, dropEnd); + } + var drag_listener = function (_event, ui) { + aoi.getWidget()._drag_helper(jQuery('.calendar_d-n-d_timeCounter', ui.helper)[0], ui.helper[0], 0); + if (aoi.getWidget().daily_owner) { + _invite_enabled(widget_object.getActionLink('invite').actionObj, event, widget_object); + } + }; + var time = jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper); + switch (_event) { + // Triggered once, when something is dragged into the timegrid's div + case EGW_AI_DRAG_OVER: + // Listen to the drag and update the helper with the time + // This part lets us drag between different timegrids + _data.ui.draggable.on('drag.et2_timegrid' + widget_object.id, drag_listener); + _data.ui.draggable.on('dragend.et2_timegrid' + widget_object.id, function () { + _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); + }); + // Remove formatting for out-of-view events (full day non-blocking) + jQuery('.calendar_calEventHeader', _data.ui.helper).css('top', ''); + jQuery('.calendar_calEventBody', _data.ui.helper).css('padding-top', ''); + // Disable invite / change actions for same calendar or already participant + var event = _data.ui.draggable.data('selected')[0]; + if (!event || event.id && event.id.indexOf('calendar') !== 0) { + event = false; + } + if (event) { + _invite_enabled(widget_object.getActionLink('invite').actionObj, event, widget_object); + } + if (time.length) { + // The out will trigger after the over, so we count + time.data('count', time.data('count') + 1); + } + else { + _data.ui.helper.prepend('
'); + } + break; + // Triggered once, when something is dragged out of the timegrid + case EGW_AI_DRAG_OUT: + // Stop listening + _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); + // Remove highlighted time square + var timegrid = aoi.getWidget(); + timegrid.gridHover.hide(); + timegrid.scrolling.scrollTop(timegrid._top_time); + // Out triggers after the over, count to not accidentally remove + time.data('count', time.data('count') - 1); + if (time.length && time.data('count') <= 0) { + time.remove(); + } + break; + } + }; + if (widget_object == null) { + // Add a new container to the object manager which will hold the widget + // objects + widget_object = parent.insertObject(false, new egwActionObject(this.id, parent, aoi, this._actionManager || parent.manager.getActionById(this.id) || parent.manager)); + } + else { + widget_object.setAOI(aoi); + } + this._actionObject = widget_object; + // Delete all old objects + widget_object.clear(); + widget_object.unregisterActions(); + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + var action_links = this._get_action_links(actions); + this._init_links_dnd(widget_object.manager, action_links); + widget_object.updateActionLinks(action_links); + }; + /** + * Automatically add dnd support for linking + * + * @param {type} mgr + * @param {type} actionLinks + */ + et2_calendar_timegrid.prototype._init_links_dnd = function (mgr, actionLinks) { + if (this.options.readonly) + return; + var self = this; + var drop_link = mgr.getActionById('egw_link_drop'); + var drop_change_participant = mgr.getActionById('change_participant'); + var drop_invite = mgr.getActionById('invite'); + var drag_action = mgr.getActionById('egw_link_drag'); + // Check if this app supports linking + if (!egw.link_get_registry(this.dataStorePrefix, 'query') || + egw.link_get_registry(this.dataStorePrefix, 'title')) { + if (drop_link) { + drop_link.remove(); + if (actionLinks.indexOf(drop_link.id) >= 0) { + actionLinks.splice(actionLinks.indexOf(drop_link.id), 1); + } + } + if (drag_action) { + drag_action.remove(); + if (actionLinks.indexOf(drag_action.id) >= 0) { + actionLinks.splice(actionLinks.indexOf(drag_action.id), 1); + } + } + return; + } + // Don't re-add + if (drop_link == null) { + // Create the drop action that links entries + drop_link = mgr.addAction('drop', 'egw_link_drop', egw.lang('Create link'), egw.image('link'), function (action, source, target) { + // Extract link IDs + var links = []; + var id = ''; + for (var i = 0; i < source.length; i++) { + // Check for no ID (invalid) or same manager (dragging an event) + if (!source[i].id) + continue; + if (source[i].manager === target.manager) { + // Find the timegrid, could have dropped on an event + var timegrid = target.iface.getWidget(); + while (target.parent && timegrid.instanceOf && !timegrid.instanceOf(et2_calendar_timegrid)) { + target = target.parent; + timegrid = target.iface.getWidget(); + } + if (timegrid && timegrid._drop_data) { + timegrid._event_drop.call(source[i].iface.getDOMNode(), timegrid, null, action.ui, timegrid._drop_data); + } + timegrid._drop_data = false; + // Ok, stop. + return false; + } + id = source[i].id.split('::'); + links.push({ app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1] }); + } + if (links.length && target && target.iface.getWidget() && target.iface.getWidget().instanceOf(et2_calendar_event)) { + // Link the entries + egw.json(self.egw().getAppName() + ".etemplate_widget_link.ajax_link.etemplate", target.id.split('::').concat([links]), function (result) { + if (result) { + this.egw().message('Linked'); + } + }, self, true, self).sendRequest(); + } + else if (links.length) { + // Get date and time + var params = jQuery.extend({}, jQuery('.drop-hover[data-date]', target.iface.getDOMNode())[0].dataset || {}); + // Add link IDs + var app_registry = egw.link_get_registry('calendar'); + params[app_registry.add_app] = []; + params[app_registry.add_id] = []; + for (var n in links) { + params[app_registry.add_app].push(links[n].app); + params[app_registry.add_id].push(links[n].id); + } + app.calendar.add(params); + } + }, true); + drop_link.acceptedTypes = ['default', 'link']; + drop_link.hideOnDisabled = true; + // Create the drop action for moving events between calendars + var invite_action = function (action, source, target) { + // Extract link IDs + var links = []; + var id = ''; + for (var i = 0; i < source.length; i++) { + // Check for no ID (invalid) or same manager (dragging an event) + if (!source[i].id) + continue; + if (source[i].manager === target.manager) { + // Find the timegrid, could have dropped on an event + var timegrid = target.iface.getWidget(); + while (target.parent && timegrid.instanceOf && !timegrid.instanceOf(et2_calendar_timegrid)) { + target = target.parent; + timegrid = target.iface.getWidget(); + } + // Leave the helper there until the update is done + var loading = action.ui.helper.clone(true).appendTo(jQuery('body')); + // and add a loading icon so user knows something is happening + if (jQuery('.calendar_timeDemo', loading).length == 0) { + jQuery('.calendar_calEventHeader', loading).addClass('loading'); + } + else { + jQuery('.calendar_timeDemo', loading).after('
'); + } + var event_data = egw.dataGetUIDdata(source[i].id).data; + et2_calendar_event.recur_prompt(event_data, function (button_id) { + if (button_id === 'cancel' || !button_id) { + return; + } + var add_owner = jQuery.extend([], timegrid.options.owner); + if (timegrid.daily_owner) { + timegrid.iterateOver(function (col) { + if (col.div.has(timegrid.gridHover).length || col.header.has(timegrid.gridHover).length) { + add_owner = col.options.owner; + } + }, this, et2_calendar_daycol); + } + egw().json('calendar.calendar_uiforms.ajax_invite', [ + button_id === 'series' ? event_data.id : event_data.app_id, + add_owner, + action.id === 'change_participant' ? + jQuery.extend([], source[i].iface.getWidget().getParent().options.owner) : + [] + ], function () { loading.remove(); }).sendRequest(true); + }); + // Ok, stop. + return false; + } + } + }; + drop_change_participant = mgr.addAction('drop', 'change_participant', egw.lang('Move to'), egw.image('participant'), invite_action, true); + drop_change_participant.acceptedTypes = ['calendar']; + drop_change_participant.hideOnDisabled = true; + drop_invite = mgr.addAction('drop', 'invite', egw.lang('Invite'), egw.image('participant'), invite_action, true); + drop_invite.acceptedTypes = ['calendar']; + drop_invite.hideOnDisabled = true; + } + if (actionLinks.indexOf(drop_link.id) < 0) { + actionLinks.push(drop_link.id); + } + actionLinks.push(drop_invite.id); + actionLinks.push(drop_change_participant.id); + // Don't re-add + if (drag_action == null) { + // Create drag action that allows linking + drag_action = mgr.addAction('drag', 'egw_link_drag', egw.lang('link'), 'link', function (action, selected) { + // Drag helper - list titles. + // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links + // TODO: Need to decide if we need to create a customized helper interface for links anyway + //return helper; + return null; + }, true); + } + // The timegrid itself is not draggable, so don't add a link. + // The action is there for the children (events) to use + if (false && actionLinks.indexOf(drag_action.id) < 0) { + actionLinks.push(drag_action.id); + } + drag_action.set_dragType(['link', 'calendar']); + }; + /** + * Get all action-links / id's of 1.-level actions from a given action object + * + * Here we are only interested in drop events. + * + * @param actions + * @returns {Array} + */ + et2_calendar_timegrid.prototype._get_action_links = function (actions) { + var action_links = []; + // TODO: determine which actions are allowed without an action (empty actions) + for (var i in actions) { + var action = actions[i]; + if (action.type == 'drop') { + action_links.push(typeof action.id != 'undefined' ? action.id : i); + } + } + return action_links; + }; + /** + * Provide specific data to be displayed. + * This is a way to set start and end dates, owner and event data in one call. + * + * Events will be retrieved automatically from the egw.data cache, so there + * is no great need to provide them. + * + * @param {Object[]} events Array of events, indexed by date in Ymd format: + * { + * 20150501: [...], + * 20150502: [...] + * } + * Days should be in order. + * {string|number|Date} events.start_date - New start date + * {string|number|Date} events.end_date - New end date + * {number|number[]|string|string[]} event.owner - Owner ID, which can + * be an account ID, a resource ID (as defined in calendar_bo, not + * necessarily an entry from the resource app), or a list containing a + * combination of both. + */ + et2_calendar_timegrid.prototype.set_value = function (events) { + if (typeof events !== 'object') + return false; + var use_days_sent = true; + if (events.start_date) { + use_days_sent = false; + } + if (events.end_date) { + use_days_sent = false; + } + _super.prototype.set_value.call(this, events); + if (use_days_sent) { + var day_list = Object.keys(events); + if (day_list.length) { + this.set_start_date(day_list[0]); + this.set_end_date(day_list[day_list.length - 1]); + } + // Sub widgets actually get their own data from egw.data, so we'll + // stick it there + var consolidated = et2_widget_view_1.et2_calendar_view.is_consolidated(this.options.owner, this.day_list.length == 1 ? 'day' : 'week'); + for (var day in events) { + var day_list_1 = []; + for (var i = 0; i < events[day].length; i++) { + day_list_1.push(events[day][i].row_id); + egw.dataStoreUID('calendar::' + events[day][i].row_id, events[day][i]); + } + // Might be split by user, so we have to check that too + for (var i = 0; i < this.options.owner.length; i++) { + var owner = consolidated ? this.options.owner : this.options.owner[i]; + var day_id = CalendarApp._daywise_cache_id(day, owner); + egw.dataStoreUID(day_id, day_list_1); + if (consolidated) + break; + } + } + } + // Reset and calculate instead of just use the keys so we can get the weekend preference + this.day_list = []; + // None of the above changed anything, hide the loader + if (!this.update_timer) { + window.setTimeout(jQuery.proxy(function () { this.loader.hide(); }, this), 200); + } + }; + /** + * Set which user owns this. Owner is passed along to the individual + * days. + * + * @param {number|number[]} _owner Account ID + * @returns {undefined} + */ + et2_calendar_timegrid.prototype.set_owner = function (_owner) { + var old = this.options.owner || 0; + _super.prototype.set_owner.call(this, _owner); + this.owner.set_label(''); + this.div.removeClass('calendar_TimeGridNoLabel'); + // Check to see if it's our own calendar, with just us showing + if (typeof _owner == 'object' && _owner.length == 1) { + var rowCount = 0; + this.getParent().iterateOver(function (widget) { + if (!widget.disabled) + rowCount++; + }, this, et2_calendar_timegrid); + // Just us, show week number + if (rowCount == 1 && _owner.length == 1 && _owner[0] == egw.user('account_id') || rowCount != 1) + _owner = false; + } + var day_count = this.day_list.length ? this.day_list.length : + this._calculate_day_list(this.options.start_date, this.options.end_date, this.options.show_weekend).length; + // @ts-ignore + if (typeof _owner == 'string' && isNaN(_owner)) { + this.set_label(''); + this.owner.set_value(this._get_owner_name(_owner)); + // Label is empty, but give extra space for the owner name + this.div.removeClass('calendar_TimeGridNoLabel'); + } + else if (!_owner || typeof _owner == 'object' && _owner.length > 1 || + // Single owner, single day + _owner.length === 1 && day_count === 1) { + // Don't show owners if more than one, show week number + this.owner.set_value(''); + if (this.options.start_date) { + this.set_label(egw.lang('wk') + ' ' + + (app.calendar ? app.calendar.date.week_number(this.options.start_date) : '')); + } + } + else { + this.owner.options.application = 'api-accounts'; + this.owner.set_value(this._get_owner_name(_owner)); + this.set_label(''); + jQuery(this.getDOMNode(this.owner)).prepend(this.owner.getDOMNode()); + } + if (this.isAttached() && (typeof old === "number" && typeof _owner === "number" && old !== this.options.owner || + // Array of ids will not compare as equal + ((typeof old === 'object' || typeof _owner === 'object') && old.toString() !== _owner.toString()) || + // Strings + typeof old === 'string' && '' + old !== '' + this.options.owner)) { + this.invalidate(true); + } + }; + /** + * Set a label for this week + * + * May conflict with owner, which is displayed when there's only one owner. + * + * @param {string} label + */ + et2_calendar_timegrid.prototype.set_label = function (label) { + this.options.label = label; + this._labelContainer.html(label); + this.gridHeader.prepend(this._labelContainer); + // If it's a short label (eg week number), don't give it an extra line + // but is empty, but give extra space for a single owner name + this.div.toggleClass('calendar_TimeGridNoLabel', label.trim().length > 0 && label.trim().length <= 6 || + this.options.owner.length > 1); + }; + /** + * Set how big the time divisions are + * + * Setting granularity to 0 will remove the time divisions and display + * each days events in a list style. This 'gridlist' is not to be confused + * with the list view, which uses a nextmatch. + * + * @param {number} minutes + */ + et2_calendar_timegrid.prototype.set_granularity = function (minutes) { + // Avoid < 0 + minutes = Math.max(0, minutes); + if (this.options.granularity !== minutes) { + if (this.options.granularity === 0 || minutes === 0) { + this.options.granularity = minutes; + // Need to re-do a bunch to make sure this is propagated + this.invalidate(); + } + else { + this.options.granularity = minutes; + this._drawTimes(); + } + } + else if (!this.update_timer) { + this.resizeTimes(); + } + }; + /** + * Turn on or off the visibility of weekends + * + * @param {boolean} weekends + */ + et2_calendar_timegrid.prototype.set_show_weekend = function (weekends) { + weekends = weekends ? true : false; + if (this.options.show_weekend !== weekends) { + this.options.show_weekend = weekends; + if (this.isAttached()) { + this.invalidate(); + } + } + }; + /** + * Call change handler, if set + */ + et2_calendar_timegrid.prototype.change = function () { + if (this.onchange) { + if (typeof this.onchange == 'function') { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.push(this); + return this.onchange.apply(this, args); + } + else { + return (et2_compileLegacyJS(this.options.onchange, this, _node))(); + } + } + }; + /** + * Call event change handler, if set + * + * @param {type} event + * @param {type} dom_node + */ + et2_calendar_timegrid.prototype.event_change = function (event, dom_node) { + if (this.onevent_change) { + var event_data = this._get_event_info(dom_node); + var event_widget = this.getWidgetById(event_data.widget_id); + et2_calendar_event.recur_prompt(event_data, jQuery.proxy(function (button_id, event_data) { + // No need to continue + if (button_id === 'cancel') + return false; + if (typeof this.onevent_change == 'function') { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(event_widget) == -1) + args.push(event_widget); + // Put button ID in event + event.button_id = button_id; + return this.onevent_change.apply(this, [event, event_widget, button_id]); + } + else { + return (et2_compileLegacyJS(this.options.onevent_change, event_widget, dom_node))(); + } + }, this)); + } + return false; + }; + et2_calendar_timegrid.prototype.get_granularity = function () { + // get option, or user's preference + if (typeof this.options.granularity === 'undefined') { + this.options.granularity = egw.preference('interval', 'calendar') || 30; + } + return parseInt(this.options.granularity); + }; + /** + * Click handler calling custom handler set via onclick attribute to this.onclick + * + * This also handles all its own actions, including navigation. If there is + * an event associated with the click, it will be found and passed to the + * onclick function. + * + * @param {Event} _ev + * @returns {boolean} Continue processing event (true) or stop (false) + */ + et2_calendar_timegrid.prototype.click = function (_ev) { + var result = true; + if (this.options.readonly) + return; + // Drag to create in progress + if (this.drag_create.start !== null) + return; + // Is this click in the event stuff, or in the header? + if (_ev.target.dataset.id || jQuery(_ev.target).parents('.calendar_calEvent').length) { + // Event came from inside, maybe a calendar event + var event = this._get_event_info(_ev.originalEvent.target); + if (typeof this.onclick == 'function') { + // Make sure function gets a reference to the widget, splice it in as 2. argument if not + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.splice(1, 0, this); + result = this.onclick.apply(this, args); + } + var event_node = jQuery(event.event_node); + if (event.id && result && !this.disabled && !this.options.readonly && + // Permissions - opening will fail if we try + event_node && !(event_node.hasClass('rowNoView'))) { + if (event.widget_id && this.getWidgetById(event.widget_id)) { + this.getWidgetById(event.widget_id).recur_prompt(); + } + else { + et2_calendar_event.recur_prompt(event); + } + return false; + } + return result; + } + else if (this.gridHeader.is(_ev.target) && _ev.target.dataset || + this._labelContainer.is(_ev.target) && this.gridHeader[0].dataset) { + app.calendar.update_state(jQuery.extend({ view: 'week' }, this._labelContainer.is(_ev.target) ? + this.gridHeader[0].dataset : + _ev.target.dataset)); + } + else if (this.options.owner.length === 1 && jQuery(this.owner.getDOMNode()).is(_ev.target)) { + // Click on the owner in header, show just that owner + app.calendar.update_state({ owner: this.options.owner }); + } + else if (this.dayHeader.has(_ev.target).length) { + // Click on a day header - let day deal with it + // First child is a selectAccount + for (var i = 1; i < this._children.length; i++) { + if (this._children[i].header && (this._children[i].header.has(_ev.target).length || this._children[i].header.is(_ev.target))) { + return this._children[i].click(_ev); + } + } + } + // No time grid, click on a day + else if (this.options.granularity === 0 && + (jQuery(_ev.target).hasClass('event_wrapper') || jQuery(_ev.target).hasClass('.calendar_calDayCol'))) { + // Default handler to open a new event at the selected time + var target = jQuery(_ev.target).hasClass('event_wrapper') ? _ev.target.parentNode : _ev.target; + var options = { + date: target.dataset.date || this.options.date, + hour: target.dataset.hour || this._parent.options.day_start, + minute: target.dataset.minute || 0, + owner: this.options.owner + }; + app.calendar.add(options); + return false; + } + }; + /** + * Mousedown handler to support drag to create + * + * @param {jQuery.Event} event + */ + et2_calendar_timegrid.prototype._mouse_down = function (event) { + if (event.which !== 1) + return; + if (this.options.readonly) + return; + var start = jQuery.extend({}, this.gridHover[0].dataset); + if (start.date) { + // Set parent for event + if (this.daily_owner) { + // Each 'day' is the same date, different user + // Find the correct row so we know the parent + var col = event.target.closest('.calendar_calDayCol'); + for (var i = 0; i < this._children.length && col; i++) { + if (this._children[i].node === col) { + this.drag_create.parent = this._children[i]; + break; + } + } + } + else { + this.drag_create.parent = this.getWidgetById(start.date); + } + // Format date + this.date_helper.set_year(start.date.substring(0, 4)); + this.date_helper.set_month(start.date.substring(4, 6)); + this.date_helper.set_date(start.date.substring(6, 8)); + if (start.hour) { + this.date_helper.set_hours(start.hour); + } + if (start.minute) { + this.date_helper.set_minutes(start.minute); + } + start.date = this.date_helper.get_value(); + this.gridHover.css('cursor', 'ns-resize'); + // Start update + var timegrid = this; + this.div.on('mousemove.dragcreate', function () { + if (timegrid.drag_create.event && timegrid.drag_create.parent && timegrid.drag_create.end) { + var end = jQuery.extend({}, timegrid.gridHover[0].dataset); + if (end.date) { + timegrid.date_helper.set_year(end.date.substring(0, 4)); + timegrid.date_helper.set_month(end.date.substring(4, 6)); + timegrid.date_helper.set_date(end.date.substring(6, 8)); + if (end.hour) { + timegrid.date_helper.set_hours(end.hour); + } + if (end.minute) { + timegrid.date_helper.set_minutes(end.minute); + } + timegrid.drag_create.end.date = timegrid.date_helper.get_value(); + } + try { + timegrid._drag_update_event(); + } + catch (e) { + timegrid._drag_create_end(); + } + } + }); + } + return this._drag_create_start(start); + }; + /** + * Mouseup handler to support drag to create + * + * @param {jQuery.Event} event + */ + et2_calendar_timegrid.prototype._mouse_up = function (event) { + if (this.options.readonly) + return; + var end = jQuery.extend({}, this.gridHover[0].dataset); + if (end.date) { + this.date_helper.set_year(end.date.substring(0, 4)); + this.date_helper.set_month(end.date.substring(4, 6)); + this.date_helper.set_date(end.date.substring(6, 8)); + if (end.hour) { + this.date_helper.set_hours(end.hour); + } + if (end.minute) { + this.date_helper.set_minutes(end.minute); + } + end.date = this.date_helper.get_value(); + } + this.div.off('mousemove.dragcreate'); + this.gridHover.css('cursor', ''); + return this._drag_create_end(end); + }; + /** + * Get time from position for drag and drop + * + * This does not return an actual time on a clock, but finds the closest + * time node (.calendar_calAddEvent or day column) to the given position. + * + * @param {number} x + * @param {number} y + * @returns {DOMNode[]} time node(s) for the given position + */ + et2_calendar_timegrid.prototype._get_time_from_position = function (x, y) { + x = Math.round(x); + y = Math.round(y); + var path = []; + var day = null; + var time = null; + var node = document.elementFromPoint(x, y); + var $node = jQuery(node); + // Ignore high level & non-time (grid itself, header parent & week label) + if ([this.node, this.gridHeader[0], this._labelContainer[0]].indexOf(node) !== -1 || + // Day labels + this.gridHeader.has(node).length && !$node.hasClass("calendar_calDayColAllDay") && !$node.hasClass('calendar_calDayColHeader')) { + return []; + } + for (var id in this.gridHover[0].dataset) { + delete this.gridHover[0].dataset[id]; + } + if (this.options.granularity == 0) { + this.gridHover.css('height', ''); + } + while (node && node != this.node && node.tagName != 'BODY' && path.length < 10) { + path.push(node); + node.style.display = 'none'; + $node = jQuery(node); + if ($node.hasClass('calendar_calDayColHeader')) { + for (var id in node.dataset) { + this.gridHover[0].dataset[id] = node.dataset[id]; + } + this.gridHover.css({ + position: 'absolute', + top: '', + bottom: '0px', + // Use 100% height if we're hiding the day labels to avoid + // any remaining space from the hidden labels + height: $node.height() > parseInt($node.css('line-height')) ? + $node.css('padding-bottom') : '100%' + }); + day = node; + this.gridHover + .attr('data-non_blocking', 'true'); + break; + } + if ($node.hasClass('calendar_calDayCol')) { + day = node; + this.gridHover + .attr('data-date', day.dataset.date); + } + if ($node.hasClass('calendar_calTimeRowTime')) { + time = node; + this.gridHover + .attr('data-hour', time.dataset.hour) + .attr('data-minute', time.dataset.minute); + break; + } + node = document.elementFromPoint(x, y); + } + for (var i = 0; i < path.length; i++) { + path[i].style.display = ''; + } + if (!day) { + return []; + } + this.gridHover + .show() + .appendTo(day); + if (time) { + this.gridHover + .height(this.rowHeight) + .position({ my: 'left top', at: 'left top', of: time }); + } + this.gridHover.css('left', ''); + return this.gridHover; + }; + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs array to add further attributes to + */ + et2_calendar_timegrid.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push('start_date', 'end_date'); + }; + et2_calendar_timegrid.prototype.getDetachedNodes = function () { + return [this.getDOMNode(this)]; + }; + et2_calendar_timegrid.prototype.setDetachedAttributes = function (_nodes, _values) { + this.div = jQuery(_nodes[0]); + if (_values.start_date) { + this.set_start_date(_values.start_date); + } + if (_values.end_date) { + this.set_end_date(_values.end_date); + } + }; + // Resizable interface + /** + * @param {boolean} [_too_small=null] Force the widget to act as if it was too small + */ + et2_calendar_timegrid.prototype.resize = function (_too_small) { + if (this.disabled || !this.div.is(':visible')) { + return; + } + /* + We expect the timegrid to be in a table with 0 or more other timegrids, + 1 per row. We want each timegrid to be as large as possible, but space + shared equally. Height can't be set to a percentage on the rows, because + that doesn't work. However, if any timegrid is too small (1/2 hour < 1 line + height), we change to showing only the working hours with no vertical + scrollbar. Each week gets as much space as it needs, and all scroll together. + */ + // How many rows? + var rowCount = 0; + this.getParent().iterateOver(function (widget) { + if (!widget.disabled) + rowCount++; + }, this, et2_calendar_timegrid); + // Take the whole tab height, or home portlet + if (this.getInstanceManager().app === 'home') { + var height = jQuery(this.getParent().getDOMNode(this)).parentsUntil('.et2_portlet').last().parent().innerHeight(); + // Allow for portlet header + height -= jQuery('.ui-widget-header', this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true); + } + else { + var height = jQuery(this.getInstanceManager().DOMContainer).parent().innerHeight(); + // Allow for toolbar + height -= jQuery('#calendar-toolbar', this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true); + } + this.options.height = Math.floor(height / rowCount); + // Allow for borders & padding + this.options.height -= 2 * ((this.div.outerWidth(true) - this.div.innerWidth()) + parseInt(this.div.parent().css('padding-top'))); + // Calculate how much space is needed, and + // if too small be bigger + var needed = ((this.day_end - this.day_start) / + (this.options.granularity / 60) * parseInt(this.div.css('line-height'))) + + this.gridHeader.outerHeight(); + var too_small = needed > this.options.height && this.options.granularity != 0; + if (this.getInstanceManager().app === 'home') { + var modify_node = jQuery(this.getParent().getDOMNode(this)).parentsUntil('.et2_portlet').last(); + } + else { + var modify_node = jQuery(this.getInstanceManager().DOMContainer); + } + modify_node + .css({ + 'overflow-y': too_small || _too_small ? 'auto' : 'hidden', + 'overflow-x': 'hidden', + 'height': too_small || _too_small ? height : '100%' + }); + if (too_small || _too_small) { + this.options.height = Math.max(this.options.height, needed); + // Set all others to match + if (!_too_small && rowCount > 1 && this.getParent()) { + window.setTimeout(jQuery.proxy(function () { + if (!this._parent) + return; + this._parent.iterateOver(function (widget) { + if (!widget.disabled) + widget.resize(true); + }, this, et2_calendar_timegrid); + }, this), 1); + return; + } + this.div.addClass('calendar_calTimeGridFixed'); + } + else { + this.div.removeClass('calendar_calTimeGridFixed'); + } + this.div.css('height', this.options.height); + // Re-do time grid + if (!this.update_timer) { + this.resizeTimes(); + } + // Try to resize width, though animations cause problems + var total_width = modify_node.parent().innerWidth() - this.days.position().left; + // Space for todos, if there + total_width -= jQuery(this.getInstanceManager().DOMContainer).siblings().has(':visible').not('#calendar-toolbar').outerWidth(); + var day_width = (total_width > 0 ? total_width : modify_node.width()) / this.day_widgets.length; + // update day widgets + for (var i = 0; i < this.day_widgets.length; i++) { + var day = this.day_widgets[i]; + // Position + day.set_left((day_width * i) + 'px'); + day.set_width(day_width + 'px'); + } + }; + /** + * Set up for printing + * + * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up + * (waiting for data) + */ + et2_calendar_timegrid.prototype.beforePrint = function () { + if (this.disabled || !this.div.is(':visible')) { + return; + } + var height_check = this.div.height(); + this.div.css('max-height', '17cm'); + if (this.div.height() != height_check) { + this.div.height('17cm'); + this._resizeTimes(); + } + // update day widgets, if not on single day view + // + // TODO: Find out why don't we update single day view + // Let the single day view participate in print calculation. + if (this.day_widgets.length > 0) { + var day_width = (100 / this.day_widgets.length); + for (var i = 0; i < this.day_widgets.length; i++) { + var day = this.day_widgets[i]; + // Position + day.set_left((i * day_width) + '%'); + day.set_width(day_width + '%'); + // For some reason the column's method does not set it correctly in Chrome + day.header[0].style.width = day_width + '%'; + } + } + // Stop Firefox from scrolling the day to the top - this would break printing in Chrome + if (navigator.userAgent.match(/(firefox|safari|iceweasel)/i) && !navigator.userAgent.match(/chrome/i)) { + var height = this.scrolling.scrollTop() + this.scrolling.height(); + this.scrolling + // Disable scroll event, or it will recalculate out of view events + .off('scroll') + // Explicitly transform to the correct place + .css({ + 'transform': 'translateY(-' + this.scrolling.scrollTop() + 'px)', + 'margin-bottom': '-' + this.scrolling.scrollTop() + 'px', + 'height': height + 'px' + }); + this.div.css({ 'height': '', 'max-height': '' }); + } + }; + /** + * Reset after printing + */ + et2_calendar_timegrid.prototype.afterPrint = function () { + this.div.css('maxHeight', ''); + this.scrolling.children().css({ 'transform': '', 'overflow': '' }); + this.div.height(this.options.height); + if (navigator.userAgent.match(/(firefox|safari|iceweasel)/i) && !navigator.userAgent.match(/chrome/i)) { + this._resizeTimes(); + this.scrolling + // Re-enable out-of-view formatting on scroll + .on('scroll', jQuery.proxy(this._scroll, this)) + // Remove translation + .css({ 'transform': '', 'margin-bottom': '' }); + } + }; + et2_calendar_timegrid._attributes = { + value: { + type: "any", + description: "An array of events, indexed by date (Ymd format)." + }, + day_start: { + name: "Day start time", + type: "string", + default: parseInt('' + egw.preference('workdaystarts', 'calendar')) || 9, + description: "Work day start time. If unset, this will default to the current user's preference" + }, + day_end: { + name: "Day end time", + type: "string", + default: parseInt('' + egw.preference('workdayends', 'calendar')) || 17, + description: "Work day end time. If unset, this will default to the current user's preference" + }, + show_weekend: { + name: "Weekends", + type: "boolean", + // @ts-ignore + default: egw.preference('days_in_weekview', 'calendar') != 5, + description: "Display weekends. The date range should still include them for proper scrolling, but they just won't be shown." + }, + granularity: { + name: "Granularity", + type: "integer", + default: parseInt('' + egw.preference('interval', 'calendar')) || 30, + description: "How many minutes per row, or 0 to display events as a list" + }, + "onchange": { + "name": "onchange", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the date range changes." + }, + "onevent_change": { + "name": "onevent_change", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when an event changes." + }, + height: { + "default": '100%' + } + }; + return et2_calendar_timegrid; +}(et2_widget_view_1.et2_calendar_view)); +exports.et2_calendar_timegrid = et2_calendar_timegrid; +et2_core_widget_1.et2_register_widget(et2_calendar_timegrid, ["calendar-timegrid"]); +//# sourceMappingURL=et2_widget_timegrid.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_view.js b/calendar/js/et2_widget_view.js index da92bc6739..f199c6635d 100644 --- a/calendar/js/et2_widget_view.js +++ b/calendar/js/et2_widget_view.js @@ -1,3 +1,4 @@ +"use strict"; /* * Egroupware * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License @@ -7,11 +8,22 @@ * @author Nathan Gray * @version $Id$ */ - -/*egw:uses - /etemplate/js/et2_core_valueWidget; -*/ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +var et2_core_valueWidget_1 = require("../../api/js/etemplate/et2_core_valueWidget"); +var et2_core_inheritance_1 = require("../../api/js/etemplate/et2_core_inheritance"); /** * Parent class for the various calendar views to reduce copied code * @@ -21,660 +33,546 @@ * * @augments et2_valueWidget */ -var et2_calendar_view = (function(){ "use strict"; return et2_valueWidget.extend( -{ - createNamespace: true, - - attributes: { - owner: { - name: "Owner", - type: "any", // Integer, or array of integers, or string like r13 (resources, addressbook) - default: [egw.user('account_id')], - description: "Account ID number of the calendar owner, if not the current user" - }, - start_date: { - name: "Start date", - type: "any" - }, - end_date: { - name: "End date", - type: "any" - } - }, - - /** - * Constructor - * - * @memberOf et2_calendar_view - * @constructor - */ - init: function init() { - this._super.apply(this, arguments); - - // Used for its date calculations - this.date_helper = et2_createWidget('date-time',{},null); - this.date_helper.loadingFinished(); - - this.loader = jQuery('
'); - this.update_timer = null; - - // Used to support dragging on empty space to create an event - this.drag_create = { - start: null, - end: null, - parent: null, - event: null - }; - }, - - destroy: function destroy() { - this._super.apply(this, arguments); - - // date_helper has no parent, so we must explicitly remove it - this.date_helper.destroy(); - this.date_helper = null; - - // Stop the invalidate timer - if(this.update_timer) - { - window.clearTimeout(this.update_timer); - } - }, - - doLoadingFinished: function() { - this._super.apply(this, arguments); - this.loader.hide(0).prependTo(this.div); - if(this.options.owner) this.set_owner(this.options.owner); - }, - - /** - * Something changed, and the view need to be re-drawn. We wait a bit to - * avoid re-drawing twice if start and end date both changed, then recreate - * as needed. - * - * @param {boolean} [trigger_event=false] Trigger an event once things are done. - * Waiting until invalidate completes prevents 2 updates when changing the date range. - * @returns {undefined} - * - * @memberOf et2_calendar_view - */ - invalidate: function invalidate(trigger_event) { - // If this wasn't a stub, we'd set this.update_timer - }, - - /** - * Returns the current start date - * - * @returns {Date} - * - * @memberOf et2_calendar_view - */ - get_start_date: function get_start_date() { - return new Date(this.options.start_date); - }, - - /** - * Returns the current start date - * - * @returns {Date} - * - * @memberOf et2_calendar_view - */ - get_end_date: function get_end_date() { - return new Date(this.options.end_date); - }, - - /** - * Change the start date - * - * Changing the start date will invalidate the display, and it will be redrawn - * after a timeout. - * - * @param {string|number|Date} new_date New starting date. Strings can be in - * any format understood by et2_widget_date, or Ymd (eg: 20160101). - * @returns {undefined} - * - * @memberOf et2_calendar_view - */ - set_start_date: function set_start_date(new_date) - { - if(!new_date || new_date === null) - { - new_date = new Date(); - } - - // Use date widget's existing functions to deal - if(typeof new_date === "object" || typeof new_date === "string" && new_date.length > 8) - { - this.date_helper.set_value(new_date); - } - else if(typeof new_date === "string") - { - this.date_helper.set_year(new_date.substring(0,4)); - // Avoid overflow into next month, since we re-use date_helper - this.date_helper.set_date(1); - this.date_helper.set_month(new_date.substring(4,6)); - this.date_helper.set_date(new_date.substring(6,8)); - } - - var old_date = this.options.start_date; - this.options.start_date = new Date(this.date_helper.getValue()); - - if(old_date !== this.options.start_date && this.isAttached()) - { - this.invalidate(true); - } - }, - - /** - * Change the end date - * - * Changing the end date will invalidate the display, and it will be redrawn - * after a timeout. - * - * @param {string|number|Date} new_date - New end date. Strings can be in - * any format understood by et2_widget_date, or Ymd (eg: 20160101). - * @returns {undefined} - * - * @memberOf et2_calendar_view - */ - set_end_date: function set_end_date(new_date) - { - if(!new_date || new_date === null) - { - new_date = new Date(); - } - // Use date widget's existing functions to deal - if(typeof new_date === "object" || typeof new_date === "string" && new_date.length > 8) - { - this.date_helper.set_value(new_date); - } - else if(typeof new_date === "string") - { - this.date_helper.set_year(new_date.substring(0,4)); - // Avoid overflow into next month, since we re-use date_helper - this.date_helper.set_date(1); - this.date_helper.set_month(new_date.substring(4,6)); - this.date_helper.set_date(new_date.substring(6,8)); - } - - var old_date = this.options.end_date; - this.options.end_date = new Date(this.date_helper.getValue()); - - if(old_date !== this.options.end_date && this.isAttached()) - { - this.invalidate(true); - } - }, - - /** - * Set which users to display - * - * Changing the owner will invalidate the display, and it will be redrawn - * after a timeout. - * - * @param {number|number[]|string|string[]} _owner - Owner ID, which can - * be an account ID, a resource ID (as defined in calendar_bo, not - * necessarily an entry from the resource app), or a list containing a - * combination of both. - * - * @memberOf et2_calendar_view - */ - set_owner: function set_owner(_owner) - { - var old = this.options.owner; - - // 0 means current user, but that causes problems for comparison, - // so we'll just switch to the actual ID - if(_owner == '0') - { - _owner = [egw.user('account_id')]; - } - if(!jQuery.isArray(_owner)) - { - if(typeof _owner === "string") - { - _owner = _owner.split(','); - } - else - { - _owner = [_owner]; - } - } - else - { - _owner = jQuery.extend([],_owner); - } - this.options.owner = _owner; - if(this.isAttached() && ( - typeof old === "number" && typeof _owner === "number" && old !== this.options.owner || - // Array of ids will not compare as equal - ((typeof old === 'object' || typeof _owner === 'object') && old.toString() !== _owner.toString()) || - // Strings - typeof old === 'string' && ''+old !== ''+this.options.owner - )) - { - this.invalidate(true); - } - }, - - /** - * Provide specific data to be displayed. - * This is a way to set start and end dates, owner and event data in one call. - * - * If events are not provided in the array, - * @param {Object[]} events Array of events, indexed by date in Ymd format: - * { - * 20150501: [...], - * 20150502: [...] - * } - * Days should be in order. - * {string|number|Date} events.start_date - New start date - * {string|number|Date} events.end_date - New end date - * {number|number[]|string|string[]} event.owner - Owner ID, which can - * be an account ID, a resource ID (as defined in calendar_bo, not - * necessarily an entry from the resource app), or a list containing a - * combination of both. - */ - set_value: function set_value(events) - { - if(typeof events !== 'object') return false; - - if(events.length && events.length > 0 || !jQuery.isEmptyObject(events)) - { - this.set_disabled(false); - } - if(events.id) - { - this.set_id(events.id); - delete events.id; - } - if(events.start_date) - { - this.set_start_date(events.start_date); - delete events.start_date; - } - if(events.end_date) - { - this.set_end_date(events.end_date); - delete events.end_date; - } - // set_owner() wants start_date set to get the correct week number - // for the corner label - if(events.owner) - { - this.set_owner(events.owner); - delete events.owner; - } - - this.value = events || {}; - - // None of the above changed anything, hide the loader - if(!this.update_timer) - { - window.setTimeout(jQuery.proxy(function() {this.loader.hide();},this),200); - } - }, - - /** - * Calendar supports many different owner types, including users & resources. - * This translates an ID to a user-friendly name. - * - * @param {string} user - * @returns {string} - * - * @memberOf et2_calendar_view - */ - _get_owner_name: function _get_owner_name(user) { - var label = undefined; - if(parseInt(user) === 0) - { - // 0 means current user - user = egw.user('account_id'); - } - if(et2_calendar_view.owner_name_cache[user]) - { - return et2_calendar_view.owner_name_cache[user]; - } - if (!isNaN(user)) - { - user = parseInt(user); - var accounts = egw.accounts('both'); - for(var j = 0; j < accounts.length; j++) - { - if(accounts[j].value === user) - { - label = accounts[j].label; - break; - } - } - } - if(typeof label === 'undefined') - { - // Not found? Ask the sidebox owner widget (it gets updated) or the original arrayMgr - var options = false - if(app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) - { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); - } - else - { - options = this.getArrayMgr("sel_options").getRoot().getEntry('owner'); - } - if(options && options.find) - { - var found = options.find(function(element) {return element.id == user;}) || {}; - if(found && found.label && found.label !== user) - { - label = found.label; - } - } - if(!label) - { - // No sidebox? Must be in home or sitemgr (no caching) - ask directly - label = '?'; - egw.jsonq('calendar_owner_etemplate_widget::ajax_owner',user,function(data) { - et2_calendar_view.owner_name_cache[user] = data; - this.invalidate(true); - - // Set owner to make sure labels get set - if(this.owner && typeof this.owner.set_value === 'function') - { - this.owner.set_value(data); - } - }.bind(this), this); - } - } - if(label) - { - et2_calendar_view.owner_name_cache[user] = label; - } - return label; - }, - - /** - * Find the event information linked to a given DOM node - * - * @param {HTMLElement} dom_node - It should have something to do with an event - * @returns {Object} - */ - _get_event_info: function _get_event_info(dom_node) - { - // Determine as much relevant info as can be found - var event_node = jQuery(dom_node).closest('[data-id]',this.div)[0]; - var day_node = jQuery(event_node).closest('[data-date]',this.div)[0]; - - var result = jQuery.extend({ - event_node: event_node, - day_node: day_node - }, - event_node ? event_node.dataset : {}, - day_node ? day_node.dataset : {} - ); - - // Widget ID should be the DOM node ID without the event_ prefix - if(event_node && event_node.id) - { - var widget_id = event_node.id || ''; - widget_id = widget_id.split('event_'); - widget_id.shift(); - result.widget_id = 'event_' + widget_id.join(''); - } - return result; - }, - - /** - * Starting (mousedown) handler to support drag to create - * - * Extending classes need to set this.drag_create.parent, which is the - * parent container (child of extending class) that will directly hold the - * event. - * - * @param {String} start Date string (JSON format) - */ - _drag_create_start: function(start) - { - this.drag_create.start = jQuery.extend({},start); - if(!this.drag_create.start.date) - { - this.drag_create.start = null; - } - this.drag_create.end = start; - - // Clear some stuff, if last time did not complete - if(this.drag_create.event) - { - if(this.drag_create.event.destroy) - { - this.drag_create.event.destroy(); - } - this.drag_create.event = null; - } - // Wait a bit before adding an "event", it may be just a click - window.setTimeout(jQuery.proxy(function() { - // Create event - this._drag_create_event(); - }, this), 250); - }, - - /** - * Create or update an event used for feedback while dragging on empty space, - * so user can see something is happening - */ - _drag_create_event: function() - { - if(!this.drag_create.parent || !this.drag_create.start) - { - return; - } - if(!this.drag_create.event) - { - this.date_helper.set_value(this.drag_create.start.date); - var value = jQuery.extend({}, - this.drag_create.start, - this.drag_create.end, - { - start: this.drag_create.start.date, - end: this.drag_create.end && this.drag_create.end.date || this.drag_create.start.date, - date: ""+this.date_helper.get_year()+ - sprintf("%02d",this.date_helper.get_month())+ - sprintf("%02d",this.date_helper.get_date()), - title: '', - description: '', - owner: this.options.owner, - participants: this.options.owner, - app: 'calendar', - whole_day_on_top: this.drag_create.start.whole_day - } - ); - this.drag_create.event = et2_createWidget('calendar-event',{ - id:'event_drag', - value: value - },this.drag_create.parent); - this.drag_create.event._values_check(value); - this.drag_create.event.doLoadingFinished(); - } - - }, - - _drag_update_event: function() - { - if(!this.drag_create.event || !this.drag_create.start || !this.drag_create.end - || !this.drag_create.parent || !this.drag_create.event._type) - { - return; - } - else if (this.drag_create.end) - { - this.drag_create.event.options.value.end = this.drag_create.end.date; - this.drag_create.event._values_check(this.drag_create.event.options.value); - } - this.drag_create.event._update() - this.drag_create.parent.position_event(this.drag_create.event); - }, - - /** - * Ending (mouseup) handler to support drag to create - * - * @param {String} end Date string (JSON format) - */ - _drag_create_end: function(end) - { - this.div.css('cursor',''); - if(typeof end === 'undefined') - { - end = {}; - } - - if(this.drag_create.start && end.date && - JSON.stringify(this.drag_create.start.date) !== JSON.stringify(end.date)) - { - // Drag from start to end, open dialog - var options = { - start: this.drag_create.start.date < end.date ? this.drag_create.start.date : end.date, - end: this.drag_create.start.date < end.date ? end.date : this.drag_create.start.date - }; - - // Whole day needs to go from 00:00 to 23:59 - if(end.whole_day || this.drag_create.start.whole_day) - { - var start = new Date(options.start); - start.setUTCHours(0); - start.setUTCMinutes(0); - options.start = start.toJSON(); - - var end = new Date(options.end); - end.setUTCHours(23); - end.setUTCMinutes(59); - options.end = end.toJSON(); - } - - // Add anything else that was set, but not date - jQuery.extend(options,this.drag_create.start, end); - delete(options.date); - - // Make sure parent is set, if needed - if (this.drag_create.parent && this.drag_create.parent.options.owner !== app.calendar.state.owner && !options.owner) - { - options.owner = this.drag_create.parent.options.owner; - } - - // Remove empties - for(var key in options) - { - if(!options[key]) delete options[key]; - } - app.calendar.add(options, this.drag_create.event); - - // Wait a bit, having these stops the click - window.setTimeout(jQuery.proxy(function() { - this.drag_create.start = null; - this.drag_create.end = null; - this.drag_create.parent = null; - if(this.drag_create.event) - { - this.drag_create.event = null; - } - },this),100); - - return false; - } - - this.drag_create.start = null; - this.drag_create.end = null; - this.drag_create.parent = null; - if(this.drag_create.event) - { - try - { - if(this.drag_create.event.destroy) - { - this.drag_create.event.destroy(); - } - } catch(e) {} - this.drag_create.event = null; - } - return true; - } - -});}).call(this); - -// Static class stuff -jQuery.extend(et2_calendar_view, -{ - /** - * Check if the view should be consolidated into one, or listed seperately - * based on the user's preferences - * - * @param {string[]} owners List of owners - * @param {string} view Name of current view (day, week) - * @returns {boolean} True of only one is needed, false if each owner needs - * to be listed seperately. - */ - is_consolidated: function is_consolidated(owners, view) - { - // Seperate owners, or consolidated? - return !( - owners.length > 1 && - (view === 'day' && owners.length < parseInt(egw.preference('day_consolidate','calendar')) || - view === 'week' && owners.length < parseInt(egw.preference('week_consolidate','calendar'))) - ); - }, - - /** - * Cache to map owner & resource IDs to names, helps cut down on server requests - */ - owner_name_cache: {}, - - holiday_cache: {}, - /** - * Fetch and cache a list of the year's holidays - * - * @param {et2_calendar_timegrid} widget - * @param {string|numeric} year - * @returns {Array} - */ - get_holidays: function(widget,year) - { - // Loaded in an iframe or something - var view = egw.window.et2_calendar_view ? egw.window.et2_calendar_view : this; - - // No country selected causes error, so skip if it's missing - if(!view || !egw.preference('country','common')) return {}; - - var cache = view.holiday_cache[year]; - if (typeof cache == 'undefined') - { - // Fetch with json instead of jsonq because there may be more than - // one widget listening for the response by the time it gets back, - // and we can't do that when it's queued. - view.holiday_cache[year] = jQuery.getJSON( - egw.link('/calendar/holidays.php', {year: year}) - ); - } - cache = view.holiday_cache[year]; - if(typeof cache.done == 'function') - { - // pending, wait for it - cache.done(jQuery.proxy(function(response) { - view.holiday_cache[this.year] = response||undefined; - - egw.window.setTimeout(jQuery.proxy(function() { - // Make sure widget hasn't been destroyed while we wait - if(typeof this.widget.free == 'undefined') - { - this.widget.day_class_holiday(); - } - },this),1); - },{widget:widget,year:year})) - .fail(jQuery.proxy(function() { - view.holiday_cache[this.year] = undefined; - }, {widget: widget, year: year})); - return {}; - } - else - { - return cache; - } - } -}); +var et2_calendar_view = /** @class */ (function (_super) { + __extends(et2_calendar_view, _super); + /** + * Constructor + * + */ + function et2_calendar_view(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_calendar_view._attributes, _child || {})) || this; + _this.dataStorePrefix = 'calendar'; + _this.update_timer = null; + // Used for its date calculations + _this._date_helper = et2_createWidget('date-time', {}, null); + _this._date_helper.loadingFinished(); + _this.loader = jQuery('
'); + _this.update_timer = null; + // Used to support dragging on empty space to create an event + _this.drag_create = { + start: null, + end: null, + parent: null, + event: null + }; + return _this; + } + et2_calendar_view.prototype.destroy = function () { + _super.prototype.destroy.call(this); + // date_helper has no parent, so we must explicitly remove it + this._date_helper.destroy(); + this._date_helper = null; + // Stop the invalidate timer + if (this.update_timer) { + window.clearTimeout(this.update_timer); + } + }; + et2_calendar_view.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.call(this); + this.loader.hide(0).prependTo(this.div); + if (this.options.owner) + this.set_owner(this.options.owner); + return true; + }; + /** + * Something changed, and the view need to be re-drawn. We wait a bit to + * avoid re-drawing twice if start and end date both changed, then recreate + * as needed. + * + * @param {boolean} [trigger_event=false] Trigger an event once things are done. + * Waiting until invalidate completes prevents 2 updates when changing the date range. + * @returns {undefined} + * + * @memberOf et2_calendar_view + */ + et2_calendar_view.prototype.invalidate = function (trigger_event) { + // If this wasn't a stub, we'd set this.update_timer + }; + /** + * Returns the current start date + * + * @returns {Date} + * + * @memberOf et2_calendar_view + */ + et2_calendar_view.prototype.get_start_date = function () { + return new Date(this.options.start_date); + }; + /** + * Returns the current start date + * + * @returns {Date} + * + * @memberOf et2_calendar_view + */ + et2_calendar_view.prototype.get_end_date = function () { + return new Date(this.options.end_date); + }; + /** + * Change the start date + * + * Changing the start date will invalidate the display, and it will be redrawn + * after a timeout. + * + * @param {string|number|Date} new_date New starting date. Strings can be in + * any format understood by et2_widget_date, or Ymd (eg: 20160101). + * @returns {undefined} + * + * @memberOf et2_calendar_view + */ + et2_calendar_view.prototype.set_start_date = function (new_date) { + if (!new_date || new_date === null) { + new_date = new Date(); + } + // Use date widget's existing functions to deal + if (typeof new_date === "object" || typeof new_date === "string" && new_date.length > 8) { + this._date_helper.set_value(new_date); + } + else if (typeof new_date === "string") { + this._date_helper.set_year(new_date.substring(0, 4)); + // Avoid overflow into next month, since we re-use date_helper + this._date_helper.set_date(1); + this._date_helper.set_month(new_date.substring(4, 6)); + this._date_helper.set_date(new_date.substring(6, 8)); + } + var old_date = this.options.start_date; + this.options.start_date = new Date(this._date_helper.getValue()); + if (old_date !== this.options.start_date && this.isAttached()) { + this.invalidate(true); + } + }; + /** + * Change the end date + * + * Changing the end date will invalidate the display, and it will be redrawn + * after a timeout. + * + * @param {string|number|Date} new_date - New end date. Strings can be in + * any format understood by et2_widget_date, or Ymd (eg: 20160101). + * @returns {undefined} + * + * @memberOf et2_calendar_view + */ + et2_calendar_view.prototype.set_end_date = function (new_date) { + if (!new_date || new_date === null) { + new_date = new Date(); + } + // Use date widget's existing functions to deal + if (typeof new_date === "object" || typeof new_date === "string" && new_date.length > 8) { + this._date_helper.set_value(new_date); + } + else if (typeof new_date === "string") { + this._date_helper.set_year(new_date.substring(0, 4)); + // Avoid overflow into next month, since we re-use date_helper + this._date_helper.set_date(1); + this._date_helper.set_month(new_date.substring(4, 6)); + this._date_helper.set_date(new_date.substring(6, 8)); + } + var old_date = this.options.end_date; + this.options.end_date = new Date(this._date_helper.getValue()); + if (old_date !== this.options.end_date && this.isAttached()) { + this.invalidate(true); + } + }; + /** + * Set which users to display + * + * Changing the owner will invalidate the display, and it will be redrawn + * after a timeout. + * + * @param {number|number[]|string|string[]} _owner - Owner ID, which can + * be an account ID, a resource ID (as defined in calendar_bo, not + * necessarily an entry from the resource app), or a list containing a + * combination of both. + * + * @memberOf et2_calendar_view + */ + et2_calendar_view.prototype.set_owner = function (_owner) { + var old = this.options.owner; + // 0 means current user, but that causes problems for comparison, + // so we'll just switch to the actual ID + if (_owner == '0') { + _owner = [egw.user('account_id')]; + } + if (!jQuery.isArray(_owner)) { + if (typeof _owner === "string") { + _owner = _owner.split(','); + } + else { + _owner = [_owner]; + } + } + else { + _owner = jQuery.extend([], _owner); + } + this.options.owner = _owner; + if (this.isAttached() && (typeof old === "number" && typeof _owner === "number" && old !== this.options.owner || + // Array of ids will not compare as equal + ((typeof old === 'object' || typeof _owner === 'object') && old.toString() !== _owner.toString()) || + // Strings + typeof old === 'string' && '' + old !== '' + this.options.owner)) { + this.invalidate(true); + } + }; + /** + * Provide specific data to be displayed. + * This is a way to set start and end dates, owner and event data in one call. + * + * If events are not provided in the array, + * @param {Object[]} events Array of events, indexed by date in Ymd format: + * { + * 20150501: [...], + * 20150502: [...] + * } + * Days should be in order. + * {string|number|Date} events.start_date - New start date + * {string|number|Date} events.end_date - New end date + * {number|number[]|string|string[]} event.owner - Owner ID, which can + * be an account ID, a resource ID (as defined in calendar_bo, not + * necessarily an entry from the resource app), or a list containing a + * combination of both. + */ + et2_calendar_view.prototype.set_value = function (events) { + if (typeof events !== 'object') + return false; + if (events.length && events.length > 0 || !jQuery.isEmptyObject(events)) { + this.set_disabled(false); + } + if (events.id) { + this.set_id(events.id); + delete events.id; + } + if (events.start_date) { + this.set_start_date(events.start_date); + delete events.start_date; + } + if (events.end_date) { + this.set_end_date(events.end_date); + delete events.end_date; + } + // set_owner() wants start_date set to get the correct week number + // for the corner label + if (events.owner) { + this.set_owner(events.owner); + delete events.owner; + } + this.value = events || {}; + // None of the above changed anything, hide the loader + if (!this.update_timer) { + window.setTimeout(jQuery.proxy(function () { this.loader.hide(); }, this), 200); + } + }; + Object.defineProperty(et2_calendar_view.prototype, "date_helper", { + get: function () { + return this._date_helper; + }, + enumerable: true, + configurable: true + }); + et2_calendar_view.prototype._createNamespace = function () { + return true; + }; + /** + * Calendar supports many different owner types, including users & resources. + * This translates an ID to a user-friendly name. + * + * @param {string} user + * @returns {string} + * + * @memberOf et2_calendar_view + */ + et2_calendar_view.prototype._get_owner_name = function (user) { + var label = undefined; + if (parseInt(user) === 0) { + // 0 means current user + user = egw.user('account_id'); + } + if (et2_calendar_view.owner_name_cache[user]) { + return et2_calendar_view.owner_name_cache[user]; + } + if (!isNaN(user)) { + user = parseInt(user); + var accounts = egw.accounts('both'); + for (var j = 0; j < accounts.length; j++) { + if (accounts[j].value === user) { + label = accounts[j].label; + break; + } + } + } + if (typeof label === 'undefined') { + // Not found? Ask the sidebox owner widget (it gets updated) or the original arrayMgr + var options = false; + if (app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { + options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); + } + else { + options = this.getArrayMgr("sel_options").getRoot().getEntry('owner'); + } + if (options && options.find) { + var found = options.find(function (element) { return element.id == user; }) || {}; + if (found && found.label && found.label !== user) { + label = found.label; + } + } + if (!label) { + // No sidebox? Must be in home or sitemgr (no caching) - ask directly + label = '?'; + egw.jsonq('calendar_owner_etemplate_widget::ajax_owner', user, function (data) { + et2_calendar_view.owner_name_cache[user] = data; + this.invalidate(true); + // Set owner to make sure labels get set + if (this.owner && typeof this.owner.set_value === 'function') { + this.owner.set_value(data); + } + }.bind(this), this); + } + } + if (label) { + et2_calendar_view.owner_name_cache[user] = label; + } + return label; + }; + /** + * Find the event information linked to a given DOM node + * + * @param {HTMLElement} dom_node - It should have something to do with an event + * @returns {Object} + */ + et2_calendar_view.prototype._get_event_info = function (dom_node) { + // Determine as much relevant info as can be found + var event_node = jQuery(dom_node).closest('[data-id]', this.div)[0]; + var day_node = jQuery(event_node).closest('[data-date]', this.div)[0]; + var result = jQuery.extend({ + event_node: event_node, + day_node: day_node + }, event_node ? event_node.dataset : {}, day_node ? day_node.dataset : {}); + // Widget ID should be the DOM node ID without the event_ prefix + if (event_node && event_node.id) { + var widget_id = event_node.id || ''; + widget_id = widget_id.split('event_'); + widget_id.shift(); + result.widget_id = 'event_' + widget_id.join(''); + } + return result; + }; + /** + * Starting (mousedown) handler to support drag to create + * + * Extending classes need to set this.drag_create.parent, which is the + * parent container (child of extending class) that will directly hold the + * event. + * + * @param {String} start Date string (JSON format) + */ + et2_calendar_view.prototype._drag_create_start = function (start) { + this.drag_create.start = jQuery.extend({}, start); + if (!this.drag_create.start.date) { + this.drag_create.start = null; + } + this.drag_create.end = start; + // Clear some stuff, if last time did not complete + if (this.drag_create.event) { + if (this.drag_create.event.destroy) { + this.drag_create.event.destroy(); + } + this.drag_create.event = null; + } + // Wait a bit before adding an "event", it may be just a click + window.setTimeout(jQuery.proxy(function () { + // Create event + this._drag_create_event(); + }, this), 250); + }; + /** + * Create or update an event used for feedback while dragging on empty space, + * so user can see something is happening + */ + et2_calendar_view.prototype._drag_create_event = function () { + if (!this.drag_create.parent || !this.drag_create.start) { + return; + } + if (!this.drag_create.event) { + this._date_helper.set_value(this.drag_create.start.date); + var value = jQuery.extend({}, this.drag_create.start, this.drag_create.end, { + start: this.drag_create.start.date, + end: this.drag_create.end && this.drag_create.end.date || this.drag_create.start.date, + date: "" + this._date_helper.get_year() + + sprintf("%02d", this._date_helper.get_month()) + + sprintf("%02d", this._date_helper.get_date()), + title: '', + description: '', + owner: this.options.owner, + participants: this.options.owner, + app: 'calendar', + whole_day_on_top: this.drag_create.start.whole_day + }); + this.drag_create.event = et2_createWidget('calendar-event', { + id: 'event_drag', + value: value + }, this.drag_create.parent); + this.drag_create.event._values_check(value); + this.drag_create.event.doLoadingFinished(); + } + }; + et2_calendar_view.prototype._drag_update_event = function () { + if (!this.drag_create.event || !this.drag_create.start || !this.drag_create.end + || !this.drag_create.parent || !this.drag_create.event._type) { + return; + } + else if (this.drag_create.end) { + this.drag_create.event.options.value.end = this.drag_create.end.date; + this.drag_create.event._values_check(this.drag_create.event.options.value); + } + this.drag_create.event._update(); + this.drag_create.parent.position_event(this.drag_create.event); + }; + /** + * Ending (mouseup) handler to support drag to create + * + * @param {String} end Date string (JSON format) + */ + et2_calendar_view.prototype._drag_create_end = function (end) { + this.div.css('cursor', ''); + if (typeof end === 'undefined') { + end = {}; + } + if (this.drag_create.start && end.date && + JSON.stringify(this.drag_create.start.date) !== JSON.stringify(end.date)) { + // Drag from start to end, open dialog + var options = { + start: this.drag_create.start.date < end.date ? this.drag_create.start.date : end.date, + end: this.drag_create.start.date < end.date ? end.date : this.drag_create.start.date + }; + // Whole day needs to go from 00:00 to 23:59 + if (end.whole_day || this.drag_create.start.whole_day) { + var start = new Date(options.start); + start.setUTCHours(0); + start.setUTCMinutes(0); + options.start = start.toJSON(); + var end = new Date(options.end); + end.setUTCHours(23); + end.setUTCMinutes(59); + options.end = end.toJSON(); + } + // Add anything else that was set, but not date + jQuery.extend(options, this.drag_create.start, end); + delete (options.date); + // Make sure parent is set, if needed + if (this.drag_create.parent && this.drag_create.parent.options.owner !== app.calendar.state.owner && !options.owner) { + options.owner = this.drag_create.parent.options.owner; + } + // Remove empties + for (var key in options) { + if (!options[key]) + delete options[key]; + } + app.calendar.add(options, this.drag_create.event); + // Wait a bit, having these stops the click + window.setTimeout(jQuery.proxy(function () { + this.drag_create.start = null; + this.drag_create.end = null; + this.drag_create.parent = null; + if (this.drag_create.event) { + this.drag_create.event = null; + } + }, this), 100); + return false; + } + this.drag_create.start = null; + this.drag_create.end = null; + this.drag_create.parent = null; + if (this.drag_create.event) { + try { + if (this.drag_create.event.destroy) { + this.drag_create.event.destroy(); + } + } + catch (e) { } + this.drag_create.event = null; + } + return true; + }; + /** + * Check if the view should be consolidated into one, or listed seperately + * based on the user's preferences + * + * @param {string[]} owners List of owners + * @param {string} view Name of current view (day, week) + * @returns {boolean} True of only one is needed, false if each owner needs + * to be listed seperately. + */ + et2_calendar_view.is_consolidated = function (owners, view) { + // Seperate owners, or consolidated? + return !(owners.length > 1 && + (view === 'day' && owners.length < parseInt('' + egw.preference('day_consolidate', 'calendar')) || + view === 'week' && owners.length < parseInt('' + egw.preference('week_consolidate', 'calendar')))); + }; + /** + * Fetch and cache a list of the year's holidays + * + * @param {et2_calendar_timegrid} widget + * @param {string|numeric} year + * @returns {Array} + */ + et2_calendar_view.get_holidays = function (widget, year) { + // Loaded in an iframe or something + var view = egw.window.et2_calendar_view ? egw.window.et2_calendar_view : this; + // No country selected causes error, so skip if it's missing + if (!view || !egw.preference('country', 'common')) + return {}; + var cache = view.holiday_cache[year]; + if (typeof cache == 'undefined') { + // Fetch with json instead of jsonq because there may be more than + // one widget listening for the response by the time it gets back, + // and we can't do that when it's queued. + view.holiday_cache[year] = jQuery.getJSON(egw.link('/calendar/holidays.php', { year: year })); + } + cache = view.holiday_cache[year]; + if (typeof cache.done == 'function') { + // pending, wait for it + cache.done(jQuery.proxy(function (response) { + view.holiday_cache[this.year] = response || undefined; + egw.window.setTimeout(jQuery.proxy(function () { + // Make sure widget hasn't been destroyed while we wait + if (typeof this.widget.free == 'undefined') { + this.widget.day_class_holiday(); + } + }, this), 1); + }, { widget: widget, year: year })) + .fail(jQuery.proxy(function () { + view.holiday_cache[this.year] = undefined; + }, { widget: widget, year: year })); + return {}; + } + else { + return cache; + } + }; + et2_calendar_view._attributes = { + owner: { + name: "Owner", + type: "any", + default: [egw.user('account_id')], + description: "Account ID number of the calendar owner, if not the current user" + }, + start_date: { + name: "Start date", + type: "any" + }, + end_date: { + name: "End date", + type: "any" + } + }; + /** + * Cache to map owner & resource IDs to names, helps cut down on server requests + */ + et2_calendar_view.owner_name_cache = {}; + et2_calendar_view.holiday_cache = {}; + return et2_calendar_view; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_calendar_view = et2_calendar_view; +//# sourceMappingURL=et2_widget_view.js.map \ No newline at end of file