RoundCube Webmail
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

745 lines
16 KiB

  1. /*
  2. +-----------------------------------------------------------------------+
  3. | RoundCube List Widget |
  4. | |
  5. | This file is part of the RoundCube Webmail client |
  6. | Copyright (C) 2006-2008, RoundCube Dev, - Switzerland |
  7. | Licensed under the GNU GPL |
  8. | |
  9. +-----------------------------------------------------------------------+
  10. | Authors: Thomas Bruederli <roundcube@gmail.com> |
  11. | Charles McNulty <charles@charlesmcnulty.com> |
  12. +-----------------------------------------------------------------------+
  13. | Requires: common.js |
  14. +-----------------------------------------------------------------------+
  15. $Id: list.js 344 2006-09-18 03:49:28Z thomasb $
  16. */
  17. /**
  18. * RoundCube List Widget class
  19. * @contructor
  20. */
  21. function rcube_list_widget(list, p)
  22. {
  23. // static contants
  24. this.ENTER_KEY = 13;
  25. this.DELETE_KEY = 46;
  26. this.list = list ? list : null;
  27. this.frame = null;
  28. this.rows = [];
  29. this.selection = [];
  30. this.shiftkey = false;
  31. this.multiselect = false;
  32. this.draggable = false;
  33. this.keyboard = false;
  34. this.toggleselect = false;
  35. this.dont_select = false;
  36. this.drag_active = false;
  37. this.last_selected = 0;
  38. this.shift_start = 0;
  39. this.in_selection_before = false;
  40. this.focused = false;
  41. this.drag_mouse_start = null;
  42. this.dblclick_time = 600;
  43. this.row_init = function(){};
  44. this.events = { click:[], dblclick:[], select:[], keypress:[], dragstart:[], dragend:[] };
  45. // overwrite default paramaters
  46. if (p && typeof(p)=='object')
  47. for (var n in p)
  48. this[n] = p[n];
  49. }
  50. rcube_list_widget.prototype = {
  51. /**
  52. * get all message rows from HTML table and init each row
  53. */
  54. init: function()
  55. {
  56. if (this.list && this.list.tBodies[0])
  57. {
  58. this.rows = new Array();
  59. var row;
  60. for(var r=0; r<this.list.tBodies[0].childNodes.length; r++)
  61. {
  62. row = this.list.tBodies[0].childNodes[r];
  63. while (row && (row.nodeType != 1 || row.style.display == 'none'))
  64. {
  65. row = row.nextSibling;
  66. r++;
  67. }
  68. this.init_row(row);
  69. }
  70. this.frame = this.list.parentNode;
  71. // set body events
  72. if (this.keyboard)
  73. rcube_event.add_listener({element:document, event:'keydown', object:this, method:'key_press'});
  74. }
  75. },
  76. /**
  77. *
  78. */
  79. init_row: function(row)
  80. {
  81. // make references in internal array and set event handlers
  82. if (row && String(row.id).match(/rcmrow([a-z0-9\-_=]+)/i))
  83. {
  84. var p = this;
  85. var uid = RegExp.$1;
  86. row.uid = uid;
  87. this.rows[uid] = {uid:uid, id:row.id, obj:row, classname:row.className};
  88. // set eventhandlers to table row
  89. row.onmousedown = function(e){ return p.drag_row(e, this.uid); };
  90. row.onmouseup = function(e){ return p.click_row(e, this.uid); };
  91. if (document.all)
  92. row.onselectstart = function() { return false; };
  93. this.row_init(this.rows[uid]);
  94. }
  95. },
  96. /**
  97. *
  98. */
  99. clear: function(sel)
  100. {
  101. var tbody = document.createElement('TBODY');
  102. this.list.insertBefore(tbody, this.list.tBodies[0]);
  103. this.list.removeChild(this.list.tBodies[1]);
  104. this.rows = new Array();
  105. if (sel) this.clear_selection();
  106. },
  107. /**
  108. * 'remove' message row from list (just hide it)
  109. */
  110. remove_row: function(uid, sel_next)
  111. {
  112. if (this.rows[uid].obj)
  113. this.rows[uid].obj.style.display = 'none';
  114. if (sel_next)
  115. this.select_next();
  116. this.rows[uid] = null;
  117. },
  118. /**
  119. *
  120. */
  121. insert_row: function(row, attop)
  122. {
  123. var tbody = this.list.tBodies[0];
  124. if (attop && tbody.rows.length)
  125. tbody.insertBefore(row, tbody.firstChild);
  126. else
  127. tbody.appendChild(row);
  128. this.init_row(row);
  129. },
  130. /**
  131. * Set focur to the list
  132. */
  133. focus: function(e)
  134. {
  135. this.focused = true;
  136. for (var n=0; n<this.selection.length; n++)
  137. {
  138. id = this.selection[n];
  139. if (this.rows[id].obj)
  140. {
  141. this.set_classname(this.rows[id].obj, 'selected', true);
  142. this.set_classname(this.rows[id].obj, 'unfocused', false);
  143. }
  144. }
  145. if (e || (e = window.event))
  146. rcube_event.cancel(e);
  147. },
  148. /**
  149. * remove focus from the list
  150. */
  151. blur: function()
  152. {
  153. var id;
  154. this.focused = false;
  155. for (var n=0; n<this.selection.length; n++)
  156. {
  157. id = this.selection[n];
  158. if (this.rows[id] && this.rows[id].obj)
  159. {
  160. this.set_classname(this.rows[id].obj, 'selected', false);
  161. this.set_classname(this.rows[id].obj, 'unfocused', true);
  162. }
  163. }
  164. },
  165. /**
  166. * onmousedown-handler of message list row
  167. */
  168. drag_row: function(e, id)
  169. {
  170. this.in_selection_before = this.in_selection(id) ? id : false;
  171. // don't do anything (another action processed before)
  172. var evtarget = rcube_event.get_target(e);
  173. if (this.dont_select || (evtarget && (evtarget.tagName == 'INPUT' || evtarget.tagName == 'IMG')))
  174. return false;
  175. // selects currently unselected row
  176. if (!this.in_selection_before)
  177. {
  178. var mod_key = rcube_event.get_modifier(e);
  179. this.select_row(id, mod_key, false);
  180. }
  181. if (this.draggable && this.selection.length)
  182. {
  183. this.drag_start = true;
  184. this.drag_mouse_start = rcube_event.get_mouse_pos(e);
  185. rcube_event.add_listener({element:document, event:'mousemove', object:this, method:'drag_mouse_move'});
  186. rcube_event.add_listener({element:document, event:'mouseup', object:this, method:'drag_mouse_up'});
  187. }
  188. return false;
  189. },
  190. /**
  191. * onmouseup-handler of message list row
  192. */
  193. click_row: function(e, id)
  194. {
  195. var now = new Date().getTime();
  196. var mod_key = rcube_event.get_modifier(e);
  197. var evtarget = rcube_event.get_target(e);
  198. if ((evtarget && (evtarget.tagName == 'INPUT' || evtarget.tagName == 'IMG')))
  199. return false;
  200. // don't do anything (another action processed before)
  201. if (this.dont_select)
  202. {
  203. this.dont_select = false;
  204. return false;
  205. }
  206. var dblclicked = now - this.rows[id].clicked < this.dblclick_time;
  207. // unselects currently selected row
  208. if (!this.drag_active && this.in_selection_before == id && !dblclicked)
  209. this.select_row(id, mod_key, false);
  210. this.drag_start = false;
  211. this.in_selection_before = false;
  212. // row was double clicked
  213. if (this.rows && dblclicked && this.in_selection(id))
  214. this.trigger_event('dblclick');
  215. else
  216. this.trigger_event('click');
  217. if (!this.drag_active)
  218. rcube_event.cancel(e);
  219. this.rows[id].clicked = now;
  220. return false;
  221. },
  222. /**
  223. * get next and previous rows that are not hidden
  224. */
  225. get_next_row: function()
  226. {
  227. if (!this.rows)
  228. return false;
  229. var last_selected_row = this.rows[this.last_selected];
  230. var new_row = last_selected_row ? last_selected_row.obj.nextSibling : null;
  231. while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
  232. new_row = new_row.nextSibling;
  233. return new_row;
  234. },
  235. get_prev_row: function()
  236. {
  237. if (!this.rows)
  238. return false;
  239. var last_selected_row = this.rows[this.last_selected];
  240. var new_row = last_selected_row ? last_selected_row.obj.previousSibling : null;
  241. while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
  242. new_row = new_row.previousSibling;
  243. return new_row;
  244. },
  245. // selects or unselects the proper row depending on the modifier key pressed
  246. select_row: function(id, mod_key, with_mouse)
  247. {
  248. var select_before = this.selection.join(',');
  249. if (!this.multiselect)
  250. mod_key = 0;
  251. if (!this.shift_start)
  252. this.shift_start = id
  253. if (!mod_key)
  254. {
  255. this.shift_start = id;
  256. this.highlight_row(id, false);
  257. }
  258. else
  259. {
  260. switch (mod_key)
  261. {
  262. case SHIFT_KEY:
  263. this.shift_select(id, false);
  264. break;
  265. case CONTROL_KEY:
  266. if (!with_mouse)
  267. this.highlight_row(id, true);
  268. break;
  269. case CONTROL_SHIFT_KEY:
  270. this.shift_select(id, true);
  271. break;
  272. default:
  273. this.highlight_row(id, false);
  274. break;
  275. }
  276. }
  277. // trigger event if selection changed
  278. if (this.selection.join(',') != select_before)
  279. this.trigger_event('select');
  280. if (this.last_selected != 0 && this.rows[this.last_selected])
  281. this.set_classname(this.rows[this.last_selected].obj, 'focused', false);
  282. // unselect if toggleselect is active and the same row was clicked again
  283. if (this.toggleselect && this.last_selected == id)
  284. {
  285. this.clear_selection();
  286. id = null;
  287. }
  288. else
  289. this.set_classname(this.rows[id].obj, 'focused', true);
  290. if (!this.selection.length)
  291. this.shift_start = null;
  292. this.last_selected = id;
  293. },
  294. /**
  295. * Alias method for select_row
  296. */
  297. select: function(id)
  298. {
  299. this.select_row(id, false);
  300. this.scrollto(id);
  301. },
  302. /**
  303. * Select row next to the last selected one.
  304. * Either below or above.
  305. */
  306. select_next: function()
  307. {
  308. var next_row = this.get_next_row();
  309. var prev_row = this.get_prev_row();
  310. var new_row = (next_row) ? next_row : prev_row;
  311. if (new_row)
  312. this.select_row(new_row.uid, false, false);
  313. },
  314. /**
  315. * Perform selection when shift key is pressed
  316. */
  317. shift_select: function(id, control)
  318. {
  319. var from_rowIndex = this.rows[this.shift_start].obj.rowIndex;
  320. var to_rowIndex = this.rows[id].obj.rowIndex;
  321. var i = ((from_rowIndex < to_rowIndex)? from_rowIndex : to_rowIndex);
  322. var j = ((from_rowIndex > to_rowIndex)? from_rowIndex : to_rowIndex);
  323. // iterate through the entire message list
  324. for (var n in this.rows)
  325. {
  326. if ((this.rows[n].obj.rowIndex >= i) && (this.rows[n].obj.rowIndex <= j))
  327. {
  328. if (!this.in_selection(n))
  329. this.highlight_row(n, true);
  330. }
  331. else
  332. {
  333. if (this.in_selection(n) && !control)
  334. this.highlight_row(n, true);
  335. }
  336. }
  337. },
  338. /**
  339. * Check if given id is part of the current selection
  340. */
  341. in_selection: function(id)
  342. {
  343. for(var n in this.selection)
  344. if (this.selection[n]==id)
  345. return true;
  346. return false;
  347. },
  348. /**
  349. * Select each row in list
  350. */
  351. select_all: function(filter)
  352. {
  353. if (!this.rows || !this.rows.length)
  354. return false;
  355. // reset but remember selection first
  356. var select_before = this.selection.join(',');
  357. this.clear_selection();
  358. for (var n in this.rows)
  359. {
  360. if (!filter || this.rows[n][filter]==true)
  361. {
  362. this.last_selected = n;
  363. this.highlight_row(n, true);
  364. }
  365. }
  366. // trigger event if selection changed
  367. if (this.selection.join(',') != select_before)
  368. this.trigger_event('select');
  369. return true;
  370. },
  371. /**
  372. * Unselect all selected rows
  373. */
  374. clear_selection: function()
  375. {
  376. var num_select = this.selection.length;
  377. for (var n=0; n<this.selection.length; n++)
  378. if (this.rows[this.selection[n]])
  379. {
  380. this.set_classname(this.rows[this.selection[n]].obj, 'selected', false);
  381. this.set_classname(this.rows[this.selection[n]].obj, 'unfocused', false);
  382. }
  383. this.selection = new Array();
  384. if (num_select)
  385. this.trigger_event('select');
  386. },
  387. /**
  388. * Getter for the selection array
  389. */
  390. get_selection: function()
  391. {
  392. return this.selection;
  393. },
  394. /**
  395. * Return the ID if only one row is selected
  396. */
  397. get_single_selection: function()
  398. {
  399. if (this.selection.length == 1)
  400. return this.selection[0];
  401. else
  402. return null;
  403. },
  404. /**
  405. * Highlight/unhighlight a row
  406. */
  407. highlight_row: function(id, multiple)
  408. {
  409. if (this.rows[id] && !multiple)
  410. {
  411. if (!this.in_selection(id))
  412. {
  413. this.clear_selection();
  414. this.selection[0] = id;
  415. this.set_classname(this.rows[id].obj, 'selected', true);
  416. }
  417. }
  418. else if (this.rows[id])
  419. {
  420. if (!this.in_selection(id)) // select row
  421. {
  422. this.selection[this.selection.length] = id;
  423. this.set_classname(this.rows[id].obj, 'selected', true);
  424. }
  425. else // unselect row
  426. {
  427. var p = find_in_array(id, this.selection);
  428. var a_pre = this.selection.slice(0, p);
  429. var a_post = this.selection.slice(p+1, this.selection.length);
  430. this.selection = a_pre.concat(a_post);
  431. this.set_classname(this.rows[id].obj, 'selected', false);
  432. this.set_classname(this.rows[id].obj, 'unfocused', false);
  433. }
  434. }
  435. },
  436. /**
  437. * Handler for keyboard events
  438. */
  439. key_press: function(e)
  440. {
  441. if (this.focused != true)
  442. return true;
  443. var keyCode = document.layers ? e.which : document.all ? event.keyCode : document.getElementById ? e.keyCode : 0;
  444. var mod_key = rcube_event.get_modifier(e);
  445. switch (keyCode)
  446. {
  447. case 40:
  448. case 38:
  449. return this.use_arrow_key(keyCode, mod_key);
  450. break;
  451. default:
  452. this.shiftkey = e.shiftKey;
  453. this.key_pressed = keyCode;
  454. this.trigger_event('keypress');
  455. }
  456. return true;
  457. },
  458. /**
  459. * Special handling method for arrow keys
  460. */
  461. use_arrow_key: function(keyCode, mod_key)
  462. {
  463. var new_row;
  464. if (keyCode == 40) // down arrow key pressed
  465. new_row = this.get_next_row();
  466. else if (keyCode == 38) // up arrow key pressed
  467. new_row = this.get_prev_row();
  468. if (new_row)
  469. {
  470. this.select_row(new_row.uid, mod_key, true);
  471. this.scrollto(new_row.uid);
  472. }
  473. return false;
  474. },
  475. /**
  476. * Try to scroll the list to make the specified row visible
  477. */
  478. scrollto: function(id)
  479. {
  480. var row = this.rows[id].obj;
  481. if (row && this.frame)
  482. {
  483. var scroll_to = Number(row.offsetTop);
  484. if (scroll_to < Number(this.frame.scrollTop))
  485. this.frame.scrollTop = scroll_to;
  486. else if (scroll_to + Number(row.offsetHeight) > Number(this.frame.scrollTop) + Number(this.frame.offsetHeight))
  487. this.frame.scrollTop = (scroll_to + Number(row.offsetHeight)) - Number(this.frame.offsetHeight);
  488. }
  489. },
  490. /**
  491. * Handler for mouse move events
  492. */
  493. drag_mouse_move: function(e)
  494. {
  495. if (this.drag_start)
  496. {
  497. // check mouse movement, of less than 3 pixels, don't start dragging
  498. var m = rcube_event.get_mouse_pos(e);
  499. if (!this.drag_mouse_start || (Math.abs(m.x - this.drag_mouse_start.x) < 3 && Math.abs(m.y - this.drag_mouse_start.y) < 3))
  500. return false;
  501. if (!this.draglayer)
  502. this.draglayer = new rcube_layer('rcmdraglayer', {x:0, y:0, width:300, vis:0, zindex:2000});
  503. // get subjects of selectedd messages
  504. var names = '';
  505. var c, node, subject, obj;
  506. for(var n=0; n<this.selection.length; n++)
  507. {
  508. if (n>12) // only show 12 lines
  509. {
  510. names += '...';
  511. break;
  512. }
  513. if (this.rows[this.selection[n]].obj)
  514. {
  515. obj = this.rows[this.selection[n]].obj;
  516. subject = '';
  517. for(c=0; c<obj.childNodes.length; c++)
  518. if (obj.childNodes[c].nodeName=='TD' && (node = obj.childNodes[c].firstChild) && (node.nodeType==3 || node.nodeName=='A'))
  519. {
  520. subject = node.nodeType==3 ? node.data : node.innerHTML;
  521. names += (subject.length > 50 ? subject.substring(0, 50)+'...' : subject) + '<br />';
  522. break;
  523. }
  524. }
  525. }
  526. this.draglayer.write(names);
  527. this.draglayer.show(1);
  528. this.drag_active = true;
  529. this.trigger_event('dragstart');
  530. }
  531. if (this.drag_active && this.draglayer)
  532. {
  533. var pos = rcube_event.get_mouse_pos(e);
  534. this.draglayer.move(pos.x+20, pos.y-5);
  535. }
  536. this.drag_start = false;
  537. return false;
  538. },
  539. /**
  540. * Handler for mouse up events
  541. */
  542. drag_mouse_up: function(e)
  543. {
  544. document.onmousemove = null;
  545. if (this.draglayer && this.draglayer.visible)
  546. this.draglayer.show(0);
  547. this.drag_active = false;
  548. this.trigger_event('dragend');
  549. rcube_event.remove_listener({element:document, event:'mousemove', object:this, method:'drag_mouse_move'});
  550. rcube_event.remove_listener({element:document, event:'mouseup', object:this, method:'drag_mouse_up'});
  551. return rcube_event.cancel(e);
  552. },
  553. /**
  554. * set/unset a specific class name
  555. */
  556. set_classname: function(obj, classname, set)
  557. {
  558. var reg = new RegExp('\s*'+classname, 'i');
  559. if (!set && obj.className.match(reg))
  560. obj.className = obj.className.replace(reg, '');
  561. else if (set && !obj.className.match(reg))
  562. obj.className += ' '+classname;
  563. },
  564. /**
  565. * Setter for object event handlers
  566. *
  567. * @param {String} Event name
  568. * @param {Function} Handler function
  569. * @return Listener ID (used to remove this handler later on)
  570. */
  571. addEventListener: function(evt, handler)
  572. {
  573. if (this.events[evt]) {
  574. var handle = this.events[evt].length;
  575. this.events[evt][handle] = handler;
  576. return handle;
  577. }
  578. else
  579. return false;
  580. },
  581. /**
  582. * Removes a specific event listener
  583. *
  584. * @param {String} Event name
  585. * @param {Int} Listener ID to remove
  586. */
  587. removeEventListener: function(evt, handle)
  588. {
  589. if (this.events[evt] && this.events[evt][handle])
  590. this.events[evt][handle] = null;
  591. },
  592. /**
  593. * This will execute all registered event handlers
  594. * @private
  595. */
  596. trigger_event: function(evt)
  597. {
  598. if (this.events[evt] && this.events[evt].length) {
  599. for (var i=0; i<this.events[evt].length; i++)
  600. if (typeof(this.events[evt][i]) == 'function')
  601. this.events[evt][i](this);
  602. }
  603. }
  604. };