From 6bccec1e0c0369c2dcc0e8f13129b661f1742fbe Mon Sep 17 00:00:00 2001 From: nathangray Date: Fri, 24 Feb 2017 08:31:39 -0700 Subject: [PATCH] Speed improvements for planner view --- calendar/js/et2_widget_event.js | 54 ++++-- calendar/js/et2_widget_planner.js | 263 +++++++++++++++++++------- calendar/js/et2_widget_planner_row.js | 51 ++++- calendar/js/et2_widget_view.js | 8 +- 4 files changed, 277 insertions(+), 99 deletions(-) diff --git a/calendar/js/et2_widget_event.js b/calendar/js/et2_widget_event.js index 7eace19ab0..89666f48f9 100644 --- a/calendar/js/et2_widget_event.js +++ b/calendar/js/et2_widget_event.js @@ -71,6 +71,11 @@ var et2_calendar_event = (function(){ "use strict"; return et2_valueWidget.exten .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) { @@ -108,6 +113,8 @@ var et2_calendar_event = (function(){ "use strict"; return et2_valueWidget.exten .appendTo(this.title); this.setDOMNode(this.div[0]); + + this._need_actions_linked = false; }, doLoadingFinished: function() { @@ -243,23 +250,7 @@ var et2_calendar_event = (function(){ "use strict"; return et2_valueWidget.exten this._actionObject.id = 'calendar::' + id; } - // 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||{}); - } catch (e) { - // something went wrong, but keep quiet about it - debugger; - } - } + this._need_actions_linked = true; // Make sure category stuff is there // Fake it to use the cache / call - if already there, these will return @@ -398,6 +389,9 @@ var et2_calendar_event = (function(){ "use strict"; return et2_valueWidget.exten 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'); @@ -861,6 +855,32 @@ var et2_calendar_event = (function(){ "use strict"; return et2_valueWidget.exten 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. * diff --git a/calendar/js/et2_widget_planner.js b/calendar/js/et2_widget_planner.js index 5db4f55d6d..bf95c24396 100644 --- a/calendar/js/et2_widget_planner.js +++ b/calendar/js/et2_widget_planner.js @@ -64,6 +64,8 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e } }, + DEFERRED_ROW_TIME: 100, + /** * Constructor * @@ -108,6 +110,8 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e this.setDOMNode(this.div[0]); this.registeredCallbacks = []; + this.cache = {}; + this._deferred_row_updates = {}; }, destroy: function() { @@ -133,6 +137,9 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e // - 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 @@ -502,7 +509,20 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e draw_row: function(sort_key, label, events) { if(['user','both'].indexOf(egw.preference('planner_show_empty_rows','calendar')) !== -1 || events.length) { - return this._drawRow(sort_key, label,events,this.options.start_date, this.options.end_date); + var row = this._drawRow(sort_key, label,events,this.options.start_date, this.options.end_date); + + // 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; } } }, @@ -767,7 +787,10 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e // Show AJAX loader this.widget.loader.show(); - this.widget.value = this.widget._fetch_data(); + this.widget.cache = {}; + this._deferred_row_updates = {}; + + this.widget._fetch_data(); this.widget._drawGrid(); @@ -849,7 +872,7 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e .append(this.grid); this.grid.empty(); - var grouper = this.groupers[isNaN(this.options.group_by) ? this.options.group_by : 'category']; + var grouper = this.grouper; if(!grouper) return; // Headers @@ -903,6 +926,14 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e { 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 = []; }, @@ -932,11 +963,6 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e row.doLoadingFinished(); } - // Add actual events - window.setTimeout(jQuery.proxy(function() { - this.row._update_events(this.events); - }, {row: row, events: events} ),0) - return row; }, @@ -1720,76 +1746,167 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e var end = new Date(this.options.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 = app.classes.calendar._daywise_cache_id(date, this.options.owner); - - 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) - { - // If displaying by category, we need the infolog (or other app) categories too - var im = this.getInstanceManager(); - for(var i = 0; i < data.length && this.options.group_by == 'category'; i++) - { - var event = egw.dataGetUIDdata('calendar::'+data[i]); - if(event && event.data && event.data.app) - { - // Fake it to use the cache / call - et2_selectbox.cat_options({ - _type:'select-cat', - getInstanceManager: function() {return im;} - }, {application:event.data.app||'calendar'}); - - // Get CSS too - egw.includeCSS('/api/categories.php?app='+event.data.app); - } - } - - this.invalidate(false); - } - }, this, this.getInstanceManager().execId,this.id); + value = value.concat(this._cache_register(t, this.options.owner, last_data)); t.setUTCDate(t.getUTCDate() + 1); } while(t < end); - // Need to get some more from the server - if(fetch && app.calendar) - { - app.calendar._fetch_data({ - first: this.options.start_date, - last: this.options.end_date, - owner: this.options.owner, - filter: this.options.filter - }, this.getInstanceManager()); - } 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]); + + // Try to determine rows interested + if(event && 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 && ( + event.data.participants[id] && this.options.group_by === 'user' || + event.data.category === id && this.options.group_by === 'category' + )) + { + 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; + break; + } + + // 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'}); + + // Get CSS too + egw.includeCSS('/api/categories.php?app='+event.data.app); + } + } + + 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; + + 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]); + } + else + { + break; + } + } + }, + /** * Provide specific data to be displayed. * This is a way to set start and end dates, owner and event data in once call. @@ -1802,7 +1919,7 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e * Days should be in order. * */ - set_value: function(events) + set_value: function set_value(events) { if(typeof events !== 'object') return false; @@ -1849,7 +1966,7 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e * @param {string|number} group_by 'user', 'month', or an integer category ID * @returns {undefined} */ - set_group_by: function(group_by) + set_group_by: function set_group_by(group_by) { if(isNaN(group_by) && typeof this.groupers[group_by] === 'undefined') { @@ -1858,6 +1975,8 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e 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); @@ -1869,7 +1988,7 @@ var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.e * * @param {boolean} weekends */ - set_show_weekend: function(weekends) + set_show_weekend: function set_show_weekend(weekends) { weekends = weekends ? true : false; if(this.options.show_weekend !== weekends) diff --git a/calendar/js/et2_widget_planner_row.js b/calendar/js/et2_widget_planner_row.js index 8fdeba8f46..752f79dc20 100644 --- a/calendar/js/et2_widget_planner_row.js +++ b/calendar/js/et2_widget_planner_row.js @@ -67,7 +67,7 @@ var et2_calendar_planner_row = (function(){ "use strict"; return et2_valueWidget this.set_end_date(this.options.end_date); this._cached_rows = []; - + this._row_height = 20; }, doLoadingFinished: function() { @@ -114,7 +114,6 @@ var et2_calendar_planner_row = (function(){ "use strict"; return et2_valueWidget 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; } @@ -436,6 +435,39 @@ var et2_calendar_planner_row = (function(){ "use strict"; return et2_valueWidget 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) + { + this.resize(); + this._update_events(events); + } + }, + /** * Load the event data for this day and create event widgets for each. * @@ -446,10 +478,11 @@ var et2_calendar_planner_row = (function(){ "use strict"; return et2_valueWidget _update_events: function(events) { // Remove all events - while(this._children.length) + while(this._children.length > 0) { - this._children[this._children.length-1].free(); - this.removeChild(this._children[this._children.length-1]); + var node = this._children[this._children.length-1]; + this.removeChild(node); + node.free(); } this._cached_rows = []; @@ -483,10 +516,8 @@ var et2_calendar_planner_row = (function(){ "use strict"; return et2_valueWidget position_event: function(event) { var rows = this._spread_events(); - var row = jQuery('
').appendTo(this.rows); - var height = rows.length * (parseInt(window.getComputedStyle(row[0]).getPropertyValue("height")) || 20); + var height = rows.length * this._row_height; var row_width = this.rows.width(); - row.remove(); for(var c = 0; c < rows.length; c++) { @@ -725,7 +756,9 @@ var et2_calendar_planner_row = (function(){ "use strict"; return et2_valueWidget return; } - this.position_event(); + var row = jQuery('
').appendTo(this.rows); + this._row_height = (parseInt(window.getComputedStyle(row[0]).getPropertyValue("height")) || 20); + row.remove(); } });}).call(this); diff --git a/calendar/js/et2_widget_view.js b/calendar/js/et2_widget_view.js index 6d4e90ba9f..ad2a4ddb21 100644 --- a/calendar/js/et2_widget_view.js +++ b/calendar/js/et2_widget_view.js @@ -246,7 +246,13 @@ var et2_calendar_view = (function(){ "use strict"; return et2_valueWidget.extend _owner = jQuery.extend([],_owner); } this.options.owner = _owner; - if(old !== this.options.owner && this.isAttached()) + 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); }