/ * *
* Roundcube List Widget
*
* This file is part of the Roundcube Webmail client
*
* @ licstart The following is the entire license notice for the
* JavaScript code in this file .
*
* Copyright ( c ) 2005 - 2014 , The Roundcube Dev Team
*
* The JavaScript code in this page is free software : you can
* redistribute it and / or modify it under the terms of the GNU
* General Public License ( GNU GPL ) as published by the Free Software
* Foundation , either version 3 of the License , or ( at your option )
* any later version . The code is distributed WITHOUT ANY WARRANTY ;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE . See the GNU GPL for more details .
*
* As additional permission under GNU GPL version 3 section 7 , you
* may distribute non - source ( e . g . , minimized or compacted ) forms of
* that code without the copy of the GNU GPL normally required by
* section 4 , provided you include this license notice and a URL
* through which recipients can access the Corresponding Source .
*
* @ licend The above is the entire license notice
* for the JavaScript code in this file .
*
* @ author Thomas Bruederli < roundcube @ gmail . com >
* @ author Charles McNulty < charles @ charlesmcnulty . com >
*
* @ requires jquery . js , common . js
* /
/ * *
* Roundcube List Widget class
* @ constructor
* /
function rcube _list _widget ( list , p )
{
// static contants
this . ENTER _KEY = 13 ;
this . DELETE _KEY = 46 ;
this . BACKSPACE _KEY = 8 ;
this . list = list ? list : null ;
this . tagname = this . list ? this . list . nodeName . toLowerCase ( ) : 'table' ;
this . id _regexp = /^rcmrow([a-z0-9\-_=\+\/]+)/i ;
this . rows = { } ;
this . selection = [ ] ;
this . rowcount = 0 ;
this . colcount = 0 ;
this . subject _col = 0 ;
this . modkey = 0 ;
this . multiselect = false ;
this . multiexpand = false ;
this . multi _selecting = false ;
this . draggable = false ;
this . column _movable = false ;
this . keyboard = false ;
this . toggleselect = false ;
this . aria _listbox = false ;
this . parent _focus = true ;
this . checkbox _selection = false ;
this . drag _active = false ;
this . col _drag _active = false ;
this . column _fixed = null ;
this . last _selected = null ;
this . shift _start = null ;
this . focused = false ;
this . drag _mouse _start = null ;
this . dblclick _time = 500 ; // default value on MS Windows is 500
this . row _init = function ( ) { } ; // @deprecated; use list.addEventListener('initrow') instead
this . pointer _touch _start = 0 ; // start time of the touch event
this . pointer _touch _time = 500 ; // maximum time a touch should be considered a left mouse button event, after this its something else (eg contextmenu event)
// overwrite default paramaters
if ( p && typeof p === 'object' )
for ( var n in p )
this [ n ] = p [ n ] ;
// register this instance
rcube _list _widget . _instances . push ( this ) ;
} ;
rcube _list _widget . prototype = {
/ * *
* get all message rows from HTML table and init each row
* /
init : function ( )
{
if ( this . tagname == 'table' && this . list && this . list . tBodies [ 0 ] ) {
this . thead = this . list . tHead ;
this . tbody = this . list . tBodies [ 0 ] ;
}
else if ( this . tagname != 'table' && this . list ) {
this . tbody = this . list ;
}
if ( $ ( this . list ) . attr ( 'role' ) == 'listbox' ) {
this . aria _listbox = true ;
if ( this . multiselect )
$ ( this . list ) . attr ( 'aria-multiselectable' , 'true' ) ;
}
var me = this ;
if ( this . tbody ) {
this . rows = { } ;
this . rowcount = 0 ;
var r , len , rows = this . tbody . childNodes ;
for ( r = 0 , len = rows . length ; r < len ; r ++ ) {
if ( rows [ r ] . nodeType == 1 )
this . rowcount += this . init _row ( rows [ r ] ) ? 1 : 0 ;
}
this . init _header ( ) ;
this . frame = this . list . parentNode ;
// set body events
if ( this . keyboard ) {
rcube _event . add _listener ( { event : 'keydown' , object : this , method : 'key_press' } ) ;
// allow the table element to receive focus.
$ ( this . list ) . attr ( 'tabindex' , '0' )
. on ( 'focus' , function ( e ) { me . focus ( e ) ; } ) ;
}
}
if ( this . parent _focus ) {
this . list . parentNode . onclick = function ( e ) { me . focus ( ) ; } ;
}
return this ;
} ,
/ * *
* Init list row and set mouse events on it
* /
init _row : function ( row )
{
row . uid = this . get _row _uid ( row ) ;
// make references in internal array and set event handlers
if ( row && row . uid ) {
var self = this , uid = row . uid ;
this . rows [ uid ] = { uid : uid , id : row . id , obj : row } ;
$ ( row ) . data ( 'uid' , uid )
// set eventhandlers to table row (only left-button-clicks in mouseup)
. mousedown ( function ( e ) { return self . drag _row ( e , this . uid ) ; } )
. mouseup ( function ( e ) {
if ( e . which == 1 && ! self . drag _active && ! $ ( e . currentTarget ) . is ( '.ui-droppable-active' ) )
return self . click _row ( e , this . uid ) ;
else
return true ;
} ) ;
// for IE and Edge differentiate between touch, touch+hold using pointer events rather than touch
if ( ( bw . ie || bw . edge ) && bw . pointer ) {
$ ( row ) . on ( 'pointerdown' , function ( e ) {
if ( e . pointerType == 'touch' ) {
self . pointer _touch _start = new Date ( ) . getTime ( ) ;
return false ;
}
} )
. on ( 'pointerup' , function ( e ) {
if ( e . pointerType == 'touch' ) {
var duration = ( new Date ( ) . getTime ( ) - self . pointer _touch _start ) ;
if ( duration <= self . pointer _touch _time ) {
self . drag _row ( e , this . uid ) ;
return self . click _row ( e , this . uid ) ;
}
}
} ) ;
}
else if ( bw . touch && row . addEventListener ) {
row . addEventListener ( 'touchstart' , function ( e ) {
if ( e . touches . length == 1 ) {
self . touchmoved = false ;
self . drag _row ( rcube _event . touchevent ( e . touches [ 0 ] ) , this . uid )
}
} , false ) ;
row . addEventListener ( 'touchend' , function ( e ) {
if ( e . changedTouches . length == 1 ) {
if ( ! self . touchmoved && ! self . click _row ( rcube _event . touchevent ( e . changedTouches [ 0 ] ) , this . uid ) )
e . preventDefault ( ) ;
}
} , false ) ;
row . addEventListener ( 'touchmove' , function ( e ) {
if ( e . changedTouches . length == 1 ) {
self . touchmoved = true ;
if ( self . drag _active )
e . preventDefault ( ) ;
}
} , false ) ;
}
// label the list row with the subject col as descriptive label
if ( this . aria _listbox ) {
var lbl _id = 'l:' + row . id ;
$ ( row )
. attr ( 'role' , 'option' )
. attr ( 'aria-labelledby' , lbl _id )
. find ( this . col _tagname ( ) ) . eq ( this . subject _column ( ) ) . attr ( 'id' , lbl _id ) ;
}
if ( document . all )
row . onselectstart = function ( ) { return false ; } ;
this . row _init ( this . rows [ uid ] ) ; // legacy support
this . triggerEvent ( 'initrow' , this . rows [ uid ] ) ;
return true ;
}
} ,
/ * *
* Init list column headers and set mouse events on them
* /
init _header : function ( )
{
if ( this . thead ) {
this . colcount = 0 ;
if ( this . fixed _header ) { // copy (modified) fixed header back to the actual table
$ ( this . list . tHead ) . replaceWith ( $ ( this . fixed _header ) . find ( 'thead' ) . clone ( ) ) ;
$ ( this . list . tHead ) . find ( 'th,td' ) . attr ( 'style' , '' ) . find ( 'a' ) . attr ( 'tabindex' , '-1' ) ; // remove fixed widths
}
else if ( ! bw . touch && this . list . className . indexOf ( 'fixedheader' ) >= 0 ) {
this . init _fixed _header ( ) ;
}
var col , r , p = this ;
// add events for list columns moving
if ( this . column _movable && this . thead && this . thead . rows ) {
for ( r = 0 ; r < this . thead . rows [ 0 ] . cells . length ; r ++ ) {
if ( this . column _fixed == r )
continue ;
col = this . thead . rows [ 0 ] . cells [ r ] ;
col . onmousedown = function ( e ) { return p . drag _column ( e , this ) ; } ;
this . colcount ++ ;
}
}
}
} ,
init _fixed _header : function ( )
{
var clone = $ ( this . list . tHead ) . clone ( ) ;
if ( ! this . fixed _header ) {
this . fixed _header = $ ( '<table>' )
. attr ( 'class' , this . list . className + ' fixedcopy' )
. attr ( 'role' , 'presentation' )
. css ( { position : 'fixed' } )
. append ( clone )
. append ( '<tbody></tbody>' ) ;
$ ( this . list ) . before ( this . fixed _header ) ;
var me = this ;
$ ( window ) . resize ( function ( ) { me . resize ( ) ; } ) ;
$ ( window ) . scroll ( function ( ) {
var w = $ ( window ) ;
me . fixed _header . css ( {
marginLeft : - w . scrollLeft ( ) + 'px' ,
marginTop : - w . scrollTop ( ) + 'px'
} ) ;
} ) ;
}
else {
$ ( this . fixed _header ) . find ( 'thead' ) . replaceWith ( clone ) ;
}
// avoid scrolling header links being focused
$ ( this . list . tHead ) . find ( 'a.sortcol' ) . attr ( 'tabindex' , '-1' ) ;
// set tabindex to fixed header sort links
clone . find ( 'a.sortcol' ) . attr ( 'tabindex' , '0' ) ;
this . thead = clone . get ( 0 ) ;
this . resize ( ) ;
} ,
resize : function ( )
{
if ( ! this . fixed _header )
return ;
var column _widths = [ ] ;
// get column widths from original thead
$ ( this . tbody ) . parent ( ) . find ( 'thead th,thead td' ) . each ( function ( index ) {
column _widths [ index ] = $ ( this ) . width ( ) ;
} ) ;
// apply fixed widths to fixed table header
$ ( this . thead ) . parent ( ) . width ( $ ( this . tbody ) . parent ( ) . width ( ) ) ;
$ ( this . thead ) . find ( 'th,td' ) . each ( function ( index ) {
$ ( this ) . width ( column _widths [ index ] ) ;
} ) ;
$ ( window ) . scroll ( ) ;
} ,
/ * *
* Remove all list rows
* /
clear : function ( sel )
{
if ( this . tagname == 'table' ) {
var tbody = document . createElement ( 'tbody' ) ;
this . list . insertBefore ( tbody , this . tbody ) ;
this . list . removeChild ( this . list . tBodies [ 1 ] ) ;
this . tbody = tbody ;
}
else {
$ ( this . row _tagname ( ) + ':not(.thead)' , this . tbody ) . remove ( ) ;
}
this . rows = { } ;
this . rowcount = 0 ;
this . last _selected = null ;
if ( sel )
this . clear _selection ( ) ;
// reset scroll position (in Opera)
if ( this . frame )
this . frame . scrollTop = 0 ;
// fix list header after removing any rows
this . resize ( ) ;
} ,
/ * *
* 'remove' message row from list ( just hide it )
* /
remove _row : function ( uid , sel _next )
{
var self = this , node = this . rows [ uid ] ? this . rows [ uid ] . obj : null ;
if ( ! node )
return ;
node . style . display = 'none' ;
if ( sel _next )
this . select _next ( ) ;
delete this . rows [ uid ] ;
this . rowcount -- ;
// fix list header after removing any rows
clearTimeout ( this . resize _timeout )
this . resize _timeout = setTimeout ( function ( ) { self . resize ( ) ; } , 50 ) ;
} ,
/ * *
* Add row to the list and initialize it
* /
insert _row : function ( row , before )
{
var self = this , tbody = this . tbody ;
// create a real dom node first
if ( row . nodeName === undefined ) {
// for performance reasons use DOM instead of jQuery here
var i , e , domcell , col ,
domrow = document . createElement ( this . row _tagname ( ) ) ;
if ( row . id ) domrow . id = row . id ;
if ( row . uid ) domrow . uid = row . uid ;
if ( row . className ) domrow . className = row . className ;
if ( row . style ) $ . extend ( domrow . style , row . style ) ;
for ( i = 0 ; row . cols && i < row . cols . length ; i ++ ) {
col = row . cols [ i ] ;
domcell = col . dom ;
if ( ! domcell ) {
domcell = document . createElement ( this . col _tagname ( ) ) ;
if ( col . className ) domcell . className = col . className ;
if ( col . innerHTML ) domcell . innerHTML = col . innerHTML ;
for ( e in col . events )
domcell [ 'on' + e ] = col . events [ e ] ;
}
domrow . appendChild ( domcell ) ;
}
row = domrow ;
}
if ( this . checkbox _selection ) {
var key , cell = document . createElement ( this . col _tagname ( ) ) ,
chbox = document . createElement ( 'input' ) ;
chbox . type = 'checkbox' ;
chbox . tabIndex = - 1 ;
chbox . onchange = function ( e ) {
self . select _row ( row . uid , key || CONTROL _KEY , true ) ;
e . stopPropagation ( ) ;
key = null ;
} ;
chbox . onmousedown = function ( e ) {
key = rcube _event . get _modifier ( e ) ;
} ;
cell . className = 'selection' ;
// make the whole cell "touchable" for touch devices
cell . onclick = function ( e ) {
if ( ! $ ( e . target ) . is ( 'input' ) ) {
key = rcube _event . get _modifier ( e ) ;
$ ( chbox ) . prop ( 'checked' , ! chbox . checked ) . change ( ) ;
}
e . stopPropagation ( ) ;
} ;
cell . appendChild ( chbox ) ;
row . insertBefore ( cell , row . firstChild ) ;
}
if ( before && tbody . childNodes . length )
tbody . insertBefore ( row , ( typeof before == 'object' && before . parentNode == tbody ) ? before : tbody . firstChild ) ;
else
tbody . appendChild ( row ) ;
this . init _row ( row ) ;
this . rowcount ++ ;
// fix list header after adding any rows
clearTimeout ( this . resize _timeout )
this . resize _timeout = setTimeout ( function ( ) { self . resize ( ) ; } , 50 ) ;
} ,
/ * *
*
* /
update _row : function ( id , cols , newid , select )
{
var row = this . rows [ id ] ;
if ( ! row ) return false ;
var i , domrow = row . obj ;
for ( i = 0 ; cols && i < cols . length ; i ++ ) {
this . get _cell ( domrow , i ) . html ( cols [ i ] ) ;
}
if ( newid ) {
delete this . rows [ id ] ;
domrow . uid = newid ;
domrow . id = 'rcmrow' + newid ;
this . init _row ( domrow ) ;
if ( select )
this . selection [ 0 ] = newid ;
if ( this . last _selected == id )
this . last _selected = newid ;
}
} ,
/ * *
* Set focus to the list
* /
focus : function ( e )
{
if ( this . focused )
return ;
this . focused = true ;
if ( e )
rcube _event . cancel ( e ) ;
var focus _elem = null ;
if ( this . last _selected && this . rows [ this . last _selected ] ) {
focus _elem = $ ( this . rows [ this . last _selected ] . obj ) . find ( this . col _tagname ( ) ) . eq ( this . subject _column ( ) ) . attr ( 'tabindex' , '0' ) ;
}
// Un-focus already focused elements (#1487123, #1487316, #1488600, #1488620)
if ( focus _elem && focus _elem . length ) {
// We now fix this by explicitly assigning focus to a dedicated link element
this . focus _noscroll ( focus _elem ) ;
}
else {
// It looks that window.focus() does the job for all browsers, but not Firefox (#1489058)
$ ( 'iframe,:focus:not(body)' ) . blur ( ) ;
window . focus ( ) ;
}
$ ( this . list ) . addClass ( 'focus' ) . removeAttr ( 'tabindex' ) ;
// set internal focus pointer to first row
if ( ! this . last _selected )
this . select _first ( CONTROL _KEY ) ;
} ,
/ * *
* remove focus from the list
* /
blur : function ( e )
{
this . focused = false ;
// avoid the table getting focus right again (on Shift+Tab)
var me = this ;
setTimeout ( function ( ) { $ ( me . list ) . attr ( 'tabindex' , '0' ) ; } , 20 ) ;
if ( this . last _selected && this . rows [ this . last _selected ] ) {
$ ( this . rows [ this . last _selected ] . obj )
. find ( this . col _tagname ( ) ) . eq ( this . subject _column ( ) ) . removeAttr ( 'tabindex' ) ;
}
$ ( this . list ) . removeClass ( 'focus' ) ;
} ,
/ * *
* Focus the given element without scrolling the list container
* /
focus _noscroll : function ( elem )
{
var y = this . frame . scrollTop || this . frame . scrollY ;
elem . focus ( ) ;
this . frame . scrollTop = y ;
} ,
/ * *
* Set / unset the given column as hidden
* /
hide _column : function ( col , hide )
{
var method = hide ? 'addClass' : 'removeClass' ;
if ( this . fixed _header )
$ ( this . row _tagname ( ) + ' ' + this . col _tagname ( ) + '.' + col , this . fixed _header ) [ method ] ( 'hidden' ) ;
$ ( this . row _tagname ( ) + ' ' + this . col _tagname ( ) + '.' + col , this . list ) [ method ] ( 'hidden' ) ;
} ,
/ * *
* onmousedown - handler of message list column
* /
drag _column : function ( e , col )
{
if ( this . colcount > 1 ) {
this . drag _start = true ;
this . drag _mouse _start = rcube _event . get _mouse _pos ( e ) ;
rcube _event . add _listener ( { event : 'mousemove' , object : this , method : 'column_drag_mouse_move' } ) ;
rcube _event . add _listener ( { event : 'mouseup' , object : this , method : 'column_drag_mouse_up' } ) ;
// enable dragging over iframes
this . add _dragfix ( ) ;
// find selected column number
for ( var i = 0 ; i < this . thead . rows [ 0 ] . cells . length ; i ++ ) {
if ( col == this . thead . rows [ 0 ] . cells [ i ] ) {
this . selected _column = i ;
break ;
}
}
}
return false ;
} ,
/ * *
* onmousedown - handler of message list row
* /
drag _row : function ( e , id )
{
// don't do anything (another action processed before)
if ( ! this . is _event _target ( e ) )
return true ;
// accept right-clicks
if ( rcube _event . get _button ( e ) == 2 )
return true ;
this . in _selection _before = e && e . istouch || this . in _selection ( id ) ? id : false ;
// selects currently unselected row
if ( ! this . in _selection _before ) {
var mod _key = rcube _event . get _modifier ( e ) ;
this . select _row ( id , mod _key , true ) ;
}
if ( this . draggable && this . selection . length && this . in _selection ( id ) ) {
this . drag _start = true ;
this . drag _mouse _start = rcube _event . get _mouse _pos ( e ) ;
rcube _event . add _listener ( { event : 'mousemove' , object : this , method : 'drag_mouse_move' } ) ;
rcube _event . add _listener ( { event : 'mouseup' , object : this , method : 'drag_mouse_up' } ) ;
if ( bw . touch ) {
rcube _event . add _listener ( { event : 'touchmove' , object : this , method : 'drag_mouse_move' } ) ;
rcube _event . add _listener ( { event : 'touchend' , object : this , method : 'drag_mouse_up' } ) ;
}
// enable dragging over iframes
this . add _dragfix ( ) ;
}
return false ;
} ,
/ * *
* onmouseup - handler of message list row
* /
click _row : function ( e , id )
{
// sanity check
if ( ! id || ! this . rows [ id ] )
return false ;
// don't do anything (another action processed before)
if ( ! this . is _event _target ( e ) )
return true ;
var now = new Date ( ) . getTime ( ) ,
dblclicked = now - this . rows [ id ] . clicked < this . dblclick _time ;
// unselects currently selected row
if ( ! this . drag _active && ! dblclicked && this . in _selection _before == id )
this . select _row ( id , rcube _event . get _modifier ( e ) , true ) ;
this . drag _start = false ;
this . in _selection _before = false ;
// row was double clicked
if ( this . rowcount && dblclicked && this . in _selection ( id ) ) {
this . triggerEvent ( 'dblclick' ) ;
now = 0 ;
}
else
this . triggerEvent ( 'click' ) ;
if ( ! this . drag _active ) {
// remove temp divs
this . del _dragfix ( ) ;
rcube _event . cancel ( e ) ;
}
this . rows [ id ] . clicked = now ;
this . focus ( ) ;
return false ;
} ,
/ * *
* Check target of the current event
* /
is _event _target : function ( e )
{
var target = rcube _event . get _target ( e ) ,
tagname = target . tagName . toLowerCase ( ) ;
return ! ( target && ( tagname == 'input' || tagname == 'img' || ( tagname != 'a' && target . onclick ) || $ ( target ) . data ( 'action-link' ) ) ) ;
} ,
/ *
* Returns thread root ID for specified row ID
* /
find _root : function ( uid )
{
var r = this . rows [ uid ] ;
if ( r && r . parent _uid )
return this . find _root ( r . parent _uid ) ;
else
return uid ;
} ,
expand _row : function ( e , id )
{
var row = this . rows [ id ] ,
evtarget = rcube _event . get _target ( e ) ,
mod _key = rcube _event . get _modifier ( e ) ;
// Don't treat double click on the expando as double click on the message.
row . clicked = 0 ;
if ( row . expanded ) {
evtarget . className = 'collapsed' ;
if ( mod _key == CONTROL _KEY || this . multiexpand )
this . collapse _all ( row ) ;
else
this . collapse ( row ) ;
}
else {
evtarget . className = 'expanded' ;
if ( mod _key == CONTROL _KEY || this . multiexpand )
this . expand _all ( row ) ;
else
this . expand ( row ) ;
}
} ,
collapse : function ( row )
{
var r , depth = row . depth ,
new _row = row ? row . obj . nextSibling : null ;
row . expanded = false ;
this . triggerEvent ( 'expandcollapse' , { uid : row . uid , expanded : row . expanded , obj : row . obj } ) ;
while ( new _row ) {
if ( new _row . nodeType == 1 ) {
r = this . rows [ new _row . uid ] ;
if ( r && r . depth <= depth )
break ;
$ ( new _row ) . css ( 'display' , 'none' ) ;
if ( r . expanded ) {
r . expanded = false ;
this . triggerEvent ( 'expandcollapse' , { uid : r . uid , expanded : r . expanded , obj : new _row } ) ;
}
}
new _row = new _row . nextSibling ;
}
this . resize ( ) ;
this . triggerEvent ( 'listupdate' ) ;
return false ;
} ,
expand : function ( row )
{
var r , p , depth , new _row , last _expanded _parent _depth ;
if ( row ) {
row . expanded = true ;
depth = row . depth ;
new _row = row . obj . nextSibling ;
this . update _expando ( row . id , true ) ;
this . triggerEvent ( 'expandcollapse' , { uid : row . uid , expanded : row . expanded , obj : row . obj } ) ;
}
else {
var tbody = this . tbody ;
new _row = tbody . firstChild ;
depth = 0 ;
last _expanded _parent _depth = 0 ;
}
while ( new _row ) {
if ( new _row . nodeType == 1 ) {
r = this . rows [ new _row . uid ] ;
if ( r ) {
if ( row && ( ! r . depth || r . depth <= depth ) )
break ;
if ( r . parent _uid ) {
p = this . rows [ r . parent _uid ] ;
if ( p && p . expanded ) {
if ( ( row && p == row ) || last _expanded _parent _depth >= p . depth - 1 ) {
last _expanded _parent _depth = p . depth ;
$ ( new _row ) . css ( 'display' , '' ) ;
r . expanded = true ;
this . triggerEvent ( 'expandcollapse' , { uid : r . uid , expanded : r . expanded , obj : new _row } ) ;
}
}
else
if ( row && ( ! p || p . depth <= depth ) )
break ;
}
}
}
new _row = new _row . nextSibling ;
}
this . resize ( ) ;
this . triggerEvent ( 'listupdate' ) ;
return false ;
} ,
collapse _all : function ( row )
{
var depth , new _row , r ;
if ( row ) {
row . expanded = false ;
depth = row . depth ;
new _row = row . obj . nextSibling ;
this . update _expando ( row . id ) ;
this . triggerEvent ( 'expandcollapse' , { uid : row . uid , expanded : row . expanded , obj : row . obj } ) ;
// don't collapse sub-root tree in multiexpand mode
if ( depth && this . multiexpand )
return false ;
}
else {
new _row = this . tbody . firstChild ;
depth = 0 ;
}
while ( new _row ) {
if ( new _row . nodeType == 1 ) {
if ( r = this . rows [ new _row . uid ] ) {
if ( row && ( ! r . depth || r . depth <= depth ) )
break ;
if ( row || r . depth )
$ ( new _row ) . css ( 'display' , 'none' ) ;
if ( r . has _children && r . expanded ) {
r . expanded = false ;
this . update _expando ( r . id , false ) ;
this . triggerEvent ( 'expandcollapse' , { uid : r . uid , expanded : r . expanded , obj : new _row } ) ;
}
}
}
new _row = new _row . nextSibling ;
}
this . resize ( ) ;
this . triggerEvent ( 'listupdate' ) ;
return false ;
} ,
expand _all : function ( row )
{
var depth , new _row , r ;
if ( row ) {
row . expanded = true ;
depth = row . depth ;
new _row = row . obj . nextSibling ;
this . update _expando ( row . id , true ) ;
this . triggerEvent ( 'expandcollapse' , { uid : row . uid , expanded : row . expanded , obj : row . obj } ) ;
}
else {
new _row = this . tbody . firstChild ;
depth = 0 ;
}
while ( new _row ) {
if ( new _row . nodeType == 1 ) {
if ( r = this . rows [ new _row . uid ] ) {
if ( row && r . depth <= depth )
break ;
$ ( new _row ) . css ( 'display' , '' ) ;
if ( r . has _children && ! r . expanded ) {
r . expanded = true ;
this . update _expando ( r . id , true ) ;
this . triggerEvent ( 'expandcollapse' , { uid : r . uid , expanded : r . expanded , obj : new _row } ) ;
}
}
}
new _row = new _row . nextSibling ;
}
this . resize ( ) ;
this . triggerEvent ( 'listupdate' ) ;
return false ;
} ,
update _expando : function ( id , expanded )
{
var expando = document . getElementById ( 'rcmexpando' + id ) ;
if ( expando )
expando . className = expanded ? 'expanded' : 'collapsed' ;
} ,
get _row _uid : function ( row )
{
if ( ! row )
return ;
if ( ! row . uid ) {
var uid = $ ( row ) . data ( 'uid' ) ;
if ( uid )
row . uid = uid ;
else if ( String ( row . id ) . match ( this . id _regexp ) )
row . uid = RegExp . $1 ;
}
return row . uid ;
} ,
/ * *
* get first / next / previous / last rows that are not hidden
* /
get _next _row : function ( )
{
if ( ! this . rowcount )
return false ;
var last _selected _row = this . rows [ this . last _selected ] ,
new _row = last _selected _row ? last _selected _row . obj . nextSibling : null ;
while ( new _row && ( new _row . nodeType != 1 || new _row . style . display == 'none' ) )
new _row = new _row . nextSibling ;
return new _row ;
} ,
get _prev _row : function ( )
{
if ( ! this . rowcount )
return false ;
var last _selected _row = this . rows [ this . last _selected ] ,
new _row = last _selected _row ? last _selected _row . obj . previousSibling : null ;
while ( new _row && ( new _row . nodeType != 1 || new _row . style . display == 'none' ) )
new _row = new _row . previousSibling ;
return new _row ;
} ,
get _first _row : function ( )
{
if ( this . rowcount ) {
var i , uid , rows = this . tbody . childNodes ;
for ( i = 0 ; i < rows . length ; i ++ )
if ( rows [ i ] . id && ( uid = this . get _row _uid ( rows [ i ] ) ) && this . rows [ uid ] )
return uid ;
}
return null ;
} ,
get _last _row : function ( )
{
if ( this . rowcount ) {
var i , uid , rows = this . tbody . childNodes ;
for ( i = rows . length - 1 ; i >= 0 ; i -- )
if ( rows [ i ] . id && ( uid = this . get _row _uid ( rows [ i ] ) ) && this . rows [ uid ] )
return uid ;
}
return null ;
} ,
get _next : function ( )
{
var row ;
if ( row = this . get _next _row ( ) ) {
return row . uid ;
}
} ,
get _prev : function ( )
{
var row ;
if ( row = this . get _prev _row ( ) ) {
return row . uid ;
}
} ,
row _tagname : function ( )
{
var row _tagnames = { table : 'tr' , ul : 'li' , '*' : 'div' } ;
return row _tagnames [ this . tagname ] || row _tagnames [ '*' ] ;
} ,
col _tagname : function ( )
{
var col _tagnames = { table : 'td' , '*' : 'span' } ;
return col _tagnames [ this . tagname ] || col _tagnames [ '*' ] ;
} ,
get _cell : function ( row , index )
{
return $ ( this . col _tagname ( ) , row ) . eq ( index + ( this . checkbox _selection ? 1 : 0 ) ) ;
} ,
/ * *
* selects or unselects the proper row depending on the modifier key pressed
* /
select _row : function ( id , mod _key , with _mouse )
{
var select _before = this . selection . join ( ',' ) ,
in _selection _before = this . in _selection ( id ) ;
if ( ! this . multiselect && with _mouse )
mod _key = 0 ;
if ( ! this . shift _start )
this . shift _start = id
if ( ! mod _key ) {
this . shift _start = id ;
this . highlight _row ( id , false ) ;
this . multi _selecting = false ;
}
else {
switch ( mod _key ) {
case SHIFT _KEY :
this . shift _select ( id , false ) ;
break ;
case CONTROL _KEY :
if ( with _mouse ) {
this . shift _start = id ;
this . highlight _row ( id , true ) ;
}
break ;
case CONTROL _SHIFT _KEY :
this . shift _select ( id , true ) ;
break ;
default :
this . highlight _row ( id , false ) ;
break ;
}
this . multi _selecting = true ;
}
if ( this . last _selected && this . rows [ this . last _selected ] ) {
$ ( this . rows [ this . last _selected ] . obj ) . removeClass ( 'focused' )
. find ( this . col _tagname ( ) ) . eq ( this . subject _column ( ) ) . removeAttr ( 'tabindex' ) ;
}
// unselect if toggleselect is active and the same row was clicked again
if ( this . toggleselect && in _selection _before && ! mod _key ) {
this . clear _selection ( ) ;
}
// trigger event if selection changed
else if ( this . selection . join ( ',' ) != select _before ) {
this . triggerEvent ( 'select' ) ;
}
if ( this . rows [ id ] ) {
$ ( this . rows [ id ] . obj ) . addClass ( 'focused' ) ;
// set cursor focus to link inside selected row
if ( this . focused )
this . focus _noscroll ( $ ( this . rows [ id ] . obj ) . find ( this . col _tagname ( ) ) . eq ( this . subject _column ( ) ) . attr ( 'tabindex' , '0' ) ) ;
}
if ( ! this . selection . length )
this . shift _start = null ;
this . last _selected = id ;
} ,
/ * *
* Alias method for select _row
* /
select : function ( id )
{
this . select _row ( id , false ) ;
this . scrollto ( id ) ;
} ,
/ * *
* Select row next to the last selected one .
* Either below or above .
* /
select _next : function ( )
{
var new _row = this . get _next _row ( ) || this . get _prev _row ( ) ;
if ( new _row )
this . select _row ( new _row . uid , false , false ) ;
} ,
/ * *
* Select first row
* /
select _first : function ( mod _key )
{
var row = this . get _first _row ( ) ;
if ( row ) {
this . select _row ( row , mod _key , false ) ;
this . scrollto ( row ) ;
}
} ,
/ * *
* Select last row
* /
select _last : function ( mod _key )
{
var row = this . get _last _row ( ) ;
if ( row ) {
this . select _row ( row , mod _key , false ) ;
this . scrollto ( row ) ;
}
} ,
/ * *
* Add all childs of the given row to selection
* /
select _children : function ( uid )
{
var i , children = this . row _children ( uid ) , len = children . length ;
for ( i = 0 ; i < len ; i ++ )
if ( ! this . in _selection ( children [ i ] ) )
this . select _row ( children [ i ] , CONTROL _KEY , true ) ;
} ,
/ * *
* Perform selection when shift key is pressed
* /
shift _select : function ( id , control )
{
if ( ! this . rows [ this . shift _start ] || ! this . selection . length )
this . shift _start = id ;
var n , i , j , to _row = this . rows [ id ] ,
from _rowIndex = this . _rowIndex ( this . rows [ this . shift _start ] . obj ) ,
to _rowIndex = this . _rowIndex ( to _row . obj ) ;
// if we're going down the list, and we hit a thread, and it's closed, select the whole thread
if ( from _rowIndex < to _rowIndex && ! to _row . expanded && to _row . has _children )
if ( to _row = this . rows [ ( this . row _children ( id ) ) . pop ( ) ] )
to _rowIndex = this . _rowIndex ( to _row . obj ) ;
i = ( ( from _rowIndex < to _rowIndex ) ? from _rowIndex : to _rowIndex ) ,
j = ( ( from _rowIndex > to _rowIndex ) ? from _rowIndex : to _rowIndex ) ;
// iterate through the entire message list
for ( n in this . rows ) {
if ( this . _rowIndex ( this . rows [ n ] . obj ) >= i && this . _rowIndex ( this . rows [ n ] . obj ) <= j ) {
if ( ! this . in _selection ( n ) ) {
this . highlight _row ( n , true ) ;
}
}
else {
if ( this . in _selection ( n ) && ! control ) {
this . highlight _row ( n , true ) ;
}
}
}
} ,
/ * *
* Helper method to emulate the rowIndex property of non - tr elements
* /
_rowIndex : function ( obj )
{
return ( obj . rowIndex !== undefined ) ? obj . rowIndex : $ ( obj ) . prevAll ( ) . length ;
} ,
/ * *
* Check if given id is part of the current selection
* /
in _selection : function ( id , index )
{
for ( var n in this . selection )
if ( this . selection [ n ] == id )
return index ? parseInt ( n ) : true ;
return false ;
} ,
/ * *
* Select each row in list
* /
select _all : function ( filter )
{
if ( ! this . rowcount )
return false ;
// reset but remember selection first
var n , select _before = this . selection . join ( ',' ) ;
this . selection = [ ] ;
for ( n in this . rows ) {
if ( ! filter || this . rows [ n ] [ filter ] == true ) {
this . last _selected = n ;
this . highlight _row ( n , true , true ) ;
}
else {
$ ( this . rows [ n ] . obj ) . removeClass ( 'selected' ) . removeAttr ( 'aria-selected' ) ;
}
}
// trigger event if selection changed
if ( this . selection . join ( ',' ) != select _before )
this . triggerEvent ( 'select' ) ;
this . focus ( ) ;
return true ;
} ,
/ * *
* Invert selection
* /
invert _selection : function ( )
{
if ( ! this . rowcount )
return false ;
// remember old selection
var n , select _before = this . selection . join ( ',' ) ;
for ( n in this . rows )
this . highlight _row ( n , true ) ;
// trigger event if selection changed
if ( this . selection . join ( ',' ) != select _before )
this . triggerEvent ( 'select' ) ;
this . focus ( ) ;
return true ;
} ,
/ * *
* Unselect selected row ( s )
* /
clear _selection : function ( id , no _event )
{
var n , num _select = this . selection . length ;
// one row
if ( id ) {
for ( n in this . selection )
if ( this . selection [ n ] == id ) {
this . selection . splice ( n , 1 ) ;
break ;
}
}
// all rows
else {
for ( n in this . selection )
if ( this . rows [ this . selection [ n ] ] ) {
$ ( this . rows [ this . selection [ n ] ] . obj ) . removeClass ( 'selected' ) . removeAttr ( 'aria-selected' ) ;
}
this . selection = [ ] ;
}
if ( this . checkbox _selection )
$ ( this . row _tagname ( ) + ':not(.selected) > .selection > input:checked' , this . list ) . prop ( 'checked' , false ) ;
if ( num _select && ! this . selection . length && ! no _event ) {
this . triggerEvent ( 'select' ) ;
this . last _selected = null ;
}
} ,
/ * *
* Getter for the selection array
* /
get _selection : function ( deep )
{
var res ;
if ( res = this . triggerEvent ( 'getselection' , deep ) )
return res ;
res = $ . merge ( [ ] , this . selection ) ;
// return children of selected threads even if only root is selected
if ( deep !== false && res . length ) {
for ( var uid , uids , i = 0 , len = res . length ; i < len ; i ++ ) {
uid = res [ i ] ;
if ( this . rows [ uid ] && this . rows [ uid ] . has _children && ! this . rows [ uid ] . expanded ) {
uids = this . row _children ( uid ) ;
for ( var j = 0 , uids _len = uids . length ; j < uids _len ; j ++ ) {
uid = uids [ j ] ;
if ( ! this . in _selection ( uid ) )
res . push ( uid ) ;
}
}
}
}
return res ;
} ,
/ * *
* Return the ID if only one row is selected
* /
get _single _selection : function ( )
{
if ( this . selection . length == 1 )
return this . selection [ 0 ] ;
else
return null ;
} ,
/ * *
* Highlight / unhighlight a row
* /
highlight _row : function ( id , multiple , norecur )
{
if ( ! this . rows [ id ] )
return ;
if ( ! multiple ) {
if ( this . selection . length > 1 || ! this . in _selection ( id ) ) {
this . clear _selection ( null , true ) ;
this . selection [ 0 ] = id ;
$ ( this . rows [ id ] . obj ) . addClass ( 'selected' ) . attr ( 'aria-selected' , 'true' ) ;
if ( this . checkbox _selection )
$ ( '.selection > input' , this . rows [ id ] . obj ) . prop ( 'checked' , true ) ;
}
}
else {
var pre , post , p = this . in _selection ( id , true ) ;
if ( p === false ) { // select row
this . selection . push ( id ) ;
$ ( this . rows [ id ] . obj ) . addClass ( 'selected' ) . attr ( 'aria-selected' , 'true' ) ;
if ( this . checkbox _selection )
$ ( '.selection > input' , this . rows [ id ] . obj ) . prop ( 'checked' , true ) ;
if ( ! norecur && ! this . rows [ id ] . expanded )
this . highlight _children ( id , true ) ;
}
else { // unselect row
pre = this . selection . slice ( 0 , p ) ;
post = this . selection . slice ( p + 1 , this . selection . length ) ;
this . selection = pre . concat ( post ) ;
$ ( this . rows [ id ] . obj ) . removeClass ( 'selected' ) . removeAttr ( 'aria-selected' ) ;
if ( this . checkbox _selection )
$ ( '.selection > input' , this . rows [ id ] . obj ) . prop ( 'checked' , false ) ;
if ( ! norecur && ! this . rows [ id ] . expanded )
this . highlight _children ( id , false ) ;
}
}
} ,
/ * *
* Highlight / unhighlight all childs of the given row
* /
highlight _children : function ( id , status )
{
var i , selected ,
children = this . row _children ( id ) , len = children . length ;
for ( i = 0 ; i < len ; i ++ ) {
selected = this . in _selection ( children [ i ] ) ;
if ( ( status && ! selected ) || ( ! status && selected ) )
this . highlight _row ( children [ i ] , true , true ) ;
}
} ,
/ * *
* Handler for keyboard events
* /
key _press : function ( e )
{
var target = e . target || { } ;
if ( ! this . focused || target . nodeName == 'INPUT' || target . nodeName == 'TEXTAREA' || target . nodeName == 'SELECT' )
return true ;
var keyCode = rcube _event . get _keycode ( e ) ,
mod _key = rcube _event . get _modifier ( e ) ;
switch ( keyCode ) {
case 40 :
case 38 :
case 63233 : // "down", in safari keypress
case 63232 : // "up", in safari keypress
// Stop propagation so that the browser doesn't scroll
rcube _event . cancel ( e ) ;
return this . use _arrow _key ( keyCode , mod _key ) ;
case 32 :
rcube _event . cancel ( e ) ;
return this . select _row ( this . last _selected , mod _key , true ) ;
case 37 : // Left arrow key
case 39 : // Right arrow key
// Stop propagation
rcube _event . cancel ( e ) ;
var ret = this . use _arrow _key ( keyCode , mod _key ) ;
this . key _pressed = keyCode ;
this . modkey = mod _key ;
this . triggerEvent ( 'keypress' ) ;
this . modkey = 0 ;
return ret ;
case 36 : // Home
this . select _first ( mod _key ) ;
return rcube _event . cancel ( e ) ;
case 35 : // End
this . select _last ( mod _key ) ;
return rcube _event . cancel ( e ) ;
case 27 :
if ( this . drag _active )
return this . drag _mouse _up ( e ) ;
if ( this . col _drag _active ) {
this . selected _column = null ;
return this . column _drag _mouse _up ( e ) ;
}
return rcube _event . cancel ( e ) ;
case 9 : // Tab
this . blur ( ) ;
break ;
case 13 : // Enter
if ( ! this . selection . length )
this . select _row ( this . last _selected , mod _key , false ) ;
default :
this . key _pressed = keyCode ;
this . modkey = mod _key ;
this . triggerEvent ( 'keypress' ) ;
this . modkey = 0 ;
if ( this . key _pressed == this . BACKSPACE _KEY )
return rcube _event . cancel ( e ) ;
}
return true ;
} ,
/ * *
* Special handling method for arrow keys
* /
use _arrow _key : function ( keyCode , mod _key )
{
var new _row ,
selected _row = this . rows [ this . last _selected ] ;
// Safari uses the nonstandard keycodes 63232/63233 for up/down, if we're
// using the keypress event (but not the keydown or keyup event).
if ( keyCode == 40 || keyCode == 63233 ) // down arrow key pressed
new _row = this . get _next _row ( ) ;
else if ( keyCode == 38 || keyCode == 63232 ) // up arrow key pressed
new _row = this . get _prev _row ( ) ;
else {
if ( ! selected _row || ! selected _row . has _children )
return ;
// expand
if ( keyCode == 39 ) {
if ( selected _row . expanded )
return ;
if ( mod _key == CONTROL _KEY || this . multiexpand )
this . expand _all ( selected _row ) ;
else
this . expand ( selected _row ) ;
}
// collapse
else {
if ( ! selected _row . expanded )
return ;
if ( mod _key == CONTROL _KEY || this . multiexpand )
this . collapse _all ( selected _row ) ;
else
this . collapse ( selected _row ) ;
}
this . update _expando ( selected _row . id , selected _row . expanded ) ;
return false ;
}
if ( new _row ) {
// simulate ctr-key if no rows are selected
if ( ! mod _key && ! this . selection . length )
mod _key = CONTROL _KEY ;
this . select _row ( new _row . uid , mod _key , false ) ;
this . scrollto ( new _row . uid ) ;
}
else if ( ! new _row && ! selected _row ) {
// select the first row if none selected yet
this . select _first ( CONTROL _KEY ) ;
}
return false ;
} ,
/ * *
* Try to scroll the list to make the specified row visible
* /
scrollto : function ( id )
{
var row = this . rows [ id ] ? this . rows [ id ] . obj : null ;
if ( row && this . frame ) {
var scroll _to = Number ( row . offsetTop ) ,
head _offset = 0 ;
// expand thread if target row is hidden (collapsed)
if ( ! scroll _to && this . rows [ id ] . parent _uid ) {
var parent = this . find _root ( this . rows [ id ] . uid ) ;
this . expand _all ( this . rows [ parent ] ) ;
scroll _to = Number ( row . offsetTop ) ;
}
if ( this . fixed _header )
head _offset = Number ( this . thead . offsetHeight ) ;
// if row is above the frame (or behind header)
if ( scroll _to < Number ( this . frame . scrollTop ) + head _offset ) {
// scroll window so that row isn't behind header
this . frame . scrollTop = scroll _to - head _offset ;
}
else if ( scroll _to + Number ( row . offsetHeight ) > Number ( this . frame . scrollTop ) + Number ( this . frame . offsetHeight ) )
this . frame . scrollTop = ( scroll _to + Number ( row . offsetHeight ) ) - Number ( this . frame . offsetHeight ) ;
}
} ,
/ * *
* Handler for mouse move events
* /
drag _mouse _move : function ( e )
{
// convert touch event
if ( e . type == 'touchmove' ) {
if ( e . touches . length == 1 && e . changedTouches . length == 1 )
e = rcube _event . touchevent ( e . changedTouches [ 0 ] ) ;
else
return rcube _event . cancel ( e ) ;
}
if ( this . drag _start ) {
// check mouse movement, of less than 3 pixels, don't start dragging
var m = rcube _event . get _mouse _pos ( e ) ,
limit = 10 , selection = [ ] , self = this ;
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 ) )
return false ;
// remember dragging start position
this . drag _start _pos = { left : m . x , top : m . y } ;
// initialize drag layer
if ( ! this . draglayer )
this . draglayer = $ ( '<div>' ) . attr ( 'id' , 'rcmdraglayer' )
. css ( { position : 'absolute' , display : 'none' , 'z-index' : 2000 } )
. appendTo ( document . body ) ;
else
this . draglayer . html ( '' ) ;
// get selected rows (in display order), don't use this.selection here
$ ( this . row _tagname ( ) + '.selected' , this . tbody ) . each ( function ( ) {
var uid = self . get _row _uid ( this ) , row = self . rows [ uid ] ;
if ( ! row || $ . inArray ( uid , selection ) > - 1 )
return ;
selection . push ( uid ) ;
// also handle children of (collapsed) trees for dragging (they might be not selected)
if ( row . has _children && ! row . expanded )
$ . each ( self . row _children ( uid ) , function ( ) {
if ( $ . inArray ( this , selection ) > - 1 )
return ;
selection . push ( this ) ;
} ) ;
// break the loop asap
if ( selection . length > limit + 1 )
return false ;
} ) ;
// append subject (of every row up to the limit) to the drag layer
$ . each ( selection , function ( i , uid ) {
if ( i > limit ) {
self . draglayer . append ( $ ( '<div>' ) . text ( '...' ) ) ;
return false ;
}
var subject _col = self . subject _column ( ) ;
$ ( '> ' + self . col _tagname ( ) , self . rows [ uid ] . obj ) . each ( function ( n , cell ) {
if ( subject _col < 0 || ( subject _col >= 0 && subject _col == n ) ) {
// remove elements marked with "skip-on-drag" class
cell = $ ( cell ) . clone ( ) ;
$ ( cell ) . find ( '.skip-on-drag' ) . remove ( ) ;
var subject = cell . text ( ) ;
if ( subject ) {
// remove leading spaces
subject = $ . trim ( subject ) ;
// truncate line to 50 characters
subject = ( subject . length > 50 ? subject . substring ( 0 , 50 ) + '...' : subject ) ;
self . draglayer . append ( $ ( '<div>' ) . text ( subject ) ) ;
return false ;
}
}
} ) ;
} ) ;
this . draglayer . show ( ) ;
this . drag _active = true ;
this . triggerEvent ( 'dragstart' ) ;
}
if ( this . drag _active && this . draglayer ) {
var pos = rcube _event . get _mouse _pos ( e ) ;
this . draglayer . css ( { left : ( pos . x + 20 ) + 'px' , top : ( pos . y - 5 + ( bw . ie ? document . documentElement . scrollTop : 0 ) ) + 'px' } ) ;
this . triggerEvent ( 'dragmove' , e ? e : window . event ) ;
}
this . drag _start = false ;
return false ;
} ,
/ * *
* Handler for mouse up events
* /
drag _mouse _up : function ( e )
{
document . onmousemove = null ;
if ( e . type == 'touchend' ) {
if ( e . changedTouches . length != 1 )
return rcube _event . cancel ( e ) ;
}
if ( this . draglayer && this . draglayer . is ( ':visible' ) ) {
if ( this . drag _start _pos )
this . draglayer . animate ( this . drag _start _pos , 300 , 'swing' ) . hide ( 20 ) ;
else
this . draglayer . hide ( ) ;
}
if ( this . drag _active )
this . focus ( ) ;
this . drag _active = false ;
rcube _event . remove _listener ( { event : 'mousemove' , object : this , method : 'drag_mouse_move' } ) ;
rcube _event . remove _listener ( { event : 'mouseup' , object : this , method : 'drag_mouse_up' } ) ;
if ( bw . touch ) {
rcube _event . remove _listener ( { event : 'touchmove' , object : this , method : 'drag_mouse_move' } ) ;
rcube _event . remove _listener ( { event : 'touchend' , object : this , method : 'drag_mouse_up' } ) ;
}
// remove temp divs
this . del _dragfix ( ) ;
this . triggerEvent ( 'dragend' , e ) ;
return rcube _event . cancel ( e ) ;
} ,
/ * *
* Handler for mouse move events for dragging list column
* /
column _drag _mouse _move : function ( e )
{
if ( this . drag _start ) {
// check mouse movement, of less than 3 pixels, don't start dragging
var i , m = rcube _event . get _mouse _pos ( e ) ;
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 ) )
return false ;
if ( ! this . col _draglayer ) {
var lpos = $ ( this . list ) . offset ( ) ,
cells = this . thead . rows [ 0 ] . cells ;
// fix layer position when list is scrolled
lpos . top += this . list . scrollTop + this . list . parentNode . scrollTop ;
// create dragging layer
this . col _draglayer = $ ( '<div>' ) . attr ( 'id' , 'rcmcoldraglayer' )
. css ( lpos ) . css ( { position : 'absolute' , 'z-index' : 2001 ,
'background-color' : 'white' , opacity : 0.75 ,
height : ( this . frame . offsetHeight - 2 ) + 'px' , width : ( this . frame . offsetWidth - 2 ) + 'px' } )
. appendTo ( document . body )
// ... and column position indicator
. append ( $ ( '<div>' ) . attr ( 'id' , 'rcmcolumnindicator' )
. css ( { position : 'absolute' , 'border-right' : '2px dotted #555' ,
'z-index' : 2002 , height : ( this . frame . offsetHeight - 2 ) + 'px' } ) ) ;
this . cols = [ ] ;
this . list _pos = this . list _min _pos = lpos . left ;
// save columns positions
for ( i = 0 ; i < cells . length ; i ++ ) {
this . cols [ i ] = cells [ i ] . offsetWidth ;
if ( this . column _fixed !== null && i <= this . column _fixed ) {
this . list _min _pos += this . cols [ i ] ;
}
}
}
this . col _draglayer . show ( ) ;
this . col _drag _active = true ;
this . triggerEvent ( 'column_dragstart' ) ;
}
// set column indicator position
if ( this . col _drag _active && this . col _draglayer ) {
var i , cpos = 0 , pos = rcube _event . get _mouse _pos ( e ) ;
for ( i = 0 ; i < this . cols . length ; i ++ ) {
if ( pos . x >= this . cols [ i ] / 2 + this . list _pos + cpos )
cpos += this . cols [ i ] ;
else
break ;
}
// handle fixed columns on left
if ( i == 0 && this . list _min _pos > pos . x )
cpos = this . list _min _pos - this . list _pos ;
// empty list needs some assignment
else if ( ! this . list . rowcount && i == this . cols . length )
cpos -= 2 ;
$ ( '#rcmcolumnindicator' ) . css ( { width : cpos + 'px' } ) ;
this . triggerEvent ( 'column_dragmove' , e ? e : window . event ) ;
}
this . drag _start = false ;
return false ;
} ,
/ * *
* Handler for mouse up events for dragging list columns
* /
column _drag _mouse _up : function ( e )
{
document . onmousemove = null ;
if ( this . col _draglayer ) {
( this . col _draglayer ) . remove ( ) ;
this . col _draglayer = null ;
}
rcube _event . remove _listener ( { event : 'mousemove' , object : this , method : 'column_drag_mouse_move' } ) ;
rcube _event . remove _listener ( { event : 'mouseup' , object : this , method : 'column_drag_mouse_up' } ) ;
// remove temp divs
this . del _dragfix ( ) ;
if ( this . col _drag _active ) {
this . col _drag _active = false ;
this . focus ( ) ;
this . triggerEvent ( 'column_dragend' , e ) ;
if ( this . selected _column !== null && this . cols && this . cols . length ) {
var i , cpos = 0 , pos = rcube _event . get _mouse _pos ( e ) ;
// find destination position
for ( i = 0 ; i < this . cols . length ; i ++ ) {
if ( pos . x >= this . cols [ i ] / 2 + this . list _pos + cpos )
cpos += this . cols [ i ] ;
else
break ;
}
if ( i != this . selected _column && i != this . selected _column + 1 ) {
this . column _replace ( this . selected _column , i ) ;
}
}
}
return rcube _event . cancel ( e ) ;
} ,
/ * *
* Returns IDs of all rows in a thread ( except root ) for specified root
* /
row _children : function ( uid )
{
if ( ! this . rows [ uid ] || ! this . rows [ uid ] . has _children )
return [ ] ;
var res = [ ] , depth = this . rows [ uid ] . depth ,
row = this . rows [ uid ] . obj . nextSibling ;
while ( row ) {
if ( row . nodeType == 1 ) {
if ( r = this . rows [ row . uid ] ) {
if ( ! r . depth || r . depth <= depth )
break ;
res . push ( r . uid ) ;
}
}
row = row . nextSibling ;
}
return res ;
} ,
/ * *
* Creates a layer for drag & drop over iframes
* /
add _dragfix : function ( )
{
$ ( 'iframe' ) . each ( function ( ) {
$ ( '<div class="iframe-dragdrop-fix"></div>' )
. css ( { background : '#fff' ,
width : this . offsetWidth + 'px' , height : this . offsetHeight + 'px' ,
position : 'absolute' , opacity : '0.001' , zIndex : 1000
} )
. css ( $ ( this ) . offset ( ) )
. appendTo ( document . body ) ;
} ) ;
} ,
/ * *
* Removes the layer for drag & drop over iframes
* /
del _dragfix : function ( )
{
$ ( 'div.iframe-dragdrop-fix' ) . remove ( ) ;
} ,
/ * *
* Replaces two columns
* /
column _replace : function ( from , to )
{
// only supported for <table> lists
if ( ! this . thead || ! this . thead . rows )
return ;
var len , cells = this . thead . rows [ 0 ] . cells ,
elem = cells [ from ] ,
before = cells [ to ] ,
td = document . createElement ( 'td' ) ;
// replace header cells
if ( before )
cells [ 0 ] . parentNode . insertBefore ( td , before ) ;
else
cells [ 0 ] . parentNode . appendChild ( td ) ;
cells [ 0 ] . parentNode . replaceChild ( elem , td ) ;
// replace list cells
for ( r = 0 , len = this . tbody . rows . length ; r < len ; r ++ ) {
row = this . tbody . rows [ r ] ;
elem = row . cells [ from ] ;
before = row . cells [ to ] ;
td = document . createElement ( 'td' ) ;
if ( before )
row . insertBefore ( td , before ) ;
else
row . appendChild ( td ) ;
row . replaceChild ( elem , td ) ;
}
// update subject column position
if ( this . subject _col == from )
this . subject _col = to > from ? to - 1 : to ;
else if ( this . subject _col < from && to <= this . subject _col )
this . subject _col ++ ;
else if ( this . subject _col > from && to >= this . subject _col )
this . subject _col -- ;
if ( this . fixed _header )
this . init _header ( ) ;
this . triggerEvent ( 'column_replace' ) ;
} ,
subject _column : function ( )
{
return this . subject _col + ( this . checkbox _selection ? 1 : 0 ) ;
}
} ;
rcube _list _widget . prototype . addEventListener = rcube _event _engine . prototype . addEventListener ;
rcube _list _widget . prototype . removeEventListener = rcube _event _engine . prototype . removeEventListener ;
rcube _list _widget . prototype . triggerEvent = rcube _event _engine . prototype . triggerEvent ;
// static
rcube _list _widget . _instances = [ ] ;