* Give newer, faster browsers the full at-once listview experience, older versions...
[citadel.git] / webcit / static / summaryview.js
1 /**
2  * Webcit Summary View v2
3  *   All comments, flowers and death threats to Mathew McBride
4  *   <matt@mcbridematt.dhs.org> / <matt@comalies>
5  * Copyright 2009 The Citadel Team
6  * Licensed under the GPL V3
7  */
8 document.observe("dom:loaded", createMessageView);
9
10 var message_view = null;
11 var loadingMsg = null;
12 var rowArray = null;
13 var currentSortMode = null;
14
15 // Header elements
16 var mlh_date = null;
17 var mlh_subject = null;
18 var mlh_from = null;
19 var currentSorterToggle = null;
20 var query = "";
21 var currentlyMarkedRows = new Object();
22 var markedRowId = null;
23
24 var mouseDownEvent = null;
25 var exitedMouseDown = false;
26
27 var currentPage = 0;
28 var sortModes = {
29   "rdate" : sortRowsByDateDescending,
30   "date" : sortRowsByDateAscending,
31   //  "reverse" : sortRowsByDateDescending,
32   "subj" : sortRowsBySubjectAscending,
33   "rsubj" : sortRowsBySubjectDescending,
34   "sender": sortRowsByFromAscending,
35   "rsender" : sortRowsByFromDescending
36 };
37 var toggles = {};
38
39 var nummsgs = 0;
40 var startmsg = 0;
41 var is_safe_mode = true;
42 /* The following code is VERY evil! Hopefully the need for it will evaporate in the future.
43    We only want newer browsers with Javascript JIT's to use the newer message view, unless the user explicitly chooses new/safe view */
44 function determineSafeMode() {
45   if (summary_view_pref == false) {
46     var userAgent = navigator.userAgent;
47     var gecko = userAgent.indexOf("Gecko/");
48     var opera = userAgent.indexOf("Presto/"); // check for rendering engine
49     var chrome = userAgent.indexOf("Chrome/");
50     var safari = userAgent.indexOf("Safari/");
51     var phone = userAgent.indexOf("Mobile");
52     if (phone > 0) {
53       is_safe_mode = true; /* Don't serve to mobiles */
54     } else if (gecko > 0) {
55       var version = userAgent.substring(gecko+6,gecko+15);
56       if (version > 20090600) {
57         is_safe_mode = false;
58       }
59     } else if (opera > 0) {
60       var prestoVersion = userAgent.substring(opera+7,opera+10);
61       if (prestoVersion >= 2.2) {
62         is_safe_mode = false;
63       }
64     } else if (chrome > 0) {
65       is_safe_mode = false;
66     } else if (safari > 0) {
67       var safariVersion = userAgent.substring(safari+7,safari+10);
68       if (safariVersion >= 525) {
69         is_safe_mode = false;
70       }
71     }
72   } else {
73   is_safe_mode = true;
74   }
75 }
76 function createMessageView() {
77   determineSafeMode();
78   message_view = document.getElementById("message_list_body");
79   loadingMsg = document.getElementById("loading");
80   getMessages();
81   mlh_date = $("mlh_date");
82   mlh_subject = $('mlh_subject');
83   mlh_from = $('mlh_from');
84   toggles["rdate"] = mlh_date;
85   toggles["date"] = mlh_date;
86   // toggles["reverse"] = mlh_date;
87   toggles["subj"] = mlh_subject;
88   toggles["rsubj"] = mlh_subject;
89   toggles["sender"] = mlh_from;
90   toggles["rsender"] = mlh_from;
91   mlh_date.observe('click',ApplySort);
92   mlh_subject.observe('click',ApplySort);
93   mlh_from.observe('click',ApplySort);
94   $(document).observe('keyup',CtdlMessageListKeyUp,false);
95   //window.oncontextmenu = function() { return false; };  
96   $('resize_msglist').observe('mousedown', CtdlResizeMouseDown);
97   $('m_refresh').observe('click', getMessages);
98   document.getElementById('m_refresh').setAttribute("href","#");
99   Event.observe(document.onresize ? document : window, "resize", normalizeHeaderTable);
100   Event.observe(document.onresize ? document : window, "resize", sizePreviewPane);
101   $('summpage').observe('change', getPage);
102   takeOverSearchOMatic();
103   setupDragDrop(); // here for now
104 }
105 function getMessages() {
106   if (loadingMsg.parentNode == null) {
107     message_view.innerHTML = "";
108     message_view.appendChild(loadingMsg);
109   }
110 roomName = getTextContent(document.getElementById("rmname"));
111  var parameters = {'room':roomName, 'startmsg': startmsg, 'stopmsg': -1};
112  if (is_safe_mode) {
113    parameters['stopmsg'] = parseInt(startmsg)+500;
114    //parameters['maxmsgs'] = 500;
115    if (currentSortMode != null) {
116      var SortBy = currentSortMode[0];
117      if (SortBy.charAt(0) == 'r') {
118        SortBy = SortBy.substr(1);
119        parameters["SortOrder"] = "0";
120      }
121      parameters["SortBy"] = SortBy;
122    }
123  } 
124  if (query.length > 0) {
125    parameters["query"] = query;
126  }
127 new Ajax.Request("roommsgs", {
128     method: 'get',
129         onSuccess: loadMessages,
130         parameters: parameters,
131         sanitize: false,
132       evalJSON: false,
133       onFailure: function(e) { alert("Failure: " + e);}
134         });
135 }
136 function loadMessages(transport) {
137   try {
138   var data = eval('('+transport.responseText+')');
139   if (!!data && transport.responseText.length < 2) {
140     alert("Message loading failed");
141   } 
142   nummsgs = data['nummsgs'];
143   var msgs = data['msgs'];
144   var length = msgs.length;
145   rowArray = new Array(); // store so they can be sorted
146   WCLog("Row array length: "+rowArray.length);
147   var start = new Date();
148   for(var i=0; i<length;i++) {
149     var trElement = document.createElement("tr");
150     var data = msgs[i];
151     var msgId = data[0];
152     var rowId = "msg_" + msgId;
153     trElement.setAttribute("id",rowId);
154     //$(trElement).observe('click', CtdlMessageListClick);
155     trElement.ctdlMsgId = msgId;
156     for(var j=1; j<5;j++) { // 1=msgId (hidden), 4 date timestamp (hidden) 6 = isNew etc. 
157       var content = data[j];
158       if(content.length < 1) {
159         content = "(blank)";
160       }
161       if (j==3) {
162         trElement.ctdlDate = content;
163       } else { 
164         try {
165       var tdElement = document.createElement("td");
166       trElement.appendChild(tdElement);
167       var txtContent = document.createTextNode(content);
168       tdElement.appendChild(txtContent);
169       var x=j;
170       if (x==4) x=3;
171       var classStmt = "col"+x;
172       //tdElement.setAttribute("class", classStmt);
173       tdElement.className = classStmt;
174         } catch (e) {
175           WCLog("Error on #"+msgId +" col"+j+":"+e);
176         }
177       }
178     }
179     if (data[5]) {
180       trElement.ctdlNewMsg = true;
181     }
182     trElement.dropEnabled = true;
183     trElement.ctdlMarked = false;
184     rowArray[i] = trElement; 
185   } 
186   var end = new Date();
187   var delta = end.getTime() - start.getTime();
188     WCLog("loadMessages construct: " + delta);
189   } catch (e) {
190     //window.alert(e+"|"+e.description);
191   }
192   if (currentSortMode == null) {
193   if (sortmode.length < 1) {
194     sortmode = "rdate";
195   }
196   currentSortMode = [sortmode, sortModes[sortmode]];
197   currentSorterToggle = toggles[sortmode];
198   }
199   if (!is_safe_mode) {
200   resortAndDisplay(currentSortMode[1]);
201   } else {
202     setupPageSelector();
203     resortAndDisplay(null);
204   }
205   if (loadingMsg.parentNode != null) {
206     loadingMsg.parentNode.removeChild(loadingMsg);
207   }
208   sizePreviewPane();
209 }
210 function resortAndDisplay(sortMode) {
211   WCLog("Begin resortAndDisplay");
212   var start = new Date();
213   /* We used to try and clear out the message_view element,
214      but stupid IE doesn't even do that properly */
215   var message_view_parent = message_view.parentNode;
216   message_view_parent.removeChild(message_view);
217   message_view = document.createElement("tbody");
218   message_view.setAttribute("id","message_list_body");
219   message_view.className="mailbox_summary";
220   message_view_parent.appendChild(message_view);
221   
222   var fragment = document.createDocumentFragment();
223   if (sortMode != null) {
224     rowArray.sort(sortMode);
225   }
226   var length = rowArray.length;
227   for(var x=0; x<length; ++x) {
228     try {
229       var currentRow = rowArray[x];
230       currentRow.setAttribute("class","");
231       var className = "";
232     if (((x-1) % 2) == 0) {
233       className = "table-alt-row";
234     } else {
235       className = "table-row";
236     }
237     if (currentRow.ctdlNewMsg) {
238       className += " new_message";
239     }
240     currentRow.className = className;
241     /* Using element.onclick is evil, but until IE 
242        supports addEventListener, it is much faster
243        than prototype observe */
244     currentRow.onclick = CtdlMessageListClick;
245     currentRow.ctdlDnDElement = summaryViewDragAndDropHandler;
246     currentRow.ctdlRowId = x;
247     fragment.appendChild(currentRow);
248     } catch (e) {
249       alert("Exception" + e);
250     }
251   }
252   message_view.appendChild(fragment);
253   var end = new Date();
254     var delta = end.getTime() - start.getTime();
255     WCLog("resortAndDisplay sort and append: " + delta);
256   ApplySorterToggle();
257   normalizeHeaderTable();
258 }
259 function sortRowsByDateAscending(a, b) {
260   var dateOne = a.ctdlDate;
261   var dateTwo = b.ctdlDate;
262   return (dateOne - dateTwo);
263 }
264 function sortRowsByDateDescending(a, b) {
265   var dateOne = a.ctdlDate;
266   var dateTwo = b.ctdlDate;
267   return (dateTwo - dateOne);
268 }
269
270 function sortRowsBySubjectAscending(a, b) {
271   var subjectOne = getTextContent(a.getElementsByTagName("td")[0]).toLowerCase();
272   var subjectTwo = getTextContent(b.getElementsByTagName("td")[0]).toLowerCase();
273   return strcmp(subjectOne, subjectTwo);
274 }
275
276 function sortRowsBySubjectDescending(a, b) {
277   var subjectOne = getTextContent(a.getElementsByTagName("td")[0]).toLowerCase();
278   var subjectTwo = getTextContent(b.getElementsByTagName("td")[0]).toLowerCase();
279   return strcmp(subjectTwo, subjectOne);
280 }
281
282 function sortRowsByFromAscending(a, b) {
283   var fromOne = getTextContent(a.getElementsByTagName("td")[1]).toLowerCase();
284   var fromTwo = getTextContent(b.getElementsByTagName("td")[1]).toLowerCase();
285   return strcmp(fromOne, fromTwo);
286 }
287
288 function sortRowsByFromDescending(a, b) {
289   var fromOne = getTextContent(a.getElementsByTagName("td")[1]).toLowerCase();
290   var fromTwo = getTextContent(b.getElementsByTagName("td")[1]).toLowerCase();
291   return strcmp(fromTwo, fromOne);
292 }
293
294 function CtdlMessageListClick(evt) {
295   /* Since element.onload is used here, test to see if evt is defined */
296   var event = evt ? evt : window.event; 
297   var target = event.target ? event.target: event.srcElement; // and again..
298   var parent = target.parentNode;
299   var msgId = parent.ctdlMsgId;
300   // If the ctrl key modifier wasn't used, unmark all rows and load the message
301   if (!event.shiftKey && !event.ctrlKey && !event.altKey) {
302     unmarkAllRows();
303     markedRowId = parent.ctdlRowId;
304     document.getElementById("preview_pane").innerHTML = "";
305     new Ajax.Updater('preview_pane', 'msg/'+msgId, {method: 'get'});
306     markRow(parent);
307     new Ajax.Request('ajax_servcmd', {
308       method: 'post',
309           parameters: 'g_cmd=SEEN ' + msgId + '|1',
310           onComplete: CtdlMarkRowAsRead(parent)});
311   // If the shift key modifier is used, mark a range...
312   } else if (event.button != 2 && event.shiftKey) {
313     markRow(parent);
314     var rowId = parent.ctdlRowId;
315     var startMarkingFrom = 0;
316     var finish = 0;
317     if (rowId > markedRowId) {
318       startMarkingFrom = markedRowId+1;
319       finish = rowId;
320     } else if (rowId < markedRowId) {
321       startMarkingFrom = rowId+1;
322       finish = markedRowId;
323     } 
324     for(var x = startMarkingFrom; x<finish; x++) {
325       WCLog("Marking row "+x);
326       markRow(rowArray[x]);
327     }
328   // If the ctrl key modifier is used, toggle one message
329   } else if (event.button != 2 && (event.ctrlKey || event.altKey)) {
330     if (parent.ctdlMarked == true) {
331       unmarkRow(parent);
332     }
333     else {
334       markRow(parent);
335     }
336   }
337 }
338 function CtdlMarkRowAsRead(rowElement) {
339   var classes = rowElement.className;
340   classes = classes.replace("new_message","");
341   rowElement.className = classes;
342 }
343 function ApplySort(event) {
344   var target = event.target;
345   var sortId = target.id;
346   removeOldSortClass();
347   currentSorterToggle = target;
348   var sortModes = getSortMode(target); // returns [[key, func],[key,func]]
349   var sortModeToUse = null;
350   if (currentSortMode[0] == sortModes[0][0]) {
351     sortModeToUse = sortModes[1];
352   } else {
353     sortModeToUse = sortModes[0];
354   }
355   currentSortMode = sortModeToUse;
356   if (is_safe_mode) {
357     getMessages(); // in safe mode, we load from server already sorted
358   } else {
359   resortAndDisplay(sortModeToUse[1]);
360   }
361 }
362 function getSortMode(toggleElem) {
363   var forward = null;
364   var reverse = null;
365   for(var key in toggles) {
366     var kr = (key.charAt(0) == 'r');
367     if (toggles[key] == toggleElem && !kr) {
368       forward = [key, sortModes[key]];
369     } else if (toggles[key] == toggleElem && kr) {
370       reverse = [key, sortModes[key]];
371     }
372   }
373   return [forward, reverse];
374 }
375 function removeOldSortClass() {
376   if (currentSorterToggle) {
377     var classes = currentSorterToggle.className;
378     /* classes = classes.replace("current_sort_mode","");
379     classes = classes.replace("sort_ascending","");
380     classes = classes.replace("sort_descending",""); */
381     currentSorterToggle.className = "";
382   }
383 }
384 function markRow(row) {
385   var msgId = row.ctdlMsgId;
386   row.className = row.className += " marked_row";
387   row.ctdlMarked = true;
388   currentlyMarkedRows[msgId] = row;
389 }
390 function unmarkRow(row) {
391   var msgId = row.ctdlMsgId;
392   row.className = row.className.replace("marked_row","");
393   row.ctdlMarked = false;
394   delete currentlyMarkedRows[msgId];
395 }
396 function unmarkAllRows() {
397   for(msgId in currentlyMarkedRows) {
398     unmarkRow(currentlyMarkedRows[msgId]);
399   }
400 }
401 function deleteAllMarkedRows() {
402   for(msgId in currentlyMarkedRows) {
403     var row = currentlyMarkedRows[msgId];
404     var rowArrayId = row.ctdlRowId;
405     row.parentNode.removeChild(row);
406     delete currentlyMarkedRows[msgId];
407     delete rowArray[rowArrayId];
408   }
409   // Now we have to reconstruct rowarray as the array length has changed */
410   var newRowArray = new Array();
411   var x=0;
412   for(var i=0; i<rowArray.length; i++) {
413     var currentRow = rowArray[i];
414     if (currentRow != null) {
415       newRowArray[x] = currentRow;
416       x++;
417     }
418   }
419   rowArray = newRowArray;
420   resortAndDisplay(null);
421 }
422
423 function deleteAllSelectedMessages() {
424     for(msgId in currentlyMarkedRows) {
425       if (!room_is_trash) {
426       new Ajax.Request('ajax_servcmd', 
427                        {method: 'post',
428                            parameters: 'g_cmd=MOVE ' + msgId + '|_TRASH_|0'
429                            });
430       } else {
431         new Ajax.Request('ajax_servcmd', {method: 'post',
432               parameters: 'g_cmd=DELE '+msgId});
433       }
434     }
435     document.getElementById("preview_pane").innerHTML = "";
436     deleteAllMarkedRows();
437 }
438
439 function CtdlMessageListKeyUp(event) {
440         var key = event.which;
441         if (key == 46) { // DELETE
442                 deleteAllSelectedMessages();
443         }
444 }
445
446 function clearMessage(msgId) {
447   var row = document.getElementById('msg_'+msgId);
448   row.parentNode.removeChild(row);
449   delete currentlyMarkedRows[msgId];
450 }
451
452 function summaryViewContextMenu() {
453   if (!exitedMouseDown) {
454     var contextSource = document.getElementById("listViewContextMenu");
455     CtdlSpawnContextMenu(mouseDownEvent, contextSource);
456   }
457 }
458
459 function summaryViewDragAndDropHandler() {
460   var element = document.createElement("div");
461   var msgList = document.createElement("ul");
462   element.appendChild(msgList);
463   for(msgId in currentlyMarkedRows) {
464     msgRow = currentlyMarkedRows[msgId];
465     var subject = getTextContent(msgRow.getElementsByTagName("td")[0]);
466     var li = document.createElement("li");
467     msgList.appendChild(li);
468     setTextContent(li, subject);
469     li.ctdlMsgId = msgId;
470   }
471   return element;
472 }
473
474 var saved_y = 0;
475 function CtdlResizeMouseDown(event) {
476   $(document).observe('mousemove', CtdlResizeMouseMove);
477   $(document).observe('mouseup', CtdlResizeMouseUp);
478   saved_y = event.clientY;
479 }
480
481 function sizePreviewPane() {
482   var preview_pane = document.getElementById("preview_pane");
483   var summary_view = document.getElementById("summary_view");
484   var banner = document.getElementById("banner");
485   var message_list_hdr = document.getElementById("message_list_hdr");
486   var content = $('global');  // we'd like to use prototype methods here
487   var childElements = content.childElements();
488   var sizeOfElementsAbove = 0;
489   var heightOfViewPort = document.viewport.getHeight() // prototypejs method
490   var bannerHeight = banner.offsetHeight;
491   var contentViewPortHeight = heightOfViewPort-banner.offsetHeight-message_list_hdr.offsetHeight;
492   contentViewPortHeight = 0.98 * contentViewPortHeight; // leave some error
493   // Set summary_view to 20%;
494   var summary_height = ctdlLocalPrefs.readPref("svheight");
495   if (summary_height == null) {
496     summary_height = 0.20 * contentViewPortHeight;
497   }
498   // Set preview_pane to the remainder
499   var preview_height = contentViewPortHeight - summary_height;
500   
501   summary_view.style.height = (summary_height)+"px";
502   preview_pane.style.height = (preview_height)+"px";
503 }
504 function CtdlResizeMouseMove(event) {
505   var clientX = event.clientX;
506   var clientY = event.clientY;
507   var summary_view = document.getElementById("summary_view");
508   var summaryViewHeight = summary_view.offsetHeight;
509   var increment = clientY-saved_y;
510   var summary_view_height = increment+summaryViewHeight;
511   summary_view.style.height = (summary_view_height)+"px";
512   // store summary view height 
513   ctdlLocalPrefs.setPref("svheight",summary_view_height);
514   var msglist = document.getElementById("preview_pane");
515   var msgListHeight = msglist.offsetHeight;
516   msglist.style.height = (msgListHeight-increment)+"px";
517   saved_y = clientY;
518   /* For some reason the grippy doesn't work without position: absolute
519      so we need to set its top pos manually all the time */
520   var resize = document.getElementById("resize_msglist");
521   var resizePos = resize.offsetTop;
522   resize.style.top = (resizePos+increment)+"px";
523 }
524 function CtdlResizeMouseUp(event) {
525   $(document).stopObserving('mousemove', CtdlResizeMouseMove);
526   $(document).stopObserving('mouseup', CtdlResizeMouseUp);
527 }
528 function ApplySorterToggle() {
529   var className = currentSorterToggle.className;
530   className += " current_sort_mode";
531   if (currentSortMode[1] == sortRowsByDateDescending ||
532       currentSortMode[1] == sortRowsBySubjectDescending ||
533       currentSortMode[1] == sortRowsByFromDescending) {
534     className += " sort_descending";
535   } else {
536     className += " sort_ascending";
537   }
538   currentSorterToggle.className = className;
539 }
540 /** Hack to make the header table line up with the data */
541 function normalizeHeaderTable() {
542   var message_list_hdr = document.getElementById("message_list_hdr");
543   var summary_view = document.getElementById("summary_view");
544   var resize_msglist = document.getElementById("resize_msglist");
545   var headerTable = message_list_hdr.getElementsByTagName("table")[0];
546   var dataTable = summary_view.getElementsByTagName("table")[0];
547   var dataTableWidth = dataTable.offsetWidth;
548   headerTable.style.width = dataTableWidth+"px";
549 }
550
551 function setupPageSelector() {
552   var summpage = document.getElementById("summpage");
553   var select_page = document.getElementById("selectpage");
554   summpage.innerHTML = "";
555   if (is_safe_mode) {
556     WCLog("unhiding parent page");
557     select_page.className = "";
558   } else {
559     return;
560   }
561   var pages = nummsgs / 500;
562   for(var i=0; i<pages; i++) {
563     var opt = document.createElement("option");
564     var startmsg = i * 500;
565     opt.setAttribute("value",startmsg);
566     if (currentPage == i) {
567       opt.setAttribute("selected","selected");
568     }
569     opt.appendChild(document.createTextNode((i+1)));
570     summpage.appendChild(opt);
571   }
572 }
573 function getPage(event) {
574   var target = event.target;
575   startmsg = target.options.item(target.selectedIndex).value;
576   currentPage = target.selectedIndex;
577   //query = ""; // We are getting a page from the _entire_ msg list, don't query
578   getMessages();
579 }
580 function takeOverSearchOMatic() {
581   var searchForm = document.getElementById("searchomatic").getElementsByTagName("form")[0];
582   // First disable the form post
583   searchForm.setAttribute("action","javascript:void();");
584   searchForm.removeAttribute("method");
585   $(searchForm).observe('submit', doSearch);
586 }
587 function doSearch() {
588   query = document.getElementById("srchquery").value;
589   getMessages();
590   return false;
591 }