1. 1 : /**
  2. 2 : * Roundcube List Widget
  3. 3 : *
  4. 4 : * This file is part of the Roundcube Webmail client
  5. 5 : *
  6. 6 : * @licstart The following is the entire license notice for the
  7. 7 : * JavaScript code in this file.
  8. 8 : *
  9. 9 : * Copyright (c) The Roundcube Dev Team
  10. 10 : *
  11. 11 : * The JavaScript code in this page is free software: you can
  12. 12 : * redistribute it and/or modify it under the terms of the GNU
  13. 13 : * General Public License (GNU GPL) as published by the Free Software
  14. 14 : * Foundation, either version 3 of the License, or (at your option)
  15. 15 : * any later version. The code is distributed WITHOUT ANY WARRANTY;
  16. 16 : * without even the implied warranty of MERCHANTABILITY or FITNESS
  17. 17 : * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
  18. 18 : *
  19. 19 : * As additional permission under GNU GPL version 3 section 7, you
  20. 20 : * may distribute non-source (e.g., minimized or compacted) forms of
  21. 21 : * that code without the copy of the GNU GPL normally required by
  22. 22 : * section 4, provided you include this license notice and a URL
  23. 23 : * through which recipients can access the Corresponding Source.
  24. 24 : *
  25. 25 : * @licend The above is the entire license notice
  26. 26 : * for the JavaScript code in this file.
  27. 27 : *
  28. 28 : * @author Thomas Bruederli <roundcube@gmail.com>
  29. 29 : * @author Charles McNulty <charles@charlesmcnulty.com>
  30. 30 : *
  31. 31 : * @requires jquery.js, common.js
  32. 32 : */
  33. 33 :
  34. 34 :
  35. 35 : /**
  36. 36 : * Roundcube List Widget class
  37. 37 : * @constructor
  38. 38 : */
  39. 39 : function rcube_list_widget(list, p)
  40. 40 : {
  41. 41 : // static constants
  42. 42 : this.ENTER_KEY = 13;
  43. 43 : this.DELETE_KEY = 46;
  44. 44 : this.BACKSPACE_KEY = 8;
  45. 45 :
  46. 46 : this.list = list ? list : null;
  47. 47 : this.tagname = this.list ? this.list.nodeName.toLowerCase() : 'table';
  48. 48 : this.id_regexp = /^rcmrow([a-z0-9\-_=\+\/]+)/i;
  49. 49 : this.rows = {};
  50. 50 : this.selection = [];
  51. 51 : this.rowcount = 0;
  52. 52 : this.colcount = 0;
  53. 53 :
  54. 54 : this.subject_col = 0;
  55. 55 : this.modkey = 0;
  56. 56 : this.multiselect = false;
  57. 57 : this.multiexpand = false;
  58. 58 : this.multi_selecting = false;
  59. 59 : this.draggable = false;
  60. 60 : this.column_movable = false;
  61. 61 : this.keyboard = false;
  62. 62 : this.toggleselect = false;
  63. 63 : this.aria_listbox = false;
  64. 64 : this.parent_focus = true;
  65. 65 : this.checkbox_selection = false;
  66. 66 :
  67. 67 : this.drag_active = false;
  68. 68 : this.col_drag_active = false;
  69. 69 : this.column_fixed = null;
  70. 70 : this.last_selected = null;
  71. 71 : this.shift_start = null;
  72. 72 : this.focused = false;
  73. 73 : this.drag_mouse_start = null;
  74. 74 : this.dblclick_time = 500; // default value on MS Windows is 500
  75. 75 : this.row_init = function(){}; // @deprecated; use list.addEventListener('initrow') instead
  76. 76 :
  77. 77 : this.touch_start_time = 0; // start time of the touch event
  78. 78 : this.touch_event_time = 500; // maximum time a touch should be considered a left mouse button event, after this its something else (eg contextmenu event)
  79. 79 :
  80. 80 : // overwrite default parameters
  81. 81 : if (p && typeof p === 'object')
  82. 82 : for (var n in p)
  83. 83 : this[n] = p[n];
  84. 84 :
  85. 85 : // register this instance
  86. 86 : rcube_list_widget._instances.push(this);
  87. 87 : };
  88. 88 :
  89. 89 :
  90. 90 : rcube_list_widget.prototype = {
  91. 91 :
  92. 92 :
  93. 93 : /**
  94. 94 : * get all message rows from HTML table and init each row
  95. 95 : */
  96. 96 : init: function()
  97. 97 : {
  98. 98 : if (this.tagname == 'table' && this.list && this.list.tBodies[0]) {
  99. 99 : this.thead = this.list.tHead;
  100. 100 : this.tbody = this.list.tBodies[0];
  101. 101 : }
  102. 102 : else if (this.tagname != 'table' && this.list) {
  103. 103 : this.tbody = this.list;
  104. 104 : }
  105. 105 :
  106. 106 : if ($(this.list).attr('role') == 'listbox') {
  107. 107 : this.aria_listbox = true;
  108. 108 : if (this.multiselect)
  109. 109 : $(this.list).attr('aria-multiselectable', 'true');
  110. 110 : }
  111. 111 :
  112. 112 : var me = this;
  113. 113 :
  114. 114 : if (this.tbody) {
  115. 115 : this.rows = {};
  116. 116 : this.rowcount = 0;
  117. 117 :
  118. 118 : var r, len, rows = this.tbody.childNodes;
  119. 119 :
  120. 120 : for (r=0, len=rows.length; r<len; r++) {
  121. 121 : if (rows[r].nodeType == 1)
  122. 122 : this.rowcount += this.init_row(rows[r]) ? 1 : 0;
  123. 123 : }
  124. 124 :
  125. 125 : this.init_header();
  126. 126 : this.frame = this.list.parentNode;
  127. 127 :
  128. 128 : // set body events
  129. 129 : if (this.keyboard) {
  130. 130 : rcube_event.add_listener({event:'keydown', object:this, method:'key_press'});
  131. 131 :
  132. 132 : // allow the table element to receive focus.
  133. 133 : $(this.list).attr('tabindex', '0')
  134. 134 : .on('focus', function(e) { me.focus(e); });
  135. 135 : }
  136. 136 : }
  137. 137 :
  138. 138 : if (this.parent_focus) {
  139. 139 : this.list.parentNode.onclick = function(e) { me.focus(); };
  140. 140 : }
  141. 141 :
  142. 142 : rcmail.triggerEvent('initlist', { obj: this.list });
  143. 143 :
  144. 144 : return this;
  145. 145 : },
  146. 146 :
  147. 147 :
  148. 148 : /**
  149. 149 : * Init list row and set mouse events on it
  150. 150 : */
  151. 151 : init_row: function(row)
  152. 152 : {
  153. 153 : row.uid = this.get_row_uid(row);
  154. 154 :
  155. 155 : // make references in internal array and set event handlers
  156. 156 : if (row && row.uid) {
  157. 157 : var self = this, uid = row.uid;
  158. 158 : this.rows[uid] = {uid:uid, id:row.id, obj:row};
  159. 159 :
  160. 160 : $(row).data('uid', uid)
  161. 161 : // set eventhandlers to table row (only left-button-clicks in mouseup)
  162. 162 : .mousedown(function(e) { return self.drag_row(e, this.uid); })
  163. 163 : .mouseup(function(e) {
  164. 164 : if (e.which == 1 && !self.drag_active && !$(e.currentTarget).is('.ui-droppable-active'))
  165. 165 : return self.click_row(e, this.uid);
  166. 166 : else
  167. 167 : return true;
  168. 168 : });
  169. 169 :
  170. 170 : // for IE and Edge (Trident) differentiate between touch, touch+hold using pointer events rather than touch
  171. 171 : if ((bw.ie || (bw.edge && bw.vendver < 75)) && bw.pointer) {
  172. 172 : $(row).on('pointerdown', function(e) {
  173. 173 : if (e.pointerType == 'touch') {
  174. 174 : self.touch_start_time = new Date().getTime();
  175. 175 : return false;
  176. 176 : }
  177. 177 : })
  178. 178 : .on('pointerup', function(e) {
  179. 179 : if (e.pointerType == 'touch') {
  180. 180 : var duration = (new Date().getTime() - self.touch_start_time);
  181. 181 : if (duration <= self.touch_event_time) {
  182. 182 : self.drag_row(e, this.uid);
  183. 183 : return self.click_row(e, this.uid);
  184. 184 : }
  185. 185 : }
  186. 186 : });
  187. 187 : }
  188. 188 : else if (bw.touch && row.addEventListener) {
  189. 189 : row.addEventListener('touchstart', function(e) {
  190. 190 : if (e.touches.length == 1) {
  191. 191 : self.touchmoved = false;
  192. 192 : self.drag_row(rcube_event.touchevent(e.touches[0]), this.uid);
  193. 193 : self.touch_start_time = new Date().getTime();
  194. 194 : }
  195. 195 : }, false);
  196. 196 : row.addEventListener('touchend', function(e) {
  197. 197 : if (e.changedTouches.length == 1) {
  198. 198 : var duration = (new Date().getTime() - self.touch_start_time);
  199. 199 : if (!self.touchmoved && duration <= self.touch_event_time && !self.click_row(rcube_event.touchevent(e.changedTouches[0]), this.uid))
  200. 200 : e.preventDefault();
  201. 201 : }
  202. 202 : }, false);
  203. 203 : row.addEventListener('touchmove', function(e) {
  204. 204 : if (e.changedTouches.length == 1) {
  205. 205 : self.touchmoved = true;
  206. 206 : if (self.drag_active)
  207. 207 : e.preventDefault();
  208. 208 : }
  209. 209 : }, false);
  210. 210 : }
  211. 211 :
  212. 212 : // label the list row with the subject col as descriptive label
  213. 213 : if (this.aria_listbox) {
  214. 214 : var lbl_id = 'l:' + row.id;
  215. 215 : $(row)
  216. 216 : .attr('role', 'option')
  217. 217 : .attr('aria-labelledby', lbl_id)
  218. 218 : .find(this.col_tagname()).eq(this.subject_column()).attr('id', lbl_id);
  219. 219 : }
  220. 220 :
  221. 221 : if (document.all)
  222. 222 : row.onselectstart = function() { return false; };
  223. 223 :
  224. 224 : this.row_init(this.rows[uid]); // legacy support
  225. 225 : this.triggerEvent('initrow', this.rows[uid]);
  226. 226 :
  227. 227 : return true;
  228. 228 : }
  229. 229 : },
  230. 230 :
  231. 231 :
  232. 232 : /**
  233. 233 : * Init list column headers and set mouse events on them
  234. 234 : */
  235. 235 : init_header: function()
  236. 236 : {
  237. 237 : if (this.thead) {
  238. 238 : this.colcount = 0;
  239. 239 :
  240. 240 : if (this.fixed_header) { // copy (modified) fixed header back to the actual table
  241. 241 : $(this.list.tHead).replaceWith($(this.fixed_header).find('thead').clone());
  242. 242 : $(this.list.tHead).find('th,td').attr('style', '').find('a').attr('tabindex', '-1'); // remove fixed widths
  243. 243 : }
  244. 244 : else if (!bw.touch && this.list.className.indexOf('fixedheader') >= 0) {
  245. 245 : this.init_fixed_header();
  246. 246 : }
  247. 247 :
  248. 248 : var col, r, p = this;
  249. 249 : // add events for list columns moving
  250. 250 : if (this.column_movable && this.thead && this.thead.rows) {
  251. 251 : for (r=0; r<this.thead.rows[0].cells.length; r++) {
  252. 252 : if (this.column_fixed == r)
  253. 253 : continue;
  254. 254 : col = this.thead.rows[0].cells[r];
  255. 255 : col.onmousedown = function(e) { return p.drag_column(e, this); };
  256. 256 : this.colcount++;
  257. 257 : }
  258. 258 : }
  259. 259 : }
  260. 260 : },
  261. 261 :
  262. 262 : /**
  263. 263 : * Set the scrollable parent object for the table's fixed header
  264. 264 : */
  265. 265 : container: window,
  266. 266 :
  267. 267 : init_fixed_header: function()
  268. 268 : {
  269. 269 : var clone = $(this.list.tHead).clone();
  270. 270 :
  271. 271 : if (!this.fixed_header) {
  272. 272 : this.fixed_header = $('<table>')
  273. 273 : .attr('id', this.list.id + '-fixedcopy')
  274. 274 : .attr('class', this.list.className + ' fixedcopy')
  275. 275 : .attr('role', 'presentation')
  276. 276 : .css({ position:'fixed' })
  277. 277 : .append(clone)
  278. 278 : .append('<tbody></tbody>');
  279. 279 : $(this.list).before(this.fixed_header);
  280. 280 :
  281. 281 : var me = this;
  282. 282 : $(window).on('resize', function() { me.resize(); });
  283. 283 : $(this.container).on('scroll', function() {
  284. 284 : var w = $(this);
  285. 285 : me.fixed_header.css({
  286. 286 : marginLeft: -w.scrollLeft() + 'px',
  287. 287 : marginTop: -w.scrollTop() + 'px'
  288. 288 : });
  289. 289 : });
  290. 290 : }
  291. 291 : else {
  292. 292 : $(this.fixed_header).find('thead').replaceWith(clone);
  293. 293 : }
  294. 294 :
  295. 295 : // avoid scrolling header links being focused
  296. 296 : $(this.list.tHead).find('a.sortcol').attr('tabindex', '-1');
  297. 297 :
  298. 298 : // set tabindex to fixed header sort links
  299. 299 : clone.find('a.sortcol').attr('tabindex', '0');
  300. 300 :
  301. 301 : this.thead = clone.get(0);
  302. 302 : this.resize();
  303. 303 : },
  304. 304 :
  305. 305 : resize: function()
  306. 306 : {
  307. 307 : if (!this.fixed_header)
  308. 308 : return;
  309. 309 :
  310. 310 : var column_widths = [];
  311. 311 :
  312. 312 : // get column widths from original thead
  313. 313 : $(this.tbody).parent().find('thead th,thead td').each(function(index) {
  314. 314 : column_widths[index] = $(this).width();
  315. 315 : });
  316. 316 :
  317. 317 : // apply fixed widths to fixed table header
  318. 318 : $(this.thead).parent().width($(this.tbody).parent().width());
  319. 319 : $(this.thead).find('th,td').each(function(index) {
  320. 320 : $(this).width(column_widths[index]);
  321. 321 : });
  322. 322 :
  323. 323 : $(window).scroll();
  324. 324 : },
  325. 325 :
  326. 326 : /**
  327. 327 : * Remove all list rows
  328. 328 : */
  329. 329 : clear: function(sel)
  330. 330 : {
  331. 331 : if (this.tagname == 'table') {
  332. 332 : var tbody = document.createElement('tbody');
  333. 333 : this.list.insertBefore(tbody, this.tbody);
  334. 334 : this.list.removeChild(this.list.tBodies[1]);
  335. 335 : this.tbody = tbody;
  336. 336 : }
  337. 337 : else {
  338. 338 : $(this.row_tagname() + ':not(.thead)', this.tbody).remove();
  339. 339 : }
  340. 340 :
  341. 341 : this.rows = {};
  342. 342 : this.rowcount = 0;
  343. 343 : this.last_selected = null;
  344. 344 :
  345. 345 : if (sel)
  346. 346 : this.clear_selection();
  347. 347 :
  348. 348 : // reset scroll position (in Opera)
  349. 349 : if (this.frame)
  350. 350 : this.frame.scrollTop = 0;
  351. 351 :
  352. 352 : // fix list header after removing any rows
  353. 353 : this.resize();
  354. 354 : },
  355. 355 :
  356. 356 :
  357. 357 : /**
  358. 358 : * 'remove' message row from list (just hide it)
  359. 359 : */
  360. 360 : remove_row: function(uid, sel_next)
  361. 361 : {
  362. 362 : var self = this, node = this.rows[uid] ? this.rows[uid].obj : null;
  363. 363 :
  364. 364 : if (!node)
  365. 365 : return;
  366. 366 :
  367. 367 : node.style.display = 'none';
  368. 368 :
  369. 369 : // Select next row before deletion, because we need the reference
  370. 370 : if (sel_next)
  371. 371 : this.select_next(uid);
  372. 372 :
  373. 373 : delete this.rows[uid];
  374. 374 : this.rowcount--;
  375. 375 :
  376. 376 : // fix list header after removing any rows
  377. 377 : clearTimeout(this.resize_timeout)
  378. 378 : this.resize_timeout = setTimeout(function() { self.resize(); }, 50);
  379. 379 : },
  380. 380 :
  381. 381 :
  382. 382 : /**
  383. 383 : * Add row to the list and initialize it
  384. 384 : */
  385. 385 : insert_row: function(row, before)
  386. 386 : {
  387. 387 : var self = this, tbody = this.tbody;
  388. 388 :
  389. 389 : // create a real dom node first
  390. 390 : if (row.nodeName === undefined) {
  391. 391 : // for performance reasons use DOM instead of jQuery here
  392. 392 : var i, e, domcell, col,
  393. 393 : domrow = document.createElement(this.row_tagname());
  394. 394 :
  395. 395 : if (row.id) domrow.id = row.id;
  396. 396 : if (row.uid) domrow.uid = row.uid;
  397. 397 : if (row.className) domrow.className = row.className;
  398. 398 : if (row.style) $.extend(domrow.style, row.style);
  399. 399 :
  400. 400 : for (i=0; row.cols && i < row.cols.length; i++) {
  401. 401 : col = row.cols[i];
  402. 402 : domcell = col.dom;
  403. 403 : if (!domcell) {
  404. 404 : domcell = document.createElement(this.col_tagname());
  405. 405 : if (col.className) domcell.className = col.className;
  406. 406 : if (col.innerHTML) domcell.innerHTML = col.innerHTML;
  407. 407 : for (e in col.events)
  408. 408 : domcell['on' + e] = col.events[e];
  409. 409 : }
  410. 410 : domrow.appendChild(domcell);
  411. 411 : }
  412. 412 :
  413. 413 : row = domrow;
  414. 414 : }
  415. 415 :
  416. 416 : if (this.checkbox_selection) {
  417. 417 : this.insert_checkbox(row);
  418. 418 : }
  419. 419 :
  420. 420 : if (before && tbody.childNodes.length)
  421. 421 : tbody.insertBefore(row, (typeof before == 'object' && before.parentNode == tbody) ? before : tbody.firstChild);
  422. 422 : else
  423. 423 : tbody.appendChild(row);
  424. 424 :
  425. 425 : this.init_row(row);
  426. 426 : this.rowcount++;
  427. 427 :
  428. 428 : // fix list header after adding any rows
  429. 429 : clearTimeout(this.resize_timeout)
  430. 430 : this.resize_timeout = setTimeout(function() { self.resize(); }, 50);
  431. 431 : },
  432. 432 :
  433. 433 : /**
  434. 434 : * Update existing record
  435. 435 : */
  436. 436 : update_row: function(id, cols, newid, select)
  437. 437 : {
  438. 438 : var row = this.rows[id];
  439. 439 : if (!row) return false;
  440. 440 :
  441. 441 : var i, domrow = row.obj;
  442. 442 : for (i = 0; cols && i < cols.length; i++) {
  443. 443 : this.get_cell(domrow, i).html(cols[i]);
  444. 444 : }
  445. 445 :
  446. 446 : if (newid) {
  447. 447 : delete this.rows[id];
  448. 448 : domrow.uid = newid;
  449. 449 : domrow.id = 'rcmrow' + newid;
  450. 450 : this.init_row(domrow);
  451. 451 :
  452. 452 : if (select)
  453. 453 : this.selection[0] = newid;
  454. 454 :
  455. 455 : if (this.last_selected == id)
  456. 456 : this.last_selected = newid;
  457. 457 : }
  458. 458 : },
  459. 459 :
  460. 460 : /**
  461. 461 : * Add selection checkbox to the list record
  462. 462 : */
  463. 463 : insert_checkbox: function(row, tag_name)
  464. 464 : {
  465. 465 : var key, self = this,
  466. 466 : cell = document.createElement(this.col_tagname(tag_name)),
  467. 467 : chbox = document.createElement('input');
  468. 468 :
  469. 469 : chbox.type = 'checkbox';
  470. 470 : chbox.tabIndex = -1;
  471. 471 : chbox.onchange = function(e) {
  472. 472 : self.select_row(row.uid, key || CONTROL_KEY, true);
  473. 473 : e.stopPropagation();
  474. 474 : key = null;
  475. 475 : };
  476. 476 : chbox.onmousedown = function(e) {
  477. 477 : key = rcube_event.get_modifier(e);
  478. 478 : };
  479. 479 :
  480. 480 : cell.className = 'selection';
  481. 481 : // make the whole cell "touchable" for touch devices
  482. 482 : cell.onclick = function(e) {
  483. 483 : if (!$(e.target).is('input')) {
  484. 484 : key = rcube_event.get_modifier(e);
  485. 485 : $(chbox).prop('checked', !chbox.checked).change();
  486. 486 : }
  487. 487 : e.stopPropagation();
  488. 488 : };
  489. 489 :
  490. 490 : cell.appendChild(chbox);
  491. 491 :
  492. 492 : row.insertBefore(cell, row.firstChild);
  493. 493 : },
  494. 494 :
  495. 495 : /**
  496. 496 : * Enable checkbox selection
  497. 497 : */
  498. 498 : enable_checkbox_selection: function()
  499. 499 : {
  500. 500 : this.checkbox_selection = true;
  501. 501 :
  502. 502 : // Add checkbox to existing records if any
  503. 503 : var r, len, cell, rows,
  504. 504 : row_tag = this.row_tagname().toUpperCase();
  505. 505 :
  506. 506 : if (this.thead) {
  507. 507 : rows = this.thead.childNodes;
  508. 508 : for (r=0, len=rows.length; r<len; r++) {
  509. 509 : if (rows[r].nodeName == row_tag && (cell = rows[r].firstChild)) {
  510. 510 : if (cell.className == 'selection')
  511. 511 : break;
  512. 512 : this.insert_checkbox(rows[r], 'thead');
  513. 513 : }
  514. 514 : }
  515. 515 : }
  516. 516 :
  517. 517 : rows = this.tbody.childNodes;
  518. 518 : for (r=0, len=rows.length; r<len; r++) {
  519. 519 : if (rows[r].nodeName == row_tag && (cell = rows[r].firstChild)) {
  520. 520 : if (cell.className == 'selection')
  521. 521 : break;
  522. 522 : this.insert_checkbox(rows[r], 'tbody');
  523. 523 : }
  524. 524 : }
  525. 525 : },
  526. 526 :
  527. 527 : /**
  528. 528 : * Set focus to the list
  529. 529 : */
  530. 530 : focus: function(e)
  531. 531 : {
  532. 532 : if (this.focused)
  533. 533 : return;
  534. 534 :
  535. 535 : this.focused = true;
  536. 536 :
  537. 537 : if (e)
  538. 538 : rcube_event.cancel(e);
  539. 539 :
  540. 540 : var focus_elem = null;
  541. 541 :
  542. 542 : if (this.last_selected && this.rows[this.last_selected]) {
  543. 543 : focus_elem = $(this.rows[this.last_selected].obj).find(this.col_tagname()).eq(this.subject_column()).attr('tabindex', '0');
  544. 544 : }
  545. 545 :
  546. 546 : // Un-focus already focused elements (#1487123, #1487316, #1488600, #1488620)
  547. 547 : if (focus_elem && focus_elem.length) {
  548. 548 : // We now fix this by explicitly assigning focus to a dedicated link element
  549. 549 : this.focus_noscroll(focus_elem);
  550. 550 : }
  551. 551 : else {
  552. 552 : // It looks that window.focus() does the job for all browsers, but not Firefox (#1489058)
  553. 553 : $('iframe,:focus:not(body)').blur();
  554. 554 : window.focus();
  555. 555 : }
  556. 556 :
  557. 557 : $(this.list).addClass('focus').removeAttr('tabindex');
  558. 558 :
  559. 559 : // set internal focus pointer to first row
  560. 560 : if (!this.last_selected)
  561. 561 : this.select_first(CONTROL_KEY);
  562. 562 : },
  563. 563 :
  564. 564 :
  565. 565 : /**
  566. 566 : * remove focus from the list
  567. 567 : */
  568. 568 : blur: function(e)
  569. 569 : {
  570. 570 : this.focused = false;
  571. 571 :
  572. 572 : // avoid the table getting focus right again (on Shift+Tab)
  573. 573 : var me = this;
  574. 574 : setTimeout(function() { $(me.list).attr('tabindex', '0'); }, 20);
  575. 575 :
  576. 576 : if (this.last_selected && this.rows[this.last_selected]) {
  577. 577 : $(this.rows[this.last_selected].obj)
  578. 578 : .find(this.col_tagname()).eq(this.subject_column()).removeAttr('tabindex');
  579. 579 : }
  580. 580 :
  581. 581 : $(this.list).removeClass('focus');
  582. 582 : },
  583. 583 :
  584. 584 : /**
  585. 585 : * Focus the given element without scrolling the list container
  586. 586 : */
  587. 587 : focus_noscroll: function(elem)
  588. 588 : {
  589. 589 : var y = this.frame.scrollTop || this.frame.scrollY;
  590. 590 : elem.focus();
  591. 591 : this.frame.scrollTop = y;
  592. 592 : },
  593. 593 :
  594. 594 :
  595. 595 : /**
  596. 596 : * Set/unset the given column as hidden
  597. 597 : */
  598. 598 : hide_column: function(col, hide)
  599. 599 : {
  600. 600 : var method = hide ? 'addClass' : 'removeClass';
  601. 601 :
  602. 602 : if (this.fixed_header)
  603. 603 : $(this.row_tagname()+' '+this.col_tagname()+'.'+col, this.fixed_header)[method]('hidden');
  604. 604 :
  605. 605 : $(this.row_tagname()+' '+this.col_tagname()+'.'+col, this.list)[method]('hidden');
  606. 606 : },
  607. 607 :
  608. 608 :
  609. 609 : /**
  610. 610 : * onmousedown-handler of message list column
  611. 611 : */
  612. 612 : drag_column: function(e, col)
  613. 613 : {
  614. 614 : if (this.colcount > 1) {
  615. 615 : this.drag_start = true;
  616. 616 : this.drag_mouse_start = rcube_event.get_mouse_pos(e);
  617. 617 :
  618. 618 : rcube_event.add_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
  619. 619 : rcube_event.add_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
  620. 620 :
  621. 621 : // enable dragging over iframes
  622. 622 : this.add_dragfix();
  623. 623 :
  624. 624 : // find selected column number
  625. 625 : for (var i=0; i<this.thead.rows[0].cells.length; i++) {
  626. 626 : if (col == this.thead.rows[0].cells[i]) {
  627. 627 : this.selected_column = i;
  628. 628 : break;
  629. 629 : }
  630. 630 : }
  631. 631 : }
  632. 632 :
  633. 633 : return false;
  634. 634 : },
  635. 635 :
  636. 636 :
  637. 637 : /**
  638. 638 : * onmousedown-handler of message list row
  639. 639 : */
  640. 640 : drag_row: function(e, id)
  641. 641 : {
  642. 642 : // don't do anything (another action processed before)
  643. 643 : if (!this.is_event_target(e))
  644. 644 : return true;
  645. 645 :
  646. 646 : // accept right-clicks
  647. 647 : if (rcube_event.get_button(e) == 2)
  648. 648 : return true;
  649. 649 :
  650. 650 : this.in_selection_before = e && e.istouch || this.in_selection(id) ? id : false;
  651. 651 :
  652. 652 : // selects currently unselected row
  653. 653 : if (!this.in_selection_before) {
  654. 654 : var mod_key = rcube_event.get_modifier(e);
  655. 655 : this.select_row(id, mod_key, true);
  656. 656 : }
  657. 657 :
  658. 658 : if (this.draggable && this.selection.length && this.in_selection(id)) {
  659. 659 : this.drag_start = true;
  660. 660 : this.drag_mouse_start = rcube_event.get_mouse_pos(e);
  661. 661 :
  662. 662 : rcube_event.add_listener({event:'mousemove', object:this, method:'drag_mouse_move'});
  663. 663 : rcube_event.add_listener({event:'mouseup', object:this, method:'drag_mouse_up'});
  664. 664 :
  665. 665 : if (bw.touch) {
  666. 666 : rcube_event.add_listener({event:'touchmove', object:this, method:'drag_mouse_move'});
  667. 667 : rcube_event.add_listener({event:'touchend', object:this, method:'drag_mouse_up'});
  668. 668 : }
  669. 669 :
  670. 670 : // enable dragging over iframes
  671. 671 : this.add_dragfix();
  672. 672 : this.focus();
  673. 673 : }
  674. 674 :
  675. 675 : return false;
  676. 676 : },
  677. 677 :
  678. 678 :
  679. 679 : /**
  680. 680 : * onmouseup-handler of message list row
  681. 681 : */
  682. 682 : click_row: function(e, id)
  683. 683 : {
  684. 684 : // sanity check
  685. 685 : if (!id || !this.rows[id])
  686. 686 : return false;
  687. 687 :
  688. 688 : // don't do anything (another action processed before)
  689. 689 : if (!this.is_event_target(e))
  690. 690 : return true;
  691. 691 :
  692. 692 : var now = new Date().getTime(),
  693. 693 : dblclicked = now - this.rows[id].clicked < this.dblclick_time;
  694. 694 :
  695. 695 : // unselects currently selected row
  696. 696 : if (!this.drag_active && !dblclicked && this.in_selection_before == id)
  697. 697 : this.select_row(id, rcube_event.get_modifier(e), true);
  698. 698 :
  699. 699 : this.drag_start = false;
  700. 700 : this.in_selection_before = false;
  701. 701 :
  702. 702 : // row was double clicked
  703. 703 : if (this.rowcount && dblclicked && this.in_selection(id)) {
  704. 704 : this.triggerEvent('dblclick');
  705. 705 : now = 0;
  706. 706 : }
  707. 707 : else
  708. 708 : this.triggerEvent('click');
  709. 709 :
  710. 710 : if (!this.drag_active) {
  711. 711 : // remove temp divs
  712. 712 : this.del_dragfix();
  713. 713 : rcube_event.cancel(e);
  714. 714 : }
  715. 715 :
  716. 716 : this.rows[id].clicked = now;
  717. 717 : this.focus();
  718. 718 :
  719. 719 : return false;
  720. 720 : },
  721. 721 :
  722. 722 :
  723. 723 : /**
  724. 724 : * Check target of the current event
  725. 725 : */
  726. 726 : is_event_target: function(e)
  727. 727 : {
  728. 728 : var target = rcube_event.get_target(e),
  729. 729 : tagname = target.tagName.toLowerCase();
  730. 730 :
  731. 731 : return !(target && (tagname == 'input' || tagname == 'img' || (tagname != 'a' && target.onclick) || $(target).data('action-link')));
  732. 732 : },
  733. 733 :
  734. 734 :
  735. 735 : /*
  736. 736 : * Returns thread root ID for specified row ID
  737. 737 : */
  738. 738 : find_root: function(uid)
  739. 739 : {
  740. 740 : var r = this.rows[uid];
  741. 741 :
  742. 742 : if (r && r.parent_uid)
  743. 743 : return this.find_root(r.parent_uid);
  744. 744 : else
  745. 745 : return uid;
  746. 746 : },
  747. 747 :
  748. 748 :
  749. 749 : expand_row: function(e, id)
  750. 750 : {
  751. 751 : var row = this.rows[id],
  752. 752 : evtarget = rcube_event.get_target(e),
  753. 753 : mod_key = rcube_event.get_modifier(e),
  754. 754 : action = (row.expanded ? 'collapse' : 'expand') + (mod_key == CONTROL_KEY || this.multiexpand ? '_all' : '');
  755. 755 :
  756. 756 : // Don't treat double click on the expando as double click on the message.
  757. 757 : row.clicked = 0;
  758. 758 :
  759. 759 : this[action](row);
  760. 760 : },
  761. 761 :
  762. 762 : collapse: function(row)
  763. 763 : {
  764. 764 : var r, depth = row.depth,
  765. 765 : new_row = row ? row.obj.nextSibling : null;
  766. 766 :
  767. 767 : row.expanded = false;
  768. 768 : this.update_expando(row.id);
  769. 769 : this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
  770. 770 :
  771. 771 : while (new_row) {
  772. 772 : if (new_row.nodeType == 1) {
  773. 773 : r = this.rows[new_row.uid];
  774. 774 : if (r && r.depth <= depth)
  775. 775 : break;
  776. 776 :
  777. 777 : $(new_row).css('display', 'none');
  778. 778 : if (r.expanded) {
  779. 779 : r.expanded = false;
  780. 780 : this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
  781. 781 : }
  782. 782 : }
  783. 783 : new_row = new_row.nextSibling;
  784. 784 : }
  785. 785 :
  786. 786 : this.resize();
  787. 787 : this.triggerEvent('listupdate');
  788. 788 :
  789. 789 : return false;
  790. 790 : },
  791. 791 :
  792. 792 : expand: function(row)
  793. 793 : {
  794. 794 : var r, p, depth, new_row, last_expanded_parent_depth;
  795. 795 :
  796. 796 : if (row) {
  797. 797 : row.expanded = true;
  798. 798 : depth = row.depth;
  799. 799 : new_row = row.obj.nextSibling;
  800. 800 : this.update_expando(row.id, true);
  801. 801 : this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
  802. 802 : }
  803. 803 : else {
  804. 804 : var tbody = this.tbody;
  805. 805 : new_row = tbody.firstChild;
  806. 806 : depth = 0;
  807. 807 : last_expanded_parent_depth = 0;
  808. 808 : }
  809. 809 :
  810. 810 : while (new_row) {
  811. 811 : if (new_row.nodeType == 1) {
  812. 812 : r = this.rows[new_row.uid];
  813. 813 : if (r) {
  814. 814 : if (row && (!r.depth || r.depth <= depth))
  815. 815 : break;
  816. 816 :
  817. 817 : if (r.parent_uid) {
  818. 818 : p = this.rows[r.parent_uid];
  819. 819 : if (p && p.expanded) {
  820. 820 : if ((row && p == row) || last_expanded_parent_depth >= p.depth - 1) {
  821. 821 : last_expanded_parent_depth = p.depth;
  822. 822 : $(new_row).css('display', '');
  823. 823 : r.expanded = true;
  824. 824 : this.update_expando(r.id, true);
  825. 825 : this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
  826. 826 : }
  827. 827 : }
  828. 828 : else
  829. 829 : if (row && (! p || p.depth <= depth))
  830. 830 : break;
  831. 831 : }
  832. 832 : }
  833. 833 : }
  834. 834 : new_row = new_row.nextSibling;
  835. 835 : }
  836. 836 :
  837. 837 : this.resize();
  838. 838 : this.triggerEvent('listupdate');
  839. 839 :
  840. 840 : return false;
  841. 841 : },
  842. 842 :
  843. 843 :
  844. 844 : collapse_all: function(row)
  845. 845 : {
  846. 846 : var depth, new_row, r;
  847. 847 :
  848. 848 : if (row) {
  849. 849 : row.expanded = false;
  850. 850 : depth = row.depth;
  851. 851 : new_row = row.obj.nextSibling;
  852. 852 : this.update_expando(row.id);
  853. 853 : this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
  854. 854 :
  855. 855 : // don't collapse sub-root tree in multiexpand mode
  856. 856 : if (depth && this.multiexpand)
  857. 857 : return false;
  858. 858 : }
  859. 859 : else {
  860. 860 : new_row = this.tbody.firstChild;
  861. 861 : depth = 0;
  862. 862 : }
  863. 863 :
  864. 864 : while (new_row) {
  865. 865 : if (new_row.nodeType == 1) {
  866. 866 : if (r = this.rows[new_row.uid]) {
  867. 867 : if (row && (!r.depth || r.depth <= depth))
  868. 868 : break;
  869. 869 :
  870. 870 : if (row || r.depth)
  871. 871 : $(new_row).css('display', 'none');
  872. 872 : if (r.expanded) {
  873. 873 : r.expanded = false;
  874. 874 : if (r.has_children) {
  875. 875 : this.update_expando(r.id);
  876. 876 : this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
  877. 877 : }
  878. 878 : }
  879. 879 : }
  880. 880 : }
  881. 881 : new_row = new_row.nextSibling;
  882. 882 : }
  883. 883 :
  884. 884 : this.resize();
  885. 885 : this.triggerEvent('listupdate');
  886. 886 :
  887. 887 : return false;
  888. 888 : },
  889. 889 :
  890. 890 :
  891. 891 : expand_all: function(row)
  892. 892 : {
  893. 893 : var depth, new_row, r;
  894. 894 :
  895. 895 : if (row) {
  896. 896 : row.expanded = true;
  897. 897 : depth = row.depth;
  898. 898 : new_row = row.obj.nextSibling;
  899. 899 : this.update_expando(row.id, true);
  900. 900 : this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
  901. 901 : }
  902. 902 : else {
  903. 903 : new_row = this.tbody.firstChild;
  904. 904 : depth = 0;
  905. 905 : }
  906. 906 :
  907. 907 : while (new_row) {
  908. 908 : if (new_row.nodeType == 1) {
  909. 909 : if (r = this.rows[new_row.uid]) {
  910. 910 : if (row && r.depth <= depth)
  911. 911 : break;
  912. 912 :
  913. 913 : $(new_row).css('display', '');
  914. 914 : if (!r.expanded) {
  915. 915 : r.expanded = true;
  916. 916 : if (r.has_children) {
  917. 917 : this.update_expando(r.id, true);
  918. 918 : this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
  919. 919 : }
  920. 920 : }
  921. 921 : }
  922. 922 : }
  923. 923 : new_row = new_row.nextSibling;
  924. 924 : }
  925. 925 :
  926. 926 : this.resize();
  927. 927 : this.triggerEvent('listupdate');
  928. 928 :
  929. 929 : return false;
  930. 930 : },
  931. 931 :
  932. 932 :
  933. 933 : update_expando: function(id, expanded)
  934. 934 : {
  935. 935 : var expando = document.getElementById('rcmexpando' + id);
  936. 936 : if (expando)
  937. 937 : expando.className = expanded ? 'expanded' : 'collapsed';
  938. 938 : },
  939. 939 :
  940. 940 : get_row_uid: function(row)
  941. 941 : {
  942. 942 : if (!row)
  943. 943 : return;
  944. 944 :
  945. 945 : if (!row.uid) {
  946. 946 : var uid = $(row).data('uid');
  947. 947 : if (uid)
  948. 948 : row.uid = uid;
  949. 949 : else if (String(row.id).match(this.id_regexp))
  950. 950 : row.uid = RegExp.$1;
  951. 951 : }
  952. 952 :
  953. 953 : return row.uid;
  954. 954 : },
  955. 955 :
  956. 956 : /**
  957. 957 : * get first/next/previous/last rows that are not hidden
  958. 958 : */
  959. 959 : get_next_row: function(uid)
  960. 960 : {
  961. 961 : if (!this.rowcount)
  962. 962 : return false;
  963. 963 :
  964. 964 : var last_selected_row = this.rows[uid || this.last_selected],
  965. 965 : new_row = last_selected_row ? last_selected_row.obj.nextSibling : null;
  966. 966 :
  967. 967 : while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
  968. 968 : new_row = new_row.nextSibling;
  969. 969 :
  970. 970 : return new_row;
  971. 971 : },
  972. 972 :
  973. 973 : get_prev_row: function(uid)
  974. 974 : {
  975. 975 : if (!this.rowcount)
  976. 976 : return false;
  977. 977 :
  978. 978 : var last_selected_row = this.rows[uid || this.last_selected],
  979. 979 : new_row = last_selected_row ? last_selected_row.obj.previousSibling : null;
  980. 980 :
  981. 981 : while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
  982. 982 : new_row = new_row.previousSibling;
  983. 983 :
  984. 984 : return new_row;
  985. 985 : },
  986. 986 :
  987. 987 : get_first_row: function()
  988. 988 : {
  989. 989 : if (this.rowcount) {
  990. 990 : var i, uid, rows = this.tbody.childNodes;
  991. 991 :
  992. 992 : for (i=0; i<rows.length; i++)
  993. 993 : if (rows[i].id && (uid = this.get_row_uid(rows[i])) && this.rows[uid])
  994. 994 : return uid;
  995. 995 : }
  996. 996 :
  997. 997 : return null;
  998. 998 : },
  999. 999 :
  1000. 1000 : get_last_row: function()
  1001. 1001 : {
  1002. 1002 : if (this.rowcount) {
  1003. 1003 : var i, uid, rows = this.tbody.childNodes;
  1004. 1004 :
  1005. 1005 : for (i=rows.length-1; i>=0; i--)
  1006. 1006 : if (rows[i].id && (uid = this.get_row_uid(rows[i])) && this.rows[uid])
  1007. 1007 : return uid;
  1008. 1008 : }
  1009. 1009 :
  1010. 1010 : return null;
  1011. 1011 : },
  1012. 1012 :
  1013. 1013 : get_next: function()
  1014. 1014 : {
  1015. 1015 : var row;
  1016. 1016 : if (row = this.get_next_row()) {
  1017. 1017 : return row.uid;
  1018. 1018 : }
  1019. 1019 : },
  1020. 1020 :
  1021. 1021 : get_prev: function()
  1022. 1022 : {
  1023. 1023 : var row;
  1024. 1024 : if (row = this.get_prev_row()) {
  1025. 1025 : return row.uid;
  1026. 1026 : }
  1027. 1027 : },
  1028. 1028 :
  1029. 1029 : row_tagname: function()
  1030. 1030 : {
  1031. 1031 : var row_tagnames = { table:'tr', ul:'li', '*':'div' };
  1032. 1032 : return row_tagnames[this.tagname] || row_tagnames['*'];
  1033. 1033 : },
  1034. 1034 :
  1035. 1035 : col_tagname: function(tagname)
  1036. 1036 : {
  1037. 1037 : var col_tagnames = { table:'td', thead:'th', tbody:'td', '*':'span' };
  1038. 1038 : return col_tagnames[tagname || this.tagname] || col_tagnames['*'];
  1039. 1039 : },
  1040. 1040 :
  1041. 1041 : get_cell: function(row, index)
  1042. 1042 : {
  1043. 1043 : return $(this.col_tagname(), row).eq(index + (this.checkbox_selection ? 1 : 0));
  1044. 1044 : },
  1045. 1045 :
  1046. 1046 : /**
  1047. 1047 : * selects or unselects the proper row depending on the modifier key pressed
  1048. 1048 : */
  1049. 1049 : select_row: function(id, mod_key, with_mouse)
  1050. 1050 : {
  1051. 1051 : var select_before = this.selection.join(','),
  1052. 1052 : in_selection_before = this.in_selection(id);
  1053. 1053 :
  1054. 1054 : if (!this.multiselect && with_mouse)
  1055. 1055 : mod_key = 0;
  1056. 1056 :
  1057. 1057 : if (!this.shift_start)
  1058. 1058 : this.shift_start = id
  1059. 1059 :
  1060. 1060 : if (!mod_key) {
  1061. 1061 : this.shift_start = id;
  1062. 1062 : this.highlight_row(id, false);
  1063. 1063 : this.multi_selecting = false;
  1064. 1064 : }
  1065. 1065 : else {
  1066. 1066 : switch (mod_key) {
  1067. 1067 : case SHIFT_KEY:
  1068. 1068 : this.shift_select(id, false);
  1069. 1069 : break;
  1070. 1070 :
  1071. 1071 : case CONTROL_KEY:
  1072. 1072 : if (with_mouse) {
  1073. 1073 : this.shift_start = id;
  1074. 1074 : this.highlight_row(id, true);
  1075. 1075 : }
  1076. 1076 : break;
  1077. 1077 :
  1078. 1078 : case CONTROL_SHIFT_KEY:
  1079. 1079 : this.shift_select(id, true);
  1080. 1080 : break;
  1081. 1081 :
  1082. 1082 : default:
  1083. 1083 : this.highlight_row(id, false);
  1084. 1084 : break;
  1085. 1085 : }
  1086. 1086 :
  1087. 1087 : this.multi_selecting = true;
  1088. 1088 : }
  1089. 1089 :
  1090. 1090 : if (this.last_selected && this.rows[this.last_selected]) {
  1091. 1091 : $(this.rows[this.last_selected].obj).removeClass('focused')
  1092. 1092 : .find(this.col_tagname()).eq(this.subject_column()).removeAttr('tabindex');
  1093. 1093 : }
  1094. 1094 :
  1095. 1095 : // unselect if toggleselect is active and the same row was clicked again
  1096. 1096 : if (this.toggleselect && in_selection_before && !mod_key) {
  1097. 1097 : this.clear_selection();
  1098. 1098 : }
  1099. 1099 : // trigger event if selection changed
  1100. 1100 : else if (this.selection.join(',') != select_before) {
  1101. 1101 : this.triggerEvent('select');
  1102. 1102 : }
  1103. 1103 :
  1104. 1104 : if (this.rows[id]) {
  1105. 1105 : $(this.rows[id].obj).addClass('focused');
  1106. 1106 : // set cursor focus to link inside selected row
  1107. 1107 : if (this.focused)
  1108. 1108 : this.focus_noscroll($(this.rows[id].obj).find(this.col_tagname()).eq(this.subject_column()).attr('tabindex', '0'));
  1109. 1109 : }
  1110. 1110 :
  1111. 1111 : if (!this.selection.length)
  1112. 1112 : this.shift_start = null;
  1113. 1113 :
  1114. 1114 : this.last_selected = id;
  1115. 1115 : },
  1116. 1116 :
  1117. 1117 :
  1118. 1118 : /**
  1119. 1119 : * Alias method for select_row
  1120. 1120 : */
  1121. 1121 : select: function(id)
  1122. 1122 : {
  1123. 1123 : this.select_row(id, false);
  1124. 1124 : this.scrollto(id);
  1125. 1125 : },
  1126. 1126 :
  1127. 1127 :
  1128. 1128 : /**
  1129. 1129 : * Select row next to the specified or last selected one
  1130. 1130 : * Either below or above.
  1131. 1131 : */
  1132. 1132 : select_next: function(uid)
  1133. 1133 : {
  1134. 1134 : var new_row = this.get_next_row(uid) || this.get_prev_row(uid);
  1135. 1135 : if (new_row)
  1136. 1136 : this.select_row(new_row.uid, false, false);
  1137. 1137 : },
  1138. 1138 :
  1139. 1139 :
  1140. 1140 : /**
  1141. 1141 : * Select first row
  1142. 1142 : */
  1143. 1143 : select_first: function(mod_key, noscroll)
  1144. 1144 : {
  1145. 1145 : var row = this.get_first_row();
  1146. 1146 : if (row) {
  1147. 1147 : this.select_row(row, mod_key, false);
  1148. 1148 :
  1149. 1149 : if (!noscroll)
  1150. 1150 : this.scrollto(row);
  1151. 1151 : }
  1152. 1152 : },
  1153. 1153 :
  1154. 1154 :
  1155. 1155 : /**
  1156. 1156 : * Select last row
  1157. 1157 : */
  1158. 1158 : select_last: function(mod_key, noscroll)
  1159. 1159 : {
  1160. 1160 : var row = this.get_last_row();
  1161. 1161 : if (row) {
  1162. 1162 : this.select_row(row, mod_key, false);
  1163. 1163 :
  1164. 1164 : if (!noscroll)
  1165. 1165 : this.scrollto(row);
  1166. 1166 : }
  1167. 1167 : },
  1168. 1168 :
  1169. 1169 :
  1170. 1170 : /**
  1171. 1171 : * Add all children of the given row to selection
  1172. 1172 : */
  1173. 1173 : select_children: function(uid)
  1174. 1174 : {
  1175. 1175 : var i, children = this.row_children(uid), len = children.length;
  1176. 1176 :
  1177. 1177 : for (i=0; i<len; i++)
  1178. 1178 : if (!this.in_selection(children[i]))
  1179. 1179 : this.select_row(children[i], CONTROL_KEY, true);
  1180. 1180 : },
  1181. 1181 :
  1182. 1182 :
  1183. 1183 : /**
  1184. 1184 : * Perform selection when shift key is pressed
  1185. 1185 : */
  1186. 1186 : shift_select: function(id, control)
  1187. 1187 : {
  1188. 1188 : if (!this.rows[this.shift_start] || !this.selection.length)
  1189. 1189 : this.shift_start = id;
  1190. 1190 :
  1191. 1191 : var n, i, j, to_row = this.rows[id],
  1192. 1192 : from_rowIndex = this._rowIndex(this.rows[this.shift_start].obj),
  1193. 1193 : to_rowIndex = this._rowIndex(to_row.obj);
  1194. 1194 :
  1195. 1195 : // if we're going down the list, and we hit a thread, and it's closed, select the whole thread
  1196. 1196 : if (from_rowIndex < to_rowIndex && !to_row.expanded && to_row.has_children)
  1197. 1197 : if (to_row = this.rows[(this.row_children(id)).pop()])
  1198. 1198 : to_rowIndex = this._rowIndex(to_row.obj);
  1199. 1199 :
  1200. 1200 : i = ((from_rowIndex < to_rowIndex) ? from_rowIndex : to_rowIndex),
  1201. 1201 : j = ((from_rowIndex > to_rowIndex) ? from_rowIndex : to_rowIndex);
  1202. 1202 :
  1203. 1203 : // iterate through the entire message list
  1204. 1204 : for (n in this.rows) {
  1205. 1205 : if (this._rowIndex(this.rows[n].obj) >= i && this._rowIndex(this.rows[n].obj) <= j) {
  1206. 1206 : if (!this.in_selection(n)) {
  1207. 1207 : this.highlight_row(n, true);
  1208. 1208 : }
  1209. 1209 : }
  1210. 1210 : else {
  1211. 1211 : if (this.in_selection(n) && !control) {
  1212. 1212 : this.highlight_row(n, true);
  1213. 1213 : }
  1214. 1214 : }
  1215. 1215 : }
  1216. 1216 : },
  1217. 1217 :
  1218. 1218 :
  1219. 1219 : /**
  1220. 1220 : * Helper method to emulate the rowIndex property of non-tr elements
  1221. 1221 : */
  1222. 1222 : _rowIndex: function(obj)
  1223. 1223 : {
  1224. 1224 : return (obj.rowIndex !== undefined) ? obj.rowIndex : $(obj).prevAll().length;
  1225. 1225 : },
  1226. 1226 :
  1227. 1227 : /**
  1228. 1228 : * Check if given id is part of the current selection
  1229. 1229 : */
  1230. 1230 : in_selection: function(id, index)
  1231. 1231 : {
  1232. 1232 : for (var n in this.selection)
  1233. 1233 : if (this.selection[n] == id)
  1234. 1234 : return index ? parseInt(n) : true;
  1235. 1235 :
  1236. 1236 : return false;
  1237. 1237 : },
  1238. 1238 :
  1239. 1239 :
  1240. 1240 : /**
  1241. 1241 : * Select each row in list
  1242. 1242 : */
  1243. 1243 : select_all: function(filter)
  1244. 1244 : {
  1245. 1245 : if (!this.rowcount)
  1246. 1246 : return false;
  1247. 1247 :
  1248. 1248 : // reset but remember selection first
  1249. 1249 : var n, select_before = this.selection.join(',');
  1250. 1250 : this.selection = [];
  1251. 1251 :
  1252. 1252 : for (n in this.rows) {
  1253. 1253 : if (!filter || this.rows[n][filter] == true) {
  1254. 1254 : this.last_selected = n;
  1255. 1255 : this.highlight_row(n, true, true);
  1256. 1256 : }
  1257. 1257 : else {
  1258. 1258 : $(this.rows[n].obj).removeClass('selected').removeAttr('aria-selected');
  1259. 1259 : }
  1260. 1260 : }
  1261. 1261 :
  1262. 1262 : // trigger event if selection changed
  1263. 1263 : if (this.selection.join(',') != select_before)
  1264. 1264 : this.triggerEvent('select');
  1265. 1265 :
  1266. 1266 : this.focus();
  1267. 1267 :
  1268. 1268 : return true;
  1269. 1269 : },
  1270. 1270 :
  1271. 1271 :
  1272. 1272 : /**
  1273. 1273 : * Invert selection
  1274. 1274 : */
  1275. 1275 : invert_selection: function()
  1276. 1276 : {
  1277. 1277 : if (!this.rowcount)
  1278. 1278 : return false;
  1279. 1279 :
  1280. 1280 : // remember old selection
  1281. 1281 : var n, select_before = this.selection.join(',');
  1282. 1282 :
  1283. 1283 : for (n in this.rows)
  1284. 1284 : this.highlight_row(n, true);
  1285. 1285 :
  1286. 1286 : // trigger event if selection changed
  1287. 1287 : if (this.selection.join(',') != select_before)
  1288. 1288 : this.triggerEvent('select');
  1289. 1289 :
  1290. 1290 : this.focus();
  1291. 1291 :
  1292. 1292 : return true;
  1293. 1293 : },
  1294. 1294 :
  1295. 1295 :
  1296. 1296 : /**
  1297. 1297 : * Unselect selected row(s)
  1298. 1298 : */
  1299. 1299 : clear_selection: function(id, no_event)
  1300. 1300 : {
  1301. 1301 : var n, num_select = this.selection.length;
  1302. 1302 :
  1303. 1303 : // one row
  1304. 1304 : if (id) {
  1305. 1305 : for (n in this.selection)
  1306. 1306 : if (this.selection[n] == id) {
  1307. 1307 : this.selection.splice(n,1);
  1308. 1308 : break;
  1309. 1309 : }
  1310. 1310 : }
  1311. 1311 : // all rows
  1312. 1312 : else {
  1313. 1313 : for (n in this.selection)
  1314. 1314 : if (this.rows[this.selection[n]]) {
  1315. 1315 : $(this.rows[this.selection[n]].obj).removeClass('selected').removeAttr('aria-selected');
  1316. 1316 : }
  1317. 1317 :
  1318. 1318 : this.selection = [];
  1319. 1319 : }
  1320. 1320 :
  1321. 1321 : if (this.checkbox_selection)
  1322. 1322 : $(this.row_tagname() + ':not(.selected) > .selection > input:checked', this.list).prop('checked', false);
  1323. 1323 :
  1324. 1324 : if (num_select && !this.selection.length && !no_event) {
  1325. 1325 : this.triggerEvent('select');
  1326. 1326 : this.last_selected = null;
  1327. 1327 : }
  1328. 1328 : },
  1329. 1329 :
  1330. 1330 :
  1331. 1331 : /**
  1332. 1332 : * Getter for the selection array
  1333. 1333 : */
  1334. 1334 : get_selection: function(deep)
  1335. 1335 : {
  1336. 1336 : var res = $.merge([], this.selection);
  1337. 1337 :
  1338. 1338 : var props = {deep: deep, res: res};
  1339. 1339 : if (this.triggerEvent('getselection', props) === false)
  1340. 1340 : return props.res;
  1341. 1341 :
  1342. 1342 : // return children of selected threads even if only root is selected
  1343. 1343 : if (deep !== false && res.length) {
  1344. 1344 : for (var uid, uids, i=0, len=res.length; i<len; i++) {
  1345. 1345 : uid = res[i];
  1346. 1346 : if (this.rows[uid] && this.rows[uid].has_children && !this.rows[uid].expanded) {
  1347. 1347 : uids = this.row_children(uid);
  1348. 1348 : for (var j=0, uids_len=uids.length; j<uids_len; j++) {
  1349. 1349 : uid = uids[j];
  1350. 1350 : if (!this.in_selection(uid))
  1351. 1351 : res.push(uid);
  1352. 1352 : }
  1353. 1353 : }
  1354. 1354 : }
  1355. 1355 : }
  1356. 1356 :
  1357. 1357 : return res;
  1358. 1358 : },
  1359. 1359 :
  1360. 1360 :
  1361. 1361 : /**
  1362. 1362 : * Return the ID if only one row is selected
  1363. 1363 : */
  1364. 1364 : get_single_selection: function()
  1365. 1365 : {
  1366. 1366 : var selection = this.get_selection(false);
  1367. 1367 :
  1368. 1368 : if (selection.length == 1)
  1369. 1369 : return selection[0];
  1370. 1370 : else
  1371. 1371 : return null;
  1372. 1372 : },
  1373. 1373 :
  1374. 1374 :
  1375. 1375 : /**
  1376. 1376 : * Highlight/unhighlight a row
  1377. 1377 : */
  1378. 1378 : highlight_row: function(id, multiple, norecur)
  1379. 1379 : {
  1380. 1380 : if (!this.rows[id])
  1381. 1381 : return;
  1382. 1382 :
  1383. 1383 : if (!multiple) {
  1384. 1384 : if (this.selection.length > 1 || !this.in_selection(id)) {
  1385. 1385 : this.clear_selection(null, true);
  1386. 1386 : this.selection[0] = id;
  1387. 1387 : $(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');
  1388. 1388 :
  1389. 1389 : if (this.checkbox_selection)
  1390. 1390 : $('.selection > input', this.rows[id].obj).prop('checked', true);
  1391. 1391 : }
  1392. 1392 : }
  1393. 1393 : else {
  1394. 1394 : var pre, post, p = this.in_selection(id, true);
  1395. 1395 :
  1396. 1396 : if (p === false) { // select row
  1397. 1397 : this.selection.push(id);
  1398. 1398 : $(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');
  1399. 1399 :
  1400. 1400 : if (this.checkbox_selection)
  1401. 1401 : $('.selection > input', this.rows[id].obj).prop('checked', true);
  1402. 1402 :
  1403. 1403 : if (!norecur && !this.rows[id].expanded)
  1404. 1404 : this.highlight_children(id, true);
  1405. 1405 : }
  1406. 1406 : else { // unselect row
  1407. 1407 : pre = this.selection.slice(0, p);
  1408. 1408 : post = this.selection.slice(p+1, this.selection.length);
  1409. 1409 :
  1410. 1410 : this.selection = pre.concat(post);
  1411. 1411 : $(this.rows[id].obj).removeClass('selected').removeAttr('aria-selected');
  1412. 1412 :
  1413. 1413 : if (this.checkbox_selection)
  1414. 1414 : $('.selection > input', this.rows[id].obj).prop('checked', false);
  1415. 1415 :
  1416. 1416 : if (!norecur && !this.rows[id].expanded)
  1417. 1417 : this.highlight_children(id, false);
  1418. 1418 : }
  1419. 1419 : }
  1420. 1420 : },
  1421. 1421 :
  1422. 1422 :
  1423. 1423 : /**
  1424. 1424 : * Highlight/unhighlight all children of the given row
  1425. 1425 : */
  1426. 1426 : highlight_children: function(id, status)
  1427. 1427 : {
  1428. 1428 : var i, selected,
  1429. 1429 : children = this.row_children(id), len = children.length;
  1430. 1430 :
  1431. 1431 : for (i=0; i<len; i++) {
  1432. 1432 : selected = this.in_selection(children[i]);
  1433. 1433 : if ((status && !selected) || (!status && selected))
  1434. 1434 : this.highlight_row(children[i], true, true);
  1435. 1435 : }
  1436. 1436 : },
  1437. 1437 :
  1438. 1438 :
  1439. 1439 : /**
  1440. 1440 : * Handler for keyboard events
  1441. 1441 : */
  1442. 1442 : key_press: function(e)
  1443. 1443 : {
  1444. 1444 : if (!this.focused || $(e.target).is('input,textarea,select'))
  1445. 1445 : return true;
  1446. 1446 :
  1447. 1447 : var keyCode = rcube_event.get_keycode(e),
  1448. 1448 : mod_key = rcube_event.get_modifier(e);
  1449. 1449 :
  1450. 1450 : switch (keyCode) {
  1451. 1451 : case 37: // Left arrow
  1452. 1452 : case 39: // Right arrow
  1453. 1453 : case 40: // Up arrow
  1454. 1454 : case 38: // Down arrow
  1455. 1455 : case 63233: // "down" in Safari keypress
  1456. 1456 : case 63232: // "up" in Safari keypress
  1457. 1457 : // Stop propagation so that the browser doesn't scroll
  1458. 1458 : rcube_event.cancel(e);
  1459. 1459 : return this.use_arrow_key(keyCode, mod_key);
  1460. 1460 :
  1461. 1461 : case 32: // Space
  1462. 1462 : rcube_event.cancel(e);
  1463. 1463 : return this.select_row(this.last_selected, mod_key, true);
  1464. 1464 :
  1465. 1465 : case 36: // Home
  1466. 1466 : this.select_first(mod_key);
  1467. 1467 : return rcube_event.cancel(e);
  1468. 1468 :
  1469. 1469 : case 35: // End
  1470. 1470 : this.select_last(mod_key);
  1471. 1471 : return rcube_event.cancel(e);
  1472. 1472 :
  1473. 1473 : case 65: // Ctrl + A
  1474. 1474 : if (mod_key == CONTROL_KEY && this.multiselect) {
  1475. 1475 : this.select_first(null, true);
  1476. 1476 : this.select_last(SHIFT_KEY, true);
  1477. 1477 : return rcube_event.cancel(e);
  1478. 1478 : }
  1479. 1479 : break;
  1480. 1480 :
  1481. 1481 : case 27: // Esc
  1482. 1482 : if (this.drag_active)
  1483. 1483 : return this.drag_mouse_up(e);
  1484. 1484 :
  1485. 1485 : if (this.col_drag_active) {
  1486. 1486 : this.selected_column = null;
  1487. 1487 : return this.column_drag_mouse_up(e);
  1488. 1488 : }
  1489. 1489 :
  1490. 1490 : return rcube_event.cancel(e);
  1491. 1491 :
  1492. 1492 : case 9: // Tab
  1493. 1493 : this.blur();
  1494. 1494 : break;
  1495. 1495 :
  1496. 1496 : case 13: // Enter
  1497. 1497 : if (!this.selection.length)
  1498. 1498 : this.select_row(this.last_selected, mod_key, false);
  1499. 1499 :
  1500. 1500 : default:
  1501. 1501 : this.key_pressed = keyCode;
  1502. 1502 : this.modkey = mod_key;
  1503. 1503 : this.triggerEvent('keypress');
  1504. 1504 : this.modkey = 0;
  1505. 1505 :
  1506. 1506 : if (this.key_pressed == this.BACKSPACE_KEY)
  1507. 1507 : return rcube_event.cancel(e);
  1508. 1508 : }
  1509. 1509 :
  1510. 1510 : return true;
  1511. 1511 : },
  1512. 1512 :
  1513. 1513 :
  1514. 1514 : /**
  1515. 1515 : * Special handling method for arrow keys
  1516. 1516 : */
  1517. 1517 : use_arrow_key: function(keyCode, mod_key)
  1518. 1518 : {
  1519. 1519 : var new_row, selected_row = this.rows[this.last_selected];
  1520. 1520 :
  1521. 1521 : if (!selected_row) {
  1522. 1522 : // select the first row if none selected yet
  1523. 1523 : this.select_first(CONTROL_KEY);
  1524. 1524 : }
  1525. 1525 : // Safari uses the non-standard keycodes 63232/63233 for up/down, if we're
  1526. 1526 : // using the keypress event (but not the keydown or keyup event).
  1527. 1527 : else if (keyCode == 40 || keyCode == 63233) // Down arrow
  1528. 1528 : new_row = this.get_next_row();
  1529. 1529 : else if (keyCode == 38 || keyCode == 63232) // Up arrow
  1530. 1530 : new_row = this.get_prev_row();
  1531. 1531 : else if (keyCode == 39 && selected_row.has_children) { // Right arrow
  1532. 1532 : if (!selected_row.expanded)
  1533. 1533 : this.expand_all(selected_row);
  1534. 1534 : else {
  1535. 1535 : // jump to the first child
  1536. 1536 : new_row = this.get_next_row();
  1537. 1537 : mod_key = null;
  1538. 1538 : }
  1539. 1539 : }
  1540. 1540 : else if (keyCode == 37) { // Left arrow
  1541. 1541 : if (selected_row.expanded && selected_row.has_children && (!selected_row.parent_uid || !this.multiexpand))
  1542. 1542 : this.collapse_all(selected_row);
  1543. 1543 : else if (selected_row.parent_uid) {
  1544. 1544 : // jump to the top-most or closest parent
  1545. 1545 : if (mod_key == CONTROL_KEY)
  1546. 1546 : new_row = this.rows[this.find_root(selected_row.uid)];
  1547. 1547 : else
  1548. 1548 : new_row = this.rows[selected_row.parent_uid];
  1549. 1549 :
  1550. 1550 : mod_key = null;
  1551. 1551 : }
  1552. 1552 : }
  1553. 1553 :
  1554. 1554 : if (new_row) {
  1555. 1555 : // simulate ctr-key if no rows are selected
  1556. 1556 : if (!mod_key && !this.selection.length)
  1557. 1557 : mod_key = CONTROL_KEY;
  1558. 1558 :
  1559. 1559 : this.select_row(new_row.uid, mod_key, false);
  1560. 1560 : this.scrollto(new_row.uid);
  1561. 1561 : }
  1562. 1562 :
  1563. 1563 : return false;
  1564. 1564 : },
  1565. 1565 :
  1566. 1566 :
  1567. 1567 : /**
  1568. 1568 : * Try to scroll the list to make the specified row visible
  1569. 1569 : */
  1570. 1570 : scrollto: function(id)
  1571. 1571 : {
  1572. 1572 : var row = this.rows[id] ? this.rows[id].obj : null;
  1573. 1573 :
  1574. 1574 : if (row && this.frame) {
  1575. 1575 : var scroll_to = Number(row.offsetTop),
  1576. 1576 : head_offset = 0;
  1577. 1577 :
  1578. 1578 : // expand thread if target row is hidden (collapsed)
  1579. 1579 : if (!scroll_to && this.rows[id].parent_uid) {
  1580. 1580 : var parent = this.find_root(this.rows[id].uid);
  1581. 1581 : this.expand_all(this.rows[parent]);
  1582. 1582 : scroll_to = Number(row.offsetTop);
  1583. 1583 : }
  1584. 1584 :
  1585. 1585 : if (this.fixed_header)
  1586. 1586 : head_offset = Number(this.thead.offsetHeight);
  1587. 1587 :
  1588. 1588 : // if row is above the frame (or behind header)
  1589. 1589 : if (scroll_to < Number(this.frame.scrollTop) + head_offset) {
  1590. 1590 : // scroll window so that row isn't behind header
  1591. 1591 : this.frame.scrollTop = scroll_to - head_offset;
  1592. 1592 : }
  1593. 1593 : else if (scroll_to + Number(row.offsetHeight) > Number(this.frame.scrollTop) + Number(this.frame.offsetHeight))
  1594. 1594 : this.frame.scrollTop = (scroll_to + Number(row.offsetHeight)) - Number(this.frame.offsetHeight);
  1595. 1595 : }
  1596. 1596 : },
  1597. 1597 :
  1598. 1598 :
  1599. 1599 : /**
  1600. 1600 : * Handler for mouse move events
  1601. 1601 : */
  1602. 1602 : drag_mouse_move: function(e)
  1603. 1603 : {
  1604. 1604 : // convert touch event
  1605. 1605 : if (e.type == 'touchmove') {
  1606. 1606 : if (e.touches.length == 1 && e.changedTouches.length == 1)
  1607. 1607 : e = rcube_event.touchevent(e.changedTouches[0]);
  1608. 1608 : else
  1609. 1609 : return rcube_event.cancel(e);
  1610. 1610 : }
  1611. 1611 :
  1612. 1612 : if (this.drag_start) {
  1613. 1613 : // check mouse movement, of less than 3 pixels, don't start dragging
  1614. 1614 : var m = rcube_event.get_mouse_pos(e),
  1615. 1615 : limit = 10, selection = [], self = this;
  1616. 1616 :
  1617. 1617 : 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))
  1618. 1618 : return false;
  1619. 1619 :
  1620. 1620 : // remember dragging start position
  1621. 1621 : this.drag_start_pos = {left: m.x, top: m.y};
  1622. 1622 :
  1623. 1623 : // initialize drag layer
  1624. 1624 : if (!this.draglayer)
  1625. 1625 : this.draglayer = $('<div>').attr('id', 'rcmdraglayer')
  1626. 1626 : .css({position: 'absolute', display: 'none', 'z-index': 2000})
  1627. 1627 : .appendTo(document.body);
  1628. 1628 : else
  1629. 1629 : this.draglayer.html('');
  1630. 1630 :
  1631. 1631 : // get selected rows (in display order), don't use this.selection here
  1632. 1632 : $(this.row_tagname() + '.selected', this.tbody).each(function() {
  1633. 1633 : var uid = self.get_row_uid(this), row = self.rows[uid];
  1634. 1634 :
  1635. 1635 : if (!row || $.inArray(uid, selection) > -1)
  1636. 1636 : return;
  1637. 1637 :
  1638. 1638 : selection.push(uid);
  1639. 1639 :
  1640. 1640 : // also handle children of (collapsed) trees for dragging (they might be not selected)
  1641. 1641 : if (row.has_children && !row.expanded)
  1642. 1642 : $.each(self.row_children(uid), function() {
  1643. 1643 : if ($.inArray(this, selection) > -1)
  1644. 1644 : return;
  1645. 1645 : selection.push(this);
  1646. 1646 : });
  1647. 1647 :
  1648. 1648 : // break the loop asap
  1649. 1649 : if (selection.length > limit + 1)
  1650. 1650 : return false;
  1651. 1651 : });
  1652. 1652 :
  1653. 1653 : var row, subject,
  1654. 1654 : subject_col = self.subject_column(),
  1655. 1655 : subject_func = function(cell) {
  1656. 1656 : if (cell) {
  1657. 1657 : // remove elements marked with "skip-on-drag" class
  1658. 1658 : cell = $(cell).clone();
  1659. 1659 : $(cell).find('.skip-on-drag').remove();
  1660. 1660 : }
  1661. 1661 : return cell ? cell.text() : '';
  1662. 1662 : };
  1663. 1663 :
  1664. 1664 : // append subject (of every row up to the limit) to the drag layer
  1665. 1665 : $.each(selection, function(i, uid) {
  1666. 1666 : if (i > limit) {
  1667. 1667 : self.draglayer.append($('<div>').text('...'));
  1668. 1668 : return false;
  1669. 1669 : }
  1670. 1670 :
  1671. 1671 : row = self.rows[uid].obj;
  1672. 1672 : subject = '';
  1673. 1673 :
  1674. 1674 : $(row).children(self.col_tagname()).each(function(n, cell) {
  1675. 1675 : if (subject_col < 0 || (subject_col >= 0 && subject_col == n)) {
  1676. 1676 : if (subject = subject_func(cell)) {
  1677. 1677 : return false;
  1678. 1678 : }
  1679. 1679 : }
  1680. 1680 : });
  1681. 1681 :
  1682. 1682 : // Subject column might be wrong, fallback to .subject
  1683. 1683 : if (!subject.length) {
  1684. 1684 : subject = subject_func($(row).children('.subject').first());
  1685. 1685 : }
  1686. 1686 :
  1687. 1687 : if (subject.length) {
  1688. 1688 : // remove leading spaces
  1689. 1689 : subject = subject.trim();
  1690. 1690 : // truncate line to 50 characters
  1691. 1691 : subject = (subject.length > 50 ? subject.substring(0, 50) + '...' : subject);
  1692. 1692 :
  1693. 1693 : self.draglayer.append($('<div>').text(subject));
  1694. 1694 : }
  1695. 1695 : });
  1696. 1696 :
  1697. 1697 : this.draglayer.show();
  1698. 1698 : this.drag_active = true;
  1699. 1699 : this.triggerEvent('dragstart');
  1700. 1700 : }
  1701. 1701 :
  1702. 1702 : if (this.drag_active && this.draglayer) {
  1703. 1703 : var pos = rcube_event.get_mouse_pos(e);
  1704. 1704 : this.draglayer.css({ left:(pos.x+20)+'px', top:(pos.y-5 + (bw.ie ? document.documentElement.scrollTop : 0))+'px' });
  1705. 1705 : this.triggerEvent('dragmove', e?e:window.event);
  1706. 1706 : }
  1707. 1707 :
  1708. 1708 : this.drag_start = false;
  1709. 1709 :
  1710. 1710 : return false;
  1711. 1711 : },
  1712. 1712 :
  1713. 1713 :
  1714. 1714 : /**
  1715. 1715 : * Handler for mouse up events
  1716. 1716 : */
  1717. 1717 : drag_mouse_up: function(e)
  1718. 1718 : {
  1719. 1719 : document.onmousemove = null;
  1720. 1720 :
  1721. 1721 : if (e.type == 'touchend') {
  1722. 1722 : if (e.changedTouches.length != 1)
  1723. 1723 : return rcube_event.cancel(e);
  1724. 1724 : }
  1725. 1725 :
  1726. 1726 : if (this.draglayer && this.draglayer.is(':visible')) {
  1727. 1727 : if (this.drag_start_pos)
  1728. 1728 : this.draglayer.animate(this.drag_start_pos, 300, 'swing').hide(20);
  1729. 1729 : else
  1730. 1730 : this.draglayer.hide();
  1731. 1731 : }
  1732. 1732 :
  1733. 1733 : if (this.drag_active)
  1734. 1734 : this.focus();
  1735. 1735 : this.drag_active = false;
  1736. 1736 :
  1737. 1737 : rcube_event.remove_listener({event:'mousemove', object:this, method:'drag_mouse_move'});
  1738. 1738 : rcube_event.remove_listener({event:'mouseup', object:this, method:'drag_mouse_up'});
  1739. 1739 :
  1740. 1740 : if (bw.touch) {
  1741. 1741 : rcube_event.remove_listener({event:'touchmove', object:this, method:'drag_mouse_move'});
  1742. 1742 : rcube_event.remove_listener({event:'touchend', object:this, method:'drag_mouse_up'});
  1743. 1743 : }
  1744. 1744 :
  1745. 1745 : // remove temp divs
  1746. 1746 : this.del_dragfix();
  1747. 1747 :
  1748. 1748 : this.triggerEvent('dragend', e);
  1749. 1749 :
  1750. 1750 : return rcube_event.cancel(e);
  1751. 1751 : },
  1752. 1752 :
  1753. 1753 :
  1754. 1754 : /**
  1755. 1755 : * Handler for mouse move events for dragging list column
  1756. 1756 : */
  1757. 1757 : column_drag_mouse_move: function(e)
  1758. 1758 : {
  1759. 1759 : if (this.drag_start) {
  1760. 1760 : // check mouse movement, of less than 3 pixels, don't start dragging
  1761. 1761 : var i, m = rcube_event.get_mouse_pos(e);
  1762. 1762 :
  1763. 1763 : 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))
  1764. 1764 : return false;
  1765. 1765 :
  1766. 1766 : if (!this.col_draglayer) {
  1767. 1767 : var lpos = $(this.list).offset(),
  1768. 1768 : cells = this.thead.rows[0].cells;
  1769. 1769 :
  1770. 1770 : // fix layer position when list is scrolled
  1771. 1771 : lpos.top += this.list.scrollTop + this.list.parentNode.scrollTop;
  1772. 1772 :
  1773. 1773 : // create dragging layer
  1774. 1774 : this.col_draglayer = $('<div>').attr('id', 'rcmcoldraglayer')
  1775. 1775 : .css(lpos).css({ position:'absolute', 'z-index':2001,
  1776. 1776 : 'background-color':'white', opacity:0.75,
  1777. 1777 : height: (this.frame.offsetHeight-2)+'px', width: (this.frame.offsetWidth-2)+'px' })
  1778. 1778 : .appendTo(document.body)
  1779. 1779 : // ... and column position indicator
  1780. 1780 : .append($('<div>').attr('id', 'rcmcolumnindicator')
  1781. 1781 : .css({ position:'absolute', 'border-right':'2px dotted #555',
  1782. 1782 : 'z-index':2002, height: (this.frame.offsetHeight-2)+'px' }));
  1783. 1783 :
  1784. 1784 : this.cols = [];
  1785. 1785 : this.list_pos = this.list_min_pos = lpos.left;
  1786. 1786 : // save columns positions
  1787. 1787 : for (i=0; i<cells.length; i++) {
  1788. 1788 : this.cols[i] = cells[i].offsetWidth;
  1789. 1789 : if (this.column_fixed !== null && i <= this.column_fixed) {
  1790. 1790 : this.list_min_pos += this.cols[i];
  1791. 1791 : }
  1792. 1792 : }
  1793. 1793 : }
  1794. 1794 :
  1795. 1795 : this.col_draglayer.show();
  1796. 1796 : this.col_drag_active = true;
  1797. 1797 : this.triggerEvent('column_dragstart');
  1798. 1798 : }
  1799. 1799 :
  1800. 1800 : // set column indicator position
  1801. 1801 : if (this.col_drag_active && this.col_draglayer) {
  1802. 1802 : var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
  1803. 1803 :
  1804. 1804 : for (i=0; i<this.cols.length; i++) {
  1805. 1805 : if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
  1806. 1806 : cpos += this.cols[i];
  1807. 1807 : else
  1808. 1808 : break;
  1809. 1809 : }
  1810. 1810 :
  1811. 1811 : // handle fixed columns on left
  1812. 1812 : if (i == 0 && this.list_min_pos > pos.x)
  1813. 1813 : cpos = this.list_min_pos - this.list_pos;
  1814. 1814 : // empty list needs some assignment
  1815. 1815 : else if (!this.list.rowcount && i == this.cols.length)
  1816. 1816 : cpos -= 2;
  1817. 1817 : $('#rcmcolumnindicator').css({ width: cpos+'px'});
  1818. 1818 : this.triggerEvent('column_dragmove', e?e:window.event);
  1819. 1819 : }
  1820. 1820 :
  1821. 1821 : this.drag_start = false;
  1822. 1822 :
  1823. 1823 : return false;
  1824. 1824 : },
  1825. 1825 :
  1826. 1826 :
  1827. 1827 : /**
  1828. 1828 : * Handler for mouse up events for dragging list columns
  1829. 1829 : */
  1830. 1830 : column_drag_mouse_up: function(e)
  1831. 1831 : {
  1832. 1832 : document.onmousemove = null;
  1833. 1833 :
  1834. 1834 : if (this.col_draglayer) {
  1835. 1835 : (this.col_draglayer).remove();
  1836. 1836 : this.col_draglayer = null;
  1837. 1837 : }
  1838. 1838 :
  1839. 1839 : rcube_event.remove_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
  1840. 1840 : rcube_event.remove_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
  1841. 1841 :
  1842. 1842 : // remove temp divs
  1843. 1843 : this.del_dragfix();
  1844. 1844 :
  1845. 1845 : if (this.col_drag_active) {
  1846. 1846 : this.col_drag_active = false;
  1847. 1847 : this.focus();
  1848. 1848 : this.triggerEvent('column_dragend', e);
  1849. 1849 :
  1850. 1850 : if (this.selected_column !== null && this.cols && this.cols.length) {
  1851. 1851 : var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
  1852. 1852 :
  1853. 1853 : // find destination position
  1854. 1854 : for (i=0; i<this.cols.length; i++) {
  1855. 1855 : if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
  1856. 1856 : cpos += this.cols[i];
  1857. 1857 : else
  1858. 1858 : break;
  1859. 1859 : }
  1860. 1860 :
  1861. 1861 : if (i != this.selected_column && i != this.selected_column+1) {
  1862. 1862 : this.column_replace(this.selected_column, i);
  1863. 1863 : }
  1864. 1864 : }
  1865. 1865 : }
  1866. 1866 :
  1867. 1867 : return rcube_event.cancel(e);
  1868. 1868 : },
  1869. 1869 :
  1870. 1870 :
  1871. 1871 : /**
  1872. 1872 : * Returns IDs of all rows in a thread (except root) for specified root
  1873. 1873 : */
  1874. 1874 : row_children: function(uid)
  1875. 1875 : {
  1876. 1876 : if (!this.rows[uid] || !this.rows[uid].has_children)
  1877. 1877 : return [];
  1878. 1878 :
  1879. 1879 : var res = [], depth = this.rows[uid].depth,
  1880. 1880 : row = this.rows[uid].obj.nextSibling;
  1881. 1881 :
  1882. 1882 : while (row) {
  1883. 1883 : if (row.nodeType == 1) {
  1884. 1884 : if (r = this.rows[row.uid]) {
  1885. 1885 : if (!r.depth || r.depth <= depth)
  1886. 1886 : break;
  1887. 1887 : res.push(r.uid);
  1888. 1888 : }
  1889. 1889 : }
  1890. 1890 : row = row.nextSibling;
  1891. 1891 : }
  1892. 1892 :
  1893. 1893 : return res;
  1894. 1894 : },
  1895. 1895 :
  1896. 1896 :
  1897. 1897 : /**
  1898. 1898 : * Creates a layer for drag&drop over iframes
  1899. 1899 : */
  1900. 1900 : add_dragfix: function()
  1901. 1901 : {
  1902. 1902 : $('iframe').each(function() {
  1903. 1903 : $('<div class="iframe-dragdrop-fix"></div>')
  1904. 1904 : .css({background: '#fff',
  1905. 1905 : width: this.offsetWidth+'px', height: this.offsetHeight+'px',
  1906. 1906 : position: 'absolute', opacity: '0.001', zIndex: 1000
  1907. 1907 : })
  1908. 1908 : .css($(this).offset())
  1909. 1909 : .appendTo(document.body);
  1910. 1910 : });
  1911. 1911 : },
  1912. 1912 :
  1913. 1913 :
  1914. 1914 : /**
  1915. 1915 : * Removes the layer for drag&drop over iframes
  1916. 1916 : */
  1917. 1917 : del_dragfix: function()
  1918. 1918 : {
  1919. 1919 : $('div.iframe-dragdrop-fix').remove();
  1920. 1920 : },
  1921. 1921 :
  1922. 1922 :
  1923. 1923 : /**
  1924. 1924 : * Replaces two columns
  1925. 1925 : */
  1926. 1926 : column_replace: function(from, to)
  1927. 1927 : {
  1928. 1928 : // only supported for <table> lists
  1929. 1929 : if (!this.thead || !this.thead.rows)
  1930. 1930 : return;
  1931. 1931 :
  1932. 1932 : var len, cells = this.thead.rows[0].cells,
  1933. 1933 : elem = cells[from],
  1934. 1934 : before = cells[to],
  1935. 1935 : td = document.createElement('td');
  1936. 1936 :
  1937. 1937 : // replace header cells
  1938. 1938 : if (before)
  1939. 1939 : cells[0].parentNode.insertBefore(td, before);
  1940. 1940 : else
  1941. 1941 : cells[0].parentNode.appendChild(td);
  1942. 1942 : cells[0].parentNode.replaceChild(elem, td);
  1943. 1943 :
  1944. 1944 : // replace list cells
  1945. 1945 : for (r=0, len=this.tbody.rows.length; r<len; r++) {
  1946. 1946 : row = this.tbody.rows[r];
  1947. 1947 :
  1948. 1948 : elem = row.cells[from];
  1949. 1949 : before = row.cells[to];
  1950. 1950 : td = document.createElement('td');
  1951. 1951 :
  1952. 1952 : if (before)
  1953. 1953 : row.insertBefore(td, before);
  1954. 1954 : else
  1955. 1955 : row.appendChild(td);
  1956. 1956 : row.replaceChild(elem, td);
  1957. 1957 : }
  1958. 1958 :
  1959. 1959 : // update subject column position
  1960. 1960 : if (this.subject_col == from)
  1961. 1961 : this.subject_col = to > from ? to - 1 : to;
  1962. 1962 : else if (this.subject_col < from && to <= this.subject_col)
  1963. 1963 : this.subject_col++;
  1964. 1964 : else if (this.subject_col > from && to >= this.subject_col)
  1965. 1965 : this.subject_col--;
  1966. 1966 :
  1967. 1967 : if (this.fixed_header)
  1968. 1968 : this.init_header();
  1969. 1969 :
  1970. 1970 : this.triggerEvent('column_replace');
  1971. 1971 : },
  1972. 1972 :
  1973. 1973 : subject_column: function()
  1974. 1974 : {
  1975. 1975 : return this.subject_col + (this.checkbox_selection ? 1 : 0);
  1976. 1976 : }
  1977. 1977 :
  1978. 1978 : };
  1979. 1979 :
  1980. 1980 : rcube_list_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
  1981. 1981 : rcube_list_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
  1982. 1982 : rcube_list_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;
  1983. 1983 :
  1984. 1984 : // static
  1985. 1985 : rcube_list_widget._instances = [];