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