12f0c107330db655b4a4a747bfd15bac34fa8bb7
[tine20] / tine20 / Calendar / js / CalendarPanelSplitPlugin.js
1 Tine.Calendar.CalendarPanelSplitPlugin = function() {
2     
3 };
4
5 /**
6  * @TODO ui for active view?
7  * @TODO we could also create views beforeload for better performace
8  * 
9  */
10 Tine.Calendar.CalendarPanelSplitPlugin.prototype = {
11     /**
12      * @property app
13      * @type Tine.Calendar.Application
14      */
15     app: null,
16     
17     /**
18      * @property attendeeViews
19      * @type Ext.util.MixedCollection
20      */
21     attendeeViews: null,
22     
23     /**
24      * @property mainStore store of main view
25      * @type Ext.Store
26      */
27     mainStore: null,
28     
29     init: function(calPanel) {
30
31         if (! Tine.Tinebase.appMgr.get('Calendar').featureEnabled('featureSplitView')) {
32             return;
33         }
34
35         this.calPanel = calPanel;
36         this.app = Tine.Tinebase.appMgr.get('Calendar');
37         this.attendeeViews = new Ext.util.MixedCollection();
38         
39         // NOTE: we can't use a normal hbox layout as it can't deal with minWidth and overflow.
40         //       As ext has no suiteable layout for this, we do a little hack
41         this.calPanel.layout = new Ext.layout.HBoxLayout({
42             align : 'stretch',
43             pack  : 'start'
44         });
45         this.calPanel.layout.onLayout = this.calPanel.layout.onLayout.createInterceptor(function() {
46             var viewCount = this.attendeeViews.getCount(),
47                 availableWidth = this.calPanel.getWidth(),
48                 minViewWidth = this.calPanel.view.boxMinWidth;
49
50             var width = availableWidth/viewCount < minViewWidth ? minViewWidth * viewCount : availableWidth;
51             this.calPanel.body.setWidth(width);
52             this.calPanel.body.setStyle('overflow-x', width > availableWidth ? 'scroll' : 'hidden');
53         }, this);
54         this.calPanel.on('afterlayout', function() {
55             this.calPanel.body.setWidth(this.calPanel.getWidth());
56             this.resizeWholeDayArea.defer(this.attendeeViews.items.length * 120, this, [true]);
57         }, this);
58         
59         this.mainStore = this.calPanel.view.store;
60         this.mainStore.on('load', this.onMainStoreLoad, this);
61         this.mainStore.on('beforeload', this.onMainStoreBeforeLoad, this);
62         
63         // NOTE: we remove from items to avoid autoDestroy
64         this.calPanel.items.remove(this.calPanel.view);
65         
66         this.calPanel.getView = this.getActiveView.createDelegate(this);
67         
68         this.calPanel.on('show', this.onCalPanelShow, this);
69         this.calPanel.on('hide', this.onCalPanelHide, this);
70         
71         this.calPanel.on('afterrender', function() {
72             this.attendeeFilterGrid = this.app.getMainScreen().getWestPanel().getAttendeeFilter();
73             this.attendeeFilterGrid.on('sortchange', this.onAttendeeFilterSortChange, this);
74         }, this);
75         
76         // hook into getDefaultData 
77         this.originalGetDefaultData = Tine.Calendar.Model.Event.getDefaultData;
78         Tine.Calendar.Model.Event.getDefaultData = this.getDefaultData.createDelegate(this);
79     },
80     
81     getDefaultData: function() {
82         var defaultData = this.originalGetDefaultData(),
83             useSplit = Tine.Calendar.CalendarPanelSplitPlugin.splitBtn.pressed,
84             centerPanel = this.app.getMainScreen().getCenterPanel(),
85             activeCalPanel = centerPanel.getCalendarPanel(centerPanel.activeView),
86             activeView = this.getActiveView(),
87             attendee = activeView && activeView.ownerCt ? activeView.ownerCt.attendee : null;
88         
89         if (useSplit && attendee && this.calPanel == activeCalPanel) {
90             Ext.apply(attendee.data, {
91                 user_type: 'user',
92                 status: 'ACCEPTED'
93             });
94             defaultData.attendee = [attendee.data];
95         }
96         
97         return defaultData;
98     },
99     
100     onCalPanelShow: function() {
101         if (Tine.Calendar.CalendarPanelSplitPlugin.splitBtn) {
102             Tine.Calendar.CalendarPanelSplitPlugin.splitBtn.enable();
103         } else {
104             Tine.Calendar.CalendarPanelSplitPlugin.SplitBtn.prototype.disabled = false;
105         }
106     },
107     
108     onCalPanelHide: function() {
109         if (Tine.Calendar.CalendarPanelSplitPlugin.splitBtn) {
110             Tine.Calendar.CalendarPanelSplitPlugin.splitBtn.disable();
111         }
112     },
113     
114     onMainStoreBeforeLoad: function(store, options) {
115         this.attendeeViews.each(function(attendeeView) {
116             attendeeView.store.fireEvent('beforeload', store, options);
117         }, this);
118     },
119     
120     onMainStoreLoad: function(store, options) {
121         var cp = this.app.getMainScreen().getCenterPanel();
122         
123         if (store !== cp.getCalendarPanel(cp.activeView).getStore()) {
124             Tine.log.debug('Tine.Calendar.CalendarPanelSplitPlugin::onMainStoreLoad try again with active subview');
125             cp.onStoreLoad(this.getActiveView().store, options);
126         }
127         
128         // create view for each attendee
129         var filteredAttendee = this.attendeeFilterGrid.getValue(),
130             attendeeStore = Tine.Calendar.Model.Attender.getAttendeeStore(filteredAttendee),
131             useSplit = Tine.Calendar.CalendarPanelSplitPlugin.splitBtn.pressed;
132             
133         // remove views not longer in filter
134         this.calPanel.items.each(function(view) {
135             if (view.attendee && (! useSplit || ! Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendeeStore, view.attendee))) {
136                 this.removeAttendeeView(view.attendee);
137             }
138             
139         }, this);
140         
141         this.manageSplitViews(filteredAttendee);
142         
143         // manage main (main is shown if no split criteria is present)
144         if (! filteredAttendee.length || ! useSplit) {
145             if (! this.attendeeViews.get('main')) {
146                 var view = this.createView({
147                     store: this.cloneStore(false)
148                 });
149                 this.attendeeViews.add('main', view);
150                 
151                 this.calPanel.add({
152                     border: false,
153                     flex: 1,
154                     items: view
155                 });
156             } else {
157                 this.attendeeViews.get('main').initData(this.cloneStore(false));
158                 this.attendeeViews.get('main').onLoad();
159             }
160         } else {
161             var main = this.attendeeViews.get('main');
162             if (main && main.ownerCt) {
163                 this.calPanel.remove(main.ownerCt);
164             }
165             
166             this.attendeeViews.removeKey('main');
167         }
168         
169         this.calPanel.doLayout();
170     },
171     
172     addAttendeeView: function(attendee, pos) {
173         var attendeeName = Tine.Calendar.AttendeeGridPanel.prototype.renderAttenderName.call(Tine.Calendar.AttendeeGridPanel.prototype, attendee.get('user_id'), false, attendee),
174             attendeeViewId = this.getAttendeeViewId(attendee);
175         
176         var filter = function(r) {
177             var attendeeStore = Tine.Calendar.Model.Attender.getAttendeeStore(r.get('attendee'));
178             return Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendeeStore, attendee);
179         };
180         
181         var store = this.cloneStore(filter);
182         
183         var view = this.createView({
184             title: attendeeName,
185             store: store,
186             print: this.onPrint.createDelegate(this)
187         });
188         
189         // manage active views
190         view.on('render', function(v){
191             v.mon(v.getEl(), 'mousedown', this.setActiveAttendeeView.createDelegate(this, [attendeeViewId]), this);
192         }, this);
193         
194         this.attendeeViews.add(attendeeViewId, view);
195         this.calPanel.insert(pos, {
196             xtype: 'tabpanel',
197             style: 'padding: 5px;',
198             id: attendeeViewId,
199             attendee: attendee,
200             plain: true,
201             flex: 1,
202             activeItem: 0,
203             items: view
204         });
205     },
206     
207     onAttendeeFilterSortChange: function() {
208         var filteredAttendee = this.attendeeFilterGrid.getValue();
209         
210         this.manageSplitViews(filteredAttendee);
211         this.calPanel.doLayout();
212     },
213     
214     manageSplitViews: function(filteredAttendee) {
215         // create view for each attendee
216         var useSplit = Tine.Calendar.CalendarPanelSplitPlugin.splitBtn.pressed;
217         
218         if (useSplit) {
219             // add subviews new to filter
220             // NOTE: we don't iterate the attendeeStore as we would loose attendee order than
221             Ext.each(filteredAttendee, function(attendeeData, idx) {
222                 var attendee = new Tine.Calendar.Model.Attender(attendeeData, attendeeData.id);
223                 var attendeeView = this.attendeeViews.get(this.getAttendeeViewId(attendee));
224                 if (! attendeeView) {
225                     this.addAttendeeView(attendee, idx);
226                 } else {
227                     //assert position
228                     this.calPanel.items.remove(attendeeView.ownerCt);
229                     this.calPanel.items.insert(idx, attendeeView.ownerCt);
230
231                     var filterFn = attendeeView.store.filterFn;
232                     attendeeView.initData(this.cloneStore(filterFn));
233                     attendeeView.onLoad();
234                 }
235             }, this);
236
237             this.resizeWholeDayArea.defer(this.attendeeViews.items.length * 120, this, [true]);
238         }
239     },
240     
241     getAttendeeViewId: function(attendee) {
242         return this.calPanel.id + '-' + attendee.get('user_type') + '-' + attendee.getUserId();
243     },
244     
245     removeAttendeeView: function(attendee) {
246         var attendeeViewId = this.getAttendeeViewId(attendee);
247         
248         //@TODO remove relayed events?
249         
250         this.attendeeViews.removeKey(attendeeViewId);
251         this.calPanel.remove(attendeeViewId);
252     },
253     
254     createView: function(config) {
255         var view = Ext.create(Ext.apply({
256             xtype: this.calPanel.view.getXType(),
257             startDate: this.calPanel.getTopToolbar().periodPicker.getPeriod().from,
258             numOfDays: this.calPanel.view.numOfDays,
259             period: this.calPanel.getTopToolbar().periodPicker.getPeriod(),
260             updatePeriod: this.updatePeriod.createDelegate(this)
261         }, config));
262         
263         this.calPanel.relayEvents(view, ['changeView', 'changePeriod', 'addEvent', 'updateEvent', 'click', 'dblclick', 'contextmenu', 'keydown']);
264         this.calPanel.view.relayEvents(view, ['changeView', 'changePeriod', 'addEvent', 'updateEvent', 'click', 'dblclick', 'contextmenu', 'keydown']);
265         this.calPanel.view.getSelectionModel().relayEvents(view.getSelectionModel(), 'selectionchange');
266         view.getSelectionModel().on('selectionchange', this.onSelectionChange.createDelegate(this, [view]));
267         
268         if (view.onBeforeScroll) {
269             view.onBeforeScroll = view.onBeforeScroll.createSequence(this.onScroll.createDelegate(this, [view], 0));
270         }
271         
272         return view;
273     },
274     
275     /**
276      * is called on scroll of a grid, scrolls the other grids
277      * 
278      * @param {Object} activeView
279      * @param {Object} e Event
280      */
281     onScroll: function(activeView, e) {
282         if (! (activeView && activeView.scroller && activeView.scroller.dom)) {
283             return;
284         }
285         
286         var scrollTop = activeView.scroller.dom.scrollTop;
287
288         this.attendeeViews.each(function(view) {
289             if (activeView.id != view.id && view.scroller) {
290                 view.scroller.dom.scrollTop = scrollTop;
291             }
292         }, this);
293     },
294
295     getActiveView: function() {
296         if (! this.attendeeViews.getCount()) {
297             return this.calPanel.view;
298         }
299
300         if (! this.activeAttendeeView || this.attendeeViews.indexOf(this.activeAttendeeView) < 0) {
301             this.activeAttendeeView = this.attendeeViews.itemAt(0);
302         }
303         return this.activeAttendeeView;
304     },
305     
306     onPrint: function(printMode) { 
307         var renderer = new Tine.Calendar.Printer.SplitViewRenderer({printMode: printMode});
308         renderer.print(this);
309     },
310     
311     onSelectionChange: function(view) {
312         view = Ext.isString(view) ? this.attendeeViews.get(view) : view;
313         this.setActiveAttendeeView(view);
314         
315         this.attendeeViews.each(function(v) {
316             if (v !== view) {
317                 v.getSelectionModel().clearSelections(true);
318             }
319         }, this);
320         
321         this.resizeWholeDayArea.defer(this.attendeeViews.items.length * 120, this);
322
323         if (this.calPanel.mainScreen) {
324             this.calPanel.mainScreen.updateEventActions.call(this.calPanel.mainScreen);
325         }
326     },
327     
328     setActiveAttendeeView: function(view) {
329         view = Ext.isString(view) ? this.attendeeViews.get(view) : view;
330         
331         this.activeAttendeeView = view;
332     },
333     
334     updatePeriod: function() {
335         var origArgs = arguments;
336         
337         this.attendeeViews.each(function(view) {
338             view.constructor.prototype.updatePeriod.apply(view, origArgs);
339         }, this);
340         
341         this.resizeWholeDayArea.defer(this.attendeeViews.items.length * 120, this, [true]);
342     },
343     
344     /**
345      * set whole day area to the height of the highest whole day area
346      */
347     resizeWholeDayArea: function(onupdate) {
348         if (this.attendeeViews.items.length > 1) {
349             var maxWholeDayAreaSize = 10;
350             var scrollerSize = this.getActiveView().scroller ? this.getActiveView().scroller.getHeight() : 0;
351             this.attendeeViews.each(function(view) {
352                 if (! view.wholeDayArea) {
353                     return;
354                 }
355
356                 if (onupdate) {
357                     view.wholeDayArea.heightIsCalculated = false;
358                 }
359                 if (view.wholeDayArea.children.length > 1
360                     && ((view.wholeDayArea.hasOwnProperty('heightIsCalculated')
361                     && view.wholeDayArea.heightIsCalculated == false)
362                     || (! view.wholeDayArea.hasOwnProperty('heightIsCalculated'))))
363                 {
364                     if (parseInt(view.wholeDayArea.clientHeight) > maxWholeDayAreaSize) {
365                         maxWholeDayAreaSize = view.wholeDayArea.clientHeight;
366                         scrollerSize = view.scroller.getHeight();
367                     }
368                 }
369
370                 if (parseInt(view.wholeDayArea.clientHeight) > maxWholeDayAreaSize) {
371                     maxWholeDayAreaSize = view.wholeDayArea.clientHeight;
372                     scrollerSize = this.container.getSize(true);
373                 }
374             });
375             this.attendeeViews.each(function(view) {
376                 if (! view.wholeDayArea) {
377                     return;
378                 }
379                 var wholeDayAreaEl = Ext.get(view.wholeDayArea);
380                 if (wholeDayAreaEl.getHeight() != maxWholeDayAreaSize) {
381                     wholeDayAreaEl.setHeight(maxWholeDayAreaSize);
382                     view.wholeDayArea.heightIsCalculated = true;
383                 } else {
384                     view.wholeDayArea.heightIsCalculated = false;
385                 }
386                 Ext.fly(view.wholeDayScroller).setHeight(maxWholeDayAreaSize);
387
388                 var hdHeight = this.mainHd.getHeight();
389                 var vh = this.container.getSize(true).height - (hdHeight);
390
391                 Ext.fly(view.scroller).setHeight(vh);
392             });
393         }
394     },
395     
396     cloneStore: function(filterFn) {
397         var clone = new Ext.data.Store({
398             fields: Tine.Calendar.Model.Event,
399             load: this.mainStore.load.createDelegate(this.mainStore),
400             proxy: this.mainStore.proxy,
401             filterFn: filterFn,
402             replaceRecord: function(o, n) {
403                 var idx = this.indexOf(o);
404                 this.remove(o);
405                 this.insert(idx, n);
406             }
407         });
408         
409         var rs = [];
410         this.mainStore.each(function(r) {
411             if (! filterFn || filterFn(r)) {
412                 rs.push(r.copy());
413             }
414         }, this);
415         
416         clone.add(rs);
417         
418         clone.on('add', this.onCloneStoreAdd, this);
419         clone.on('update', this.onCloneStoreUpdate, this);
420         clone.on('remove', this.onCloneStoreRemove, this);
421         return clone;
422     },
423     
424     onCloneStoreAdd: function(store, rs) {
425         Ext.each(rs, function(r){
426             this.attendeeViews.each(function(view) {
427                 if (view.store != store) {
428                     if (! view.store.filterFn || view.store.filterFn(r)) {
429                         view.store.un('add', this.onCloneStoreAdd, this);
430                         view.store.add([r.copy()]);
431                         view.store.on('add', this.onCloneStoreAdd, this);
432                     }
433                 }
434             }, this);
435             
436             // check if events fits into view @see Tine.Calendar.MainScreenCenterPanel::congruenceFilterCheck
437             if (store.filterFn && !store.filterFn(r)) {
438                 (function() {
439                     if (this.ui && this.ui.rendered) {
440                         this.ui.markOutOfFilter();
441                     }
442                 }).defer(25, r)
443             }
444         }, this);
445     },
446     
447     onCloneStoreUpdate: function(store, r) {
448         this.attendeeViews.each(function(view) {
449             if (view.store != store) {
450                 view.store.un('add', this.onCloneStoreAdd, this);
451                 view.store.un('remove', this.onCloneStoreRemove, this);
452                 
453                 var cr = view.store.getById(r.id);
454                 if (cr) {
455                     view.store.remove(cr);
456                     view.store.add([r.copy()]);
457                 }
458                 
459                 view.store.on('add', this.onCloneStoreAdd, this);
460                 view.store.on('remove', this.onCloneStoreRemove, this);
461             }
462         }, this);
463     },
464     
465     onCloneStoreRemove: function(store, r) {
466         this.attendeeViews.each(function(view) {
467             if (view.store != store) {
468                 view.store.un('remove', this.onCloneStoreRemove, this);
469                 view.store.remove(view.store.getById(r.id));
470                 view.store.on('remove', this.onCloneStoreRemove, this);
471             }
472         }, this);
473     }
474 };
475
476 Ext.preg('Calendar.CalendarPanelSplitPlugin', Tine.Calendar.CalendarPanelSplitPlugin);
477
478 Tine.Calendar.CalendarPanelSplitPlugin.SplitBtn = Ext.extend(Ext.Button, {
479     enableToggle: true,
480     pressed: true,
481     disabled: true,
482     scale: 'medium',
483     rowspan: 2,
484     icon: 'images/oxygen/22x22/actions/fileview-column.png',
485     iconAlign: 'top',
486     text: 'Split',
487     stateful: true,
488     stateId: 'cal-calpanel-split-btn',
489     stateEvents: ['toggle'],
490     
491     initComponent: function() {
492         if (! Tine.Tinebase.appMgr.get('Calendar').featureEnabled('featureSplitView')) {
493             // hide button and make sure it isn't pressed
494             Tine.log.info('Split view feature is deactivated');
495             this.hidden = true;
496             this.pressed = false;
497         }
498
499         Tine.Calendar.CalendarPanelSplitPlugin.SplitBtn.superclass.initComponent.apply(this, arguments);
500         Tine.Calendar.CalendarPanelSplitPlugin.splitBtn = this;
501     },
502     
503     handler: function() {
504         Tine.Tinebase.appMgr.get('Calendar').getMainScreen().getCenterPanel().refresh();
505     },
506     
507     getState: function() {
508         return {pressed: this.pressed}
509     }
510 });
511
512 Tine.Calendar.Printer.SplitViewRenderer = Ext.extend(Tine.Calendar.Printer.BaseRenderer, {
513     getAdditionalHeaders: Tine.Calendar.Printer.DaysViewRenderer.prototype.getAdditionalHeaders,
514     generateBody: function(splitView) {
515         var viewRenderer = splitView.calPanel.view.printRenderer,
516         htmlArray = [];
517         
518         this.paperHeight = viewRenderer.paperHeight;
519         
520         splitView.attendeeViews.each(function(v, i) {
521             var renderer = new v.printRenderer({printMode: this.printMode});
522             renderer.extraTitle = v.title + ' // ';
523             renderer.titleStyle = i > 0 ? 'page-break-before:always' : '';
524
525             htmlArray.push('<div class="page">' + renderer.generateBody(v) + '</div>');
526         }, this);
527         
528         return htmlArray.join('');
529     }
530 });
531
532 // self register
533 Tine.Calendar.CalendarPanel.prototype.plugins = '[{"ptype": "Calendar.CalendarPanelSplitPlugin"}]';
534 Ext.ux.ItemRegistry.registerItem('Calendar-MainScreenPanel-ViewBtnGrp', Tine.Calendar.CalendarPanelSplitPlugin.SplitBtn, -10);