* multi-select in webcit is back, apologies to those who had to wait for it.
[citadel.git] / webcit / static / summaryview.js
1 /** Webcit Summary View v2
2     All comments, flowers and death threats to Mathew McBride
3     <matt@mcbridematt.dhs.org> / <matt@comalies>
4 */
5 document.observe("dom:loaded", createMessageView);
6
7 var message_view = null;
8 var loadingMsg = null;
9 var rowArray = null;
10 var currentSortMode = null;
11
12 // Header elements
13 var mlh_date = null;
14 var mlh_subject = null;
15 var mlh_from = null;
16 var currentSorterToggle = null;
17 var query = "";
18 var currentlyMarkedRows = new Object();
19 var markedRowId = null;
20
21 var mouseDownEvent = null;
22 var exitedMouseDown = false;
23
24 var sortModes = {
25   "rdate" : sortRowsByDateDescending,
26   "date" : sortRowsByDateAscending,
27   //  "reverse" : sortRowsByDateDescending,
28   "subj" : sortRowsBySubjectAscending,
29   "rsubj" : sortRowsBySubjectDescending,
30   "sender": sortRowsByFromAscending,
31   "rsender" : sortRowsByFromDescending
32 };
33 var toggles = {};
34
35 var nummsgs = 0;
36 var startmsg = 0;
37 function createMessageView() {
38   message_view = document.getElementById("message_list_body");
39   loadingMsg = document.getElementById("loading");
40   getMessages();
41   mlh_date = $("mlh_date");
42   mlh_subject = $('mlh_subject');
43   mlh_from = $('mlh_from');
44   toggles["rdate"] = mlh_date;
45   toggles["date"] = mlh_date;
46   // toggles["reverse"] = mlh_date;
47   toggles["subj"] = mlh_subject;
48   toggles["rsubj"] = mlh_subject;
49   toggles["sender"] = mlh_from;
50   toggles["rsender"] = mlh_from;
51   mlh_date.observe('click',ApplySort);
52   mlh_subject.observe('click',ApplySort);
53   mlh_from.observe('click',ApplySort);
54   $(document).observe('keyup',CtdlMessageListKeyUp,false);
55   window.oncontextmenu = function() { return false; };  
56   $('resize_msglist').observe('mousedown', CtdlResizeMouseDown);
57   $('m_refresh').observe('click', getMessages);
58   document.getElementById('m_refresh').setAttribute("href","#");
59   Event.observe(document.onresize ? document : window, "resize", normalizeHeaderTable);
60   sizePreviewPane();
61   Event.observe(document.onresize ? document : window, "resize", sizePreviewPane);
62   $('summpage').observe('change', getPage);
63   takeOverSearchOMatic();
64   setupDragDrop(); // here for now
65 }
66 function getMessages() {
67   if (loadingMsg.parentNode == null) {
68     message_view.innerHTML = "";
69     message_view.appendChild(loadingMsg);
70   }
71 roomName = getTextContent(document.getElementById("rmname"));
72  var parameters = {'room':roomName, 'startmsg': startmsg};
73  if (is_safe_mode) {
74    parameters['maxmsgs'] = 500;
75    if (currentSortMode != null) {
76      var SortBy = currentSortMode[0];
77      if (SortBy.charAt(0) == 'r') {
78        SortBy = SortBy.substr(1);
79        parameters["SortOrder"] = "2";
80      }
81      parameters["SortBy"] = SortBy;
82    }
83  } 
84  if (query.length > 0) {
85    parameters["query"] = query;
86  }
87 new Ajax.Request("roommsgs", {
88     method: 'get',
89         onSuccess: loadMessages,
90         parameters: parameters,
91         sanitize: false,
92       evalJSON: false,
93       onFailure: function(e) { alert("Failure: " + e);}
94         });
95 }
96 function loadMessages(transport) {
97   try {
98   var data = eval('('+transport.responseText+')');
99   if (!!data && transport.responseText.length < 2) {
100     alert("Message loading failed");
101   } 
102   nummsgs = data['nummsgs'];
103   var msgs = data['msgs'];
104   var length = msgs.length;
105   rowArray = new Array(); // store so they can be sorted
106   var start = new Date();
107   for(var i=0; i<length;i++) {
108     var trElement = document.createElement("tr");
109     var data = msgs[i];
110     var msgId = data[0];
111     var rowId = "msg_" + msgId;
112     trElement.setAttribute("id",rowId);
113     //$(trElement).observe('click', CtdlMessageListClick);
114     trElement.ctdlMsgId = msgId;
115     for(var j=1; j<5;j++) { // 1=msgId (hidden), 4 date timestamp (hidden) 6 = isNew etc. 
116       var content = data[j];
117       if(content.length < 1) {
118         content = "(blank)";
119       }
120       if (j==3) {
121         trElement.ctdlDate = content;
122       } else { 
123         try {
124       var tdElement = document.createElement("td");
125       trElement.appendChild(tdElement);
126       var txtContent = document.createTextNode(content);
127       tdElement.appendChild(txtContent);
128       var x=j;
129       if (x==4) x=3;
130       var classStmt = "col"+x;
131       tdElement.setAttribute("class", classStmt);
132         } catch (e) {
133           WCLog("Error on #"+msgId +" col"+j+":"+e);
134         }
135       }
136     }
137     if (data[5]) {
138       trElement.setAttribute("class", "new_message");
139     }
140     trElement.dropEnabled = true;
141     trElement.ctdlMarked = false;
142     rowArray[i] = trElement; 
143   } 
144   var end = new Date();
145   var delta = end.getTime() - start.getTime();
146     WCLog("loadMessages construct: " + delta);
147   } catch (e) {
148     window.alert(e);
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(sortRowsByDateDescending);
159   } else {
160     resortAndDisplay(null);
161   }
162   loadingMsg.parentNode.removeChild(loadingMsg);
163   setupPageSelector();
164   query = "";
165 }
166 function resortAndDisplay(sortMode) {
167   var start = new Date();
168   emptyElement(message_view);
169   var fragment = document.createDocumentFragment();
170   if (sortMode != null) {
171     rowArray.sort(sortMode);
172   }
173   var length = rowArray.length;
174   for(var x=0; x<length; ++x) {
175     try {
176       var currentRow = rowArray[x];
177       var className = currentRow.className;
178     className = className.replace("table-alt-row","");
179     className = className.replace("table-row","");
180     if (((x-1) % 2) == 0) {
181       className += " table-alt-row";
182     } else {
183       className += " table-row";
184     }
185     currentRow.className = className;
186     /* Using element.onclick is evil, but until IE 
187        supports addEventListener, it is much faster
188        than prototype observe */
189     currentRow.onclick = CtdlMessageListClick;
190     currentRow.ctdlDnDElement = summaryViewDragAndDropHandler;
191     currentRow.ctdlRowId = x;
192     fragment.appendChild(currentRow);
193     } catch (e) {
194       alert("Exception" + e);
195     }
196   }
197   message_view.appendChild(fragment);
198   var end = new Date();
199     var delta = end.getTime() - start.getTime();
200     WCLog("resortAndDisplay sort and append: " + delta);
201   ApplySorterToggle();
202   normalizeHeaderTable();
203 }
204 function sortRowsByDateAscending(a, b) {
205   var dateOne = a.ctdlDate;
206   var dateTwo = b.ctdlDate;
207   return (dateOne - dateTwo);
208 }
209 function sortRowsByDateDescending(a, b) {
210   var dateOne = a.ctdlDate;
211   var dateTwo = b.ctdlDate;
212   return (dateTwo - dateOne);
213 }
214
215 function sortRowsBySubjectAscending(a, b) {
216   var subjectOne = getTextContent(a.getElementsByTagName("td")[0]).toLowerCase();
217   var subjectTwo = getTextContent(b.getElementsByTagName("td")[0]).toLowerCase();
218   return (subjectOne.charCodeAt(0) - subjectTwo.charCodeAt(0));
219 }
220
221 function sortRowsBySubjectDescending(a, b) {
222   var subjectOne = getTextContent(a.getElementsByTagName("td")[0]).toLowerCase();
223   var subjectTwo = getTextContent(b.getElementsByTagName("td")[0]).toLowerCase();
224   return (subjectTwo.charCodeAt(0) - subjectOne.charCodeAt(0));
225 }
226
227 function sortRowsByFromAscending(a, b) {
228   var fromOne = getTextContent(a.getElementsByTagName("td")[1]).toLowerCase();
229   var fromTwo = getTextContent(b.getElementsByTagName("td")[1]).toLowerCase();
230   return (fromOne.charCodeAt(0) - fromTwo.charCodeAt(0));
231 }
232
233 function sortRowsByFromDescending(a, b) {
234   var fromOne = getTextContent(a.getElementsByTagName("td")[1]).toLowerCase();
235   var fromTwo = getTextContent(b.getElementsByTagName("td")[1]).toLowerCase();
236   return (fromTwo.charCodeAt(0) - fromOne.charCodeAt(0));
237 }
238
239 function CtdlMessageListClick(evt) {
240   /* Since element.onload is used here, test to see if evt is defined */
241   var event = evt ? evt : window.event; 
242   var target = event.target ? event.target: event.srcElement; // and again..
243   var parent = target.parentNode;
244   var msgId = parent.ctdlMsgId;
245   // If the ctrl key modifier wasn't used, unmark all rows and load the message
246   if (!event.shiftKey && !event.ctrlKey && !event.altKey) {
247     unmarkAllRows();
248     markedRowId = parent.ctdlRowId;
249     new Ajax.Updater('preview_pane', 'msg/'+msgId, {method: 'get'});
250     markRow(parent);
251     new Ajax.Request('ajax_servcmd', {
252       method: 'post',
253           parameters: 'g_cmd=SEEN ' + msgId + '|1',
254           onComplete: CtdlMarkRowAsRead(parent)});
255   } else if (event.button != 2 && event.shiftKey) {
256     markRow(parent);
257     var rowId = parent.ctdlRowId;
258     var startMarkingFrom = 0;
259     var finish = 0;
260     if (rowId > markedRowId) {
261       startMarkingFrom = markedRowId+1;
262       finish = rowId;
263     } else if (rowId < markedRowId) {
264       startMarkingFrom = rowId+1;
265       finish = markedRowId;
266     } 
267     for(var x = startMarkingFrom; x<finish; x++) {
268       WCLog("Marking row "+x);
269       markRow(rowArray[x]);
270     }
271   } else if (event.button != 2 && (event.ctrlKey || event.altKey)) {
272     markRow(parent);
273   }
274 }
275 function CtdlMarkRowAsRead(rowElement) {
276   var classes = rowElement.className;
277   classes = classes.replace("new_message","");
278   rowElement.className = classes;
279 }
280 function ApplySort(event) {
281   var target = event.target;
282   var sortId = target.id;
283   removeOldSortClass();
284   currentSorterToggle = target;
285   var sortModes = getSortMode(target); // returns [[key, func],[key,func]]
286   var sortModeToUse = null;
287   if (currentSortMode[0] == sortModes[0][0]) {
288     sortModeToUse = sortModes[1];
289   } else {
290     sortModeToUse = sortModes[0];
291   }
292   currentSortMode = sortModeToUse;
293   if (is_safe_mode) {
294     getMessages(); // in safe mode, we load from server already sorted
295   } else {
296   resortAndDisplay(sortModeToUse[1]);
297   }
298 }
299 function getSortMode(toggleElem) {
300   var forward = null;
301   var reverse = null;
302   for(var key in toggles) {
303     var kr = (key.charAt(0) == 'r');
304     if (toggles[key] == toggleElem && !kr) {
305       forward = [key, sortModes[key]];
306     } else if (toggles[key] == toggleElem && kr) {
307       reverse = [key, sortModes[key]];
308     }
309   }
310   return [forward, reverse];
311 }
312 function removeOldSortClass() {
313   if (currentSorterToggle) {
314     var classes = currentSorterToggle.className;
315     classes = classes.replace("current_sort_mode","");
316     classes = classes.replace("sort_ascending","");
317     classes = classes.replace("sort_descending","");
318     currentSorterToggle.className = classes;
319   }
320 }
321 function markRow( row) {
322   var msgId = row.ctdlMsgId;
323   row.className = row.className += " marked_row";
324   row.ctdlMarked = true;
325   currentlyMarkedRows[msgId] = row;
326 }
327 function unmarkRow(row) {
328   var msgId = row.ctdlMsgId;
329   row.className = row.className.replace("marked_row","");
330   row.ctdlMarked = false;
331   delete currentlyMarkedRows[msgId];
332 }
333 function unmarkAllRows() {
334   for(msgId in currentlyMarkedRows) {
335     unmarkRow(currentlyMarkedRows[msgId]);
336   }
337 }
338 function deleteAllMarkedRows() {
339   for(msgId in currentlyMarkedRows) {
340     var row = currentlyMarkedRows[msgId];
341     var rowArrayId = row.ctdlRowId;
342     row.parentNode.removeChild(row);
343     delete currentlyMarkedRows[msgId];
344     delete rowArray[rowArrayId];
345   }
346   // Now we have to reconstruct rowarray as the array length has changed */
347   var newRowArray = new Array();
348   var x=0;
349   for(var i=0; i<rowArray.length; i++) {
350     var currentRow = rowArray[i];
351     if (currentRow != null) {
352       newRowArray[x] = currentRow;
353       x++;
354     }
355   }
356   rowArray = newRowArray;
357   resortAndDisplay(null);
358 }
359 function CtdlMessageListKeyUp(event) {
360   var key = event.which;
361   if (key == 46) { // DELETE
362     for(msgId in currentlyMarkedRows) {
363       new Ajax.Request('ajax_servcmd', 
364                        {method: 'post',
365                            parameters: 'g_cmd=MOVE ' + msgId + '|_TRASH_|0'
366                            });
367     }
368     deleteAllMarkedRows();
369   }
370 }
371
372 function clearMessage(msgId) {
373   var row = document.getElementById('msg_'+msgId);
374   row.parentNode.removeChild(row);
375   delete currentlyMarkedRows[msgId];
376 }
377
378 function summaryViewContextMenu() {
379   if (!exitedMouseDown) {
380     var contextSource = document.getElementById("listViewContextMenu");
381     CtdlSpawnContextMenu(mouseDownEvent, contextSource);
382   }
383 }
384
385 function summaryViewDragAndDropHandler() {
386   var element = document.createElement("div");
387   var msgList = document.createElement("ul");
388   element.appendChild(msgList);
389   for(msgId in currentlyMarkedRows) {
390     msgRow = currentlyMarkedRows[msgId];
391     var subject = getTextContent(msgRow.getElementsByTagName("td")[0]);
392     var li = document.createElement("li");
393     msgList.appendChild(li);
394     setTextContent(li, subject);
395     li.ctdlMsgId = msgId;
396   }
397   return element;
398 }
399
400 var saved_y = 0;
401 function CtdlResizeMouseDown(event) {
402   $(document).observe('mousemove', CtdlResizeMouseMove);
403   $(document).observe('mouseup', CtdlResizeMouseUp);
404   saved_y = event.clientY;
405 }
406
407 function sizePreviewPane() {
408   var preview_pane = document.getElementById("preview_pane");
409   var content = $('content');  // we'd like to use prototype methods here
410   var childElements = content.childElements();
411   var sizeOfElementsAbove = 0;
412   var heightOfContent = content.offsetHeight;
413   for(var i=0; i<childElements.length; i++) {
414     var element = childElements[i];
415     if (element.id != 'preview_pane') {
416       var height = element.offsetHeight;
417       sizeOfElementsAbove += height;
418     }
419   }
420   preview_pane.style.height = (heightOfContent-sizeOfElementsAbove)+"px";
421 }
422 function CtdlResizeMouseMove(event) {
423   var clientX = event.clientX;
424   var clientY = event.clientY;
425   var summary_view = document.getElementById("summary_view");
426   var summaryViewHeight = summary_view.offsetHeight;
427   var increment = clientY-saved_y;
428   summary_view.style.height = (increment+summaryViewHeight)+"px";
429   var msglist = document.getElementById("preview_pane");
430   var msgListHeight = msglist.offsetHeight;
431   msglist.style.height = (msgListHeight-increment)+"px";
432   saved_y = clientY;
433   /* For some reason the grippy doesn't work without position: absolute
434      so we need to set its top pos manually all the time */
435   var resize = document.getElementById("resize_msglist");
436   var resizePos = resize.offsetTop;
437   resize.style.top = (resizePos+increment)+"px";
438 }
439 function CtdlResizeMouseUp(event) {
440   $(document).stopObserving('mousemove', CtdlResizeMouseMove);
441   $(document).stopObserving('mouseup', CtdlResizeMouseUp);
442 }
443 function ApplySorterToggle() {
444   var className = currentSorterToggle.className;
445   className += " current_sort_mode";
446   if (currentSortMode[1] == sortRowsByDateDescending ||
447       currentSortMode[1] == sortRowsBySubjectDescending ||
448       currentSortMode[1] == sortRowsByFromDescending) {
449     className += " sort_descending";
450   } else {
451     className += " sort_ascending";
452   }
453   currentSorterToggle.className = className;
454 }
455 /** Hack to make the header table line up with the data */
456 function normalizeHeaderTable() {
457   var message_list_hdr = document.getElementById("message_list_hdr");
458   var summary_view = document.getElementById("summary_view");
459   var headerTable = message_list_hdr.getElementsByTagName("table")[0];
460   var dataTable = summary_view.getElementsByTagName("table")[0];
461   var dataTableWidth = dataTable.offsetWidth;
462   headerTable.style.width = dataTableWidth+"px";
463 }
464
465 function setupPageSelector() {
466   var summpage = document.getElementById("summpage");
467   summpage.innerHTML = "";
468   if (is_safe_mode) {
469     summpage.parentNode.style.display="inline !important"; //override webcit.css
470   } else {
471     return;
472   }
473   var pages = nummsgs / 500;
474   for(var i=0; i<pages; i++) {
475     var opt = document.createElement("option");
476     var startmsg = i * 500;
477     opt.setAttribute("value",startmsg);
478     opt.appendChild(document.createTextNode((i+1)));
479     summpage.appendChild(opt);
480   }
481 }
482 function getPage(event) {
483   var target = event.target;
484   startmsg = target.options.item(target.selectedIndex).value;
485   getMessages();
486 }
487 function takeOverSearchOMatic() {
488   var searchForm = document.getElementById("searchomatic").getElementsByTagName("form")[0];
489   // First disable the form post
490   searchForm.setAttribute("action","javascript:void();");
491   searchForm.removeAttribute("method");
492   $(searchForm).observe('submit', doSearch);
493 }
494 function doSearch() {
495   query = document.getElementById("srchquery").value;
496   getMessages();
497   return false;
498 }