Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117754080
treelist.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
31 KB
Referenced Files
None
Subscribers
None
treelist.js
View Options
/**
* Roundcube Treelist 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) 2013-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>
* @requires jquery.js, common.js
*/
/**
* Roundcube Treelist widget class
* @constructor
*/
function
rcube_treelist_widget
(
node
,
p
)
{
// apply some defaults to p
p
=
$
.
extend
({
id_prefix
:
''
,
autoexpand
:
1000
,
selectable
:
false
,
scroll_delay
:
500
,
scroll_step
:
5
,
scroll_speed
:
20
,
save_state
:
false
,
keyboard
:
true
,
tabexit
:
true
,
parent_focus
:
false
,
check_droptarget
:
function
(
node
)
{
return
!
node
.
virtual
;
}
},
p
||
{});
var
container
=
$
(
node
),
data
=
p
.
data
||
[],
indexbyid
=
{},
selection
=
null
,
drag_active
=
false
,
search_active
=
false
,
last_search
=
''
,
has_focus
=
false
,
box_coords
=
{},
item_coords
=
[],
autoexpand_timer
,
autoexpand_item
,
body_scroll_top
=
0
,
list_scroll_top
=
0
,
scroll_timer
,
searchfield
,
tree_state
,
ui_droppable
,
ui_draggable
,
draggable_opts
,
droppable_opts
,
list_id
=
(
container
.
attr
(
'id'
)
||
p
.
id_prefix
||
'0'
),
me
=
this
;
/////// export public members and methods
this
.
container
=
container
;
this
.
expand
=
expand
;
this
.
collapse
=
collapse
;
this
.
select
=
select
;
this
.
render
=
render
;
this
.
reset
=
reset
;
this
.
drag_start
=
drag_start
;
this
.
drag_end
=
drag_end
;
this
.
intersects
=
intersects
;
this
.
droppable
=
droppable
;
this
.
draggable
=
draggable
;
this
.
is_draggable
=
is_draggable
;
this
.
update
=
update_node
;
this
.
insert
=
insert
;
this
.
remove
=
remove
;
this
.
get_item
=
get_item
;
this
.
get_node
=
get_node
;
this
.
get_selection
=
get_selection
;
this
.
get_next
=
get_next
;
this
.
get_prev
=
get_prev
;
this
.
get_single_selection
=
get_selection
;
this
.
is_search
=
is_search
;
this
.
reset_search
=
reset_search
;
/////// startup code (constructor)
// abort if node not found
if
(
!
container
.
length
)
return
;
if
(
p
.
data
)
index_data
({
children
:
data
});
// load data from DOM
else
update_data
();
// scroll to the selected item
if
(
selection
)
{
scroll_to_node
(
id2dom
(
selection
,
true
));
}
container
.
attr
(
'role'
,
'tree'
)
.
on
(
'focusin'
,
function
(
e
)
{
// TODO: only accept focus on virtual nodes from keyboard events
has_focus
=
true
;
})
.
on
(
'focusout'
,
function
(
e
)
{
has_focus
=
false
;
})
// register click handlers on list
.
on
(
'click'
,
'div.treetoggle'
,
function
(
e
)
{
toggle
(
dom2id
(
$
(
this
).
parent
()));
e
.
stopPropagation
();
})
.
on
(
'click'
,
'li'
,
function
(
e
)
{
// do not select record on checkbox/input click
if
(
$
(
e
.
target
).
is
(
'input'
))
return
true
;
var
node
=
p
.
selectable
?
indexbyid
[
dom2id
(
$
(
this
))]
:
null
;
if
(
node
&&
!
node
.
virtual
)
{
select
(
node
.
id
);
e
.
stopPropagation
();
}
})
// mute clicks on virtual folder links (they need tabindex="0" in order to be selectable by keyboard)
.
on
(
'mousedown'
,
'a'
,
function
(
e
)
{
var
link
=
$
(
e
.
target
),
node
=
indexbyid
[
dom2id
(
link
.
closest
(
'li'
))];
if
(
node
&&
node
.
virtual
&&
!
link
.
attr
(
'href'
))
{
e
.
preventDefault
();
e
.
stopPropagation
();
return
false
;
}
});
// activate search function
if
(
p
.
searchbox
)
{
searchfield
=
$
(
p
.
searchbox
).
off
(
'keyup.treelist'
).
on
(
'keyup.treelist'
,
function
(
e
)
{
var
key
=
rcube_event
.
get_keycode
(
e
),
mod
=
rcube_event
.
get_modifier
(
e
);
switch
(
key
)
{
case
9
:
// tab
break
;
case
13
:
// enter
search
(
this
.
value
,
true
);
return
rcube_event
.
cancel
(
e
);
case
27
:
// escape
reset_search
();
break
;
case
38
:
// arrow up
case
37
:
// left
case
39
:
// right
case
40
:
// arrow down
return
;
// ignore arrow keys
default
:
search
(
this
.
value
,
false
);
break
;
}
}).
attr
(
'autocomplete'
,
'off'
);
// find the reset button for this search field
searchfield
.
parent
().
find
(
'a.reset'
).
off
(
'click.treelist'
).
on
(
'click.treelist'
,
function
(
e
)
{
reset_search
();
return
false
;
})
}
$
(
document
.
body
).
on
(
'keydown'
,
keypress
);
// catch focus when clicking the list container area
if
(
p
.
parent_focus
)
{
container
.
parent
(
':not(body)'
).
click
(
function
(
e
)
{
// click on a checkbox does not catch the focus
if
(
$
(
e
.
target
).
is
(
'input'
))
return
true
;
if
(
!
has_focus
&&
selection
)
{
$
(
get_item
(
selection
)).
find
(
':focusable'
).
first
().
focus
();
}
else
if
(
!
has_focus
)
{
container
.
children
(
'li:has(:focusable)'
).
first
().
find
(
':focusable'
).
first
().
focus
();
}
});
}
/////// private methods
/**
* Collaps a the node with the given ID
*/
function
collapse
(
id
,
recursive
,
set
)
{
var
node
;
if
(
node
=
indexbyid
[
id
])
{
node
.
collapsed
=
typeof
set
==
'undefined'
||
set
;
update_dom
(
node
);
if
(
recursive
&&
node
.
children
)
{
for
(
var
i
=
0
;
i
<
node
.
children
.
length
;
i
++
)
{
collapse
(
node
.
children
[
i
].
id
,
recursive
,
set
);
}
}
me
.
triggerEvent
(
node
.
collapsed
?
'collapse'
:
'expand'
,
node
);
save_state
(
id
,
node
.
collapsed
);
}
}
/**
* Expand a the node with the given ID
*/
function
expand
(
id
,
recursive
)
{
collapse
(
id
,
recursive
,
false
);
}
/**
* Toggle collapsed state of a list node
*/
function
toggle
(
id
,
recursive
)
{
var
node
;
if
(
node
=
indexbyid
[
id
])
{
collapse
(
id
,
recursive
,
!
node
.
collapsed
);
}
}
/**
* Select a tree node by it's ID
*/
function
select
(
id
)
{
// allow subscribes to prevent selection change
if
(
me
.
triggerEvent
(
'beforeselect'
,
indexbyid
[
id
])
===
false
)
{
return
;
}
if
(
selection
)
{
id2dom
(
selection
,
true
).
removeClass
(
'selected'
).
removeAttr
(
'aria-selected'
);
if
(
search_active
)
id2dom
(
selection
).
removeClass
(
'selected'
).
removeAttr
(
'aria-selected'
);
selection
=
null
;
}
if
(
!
id
)
return
;
var
li
=
id2dom
(
id
,
true
);
if
(
li
.
length
)
{
li
.
addClass
(
'selected'
).
attr
(
'aria-selected'
,
'true'
);
selection
=
id
;
// TODO: expand all parent nodes if collapsed
if
(
search_active
)
id2dom
(
id
).
addClass
(
'selected'
).
attr
(
'aria-selected'
,
'true'
);
scroll_to_node
(
li
);
}
me
.
triggerEvent
(
'select'
,
indexbyid
[
id
]);
}
/**
* Getter for the currently selected node ID
*/
function
get_selection
()
{
return
selection
;
}
/**
* Return the DOM element of the list item with the given ID
*/
function
get_node
(
id
)
{
return
indexbyid
[
id
];
}
/**
* Return the DOM element of the list item with the given ID
*/
function
get_item
(
id
,
real
)
{
return
id2dom
(
id
,
real
).
get
(
0
);
}
/**
* Insert the given node
*/
function
insert
(
node
,
parent_id
,
sort
)
{
var
li
,
parent_li
,
parent_node
=
parent_id
?
indexbyid
[
parent_id
]
:
null
search_
=
search_active
;
// ignore, already exists
if
(
indexbyid
[
node
.
id
])
{
return
;
}
// apply saved state
state
=
get_state
(
node
.
id
,
node
.
collapsed
);
if
(
state
!==
undefined
)
{
node
.
collapsed
=
state
;
}
// insert as child of an existing node
if
(
parent_node
)
{
node
.
level
=
parent_node
.
level
+
1
;
if
(
!
parent_node
.
children
)
parent_node
.
children
=
[];
search_active
=
false
;
parent_node
.
children
.
push
(
node
);
parent_li
=
id2dom
(
parent_id
);
// re-render the entire subtree
if
(
parent_node
.
children
.
length
==
1
)
{
render_node
(
parent_node
,
null
,
parent_li
);
li
=
id2dom
(
node
.
id
);
}
else
{
// append new node to parent's child list
li
=
render_node
(
node
,
parent_li
.
children
(
'ul'
).
first
());
}
// list is in search mode
if
(
search_
)
{
search_active
=
search_
;
// add clone to current search results (top level)
if
(
!
li
.
is
(
':visible'
))
{
$
(
'<li>'
)
.
attr
(
'id'
,
li
.
attr
(
'id'
)
+
'--xsR'
)
.
attr
(
'class'
,
li
.
attr
(
'class'
))
.
addClass
(
'searchresult__'
)
.
append
(
li
.
children
().
first
().
clone
(
true
,
true
))
.
appendTo
(
container
);
}
}
}
// insert at top level
else
{
node
.
level
=
0
;
data
.
push
(
node
);
li
=
render_node
(
node
,
container
);
}
indexbyid
[
node
.
id
]
=
node
;
// set new reference to node.html after insert
// will otherwise vanish in Firefox 3.6
if
(
typeof
node
.
html
==
'object'
)
{
indexbyid
[
node
.
id
].
html
=
id2dom
(
node
.
id
,
true
).
children
();
}
if
(
sort
)
{
resort_node
(
li
,
typeof
sort
==
'string'
?
'[class~="'
+
sort
+
'"]'
:
''
);
}
}
/**
* Update properties of an existing node
*/
function
update_node
(
id
,
updates
,
sort
)
{
var
li
,
parent_ul
,
parent_node
,
old_parent
,
node
=
indexbyid
[
id
];
if
(
node
)
{
li
=
id2dom
(
id
);
parent_ul
=
li
.
parent
();
if
(
updates
.
id
||
updates
.
html
||
updates
.
children
||
updates
.
classes
||
updates
.
parent
)
{
if
(
updates
.
parent
&&
(
parent_node
=
indexbyid
[
updates
.
parent
]))
{
// remove reference from old parent's child list
if
(
parent_ul
.
closest
(
'li'
).
length
&&
(
old_parent
=
indexbyid
[
dom2id
(
parent_ul
.
closest
(
'li'
))]))
{
old_parent
.
children
=
$
.
grep
(
old_parent
.
children
,
function
(
elem
,
i
){
return
elem
.
id
!=
node
.
id
;
});
}
// append to new parent node
parent_ul
=
id2dom
(
updates
.
parent
).
children
(
'ul'
).
first
();
if
(
!
parent_node
.
children
)
parent_node
.
children
=
[];
parent_node
.
children
.
push
(
node
);
}
else
if
(
updates
.
parent
!==
undefined
)
{
parent_ul
=
container
;
}
$
.
extend
(
node
,
updates
);
li
=
render_node
(
node
,
parent_ul
,
li
);
}
if
(
node
.
id
!=
id
)
{
delete
indexbyid
[
id
];
indexbyid
[
node
.
id
]
=
node
;
}
if
(
sort
)
{
resort_node
(
li
,
typeof
sort
==
'string'
?
'[class~="'
+
sort
+
'"]'
:
''
);
}
}
}
/**
* Helper method to sort the list of the given item
*/
function
resort_node
(
li
,
filter
)
{
var
first
,
sibling
,
myid
=
li
.
get
(
0
).
id
,
sortname
=
li
.
children
().
first
().
text
().
toUpperCase
();
li
.
parent
().
children
(
'li'
+
filter
).
each
(
function
(
i
,
elem
)
{
if
(
i
==
0
)
first
=
elem
;
if
(
elem
.
id
==
myid
)
{
// skip
}
else
if
(
elem
.
id
!=
myid
&&
sortname
>=
$
(
elem
).
children
().
first
().
text
().
toUpperCase
())
{
sibling
=
elem
;
}
else
{
return
false
;
}
});
if
(
sibling
)
{
li
.
insertAfter
(
sibling
);
}
else
if
(
first
&&
first
.
id
!=
myid
)
{
li
.
insertBefore
(
first
);
}
// reload data from dom
update_data
();
}
/**
* Remove the item with the given ID
*/
function
remove
(
id
)
{
var
node
,
li
,
parent
;
if
(
node
=
indexbyid
[
id
])
{
li
=
id2dom
(
id
,
true
);
parent
=
li
.
parent
();
li
.
remove
();
node
.
deleted
=
true
;
delete
indexbyid
[
id
];
if
(
search_active
)
{
id2dom
(
id
,
false
).
remove
();
}
// remove tree-toggle button and children list
if
(
!
parent
.
children
().
length
)
{
parent
.
parent
().
find
(
'div.treetoggle'
).
remove
();
parent
.
remove
();
}
return
true
;
}
return
false
;
}
/**
* (Re-)read tree data from DOM
*/
function
update_data
()
{
data
=
walk_list
(
container
,
0
);
}
/**
* Apply the 'collapsed' status of the data node to the corresponding DOM element(s)
*/
function
update_dom
(
node
)
{
var
li
=
id2dom
(
node
.
id
,
true
);
li
.
attr
(
'aria-expanded'
,
node
.
collapsed
?
'false'
:
'true'
);
li
.
children
(
'ul'
).
first
()[(
node
.
collapsed
?
'hide'
:
'show'
)]();
li
.
children
(
'div.treetoggle'
).
removeClass
(
'collapsed expanded'
).
addClass
(
node
.
collapsed
?
'collapsed'
:
'expanded'
);
me
.
triggerEvent
(
'toggle'
,
node
);
}
/**
*
*/
function
reset
(
keep_content
)
{
select
(
''
);
data
=
[];
indexbyid
=
{};
drag_active
=
false
;
if
(
keep_content
)
{
if
(
draggable_opts
)
{
if
(
ui_draggable
)
draggable
(
'destroy'
);
draggable
(
draggable_opts
);
}
if
(
droppable_opts
)
{
if
(
ui_droppable
)
droppable
(
'destroy'
);
droppable
(
droppable_opts
);
}
update_data
();
}
else
{
container
.
html
(
''
);
}
reset_search
();
}
/**
*
*/
function
search
(
q
,
enter
)
{
q
=
String
(
q
).
toLowerCase
();
if
(
!
q
.
length
)
return
reset_search
();
else
if
(
q
==
last_search
&&
!
enter
)
return
0
;
var
hits
=
[];
var
search_tree
=
function
(
items
)
{
$
.
each
(
items
,
function
(
i
,
node
)
{
var
li
,
sli
;
if
(
!
node
.
virtual
&&
!
node
.
deleted
&&
String
(
node
.
text
).
toLowerCase
().
indexOf
(
q
)
>=
0
&&
hits
.
indexOf
(
node
.
id
)
<
0
)
{
li
=
id2dom
(
node
.
id
);
// skip already filtered nodes
if
(
li
.
data
(
'filtered'
))
return
;
sli
=
$
(
'<li>'
)
.
attr
(
'id'
,
li
.
attr
(
'id'
)
+
'--xsR'
)
.
attr
(
'class'
,
li
.
attr
(
'class'
))
.
addClass
(
'searchresult__'
)
// append all elements like links and inputs, but not sub-trees
.
append
(
li
.
children
(
':not(div.treetoggle,ul)'
).
clone
(
true
,
true
))
.
appendTo
(
container
);
hits
.
push
(
node
.
id
);
}
if
(
node
.
children
&&
node
.
children
.
length
)
{
search_tree
(
node
.
children
);
}
});
};
// reset old search results
if
(
search_active
)
{
$
(
container
).
children
(
'li.searchresult__'
).
remove
();
search_active
=
false
;
}
// hide all list items
$
(
container
).
children
(
'li'
).
hide
().
removeClass
(
'selected'
);
// search recursively in tree (to keep sorting order)
search_tree
(
data
);
search_active
=
true
;
me
.
triggerEvent
(
'search'
,
{
query
:
q
,
last
:
last_search
,
count
:
hits
.
length
,
ids
:
hits
,
execute
:
enter
||
false
});
last_search
=
q
;
return
hits
.
count
;
}
/**
*
*/
function
reset_search
()
{
if
(
searchfield
)
searchfield
.
val
(
''
);
$
(
container
).
children
(
'li.searchresult__'
).
remove
();
$
(
container
).
children
(
'li'
).
filter
(
function
()
{
return
!
$
(
this
).
data
(
'filtered'
);
}).
show
();
search_active
=
false
;
me
.
triggerEvent
(
'search'
,
{
query
:
false
,
last
:
last_search
});
last_search
=
''
;
if
(
selection
)
select
(
selection
);
}
/**
*
*/
function
is_search
()
{
return
search_active
;
}
/**
* Render the tree list from the internal data structure
*/
function
render
()
{
if
(
me
.
triggerEvent
(
'renderBefore'
,
data
)
===
false
)
return
;
// remove all child nodes
container
.
html
(
''
);
// render child nodes
for
(
var
i
=
0
;
i
<
data
.
length
;
i
++
)
{
data
[
i
].
level
=
0
;
render_node
(
data
[
i
],
container
);
}
me
.
triggerEvent
(
'renderAfter'
,
container
);
}
/**
* Render a specific node into the DOM list
*/
function
render_node
(
node
,
parent
,
replace
)
{
if
(
node
.
deleted
)
return
;
var
li
=
$
(
'<li>'
)
.
attr
(
'id'
,
p
.
id_prefix
+
(
p
.
id_encode
?
p
.
id_encode
(
node
.
id
)
:
node
.
id
))
.
attr
(
'role'
,
'treeitem'
)
.
addClass
((
node
.
classes
||
[]).
join
(
' '
))
.
data
(
'id'
,
node
.
id
);
if
(
replace
)
{
replace
.
replaceWith
(
li
);
if
(
parent
)
li
.
appendTo
(
parent
);
}
else
li
.
appendTo
(
parent
);
if
(
typeof
node
.
html
==
'string'
)
li
.
html
(
node
.
html
);
else
if
(
typeof
node
.
html
==
'object'
)
li
.
append
(
node
.
html
);
if
(
!
node
.
text
)
node
.
text
=
li
.
children
().
first
().
text
();
if
(
node
.
virtual
)
li
.
addClass
(
'virtual'
);
if
(
node
.
id
==
selection
)
li
.
addClass
(
'selected'
);
// add child list and toggle icon
if
(
node
.
children
&&
node
.
children
.
length
)
{
li
.
attr
(
'aria-expanded'
,
node
.
collapsed
?
'false'
:
'true'
);
$
(
'<div class="treetoggle '
+
(
node
.
collapsed
?
'collapsed'
:
'expanded'
)
+
'"> </div>'
).
appendTo
(
li
);
var
ul
=
$
(
'<ul>'
).
appendTo
(
li
).
attr
(
'class'
,
node
.
childlistclass
).
attr
(
'role'
,
'group'
);
if
(
node
.
collapsed
)
ul
.
hide
();
for
(
var
i
=
0
;
i
<
node
.
children
.
length
;
i
++
)
{
node
.
children
[
i
].
level
=
node
.
level
+
1
;
render_node
(
node
.
children
[
i
],
ul
);
}
}
return
li
;
}
/**
* Recursively walk the DOM tree and build an internal data structure
* representing the skeleton of this tree list.
*/
function
walk_list
(
ul
,
level
)
{
var
result
=
[];
ul
.
children
(
'li'
).
each
(
function
(
i
,
e
){
var
state
,
li
=
$
(
e
),
sublist
=
li
.
children
(
'ul'
);
var
node
=
{
id
:
dom2id
(
li
),
classes
:
String
(
li
.
attr
(
'class'
)).
split
(
' '
),
virtual
:
li
.
hasClass
(
'virtual'
),
level
:
level
,
html
:
li
.
children
().
first
().
get
(
0
).
outerHTML
,
text
:
li
.
children
().
first
().
text
(),
children
:
walk_list
(
sublist
,
level
+
1
)
}
if
(
sublist
.
length
)
{
node
.
childlistclass
=
sublist
.
attr
(
'class'
);
}
if
(
node
.
children
.
length
)
{
if
(
node
.
collapsed
===
undefined
)
node
.
collapsed
=
sublist
.
css
(
'display'
)
==
'none'
;
// apply saved state
state
=
get_state
(
node
.
id
,
node
.
collapsed
);
if
(
state
!==
undefined
)
{
node
.
collapsed
=
state
;
sublist
[(
state
?
'hide'
:
'show'
)]();
}
if
(
!
li
.
children
(
'div.treetoggle'
).
length
)
$
(
'<div class="treetoggle '
+
(
node
.
collapsed
?
'collapsed'
:
'expanded'
)
+
'"> </div>'
).
appendTo
(
li
);
li
.
attr
(
'aria-expanded'
,
node
.
collapsed
?
'false'
:
'true'
);
}
if
(
li
.
hasClass
(
'selected'
))
{
li
.
attr
(
'aria-selected'
,
'true'
);
selection
=
node
.
id
;
}
li
.
data
(
'id'
,
node
.
id
);
// declare list item as treeitem
li
.
attr
(
'role'
,
'treeitem'
).
attr
(
'aria-level'
,
node
.
level
+
1
);
// allow virtual nodes to receive focus
if
(
node
.
virtual
)
{
li
.
children
(
'a:first'
).
attr
(
'tabindex'
,
'0'
);
}
result
.
push
(
node
);
indexbyid
[
node
.
id
]
=
node
;
});
ul
.
attr
(
'role'
,
level
==
0
?
'tree'
:
'group'
);
return
result
;
}
/**
* Recursively walk the data tree and index nodes by their ID
*/
function
index_data
(
node
)
{
if
(
node
.
id
)
{
indexbyid
[
node
.
id
]
=
node
;
}
for
(
var
c
=
0
;
node
.
children
&&
c
<
node
.
children
.
length
;
c
++
)
{
index_data
(
node
.
children
[
c
]);
}
}
/**
* Get the (stripped) node ID from the given DOM element
*/
function
dom2id
(
li
)
{
var
domid
=
String
(
li
.
attr
(
'id'
)).
replace
(
new
RegExp
(
'^'
+
(
p
.
id_prefix
)
||
'%'
),
''
).
replace
(
/--xsR$/
,
''
);
return
p
.
id_decode
?
p
.
id_decode
(
domid
)
:
domid
;
}
/**
* Get the <li> element for the given node ID
*/
function
id2dom
(
id
,
real
)
{
var
domid
=
p
.
id_encode
?
p
.
id_encode
(
id
)
:
id
,
suffix
=
search_active
&&
!
real
?
'--xsR'
:
''
;
return
$
(
'#'
+
p
.
id_prefix
+
domid
+
suffix
,
container
);
}
/**
* Scroll the parent container to make the given list item visible
*/
function
scroll_to_node
(
li
)
{
var
scroller
=
container
.
parent
(),
current_offset
=
scroller
.
scrollTop
(),
rel_offset
=
li
.
offset
().
top
-
scroller
.
offset
().
top
;
if
(
rel_offset
<
0
||
rel_offset
+
li
.
height
()
>
scroller
.
height
())
scroller
.
scrollTop
(
rel_offset
+
current_offset
);
}
/**
* Save node collapse state to localStorage
*/
function
save_state
(
id
,
collapsed
)
{
if
(
p
.
save_state
&&
window
.
rcmail
)
{
var
key
=
'treelist-'
+
list_id
;
if
(
!
tree_state
)
{
tree_state
=
rcmail
.
local_storage_get_item
(
key
,
{});
}
if
(
tree_state
[
id
]
!=
collapsed
)
{
tree_state
[
id
]
=
collapsed
;
rcmail
.
local_storage_set_item
(
key
,
tree_state
);
}
}
}
/**
* Read node collapse state from localStorage
*/
function
get_state
(
id
)
{
if
(
p
.
save_state
&&
window
.
rcmail
)
{
if
(
!
tree_state
)
{
tree_state
=
rcmail
.
local_storage_get_item
(
'treelist-'
+
list_id
,
{});
}
return
tree_state
[
id
];
}
return
undefined
;
}
/**
* Handler for keyboard events on treelist
*/
function
keypress
(
e
)
{
var
target
=
e
.
target
||
{},
keyCode
=
rcube_event
.
get_keycode
(
e
);
if
(
!
has_focus
||
target
.
nodeName
==
'INPUT'
&&
keyCode
!=
38
&&
keyCode
!=
40
||
target
.
nodeName
==
'TEXTAREA'
||
target
.
nodeName
==
'SELECT'
)
return
true
;
switch
(
keyCode
)
{
case
38
:
case
40
:
case
63232
:
// 'up', in safari keypress
case
63233
:
// 'down', in safari keypress
var
li
=
p
.
keyboard
?
container
.
find
(
':focus'
).
closest
(
'li'
)
:
[];
if
(
li
.
length
)
{
focus_next
(
li
,
(
mod
=
keyCode
==
38
||
keyCode
==
63232
?
-
1
:
1
));
}
return
rcube_event
.
cancel
(
e
);
case
37
:
// Left arrow key
case
39
:
// Right arrow key
var
id
,
node
,
li
=
container
.
find
(
':focus'
).
closest
(
'li'
);
if
(
li
.
length
)
{
id
=
dom2id
(
li
);
node
=
indexbyid
[
id
];
if
(
node
&&
node
.
children
.
length
&&
node
.
collapsed
!=
(
keyCode
==
37
))
toggle
(
id
,
rcube_event
.
get_modifier
(
e
)
==
SHIFT_KEY
);
// toggle subtree
}
return
false
;
case
9
:
// Tab
if
(
p
.
keyboard
&&
p
.
tabexit
)
{
// jump to last/first item to move focus away from the treelist widget by tab
var
limit
=
rcube_event
.
get_modifier
(
e
)
==
SHIFT_KEY
?
'first'
:
'last'
;
focus_noscroll
(
container
.
find
(
'li[role=treeitem]:has(a)'
)[
limit
]().
find
(
'a:'
+
limit
));
}
break
;
}
return
true
;
}
function
focus_next
(
li
,
dir
,
from_child
)
{
var
mod
=
dir
<
0
?
'prev'
:
'next'
,
next
=
li
[
mod
](),
limit
,
parent
;
if
(
dir
>
0
&&
!
from_child
&&
li
.
children
(
'ul[role=group]:visible'
).
length
)
{
li
.
children
(
'ul'
).
children
(
'li:first'
).
find
(
'a:first'
).
focus
();
}
else
if
(
dir
<
0
&&
!
from_child
&&
next
.
children
(
'ul[role=group]:visible'
).
length
)
{
next
.
children
(
'ul'
).
children
(
'li:last'
).
find
(
'a:first'
).
focus
();
}
else
if
(
next
.
length
&&
next
.
find
(
'a:first'
).
focus
().
length
)
{
// focused
}
else
{
parent
=
li
.
parent
().
closest
(
'li[role=treeitem]'
);
if
(
parent
.
length
)
if
(
dir
<
0
)
{
parent
.
find
(
'a:first'
).
focus
();
}
else
{
focus_next
(
parent
,
dir
,
true
);
}
}
}
/**
* Focus the given element without scrolling the list container
*/
function
focus_noscroll
(
elem
)
{
if
(
elem
.
length
)
{
var
frame
=
container
.
parent
().
get
(
0
)
||
{
scrollTop
:
0
},
y
=
frame
.
scrollTop
||
frame
.
scrollY
;
elem
.
focus
();
frame
.
scrollTop
=
y
;
}
}
function
get_next
()
{
var
node
,
child
;
if
(
selection
&&
(
node
=
id2dom
(
selection
)))
{
child
=
node
.
children
(
'ul'
).
children
(
'li:first'
);
if
(
child
.
length
)
{
return
dom2id
(
child
);
}
child
=
node
.
next
();
if
(
child
.
length
)
{
return
dom2id
(
child
);
}
while
((
node
=
node
.
parent
(
'ul'
).
parent
(
'li'
))
&&
node
.
length
)
{
child
=
node
.
next
();
if
(
child
.
length
)
{
return
dom2id
(
child
);
}
}
}
}
function
get_prev
()
{
var
node
,
prev
,
child
;
if
(
selection
&&
(
node
=
id2dom
(
selection
)))
{
prev
=
node
.
prev
();
child
=
prev
.
find
(
'li:last'
);
if
(
child
.
length
)
{
return
dom2id
(
child
);
}
if
(
prev
.
length
)
{
return
dom2id
(
prev
);
}
node
=
node
.
parent
().
parent
();
if
(
node
.
length
&&
node
.
is
(
'li'
))
{
return
dom2id
(
node
);
}
}
}
///// drag & drop support
/**
* When dragging starts, compute absolute bounding boxes of the list and it's items
* for faster comparisons while mouse is moving
*/
function
drag_start
(
force
)
{
if
(
!
force
&&
drag_active
)
return
;
drag_active
=
true
;
var
li
,
item
,
height
,
pos
=
container
.
offset
();
body_scroll_top
=
bw
.
ie
?
0
:
window
.
pageYOffset
;
list_scroll_top
=
container
.
parent
().
scrollTop
();
pos
.
top
+=
list_scroll_top
;
box_coords
=
{
x1
:
pos
.
left
,
y1
:
pos
.
top
,
x2
:
pos
.
left
+
container
.
width
(),
y2
:
pos
.
top
+
container
.
height
()
};
item_coords
=
[];
for
(
var
id
in
indexbyid
)
{
li
=
id2dom
(
id
);
item
=
li
.
children
().
first
().
get
(
0
);
if
(
item
&&
(
height
=
item
.
offsetHeight
))
{
pos
=
$
(
item
).
offset
();
pos
.
top
+=
list_scroll_top
;
item_coords
[
id
]
=
{
x1
:
pos
.
left
,
y1
:
pos
.
top
,
x2
:
pos
.
left
+
item
.
offsetWidth
,
y2
:
pos
.
top
+
height
,
on
:
id
==
autoexpand_item
};
}
}
// enable auto-scrolling of list container
if
(
container
.
height
()
>
container
.
parent
().
height
())
{
container
.
parent
()
.
mousemove
(
function
(
e
)
{
var
scroll
=
0
,
mouse
=
rcube_event
.
get_mouse_pos
(
e
);
mouse
.
y
-=
container
.
parent
().
offset
().
top
;
if
(
mouse
.
y
<
25
&&
list_scroll_top
>
0
)
{
scroll
=
-
1
;
// up
}
else
if
(
mouse
.
y
>
container
.
parent
().
height
()
-
25
)
{
scroll
=
1
;
// down
}
if
(
drag_active
&&
scroll
!=
0
)
{
if
(
!
scroll_timer
)
scroll_timer
=
window
.
setTimeout
(
function
(){
drag_scroll
(
scroll
);
},
p
.
scroll_delay
);
}
else
if
(
scroll_timer
)
{
window
.
clearTimeout
(
scroll_timer
);
scroll_timer
=
null
;
}
})
.
mouseleave
(
function
()
{
if
(
scroll_timer
)
{
window
.
clearTimeout
(
scroll_timer
);
scroll_timer
=
null
;
}
});
}
}
/**
* Signal that dragging has stopped
*/
function
drag_end
()
{
if
(
!
drag_active
)
return
;
drag_active
=
false
;
scroll_timer
=
null
;
if
(
autoexpand_timer
)
{
clearTimeout
(
autoexpand_timer
);
autoexpand_timer
=
null
;
autoexpand_item
=
null
;
}
$
(
'li.droptarget'
,
container
).
removeClass
(
'droptarget'
);
}
/**
* Scroll list container in the given direction
*/
function
drag_scroll
(
dir
)
{
if
(
!
drag_active
)
return
;
var
old_top
=
list_scroll_top
;
container
.
parent
().
get
(
0
).
scrollTop
+=
p
.
scroll_step
*
dir
;
list_scroll_top
=
container
.
parent
().
scrollTop
();
scroll_timer
=
null
;
if
(
list_scroll_top
!=
old_top
)
scroll_timer
=
window
.
setTimeout
(
function
(){
drag_scroll
(
dir
);
},
p
.
scroll_speed
);
}
/**
* Determine if the given mouse coords intersect the list and one of its items
*/
function
intersects
(
mouse
,
highlight
)
{
// offsets to compensate for scrolling while dragging a message
var
boffset
=
bw
.
ie
?
-
document
.
documentElement
.
scrollTop
:
body_scroll_top
,
moffset
=
container
.
parent
().
scrollTop
(),
result
=
null
;
mouse
.
top
=
mouse
.
y
+
moffset
-
boffset
;
// no intersection with list bounding box
if
(
mouse
.
x
<
box_coords
.
x1
||
mouse
.
x
>=
box_coords
.
x2
||
mouse
.
top
<
box_coords
.
y1
||
mouse
.
top
>=
box_coords
.
y2
)
{
// TODO: optimize performance for this operation
if
(
highlight
)
$
(
'li.droptarget'
,
container
).
removeClass
(
'droptarget'
);
return
result
;
}
// check intersection with visible list items
var
id
,
pos
,
node
;
for
(
id
in
item_coords
)
{
pos
=
item_coords
[
id
];
if
(
mouse
.
x
>=
pos
.
x1
&&
mouse
.
x
<
pos
.
x2
&&
mouse
.
top
>=
pos
.
y1
&&
mouse
.
top
<
pos
.
y2
)
{
node
=
indexbyid
[
id
];
// if the folder is collapsed, expand it after the configured time
if
(
node
.
children
&&
node
.
children
.
length
&&
node
.
collapsed
&&
p
.
autoexpand
&&
autoexpand_item
!=
id
)
{
if
(
autoexpand_timer
)
clearTimeout
(
autoexpand_timer
);
autoexpand_item
=
id
;
autoexpand_timer
=
setTimeout
(
function
()
{
expand
(
autoexpand_item
);
drag_start
(
true
);
// re-calculate item coords
autoexpand_item
=
null
;
if
(
ui_droppable
)
$
.
ui
.
ddmanager
.
prepareOffsets
(
$
.
ui
.
ddmanager
.
current
,
null
);
},
p
.
autoexpand
);
}
else
if
(
autoexpand_timer
&&
autoexpand_item
!=
id
)
{
clearTimeout
(
autoexpand_timer
);
autoexpand_item
=
null
;
autoexpand_timer
=
null
;
}
// check if this item is accepted as drop target
if
(
p
.
check_droptarget
(
node
))
{
if
(
highlight
)
{
id2dom
(
id
).
addClass
(
'droptarget'
);
pos
.
on
=
true
;
}
result
=
id
;
}
else
{
result
=
null
;
}
}
else
if
(
pos
.
on
)
{
id2dom
(
id
).
removeClass
(
'droptarget'
);
pos
.
on
=
false
;
}
}
return
result
;
}
/**
* Wrapper for jQuery.UI.droppable() activation on this widget
*
* @param object Options as passed to regular .droppable() function
*/
function
droppable
(
opts
)
{
if
(
!
opts
)
opts
=
{};
if
(
$
.
type
(
opts
)
==
'string'
)
{
if
(
opts
==
'destroy'
)
{
ui_droppable
=
null
;
}
$
(
'li:not(.virtual)'
,
container
).
droppable
(
opts
);
return
this
;
}
droppable_opts
=
opts
;
var
my_opts
=
$
.
extend
({
greedy
:
true
,
tolerance
:
'pointer'
,
hoverClass
:
'droptarget'
,
addClasses
:
false
},
opts
);
my_opts
.
activate
=
function
(
e
,
ui
)
{
drag_start
();
ui_droppable
=
ui
;
if
(
opts
.
activate
)
opts
.
activate
(
e
,
ui
);
};
my_opts
.
deactivate
=
function
(
e
,
ui
)
{
drag_end
();
ui_droppable
=
null
;
if
(
opts
.
deactivate
)
opts
.
deactivate
(
e
,
ui
);
};
my_opts
.
over
=
function
(
e
,
ui
)
{
intersects
(
rcube_event
.
get_mouse_pos
(
e
),
false
);
if
(
opts
.
over
)
opts
.
over
(
e
,
ui
);
};
$
(
'li:not(.virtual)'
,
container
).
droppable
(
my_opts
);
return
this
;
}
/**
* Wrapper for jQuery.UI.draggable() activation on this widget
*
* @param object Options as passed to regular .draggable() function
*/
function
draggable
(
opts
)
{
if
(
!
opts
)
opts
=
{};
if
(
$
.
type
(
opts
)
==
'string'
)
{
if
(
opts
==
'destroy'
)
{
ui_draggable
=
null
;
}
$
(
'li:not(.virtual)'
,
container
).
draggable
(
opts
);
return
this
;
}
draggable_opts
=
opts
;
var
my_opts
=
$
.
extend
({
appendTo
:
'body'
,
revert
:
'invalid'
,
iframeFix
:
true
,
addClasses
:
false
,
cursorAt
:
{
left
:
-
20
,
top
:
5
},
create
:
function
(
e
,
ui
)
{
ui_draggable
=
ui
;
},
helper
:
function
(
e
)
{
return
$
(
'<div>'
).
attr
(
'id'
,
'rcmdraglayer'
)
.
text
(
$
.
trim
(
$
(
e
.
target
).
first
().
text
()));
}
},
opts
);
$
(
'li:not(.virtual)'
,
container
).
draggable
(
my_opts
);
return
this
;
}
function
is_draggable
()
{
return
!!
ui_draggable
;
}
}
// use event processing functions from Roundcube's rcube_event_engine
rcube_treelist_widget
.
prototype
.
addEventListener
=
rcube_event_engine
.
prototype
.
addEventListener
;
rcube_treelist_widget
.
prototype
.
removeEventListener
=
rcube_event_engine
.
prototype
.
removeEventListener
;
rcube_treelist_widget
.
prototype
.
triggerEvent
=
rcube_event_engine
.
prototype
.
triggerEvent
;
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 6:35 AM (1 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18722874
Default Alt Text
treelist.js (31 KB)
Attached To
Mode
R113 roundcubemail
Attached
Detach File
Event Timeline