diff --git a/calendar/js/app.js b/calendar/js/app.js index 87e480b3c6..4ace66044e 100644 --- a/calendar/js/app.js +++ b/calendar/js/app.js @@ -3040,7 +3040,7 @@ app.classes.calendar = AppJS.extend( { var tempDate = new Date(); var today = new Date(tempDate.getFullYear(), tempDate.getUTCMonth(), tempDate.getUTCDate()); - var holidays = et2_calendar_daycol.get_holidays({day_class_holiday: function() {}}, date.getFullYear()); + 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())]; diff --git a/calendar/js/et2_widget_daycol.js b/calendar/js/et2_widget_daycol.js index 2b19cf45dd..a3d84204e6 100644 --- a/calendar/js/et2_widget_daycol.js +++ b/calendar/js/et2_widget_daycol.js @@ -17,11 +17,11 @@ */ /** - * Class which implements the "calendar-timegrid" XET-Tag for displaying a span of days + * Class which implements the "calendar-timegrid" XET-Tag for displaying a single days * - * This widget is responsible for the times on the side + * This widget is responsible mostly for positioning its events * - * @augments et2_DOMWidget + * @augments et2_valueWidget */ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizeable], { @@ -35,7 +35,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea }, owner: { name: "Owner", - type: "any", // Integer, or array of integers + type: "any", // Integer, string, or array of either default: 0, description: "Account ID number of the calendar owner, if not the current user" }, @@ -83,7 +83,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea this.date_helper = et2_createWidget('date-time',{},null); this.date_helper.loadingFinished(); - // Init to defaults, just in case + // Init to defaults, just in case - they will be updated from parent this.display_settings = { wd_start: 60*9, wd_end: 60*17, @@ -128,7 +128,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea }, /** - * Draw the individual divs for clicking + * Draw the individual divs for clicking to add an event */ _draw: function() { // Remove any existing @@ -197,7 +197,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea * Set the date * * @param {string|Date} _date New date - * @param {Object[]} events=false List of events to be displayed, or false to + * @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. @@ -295,7 +295,10 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea /** * Set the owner of this day * - * @param {number|number[]} _owner Account ID + * @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) { @@ -330,6 +333,9 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea /** * 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} @@ -406,7 +412,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea ); // Holidays and birthdays - var holidays = et2_calendar_daycol.get_holidays(this,this.options.date.substring(0,4)); + var holidays = et2_calendar_view.get_holidays(this,this.options.date.substring(0,4)); var holiday_list = []; if(holidays && holidays[this.options.date]) { @@ -457,8 +463,8 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea { var node = this._children[this._children.length-1]; this.removeChild(node); - node.free(); - } + node.free(); + } // Make sure children are in cronological order, or columns are backwards events.sort(function(a,b) { @@ -547,7 +553,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea event.title.css('top',timegrid.scrolling.scrollTop() - event.div.position().top); event.body.css('padding-top',timegrid.scrolling.scrollTop() - event.div.position().top); } - // Too many in list view, show indicator + // Too many in gridlist view, show indicator else if (this.display_settings.granularity === 0 && hidden.completely) { var day = this; @@ -728,10 +734,11 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea }, /** - * Position the event according to it's time and how this widget is laid + * Position the event according to its time and how this widget is laid * out. * - * @param {undefined|Object|et2_calendar_event} event + * @param {et2_calendar_event} [event] - Event to be updated + * If a single event is not provided, all events are repositioned. */ position_event: function(event) { @@ -750,8 +757,6 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea } if (left + width > 100.0) width = 98.0 - left; - var whole_day_counter = 0; - for(var i = 0; (columns[c].indexOf(event) >= 0 || !event) && i < columns[c].length; i++) { // Calculate vertical positioning @@ -794,8 +799,8 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea columns[c][i].div.appendTo(this.div); this._parent.resizeTimes(); } - top = this._time_to_position(columns[c][i].options.value.start_m,whole_day_counter); - height = this._time_to_position(columns[c][i].options.value.end_m,whole_day_counter)-top; + 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 @@ -831,21 +836,16 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea * This calculation is a percentage from 00:00 to 23:59 * * @param {int} time in minutes from midnight - * @param {int} [row_offset=0] Add extra spacing for additional rows * @return {float} position in percent */ - _time_to_position: function(time,row_offset) + _time_to_position: function(time) { var pos = 0.0; - if(typeof row_offset === 'undefined') - { - row_offset = 0; - } // 24h - pos = ((time / 60) / 24) * 100 + pos = ((time / 60) / 24) * 100; - pos = pos.toFixed(1) + pos = pos.toFixed(1); return pos; }, @@ -957,54 +957,3 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea }); et2_register_widget(et2_calendar_daycol, ["calendar-daycol"]); - -// Static class stuff -jQuery.extend(et2_calendar_daycol, -{ - 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 - if(!egw.window.et2_calendar_daycol) return {}; - - var cache = egw.window.et2_calendar_daycol.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. - egw.window.et2_calendar_daycol.holiday_cache[year] = egw.json( - 'calendar_timegrid_etemplate_widget::ajax_get_holidays', - [year] - ).sendRequest(true); - } - cache = egw.window.et2_calendar_daycol.holiday_cache[year]; - if(typeof cache.done == 'function') - { - // pending, wait for it - cache.done(jQuery.proxy(function(response) { - egw.window.et2_calendar_daycol.holiday_cache[this.year] = response.response[0].data||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})); - return {}; - } - else - { - return cache; - } - } -}); \ No newline at end of file diff --git a/calendar/js/et2_widget_event.js b/calendar/js/et2_widget_event.js index 8c16d6d820..e1f0c95f2e 100644 --- a/calendar/js/et2_widget_event.js +++ b/calendar/js/et2_widget_event.js @@ -16,7 +16,28 @@ */ /** - * Class for a single event, displayed in a timegrid + * Class for a single event, displayed in either the timegrid or planner view + * + * It is possible to directly provide all information directly, but calendar + * uses egw.data for caching, so ID is all that is needed. + * + * Note that there are several pieces of information that have 'ID' in them: + * - row_id - used by both et2_calendar_event and the nextmatch to uniquely + * identify a particular entry or entry ocurrence + * - id - Recurring events may have their recurrence as a timestamp after their ID, + * such as '194:1453318200', or not. It's usually (always?) the same as row ID. + * - app_id - the ID according to the source application. For calendar, this + * is the same as ID (but always with the recurrence), for other apps this is + * usually just an integer. With app_id and app, you should be able to call + * egw.open() and get the specific entry. + * - Events from other apps will have their app name prepended to their ID, such + * as 'infolog123', so app_id and id will be different for these events + * - Cache ID is the same as other apps, and looks like 'calendar::' + * - The DOM ID for the containing div is event_ + * + * Events are expected to be added to either et2_calendar_daycol or + * et2_calendar_planner_row rather than either et2_calendar_timegrid or + * et2_calendar_planner directly. * * * @augments et2_valueWidget @@ -88,20 +109,12 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], doLoadingFinished: function() { this._super.apply(this, arguments); - // Parent will have everything we need, just load it from there - if(this.title.text() == '' && this.options.date && - this._parent && this._parent.instanceOf(et2_calendar_timegrid)) - { - // Forces an update - var date = this.options.date; - this.options.date = ''; - this.set_date(date); - } + // 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._UID_callback, this, this.getInstanceManager().execId, this.id @@ -132,7 +145,7 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], // Unregister, or we'll continue to be notified... if(this.options.value) { - var old_app_id = this.options.value.app_id; + var old_app_id = this.options.value.row_id; egw.dataUnregisterUID('calendar::'+old_app_id,false,this); } }, @@ -160,9 +173,12 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], egw.dataStoreUID('calendar::'+id, _value); } }, - + + /** + * Callback for changes in cached data + */ _UID_callback: function _UID_callback(event) { - // Make sure id is a string + // Make sure id is a string, check values if(event) { this._values_check(event); @@ -201,6 +217,9 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], } }, + /** + * Draw the event + */ _update: function(event) { // Copy new information @@ -329,6 +348,7 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], Math.max(0.8, parseFloat(jQuery.Color(this.title.css('background-color')).lightness())) )); + // Tooltip this.set_statustext(this._tooltip()); }, @@ -361,6 +381,11 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], return status_class; }, + /** + * Create tooltip shown on hover + * + * @return {String} + */ _tooltip: function() { if(!this.div) return ''; @@ -588,6 +613,22 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], 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. + * + * @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 @@ -672,7 +713,9 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], /** * Show the recur prompt for this event * - * @param {function} callback + * 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) @@ -683,7 +726,9 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], /** * Show the series split prompt for this event * - * @param {function} callback + * Calls et2_calendar_event.series_split_prompt with this event's value. + * + * @param {et2_calendar_event~prompt_callback} callback */ series_split_prompt: function(callback) { @@ -750,17 +795,33 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], et2_register_widget(et2_calendar_event, ["calendar-event"]); // Static class stuff +/** + * @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} 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 {Function} [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 + * @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} */ @@ -820,11 +881,14 @@ et2_calendar_event.recur_prompt = function(event_data, callback, extra_data) * 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 {Function} [callback] - Callback is called with the button (ok or cancel) and the event data. - * + * @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) @@ -889,7 +953,7 @@ et2_calendar_event.split_status = function(status,quantity,role) /** * The egw_action system requires an egwActionObjectInterface Interface implementation - * to tie actions to DOM nodes. This one can be used by any widget. + * to tie actions to DOM nodes. I'm not sure if we need this. * * The class extension is different than the widgets * diff --git a/calendar/js/et2_widget_planner.js b/calendar/js/et2_widget_planner.js index d6a7e38cce..778400fdbc 100644 --- a/calendar/js/et2_widget_planner.js +++ b/calendar/js/et2_widget_planner.js @@ -19,10 +19,11 @@ /** * Class which implements the "calendar-planner" XET-Tag for displaying a longer - * ( > 10 days) span of time + * ( > 10 days) span of time. Events can be grouped into rows by either user, + * category, or month. Their horizontal position and size in the row is determined + * by their start date and duration relative to the displayed date range. * - * @augments et2_valueWidget - * @class + * @augments et2_calendar_view */ var et2_calendar_planner = et2_calendar_view.extend([et2_IDetachedDOM, et2_IResizeable], { @@ -109,12 +110,6 @@ var et2_calendar_planner = et2_calendar_view.extend([et2_IDetachedDOM, et2_IResi { egw.dataUnregisterUID(this.registeredCallbacks[i],false,this); } - - // Stop the invalidate timer - if(this.update_timer) - { - window.clearTimeout(this.update_timer); - } }, doLoadingFinished: function() { @@ -1114,7 +1109,7 @@ var et2_calendar_planner = et2_calendar_view.extend([et2_IDetachedDOM, et2_IResi var day_class = ''; // Holidays and birthdays - var holidays = et2_calendar_daycol.get_holidays(this,date.getUTCFullYear()); + 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()); @@ -1434,7 +1429,7 @@ var et2_calendar_planner = et2_calendar_view.extend([et2_IDetachedDOM, et2_IResi var drop_date = this.dropEnd.toJSON() ||false; var event_data = planner._get_event_info(ui.draggable); - var event_widget = planner.getWidgetById('event_'+event_data.id); + var event_widget = planner.getWidgetById(event_data.widget_id); if(event_widget) { event_widget._parent.date_helper.set_value(drop_date); @@ -1582,21 +1577,7 @@ var et2_calendar_planner = et2_calendar_view.extend([et2_IDetachedDOM, et2_IResi { if(typeof events !== 'object') return false; - if(events.owner) - { - this.set_owner(events.owner); - delete events.owner; - } - 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; - } + this._super.apply(this, arguments); if(typeof events.length === "undefined" && events) { @@ -1719,7 +1700,7 @@ var et2_calendar_planner = et2_calendar_view.extend([et2_IDetachedDOM, et2_IResi * onclick function. * * @param {Event} _ev - * @returns {boolean} + * @returns {boolean} Continue processing event (true) or stop (false) */ click: function(_ev) { @@ -1793,29 +1774,14 @@ var et2_calendar_planner = et2_calendar_view.extend([et2_IDetachedDOM, et2_IResi return false; } }, - - _get_event_info: function(dom_node) - { - // Determine as much relevant info as can be found - var event_node = $j(dom_node).closest('[data-id]',this.div)[0]; - var day_node = $j(event_node).closest('[data-date]',this.div)[0]; - - return jQuery.extend({ - event_node: event_node, - day_node: day_node, - }, - event_node ? event_node.dataset : {}, - day_node ? day_node.dataset : {} - ); - }, - /** * Get time from position * * @param {number} x * @param {number} y - * @returns {DOMNode[]} time node(s) for the given position + * @returns {Date|Boolean} A time for the given position, or false if one + * could not be determined. */ _get_time_from_position: function(x,y) { diff --git a/calendar/js/et2_widget_timegrid.js b/calendar/js/et2_widget_timegrid.js index 951a1d770e..0d5d184c3b 100644 --- a/calendar/js/et2_widget_timegrid.js +++ b/calendar/js/et2_widget_timegrid.js @@ -20,9 +20,16 @@ /** * Class which implements the "calendar-timegrid" XET-Tag for displaying a span of days * - * This widget is responsible for the times on the side + * This widget is responsible for the times on the side, and it is also the + * controller for both positioning and setting the day columns. Day columns are + * recycled rather than removed and re-created to reduce reloading. Similarly, + * the horizontal time grid (when used - see granularity attribute) is only + * redrawn or resized when needed. Unfortunately resizing is needed every time + * the all day section has an event added or removed so the full work day from + * start time to end time is properly displayed. * - * @augments et2_DOMWidget + * + * @augments et2_calendar_view */ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IResizeable], { @@ -126,12 +133,6 @@ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IRes }, destroy: function() { - // Stop the invalidate timer - if(this.update_timer) - { - window.clearTimeout(this.update_timer); - } - // Stop listening to tab changes $j(framework.getApplicationByName('calendar').tab.contentDiv).off('show.' + this.id); @@ -151,11 +152,7 @@ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IRes this.scrolling = null; this._labelContainer = null; - // Stop the invalidate timer - if(this.update_timer) - { - window.clearTimeout(this.update_timer); - } + // Stop the resize timer if(this.resize_timer) { window.clearTimeout(this.resize_timer); @@ -386,11 +383,11 @@ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IRes var drop_date = dropEnd.date||false; var event_data = timegrid._get_event_info(ui.draggable); - var event_widget = timegrid.getWidgetById('event_'+event_data.id); + var event_widget = timegrid.getWidgetById(event_data.widget_id); if(!event_widget) { // Widget was moved across weeks / owners - event_widget = timegrid.getParent().getWidgetById('event_'+event_data.id); + event_widget = timegrid.getParent().getWidgetById(event_data.widget_id); } if(event_widget) { @@ -611,7 +608,7 @@ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IRes /** * Creates the DOM nodes for the times in the left column, and the horizontal - * lines (mostly via CSS) that span the whole time span. + * lines (mostly via CSS) that span the whole date range. */ _drawTimes: function() { $j('.calendar_calTimeRow',this.div).remove(); @@ -726,7 +723,8 @@ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IRes /** * 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() { // Wait a bit to see if anything else changes, then re-draw the times @@ -746,7 +744,11 @@ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IRes } },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')) @@ -876,6 +878,7 @@ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IRes day.set_left((day_width * i) + 'px'); if(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]); @@ -883,7 +886,7 @@ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IRes } else { - // Go back to self-calculated date + // 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); @@ -1258,50 +1261,41 @@ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IRes /** * Provide specific data to be displayed. - * This is a way to set start and end dates, owner and event data in once call. + * 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. - * + * @param {string|number|Date} events.start_date - New start date + * @param {string|number|Date} events.end_date - New end date + * @param {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(events) { if(typeof events !== 'object') return false; - this.loader.show(); var use_days_sent = true; - 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; use_days_sent = false; } if(events.end_date) { - this.set_end_date(events.end_date); - delete events.end_date; use_days_sent = false; } - // 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 || {}; + this._super.apply(this,arguments); + if(use_days_sent) { var day_list = Object.keys(events); @@ -1400,6 +1394,10 @@ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IRes /** * 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 */ @@ -1514,7 +1512,7 @@ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IRes * onclick function. * * @param {Event} _ev - * @returns {boolean} + * @returns {boolean} Continue processing event (true) or stop (false) */ click: function(_ev) { @@ -1577,23 +1575,11 @@ var et2_calendar_timegrid = et2_calendar_view.extend([et2_IDetachedDOM, et2_IRes } }, - _get_event_info: function(dom_node) - { - // Determine as much relevant info as can be found - var event_node = $j(dom_node).closest('[data-id]',this.div)[0]; - var day_node = $j(event_node).closest('[data-date]',this.div)[0]; - - return jQuery.extend({ - event_node: event_node, - day_node: day_node, - }, - event_node ? event_node.dataset : {}, - day_node ? day_node.dataset : {} - ); - }, - /** - * Get time from position + * 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 diff --git a/calendar/js/et2_widget_view.js b/calendar/js/et2_widget_view.js index cae483c8b2..f02f317b59 100644 --- a/calendar/js/et2_widget_view.js +++ b/calendar/js/et2_widget_view.js @@ -17,6 +17,10 @@ /** * Parent class for the various calendar views to reduce copied code * + * + * et2_calendar_view is responsible for its own loader div, which is displayed while + * the times & days are redrawn. + * * @augments et2_valueWidget */ var et2_calendar_view = et2_valueWidget.extend( @@ -54,6 +58,7 @@ var et2_calendar_view = et2_valueWidget.extend( this.date_helper.loadingFinished(); this.loader = $j('
'); + this.update_timer = null; }, destroy: function destroy() { @@ -62,6 +67,12 @@ var et2_calendar_view = et2_valueWidget.extend( // 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() { @@ -80,7 +91,9 @@ var et2_calendar_view = et2_valueWidget.extend( * * @memberOf et2_calendar_view */ - invalidate: function invalidate(trigger_event) {}, + invalidate: function invalidate(trigger_event) { + // If this wasn't a stub, we'd set this.update_timer + }, /** * Returns the current start date @@ -107,7 +120,11 @@ var et2_calendar_view = et2_valueWidget.extend( /** * Change the start date * - * @param {string|number|Date} new_date New starting 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 @@ -143,7 +160,11 @@ var et2_calendar_view = et2_valueWidget.extend( /** * Change the end date * - * @param {string|number|Date} new_date New 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 @@ -178,7 +199,13 @@ var et2_calendar_view = et2_valueWidget.extend( /** * Set which users to display * - * @param {number|number[]|string|string[]} _owner Account ID + * 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 */ @@ -207,6 +234,60 @@ var et2_calendar_view = et2_valueWidget.extend( } }, + /** + * 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. + * @param {string|number|Date} events.start_date - New start date + * @param {string|number|Date} events.end_date - New end date + * @param {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.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),100); + } + }, + /** * Calendar supports many different owner types, including users & resources. * This translates an ID to a user-friendly name. @@ -252,5 +333,84 @@ var et2_calendar_view = et2_valueWidget.extend( } } return user; + }, + + /** + * 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 = $j(dom_node).closest('[data-id]',this.div)[0]; + var day_node = $j(event_node).closest('[data-date]',this.div)[0]; + + // Widget ID should be the DOM node ID without the event_ prefix + var widget_id = event_node.id || ''; + widget_id = widget_id.split('event_'); + widget_id.shift(); + + return jQuery.extend({ + event_node: event_node, + day_node: day_node, + widget_id: 'event_' + widget_id.join('') + }, + event_node ? event_node.dataset : {}, + day_node ? day_node.dataset : {} + ); + }, + +}); + +// Static class stuff +jQuery.extend(et2_calendar_view, +{ + 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 + if(!egw.window.et2_calendar_view) return {}; + + var cache = egw.window.et2_calendar_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. + egw.window.et2_calendar_view.holiday_cache[year] = egw.json( + 'calendar_timegrid_etemplate_widget::ajax_get_holidays', + [year] + ).sendRequest(true); + } + cache = egw.window.et2_calendar_view.holiday_cache[year]; + if(typeof cache.done == 'function') + { + // pending, wait for it + cache.done(jQuery.proxy(function(response) { + egw.window.et2_calendar_view.holiday_cache[this.year] = response.response[0].data||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})); + return {}; + } + else + { + return cache; + } } }); \ No newline at end of file