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.

1440 lines
34 KiB

  1. /*
  2. +-----------------------------------------------------------------------+
  3. | Roundcube List Widget |
  4. | |
  5. | This file is part of the Roundcube Webmail client |
  6. | Copyright (C) 2006-2009, The Roundcube Dev Team |
  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$
  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.BACKSPACE_KEY = 8;
  27. this.list = list ? list : null;
  28. this.frame = null;
  29. this.rows = [];
  30. this.selection = [];
  31. this.rowcount = 0;
  32. this.colcount = 0;
  33. this.subject_col = -1;
  34. this.modkey = 0;
  35. this.multiselect = false;
  36. this.multiexpand = false;
  37. this.multi_selecting = false;
  38. this.draggable = false;
  39. this.column_movable = false;
  40. this.keyboard = false;
  41. this.toggleselect = false;
  42. this.dont_select = false;
  43. this.drag_active = false;
  44. this.col_drag_active = false;
  45. this.column_fixed = null;
  46. this.last_selected = 0;
  47. this.shift_start = 0;
  48. this.in_selection_before = false;
  49. this.focused = false;
  50. this.drag_mouse_start = null;
  51. this.dblclick_time = 600;
  52. this.row_init = function(){};
  53. // overwrite default paramaters
  54. if (p && typeof p === 'object')
  55. for (var n in p)
  56. this[n] = p[n];
  57. };
  58. rcube_list_widget.prototype = {
  59. /**
  60. * get all message rows from HTML table and init each row
  61. */
  62. init: function()
  63. {
  64. if (this.list && this.list.tBodies[0]) {
  65. this.rows = [];
  66. this.rowcount = 0;
  67. var r, len, rows = this.list.tBodies[0].rows;
  68. for (r=0, len=rows.length; r<len; r++) {
  69. this.init_row(rows[r]);
  70. this.rowcount++;
  71. }
  72. this.init_header();
  73. this.frame = this.list.parentNode;
  74. // set body events
  75. if (this.keyboard) {
  76. rcube_event.add_listener({event:bw.opera?'keypress':'keydown', object:this, method:'key_press'});
  77. rcube_event.add_listener({event:'keydown', object:this, method:'key_down'});
  78. }
  79. }
  80. },
  81. /**
  82. * Init list row and set mouse events on it
  83. */
  84. init_row: function(row)
  85. {
  86. // make references in internal array and set event handlers
  87. if (row && String(row.id).match(/^rcmrow([a-z0-9\-_=\+\/]+)/i)) {
  88. var self = this,
  89. uid = RegExp.$1;
  90. row.uid = uid;
  91. this.rows[uid] = {uid:uid, id:row.id, obj:row};
  92. // set eventhandlers to table row
  93. row.onmousedown = function(e){ return self.drag_row(e, this.uid); };
  94. row.onmouseup = function(e){ return self.click_row(e, this.uid); };
  95. if (bw.iphone || bw.ipad) {
  96. row.addEventListener('touchstart', function(e) {
  97. if (e.touches.length == 1) {
  98. if (!self.drag_row(rcube_event.touchevent(e.touches[0]), this.uid))
  99. e.preventDefault();
  100. }
  101. }, false);
  102. row.addEventListener('touchend', function(e) {
  103. if (e.changedTouches.length == 1)
  104. if (!self.click_row(rcube_event.touchevent(e.changedTouches[0]), this.uid))
  105. e.preventDefault();
  106. }, false);
  107. }
  108. if (document.all)
  109. row.onselectstart = function() { return false; };
  110. this.row_init(this.rows[uid]);
  111. }
  112. },
  113. /**
  114. * Init list column headers and set mouse events on them
  115. */
  116. init_header: function()
  117. {
  118. if (this.list && this.list.tHead) {
  119. this.colcount = 0;
  120. var col, r, p = this;
  121. // add events for list columns moving
  122. if (this.column_movable && this.list.tHead && this.list.tHead.rows) {
  123. for (r=0; r<this.list.tHead.rows[0].cells.length; r++) {
  124. if (this.column_fixed == r)
  125. continue;
  126. col = this.list.tHead.rows[0].cells[r];
  127. col.onmousedown = function(e){ return p.drag_column(e, this); };
  128. this.colcount++;
  129. }
  130. }
  131. }
  132. },
  133. /**
  134. * Remove all list rows
  135. */
  136. clear: function(sel)
  137. {
  138. var tbody = document.createElement('tbody');
  139. this.list.insertBefore(tbody, this.list.tBodies[0]);
  140. this.list.removeChild(this.list.tBodies[1]);
  141. this.rows = [];
  142. this.rowcount = 0;
  143. if (sel)
  144. this.clear_selection();
  145. // reset scroll position (in Opera)
  146. if (this.frame)
  147. this.frame.scrollTop = 0;
  148. },
  149. /**
  150. * 'remove' message row from list (just hide it)
  151. */
  152. remove_row: function(uid, sel_next)
  153. {
  154. var obj = this.rows[uid] ? this.rows[uid].obj : null;
  155. if (!obj)
  156. return;
  157. obj.style.display = 'none';
  158. if (sel_next)
  159. this.select_next();
  160. delete this.rows[uid];
  161. this.rowcount--;
  162. },
  163. /**
  164. * Add row to the list and initialize it
  165. */
  166. insert_row: function(row, attop)
  167. {
  168. var tbody = this.list.tBodies[0];
  169. if (attop && tbody.rows.length)
  170. tbody.insertBefore(row, tbody.firstChild);
  171. else
  172. tbody.appendChild(row);
  173. this.init_row(row);
  174. this.rowcount++;
  175. },
  176. /**
  177. * Set focus to the list
  178. */
  179. focus: function(e)
  180. {
  181. var n, id;
  182. this.focused = true;
  183. for (n in this.selection) {
  184. id = this.selection[n];
  185. if (this.rows[id] && this.rows[id].obj) {
  186. $(this.rows[id].obj).addClass('selected').removeClass('unfocused');
  187. }
  188. }
  189. // Un-focus already focused elements
  190. $('*:focus', window).blur();
  191. $('iframe').each(function() { this.blur(); });
  192. if (e || (e = window.event))
  193. rcube_event.cancel(e);
  194. },
  195. /**
  196. * remove focus from the list
  197. */
  198. blur: function()
  199. {
  200. var n, id;
  201. this.focused = false;
  202. for (n in this.selection) {
  203. id = this.selection[n];
  204. if (this.rows[id] && this.rows[id].obj) {
  205. $(this.rows[id].obj).removeClass('selected').addClass('unfocused');
  206. }
  207. }
  208. },
  209. /**
  210. * onmousedown-handler of message list column
  211. */
  212. drag_column: function(e, col)
  213. {
  214. if (this.colcount > 1) {
  215. this.drag_start = true;
  216. this.drag_mouse_start = rcube_event.get_mouse_pos(e);
  217. rcube_event.add_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
  218. rcube_event.add_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
  219. // enable dragging over iframes
  220. this.add_dragfix();
  221. // find selected column number
  222. for (var i=0; i<this.list.tHead.rows[0].cells.length; i++) {
  223. if (col == this.list.tHead.rows[0].cells[i]) {
  224. this.selected_column = i;
  225. break;
  226. }
  227. }
  228. }
  229. return false;
  230. },
  231. /**
  232. * onmousedown-handler of message list row
  233. */
  234. drag_row: function(e, id)
  235. {
  236. // don't do anything (another action processed before)
  237. var evtarget = rcube_event.get_target(e),
  238. tagname = evtarget.tagName.toLowerCase();
  239. if (this.dont_select || (evtarget && (tagname == 'input' || tagname == 'img')))
  240. return true;
  241. // accept right-clicks
  242. if (rcube_event.get_button(e) == 2)
  243. return true;
  244. this.in_selection_before = this.in_selection(id) ? id : false;
  245. // selects currently unselected row
  246. if (!this.in_selection_before) {
  247. var mod_key = rcube_event.get_modifier(e);
  248. this.select_row(id, mod_key, false);
  249. }
  250. if (this.draggable && this.selection.length) {
  251. this.drag_start = true;
  252. this.drag_mouse_start = rcube_event.get_mouse_pos(e);
  253. rcube_event.add_listener({event:'mousemove', object:this, method:'drag_mouse_move'});
  254. rcube_event.add_listener({event:'mouseup', object:this, method:'drag_mouse_up'});
  255. if (bw.iphone || bw.ipad) {
  256. rcube_event.add_listener({event:'touchmove', object:this, method:'drag_mouse_move'});
  257. rcube_event.add_listener({event:'touchend', object:this, method:'drag_mouse_up'});
  258. }
  259. // enable dragging over iframes
  260. this.add_dragfix();
  261. }
  262. return false;
  263. },
  264. /**
  265. * onmouseup-handler of message list row
  266. */
  267. click_row: function(e, id)
  268. {
  269. var now = new Date().getTime(),
  270. mod_key = rcube_event.get_modifier(e),
  271. evtarget = rcube_event.get_target(e),
  272. tagname = evtarget.tagName.toLowerCase();
  273. if ((evtarget && (tagname == 'input' || tagname == 'img')))
  274. return true;
  275. // don't do anything (another action processed before)
  276. if (this.dont_select) {
  277. this.dont_select = false;
  278. return false;
  279. }
  280. var dblclicked = now - this.rows[id].clicked < this.dblclick_time;
  281. // unselects currently selected row
  282. if (!this.drag_active && this.in_selection_before == id && !dblclicked)
  283. this.select_row(id, mod_key, false);
  284. this.drag_start = false;
  285. this.in_selection_before = false;
  286. // row was double clicked
  287. if (this.rows && dblclicked && this.in_selection(id))
  288. this.triggerEvent('dblclick');
  289. else
  290. this.triggerEvent('click');
  291. if (!this.drag_active) {
  292. // remove temp divs
  293. this.del_dragfix();
  294. rcube_event.cancel(e);
  295. }
  296. this.rows[id].clicked = now;
  297. return false;
  298. },
  299. /*
  300. * Returns thread root ID for specified row ID
  301. */
  302. find_root: function(uid)
  303. {
  304. var r = this.rows[uid];
  305. if (r && r.parent_uid)
  306. return this.find_root(r.parent_uid);
  307. else
  308. return uid;
  309. },
  310. expand_row: function(e, id)
  311. {
  312. var row = this.rows[id],
  313. evtarget = rcube_event.get_target(e),
  314. mod_key = rcube_event.get_modifier(e);
  315. // Don't select this message
  316. this.dont_select = true;
  317. // Don't treat double click on the expando as double click on the message.
  318. row.clicked = 0;
  319. if (row.expanded) {
  320. evtarget.className = 'collapsed';
  321. if (mod_key == CONTROL_KEY || this.multiexpand)
  322. this.collapse_all(row);
  323. else
  324. this.collapse(row);
  325. }
  326. else {
  327. evtarget.className = 'expanded';
  328. if (mod_key == CONTROL_KEY || this.multiexpand)
  329. this.expand_all(row);
  330. else
  331. this.expand(row);
  332. }
  333. },
  334. collapse: function(row)
  335. {
  336. row.expanded = false;
  337. this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded });
  338. var depth = row.depth;
  339. var new_row = row ? row.obj.nextSibling : null;
  340. var r;
  341. while (new_row) {
  342. if (new_row.nodeType == 1) {
  343. var r = this.rows[new_row.uid];
  344. if (r && r.depth <= depth)
  345. break;
  346. $(new_row).css('display', 'none');
  347. if (r.expanded) {
  348. r.expanded = false;
  349. this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded });
  350. }
  351. }
  352. new_row = new_row.nextSibling;
  353. }
  354. return false;
  355. },
  356. expand: function(row)
  357. {
  358. var r, p, depth, new_row, last_expanded_parent_depth;
  359. if (row) {
  360. row.expanded = true;
  361. depth = row.depth;
  362. new_row = row.obj.nextSibling;
  363. this.update_expando(row.uid, true);
  364. this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded });
  365. }
  366. else {
  367. var tbody = this.list.tBodies[0];
  368. new_row = tbody.firstChild;
  369. depth = 0;
  370. last_expanded_parent_depth = 0;
  371. }
  372. while (new_row) {
  373. if (new_row.nodeType == 1) {
  374. r = this.rows[new_row.uid];
  375. if (r) {
  376. if (row && (!r.depth || r.depth <= depth))
  377. break;
  378. if (r.parent_uid) {
  379. p = this.rows[r.parent_uid];
  380. if (p && p.expanded) {
  381. if ((row && p == row) || last_expanded_parent_depth >= p.depth - 1) {
  382. last_expanded_parent_depth = p.depth;
  383. $(new_row).css('display', '');
  384. r.expanded = true;
  385. this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded });
  386. }
  387. }
  388. else
  389. if (row && (! p || p.depth <= depth))
  390. break;
  391. }
  392. }
  393. }
  394. new_row = new_row.nextSibling;
  395. }
  396. return false;
  397. },
  398. collapse_all: function(row)
  399. {
  400. var depth, new_row, r;
  401. if (row) {
  402. row.expanded = false;
  403. depth = row.depth;
  404. new_row = row.obj.nextSibling;
  405. this.update_expando(row.uid);
  406. this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded });
  407. // don't collapse sub-root tree in multiexpand mode
  408. if (depth && this.multiexpand)
  409. return false;
  410. }
  411. else {
  412. new_row = this.list.tBodies[0].firstChild;
  413. depth = 0;
  414. }
  415. while (new_row) {
  416. if (new_row.nodeType == 1) {
  417. if (r = this.rows[new_row.uid]) {
  418. if (row && (!r.depth || r.depth <= depth))
  419. break;
  420. if (row || r.depth)
  421. $(new_row).css('display', 'none');
  422. if (r.has_children && r.expanded) {
  423. r.expanded = false;
  424. this.update_expando(r.uid, false);
  425. this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded });
  426. }
  427. }
  428. }
  429. new_row = new_row.nextSibling;
  430. }
  431. return false;
  432. },
  433. expand_all: function(row)
  434. {
  435. var depth, new_row, r;
  436. if (row) {
  437. row.expanded = true;
  438. depth = row.depth;
  439. new_row = row.obj.nextSibling;
  440. this.update_expando(row.uid, true);
  441. this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded });
  442. }
  443. else {
  444. new_row = this.list.tBodies[0].firstChild;
  445. depth = 0;
  446. }
  447. while (new_row) {
  448. if (new_row.nodeType == 1) {
  449. if (r = this.rows[new_row.uid]) {
  450. if (row && r.depth <= depth)
  451. break;
  452. $(new_row).css('display', '');
  453. if (r.has_children && !r.expanded) {
  454. r.expanded = true;
  455. this.update_expando(r.uid, true);
  456. this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded });
  457. }
  458. }
  459. }
  460. new_row = new_row.nextSibling;
  461. }
  462. return false;
  463. },
  464. update_expando: function(uid, expanded)
  465. {
  466. var expando = document.getElementById('rcmexpando' + uid);
  467. if (expando)
  468. expando.className = expanded ? 'expanded' : 'collapsed';
  469. },
  470. /**
  471. * get first/next/previous/last rows that are not hidden
  472. */
  473. get_next_row: function()
  474. {
  475. if (!this.rows)
  476. return false;
  477. var last_selected_row = this.rows[this.last_selected],
  478. new_row = last_selected_row ? last_selected_row.obj.nextSibling : null;
  479. while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
  480. new_row = new_row.nextSibling;
  481. return new_row;
  482. },
  483. get_prev_row: function()
  484. {
  485. if (!this.rows)
  486. return false;
  487. var last_selected_row = this.rows[this.last_selected],
  488. new_row = last_selected_row ? last_selected_row.obj.previousSibling : null;
  489. while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
  490. new_row = new_row.previousSibling;
  491. return new_row;
  492. },
  493. get_first_row: function()
  494. {
  495. if (this.rowcount) {
  496. var i, len, rows = this.list.tBodies[0].rows;
  497. for (i=0, len=rows.length-1; i<len; i++)
  498. if (rows[i].id && String(rows[i].id).match(/^rcmrow([a-z0-9\-_=\+\/]+)/i) && this.rows[RegExp.$1] != null)
  499. return RegExp.$1;
  500. }
  501. return null;
  502. },
  503. get_last_row: function()
  504. {
  505. if (this.rowcount) {
  506. var i, rows = this.list.tBodies[0].rows;
  507. for (i=rows.length-1; i>=0; i--)
  508. if (rows[i].id && String(rows[i].id).match(/^rcmrow([a-z0-9\-_=\+\/]+)/i) && this.rows[RegExp.$1] != null)
  509. return RegExp.$1;
  510. }
  511. return null;
  512. },
  513. /**
  514. * selects or unselects the proper row depending on the modifier key pressed
  515. */
  516. select_row: function(id, mod_key, with_mouse)
  517. {
  518. var select_before = this.selection.join(',');
  519. if (!this.multiselect)
  520. mod_key = 0;
  521. if (!this.shift_start)
  522. this.shift_start = id
  523. if (!mod_key) {
  524. this.shift_start = id;
  525. this.highlight_row(id, false);
  526. this.multi_selecting = false;
  527. }
  528. else {
  529. switch (mod_key) {
  530. case SHIFT_KEY:
  531. this.shift_select(id, false);
  532. break;
  533. case CONTROL_KEY:
  534. if (!with_mouse)
  535. this.highlight_row(id, true);
  536. break;
  537. case CONTROL_SHIFT_KEY:
  538. this.shift_select(id, true);
  539. break;
  540. default:
  541. this.highlight_row(id, false);
  542. break;
  543. }
  544. this.multi_selecting = true;
  545. }
  546. // trigger event if selection changed
  547. if (this.selection.join(',') != select_before)
  548. this.triggerEvent('select');
  549. if (this.last_selected != 0 && this.rows[this.last_selected])
  550. $(this.rows[this.last_selected].obj).removeClass('focused');
  551. // unselect if toggleselect is active and the same row was clicked again
  552. if (this.toggleselect && this.last_selected == id) {
  553. this.clear_selection();
  554. id = null;
  555. }
  556. else
  557. $(this.rows[id].obj).addClass('focused');
  558. if (!this.selection.length)
  559. this.shift_start = null;
  560. this.last_selected = id;
  561. },
  562. /**
  563. * Alias method for select_row
  564. */
  565. select: function(id)
  566. {
  567. this.select_row(id, false);
  568. this.scrollto(id);
  569. },
  570. /**
  571. * Select row next to the last selected one.
  572. * Either below or above.
  573. */
  574. select_next: function()
  575. {
  576. var next_row = this.get_next_row(),
  577. prev_row = this.get_prev_row(),
  578. new_row = (next_row) ? next_row : prev_row;
  579. if (new_row)
  580. this.select_row(new_row.uid, false, false);
  581. },
  582. /**
  583. * Select first row
  584. */
  585. select_first: function(mod_key)
  586. {
  587. var row = this.get_first_row();
  588. if (row) {
  589. if (mod_key) {
  590. this.shift_select(row, mod_key);
  591. this.triggerEvent('select');
  592. this.scrollto(row);
  593. }
  594. else {
  595. this.select(row);
  596. }
  597. }
  598. },
  599. /**
  600. * Select last row
  601. */
  602. select_last: function(mod_key)
  603. {
  604. var row = this.get_last_row();
  605. if (row) {
  606. if (mod_key) {
  607. this.shift_select(row, mod_key);
  608. this.triggerEvent('select');
  609. this.scrollto(row);
  610. }
  611. else {
  612. this.select(row);
  613. }
  614. }
  615. },
  616. /**
  617. * Add all childs of the given row to selection
  618. */
  619. select_childs: function(uid)
  620. {
  621. if (!this.rows[uid] || !this.rows[uid].has_children)
  622. return;
  623. var depth = this.rows[uid].depth,
  624. row = this.rows[uid].obj.nextSibling;
  625. while (row) {
  626. if (row.nodeType == 1) {
  627. if ((r = this.rows[row.uid])) {
  628. if (!r.depth || r.depth <= depth)
  629. break;
  630. if (!this.in_selection(r.uid))
  631. this.select_row(r.uid, CONTROL_KEY);
  632. }
  633. }
  634. row = row.nextSibling;
  635. }
  636. },
  637. /**
  638. * Perform selection when shift key is pressed
  639. */
  640. shift_select: function(id, control)
  641. {
  642. if (!this.rows[this.shift_start] || !this.selection.length)
  643. this.shift_start = id;
  644. var n, from_rowIndex = this.rows[this.shift_start].obj.rowIndex,
  645. to_rowIndex = this.rows[id].obj.rowIndex,
  646. i = ((from_rowIndex < to_rowIndex)? from_rowIndex : to_rowIndex),
  647. j = ((from_rowIndex > to_rowIndex)? from_rowIndex : to_rowIndex);
  648. // iterate through the entire message list
  649. for (n in this.rows) {
  650. if (this.rows[n].obj.rowIndex >= i && this.rows[n].obj.rowIndex <= j) {
  651. if (!this.in_selection(n)) {
  652. this.highlight_row(n, true);
  653. }
  654. }
  655. else {
  656. if (this.in_selection(n) && !control) {
  657. this.highlight_row(n, true);
  658. }
  659. }
  660. }
  661. },
  662. /**
  663. * Check if given id is part of the current selection
  664. */
  665. in_selection: function(id)
  666. {
  667. for (var n in this.selection)
  668. if (this.selection[n]==id)
  669. return true;
  670. return false;
  671. },
  672. /**
  673. * Select each row in list
  674. */
  675. select_all: function(filter)
  676. {
  677. if (!this.rows || !this.rows.length)
  678. return false;
  679. // reset but remember selection first
  680. var n, select_before = this.selection.join(',');
  681. this.selection = [];
  682. for (n in this.rows) {
  683. if (!filter || this.rows[n][filter] == true) {
  684. this.last_selected = n;
  685. this.highlight_row(n, true);
  686. }
  687. else {
  688. $(this.rows[n].obj).removeClass('selected').removeClass('unfocused');
  689. }
  690. }
  691. // trigger event if selection changed
  692. if (this.selection.join(',') != select_before)
  693. this.triggerEvent('select');
  694. this.focus();
  695. return true;
  696. },
  697. /**
  698. * Invert selection
  699. */
  700. invert_selection: function()
  701. {
  702. if (!this.rows || !this.rows.length)
  703. return false;
  704. // remember old selection
  705. var n, select_before = this.selection.join(',');
  706. for (n in this.rows)
  707. this.highlight_row(n, true);
  708. // trigger event if selection changed
  709. if (this.selection.join(',') != select_before)
  710. this.triggerEvent('select');
  711. this.focus();
  712. return true;
  713. },
  714. /**
  715. * Unselect selected row(s)
  716. */
  717. clear_selection: function(id)
  718. {
  719. var n, num_select = this.selection.length;
  720. // one row
  721. if (id) {
  722. for (n in this.selection)
  723. if (this.selection[n] == id) {
  724. this.selection.splice(n,1);
  725. break;
  726. }
  727. }
  728. // all rows
  729. else {
  730. for (n in this.selection)
  731. if (this.rows[this.selection[n]]) {
  732. $(this.rows[this.selection[n]].obj).removeClass('selected').removeClass('unfocused');
  733. }
  734. this.selection = [];
  735. }
  736. if (num_select && !this.selection.length)
  737. this.triggerEvent('select');
  738. },
  739. /**
  740. * Getter for the selection array
  741. */
  742. get_selection: function()
  743. {
  744. return this.selection;
  745. },
  746. /**
  747. * Return the ID if only one row is selected
  748. */
  749. get_single_selection: function()
  750. {
  751. if (this.selection.length == 1)
  752. return this.selection[0];
  753. else
  754. return null;
  755. },
  756. /**
  757. * Highlight/unhighlight a row
  758. */
  759. highlight_row: function(id, multiple)
  760. {
  761. if (this.rows[id] && !multiple) {
  762. if (this.selection.length > 1 || !this.in_selection(id)) {
  763. this.clear_selection();
  764. this.selection[0] = id;
  765. $(this.rows[id].obj).addClass('selected');
  766. }
  767. }
  768. else if (this.rows[id]) {
  769. if (!this.in_selection(id)) { // select row
  770. this.selection[this.selection.length] = id;
  771. $(this.rows[id].obj).addClass('selected');
  772. }
  773. else { // unselect row
  774. var p = $.inArray(id, this.selection),
  775. a_pre = this.selection.slice(0, p),
  776. a_post = this.selection.slice(p+1, this.selection.length);
  777. this.selection = a_pre.concat(a_post);
  778. $(this.rows[id].obj).removeClass('selected').removeClass('unfocused');
  779. }
  780. }
  781. },
  782. /**
  783. * Handler for keyboard events
  784. */
  785. key_press: function(e)
  786. {
  787. var target = e.target || {};
  788. if (this.focused != true || target.nodeName == 'INPUT' || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')
  789. return true;
  790. var keyCode = rcube_event.get_keycode(e),
  791. mod_key = rcube_event.get_modifier(e);
  792. switch (keyCode) {
  793. case 40:
  794. case 38:
  795. case 63233: // "down", in safari keypress
  796. case 63232: // "up", in safari keypress
  797. // Stop propagation so that the browser doesn't scroll
  798. rcube_event.cancel(e);
  799. return this.use_arrow_key(keyCode, mod_key);
  800. case 61:
  801. case 107: // Plus sign on a numeric keypad (fc11 + firefox 3.5.2)
  802. case 109:
  803. case 32:
  804. // Stop propagation
  805. rcube_event.cancel(e);
  806. var ret = this.use_plusminus_key(keyCode, mod_key);
  807. this.key_pressed = keyCode;
  808. this.modkey = mod_key;
  809. this.triggerEvent('keypress');
  810. this.modkey = 0;
  811. return ret;
  812. case 36: // Home
  813. this.select_first(mod_key);
  814. return rcube_event.cancel(e);
  815. case 35: // End
  816. this.select_last(mod_key);
  817. return rcube_event.cancel(e);
  818. default:
  819. this.key_pressed = keyCode;
  820. this.modkey = mod_key;
  821. this.triggerEvent('keypress');
  822. this.modkey = 0;
  823. if (this.key_pressed == this.BACKSPACE_KEY)
  824. return rcube_event.cancel(e);
  825. }
  826. return true;
  827. },
  828. /**
  829. * Handler for keydown events
  830. */
  831. key_down: function(e)
  832. {
  833. var target = e.target || {};
  834. if (this.focused != true || target.nodeName == 'INPUT' || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')
  835. return true;
  836. switch (rcube_event.get_keycode(e)) {
  837. case 27:
  838. if (this.drag_active)
  839. return this.drag_mouse_up(e);
  840. if (this.col_drag_active) {
  841. this.selected_column = null;
  842. return this.column_drag_mouse_up(e);
  843. }
  844. case 40:
  845. case 38:
  846. case 63233:
  847. case 63232:
  848. case 61:
  849. case 107:
  850. case 109:
  851. case 32:
  852. if (!rcube_event.get_modifier(e) && this.focused)
  853. return rcube_event.cancel(e);
  854. default:
  855. }
  856. return true;
  857. },
  858. /**
  859. * Special handling method for arrow keys
  860. */
  861. use_arrow_key: function(keyCode, mod_key)
  862. {
  863. var new_row;
  864. // Safari uses the nonstandard keycodes 63232/63233 for up/down, if we're
  865. // using the keypress event (but not the keydown or keyup event).
  866. if (keyCode == 40 || keyCode == 63233) // down arrow key pressed
  867. new_row = this.get_next_row();
  868. else if (keyCode == 38 || keyCode == 63232) // up arrow key pressed
  869. new_row = this.get_prev_row();
  870. if (new_row) {
  871. this.select_row(new_row.uid, mod_key, false);
  872. this.scrollto(new_row.uid);
  873. }
  874. return false;
  875. },
  876. /**
  877. * Special handling method for +/- keys
  878. */
  879. use_plusminus_key: function(keyCode, mod_key)
  880. {
  881. var selected_row = this.rows[this.last_selected];
  882. if (!selected_row)
  883. return;
  884. if (keyCode == 32)
  885. keyCode = selected_row.expanded ? 109 : 61;
  886. if (keyCode == 61 || keyCode == 107)
  887. if (mod_key == CONTROL_KEY || this.multiexpand)
  888. this.expand_all(selected_row);
  889. else
  890. this.expand(selected_row);
  891. else
  892. if (mod_key == CONTROL_KEY || this.multiexpand)
  893. this.collapse_all(selected_row);
  894. else
  895. this.collapse(selected_row);
  896. this.update_expando(selected_row.uid, selected_row.expanded);
  897. return false;
  898. },
  899. /**
  900. * Try to scroll the list to make the specified row visible
  901. */
  902. scrollto: function(id)
  903. {
  904. var row = this.rows[id].obj;
  905. if (row && this.frame) {
  906. var scroll_to = Number(row.offsetTop);
  907. // expand thread if target row is hidden (collapsed)
  908. if (!scroll_to && this.rows[id].parent_uid) {
  909. var parent = this.find_root(this.rows[id].uid);
  910. this.expand_all(this.rows[parent]);
  911. scroll_to = Number(row.offsetTop);
  912. }
  913. if (scroll_to < Number(this.frame.scrollTop))
  914. this.frame.scrollTop = scroll_to;
  915. else if (scroll_to + Number(row.offsetHeight) > Number(this.frame.scrollTop) + Number(this.frame.offsetHeight))
  916. this.frame.scrollTop = (scroll_to + Number(row.offsetHeight)) - Number(this.frame.offsetHeight);
  917. }
  918. },
  919. /**
  920. * Handler for mouse move events
  921. */
  922. drag_mouse_move: function(e)
  923. {
  924. // convert touch event
  925. if (e.type == 'touchmove') {
  926. if (e.changedTouches.length == 1)
  927. e = rcube_event.touchevent(e.changedTouches[0]);
  928. else
  929. return rcube_event.cancel(e);
  930. }
  931. if (this.drag_start) {
  932. // check mouse movement, of less than 3 pixels, don't start dragging
  933. var m = rcube_event.get_mouse_pos(e);
  934. 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))
  935. return false;
  936. if (!this.draglayer)
  937. this.draglayer = $('<div>').attr('id', 'rcmdraglayer')
  938. .css({ position:'absolute', display:'none', 'z-index':2000 })
  939. .appendTo(document.body);
  940. // also select childs of (collapsed) threads for dragging
  941. var n, uid, selection = $.merge([], this.selection);
  942. for (n in selection) {
  943. uid = selection[n];
  944. if (this.rows[uid].has_children && !this.rows[uid].expanded)
  945. this.select_childs(uid);
  946. }
  947. // reset content
  948. this.draglayer.html('');
  949. // get subjects of selected messages
  950. var c, i, n, subject, obj;
  951. for (n=0; n<this.selection.length; n++) {
  952. // only show 12 lines
  953. if (n>12) {
  954. this.draglayer.append('...');
  955. break;
  956. }
  957. if (obj = this.rows[this.selection[n]].obj) {
  958. subject = '';
  959. for (c=0, i=0; i<obj.childNodes.length; i++) {
  960. if (obj.childNodes[i].nodeName == 'TD') {
  961. if (n == 0)
  962. this.drag_start_pos = $(obj.childNodes[i]).offset();
  963. if (this.subject_col < 0 || (this.subject_col >= 0 && this.subject_col == c)) {
  964. var entry, node, tmp_node, nodes = obj.childNodes[i].childNodes;
  965. // find text node
  966. for (m=0; m<nodes.length; m++) {
  967. if ((tmp_node = obj.childNodes[i].childNodes[m]) && (tmp_node.nodeType==3 || tmp_node.nodeName=='A'))
  968. node = tmp_node;
  969. }
  970. if (!node)
  971. break;
  972. subject = $(node).text();
  973. // remove leading spaces
  974. subject = $.trim(subject);
  975. // truncate line to 50 characters
  976. subject = (subject.length > 50 ? subject.substring(0, 50) + '...' : subject);
  977. entry = $('<div>').text(subject);
  978. this.draglayer.append(entry);
  979. break;
  980. }
  981. c++;
  982. }
  983. }
  984. }
  985. }
  986. this.draglayer.show();
  987. this.drag_active = true;
  988. this.triggerEvent('dragstart');
  989. }
  990. if (this.drag_active && this.draglayer) {
  991. var pos = rcube_event.get_mouse_pos(e);
  992. this.draglayer.css({ left:(pos.x+20)+'px', top:(pos.y-5 + (bw.ie ? document.documentElement.scrollTop : 0))+'px' });
  993. this.triggerEvent('dragmove', e?e:window.event);
  994. }
  995. this.drag_start = false;
  996. return false;
  997. },
  998. /**
  999. * Handler for mouse up events
  1000. */
  1001. drag_mouse_up: function(e)
  1002. {
  1003. document.onmousemove = null;
  1004. if (e.type == 'touchend') {
  1005. if (e.changedTouches.length != 1)
  1006. return rcube_event.cancel(e);
  1007. }
  1008. if (this.draglayer && this.draglayer.is(':visible')) {
  1009. if (this.drag_start_pos)
  1010. this.draglayer.animate(this.drag_start_pos, 300, 'swing').hide(20);
  1011. else
  1012. this.draglayer.hide();
  1013. }
  1014. if (this.drag_active)
  1015. this.focus();
  1016. this.drag_active = false;
  1017. rcube_event.remove_listener({event:'mousemove', object:this, method:'drag_mouse_move'});
  1018. rcube_event.remove_listener({event:'mouseup', object:this, method:'drag_mouse_up'});
  1019. if (bw.iphone || bw.ipad) {
  1020. rcube_event.remove_listener({event:'touchmove', object:this, method:'drag_mouse_move'});
  1021. rcube_event.remove_listener({event:'touchend', object:this, method:'drag_mouse_up'});
  1022. }
  1023. // remove temp divs
  1024. this.del_dragfix();
  1025. this.triggerEvent('dragend');
  1026. return rcube_event.cancel(e);
  1027. },
  1028. /**
  1029. * Handler for mouse move events for dragging list column
  1030. */
  1031. column_drag_mouse_move: function(e)
  1032. {
  1033. if (this.drag_start) {
  1034. // check mouse movement, of less than 3 pixels, don't start dragging
  1035. var i, m = rcube_event.get_mouse_pos(e);
  1036. 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))
  1037. return false;
  1038. if (!this.col_draglayer) {
  1039. var lpos = $(this.list).offset(),
  1040. cells = this.list.tHead.rows[0].cells;
  1041. // create dragging layer
  1042. this.col_draglayer = $('<div>').attr('id', 'rcmcoldraglayer')
  1043. .css(lpos).css({ position:'absolute', 'z-index':2001,
  1044. 'background-color':'white', opacity:0.75,
  1045. height: (this.frame.offsetHeight-2)+'px', width: (this.frame.offsetWidth-2)+'px' })
  1046. .appendTo(document.body)
  1047. // ... and column position indicator
  1048. .append($('<div>').attr('id', 'rcmcolumnindicator')
  1049. .css({ position:'absolute', 'border-right':'2px dotted #555',
  1050. 'z-index':2002, height: (this.frame.offsetHeight-2)+'px' }));
  1051. this.cols = [];
  1052. this.list_pos = this.list_min_pos = lpos.left;
  1053. // save columns positions
  1054. for (i=0; i<cells.length; i++) {
  1055. this.cols[i] = cells[i].offsetWidth;
  1056. if (this.column_fixed !== null && i <= this.column_fixed) {
  1057. this.list_min_pos += this.cols[i];
  1058. }
  1059. }
  1060. }
  1061. this.col_draglayer.show();
  1062. this.col_drag_active = true;
  1063. this.triggerEvent('column_dragstart');
  1064. }
  1065. // set column indicator position
  1066. if (this.col_drag_active && this.col_draglayer) {
  1067. var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
  1068. for (i=0; i<this.cols.length; i++) {
  1069. if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
  1070. cpos += this.cols[i];
  1071. else
  1072. break;
  1073. }
  1074. // handle fixed columns on left
  1075. if (i == 0 && this.list_min_pos > pos.x)
  1076. cpos = this.list_min_pos - this.list_pos;
  1077. // empty list needs some assignment
  1078. else if (!this.list.rowcount && i == this.cols.length)
  1079. cpos -= 2;
  1080. $('#rcmcolumnindicator').css({ width: cpos+'px'});
  1081. this.triggerEvent('column_dragmove', e?e:window.event);
  1082. }
  1083. this.drag_start = false;
  1084. return false;
  1085. },
  1086. /**
  1087. * Handler for mouse up events for dragging list columns
  1088. */
  1089. column_drag_mouse_up: function(e)
  1090. {
  1091. document.onmousemove = null;
  1092. if (this.col_draglayer) {
  1093. (this.col_draglayer).remove();
  1094. this.col_draglayer = null;
  1095. }
  1096. if (this.col_drag_active)
  1097. this.focus();
  1098. this.col_drag_active = false;
  1099. rcube_event.remove_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
  1100. rcube_event.remove_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
  1101. // remove temp divs
  1102. this.del_dragfix();
  1103. if (this.selected_column !== null && this.cols && this.cols.length) {
  1104. var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
  1105. // find destination position
  1106. for (i=0; i<this.cols.length; i++) {
  1107. if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
  1108. cpos += this.cols[i];
  1109. else
  1110. break;
  1111. }
  1112. if (i != this.selected_column && i != this.selected_column+1) {
  1113. this.column_replace(this.selected_column, i);
  1114. }
  1115. }
  1116. this.triggerEvent('column_dragend');
  1117. return rcube_event.cancel(e);
  1118. },
  1119. /**
  1120. * Creates a layer for drag&drop over iframes
  1121. */
  1122. add_dragfix: function()
  1123. {
  1124. $('iframe').each(function() {
  1125. $('<div class="iframe-dragdrop-fix"></div>')
  1126. .css({background: '#fff',
  1127. width: this.offsetWidth+'px', height: this.offsetHeight+'px',
  1128. position: 'absolute', opacity: '0.001', zIndex: 1000
  1129. })
  1130. .css($(this).offset())
  1131. .appendTo(document.body);
  1132. });
  1133. },
  1134. /**
  1135. * Removes the layer for drag&drop over iframes
  1136. */
  1137. del_dragfix: function()
  1138. {
  1139. $('div.iframe-dragdrop-fix').each(function() { this.parentNode.removeChild(this); });
  1140. },
  1141. /**
  1142. * Replaces two columns
  1143. */
  1144. column_replace: function(from, to)
  1145. {
  1146. var len, cells = this.list.tHead.rows[0].cells,
  1147. elem = cells[from],
  1148. before = cells[to],
  1149. td = document.createElement('td');
  1150. // replace header cells
  1151. if (before)
  1152. cells[0].parentNode.insertBefore(td, before);
  1153. else
  1154. cells[0].parentNode.appendChild(td);
  1155. cells[0].parentNode.replaceChild(elem, td);
  1156. // replace list cells
  1157. for (r=0, len=this.list.tBodies[0].rows.length; r<len; r++) {
  1158. row = this.list.tBodies[0].rows[r];
  1159. elem = row.cells[from];
  1160. before = row.cells[to];
  1161. td = document.createElement('td');
  1162. if (before)
  1163. row.insertBefore(td, before);
  1164. else
  1165. row.appendChild(td);
  1166. row.replaceChild(elem, td);
  1167. }
  1168. // update subject column position
  1169. if (this.subject_col == from)
  1170. this.subject_col = to > from ? to - 1 : to;
  1171. else if (this.subject_col < from && to <= this.subject_col)
  1172. this.subject_col++;
  1173. else if (this.subject_col > from && to >= this.subject_col)
  1174. this.subject_col--;
  1175. this.triggerEvent('column_replace');
  1176. }
  1177. };
  1178. rcube_list_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
  1179. rcube_list_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
  1180. rcube_list_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;