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