1337 lines
39 KiB
JavaScript
1337 lines
39 KiB
JavaScript
/** \file html/javascript/form_elements.js
|
|
* \brief DI webinterface JavaScript for the form elements
|
|
* \author Jack Weeland, Core|Vision
|
|
* \version $Revision: 26458 $
|
|
* \date $Date: 2016-03-21 14:36:35 +0100 (Mon, 21 Mar 2016) $
|
|
*
|
|
* JavaScript functions for the form elements
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Globals
|
|
*/
|
|
var shiftKeyDown = false;
|
|
var ctrlKeyDown = false;
|
|
|
|
// Safari needs special handling (seemingly all versions, but show Safari fix
|
|
// their drag&drop bug, version testing must be added below)
|
|
// Erm.. Google Chrome also includes a Safari version number
|
|
var safariDragDropBug = (navigator.userAgent.indexOf('Safari/') != -1) && (navigator.userAgent.indexOf('Chrome/') == -1);
|
|
// Mobile browser with touch events?
|
|
var isMobile = (navigator.userAgent.indexOf('Mobile') != -1);
|
|
|
|
/**
|
|
* Compatibility with older versions of IE
|
|
*/
|
|
function addEventHandler(element, event, handler)
|
|
{
|
|
if( element.addEventListener )
|
|
element.addEventListener(event, handler);
|
|
else if( element.attachEvent )
|
|
element.attachEvent('on' + event, handler);
|
|
}
|
|
|
|
function removeEventHandler(element, event, handler)
|
|
{
|
|
if( element.removeEventListener )
|
|
element.removeEventListener(event, handler);
|
|
else if( element.detachEvent )
|
|
element.detachEvent(event, handler);
|
|
}
|
|
|
|
/**
|
|
* Foldable div (and similar) via PHP
|
|
*/
|
|
function Expand(name)
|
|
{
|
|
var state = getElement(name);
|
|
|
|
if( state.value == 1 ) {
|
|
state.value = 0;
|
|
}
|
|
else {
|
|
state.value = 1;
|
|
}
|
|
|
|
onSubmit('expand','',1);
|
|
}
|
|
|
|
/**
|
|
* Selection box (support for 'print_selection()')
|
|
*/
|
|
|
|
// The selection list is an UL element and UL elements do not receive
|
|
// the input focus, so we need to emulate that using the global event
|
|
// handlers at the bottom of this file
|
|
var selection_box_focus;
|
|
var selection_box_open;
|
|
var selection_box_state;
|
|
// JavaScripts breaks down after a "recall", where the inner HTML of
|
|
// the content div is replaced and the JS objects forthe selection
|
|
// boxes and left/right boxes are recreated using the same name
|
|
// (the events don't work after that)
|
|
var selection_boxes = [];
|
|
var left_right_boxes = [];
|
|
// drag&drop selection and move for left/right boxes
|
|
var selection_drag = null, selection_move = null;
|
|
|
|
/**
|
|
* Selection box (support for 'print_selection()')
|
|
*/
|
|
|
|
function SelectionBox(name, type, icon_collapsed, icon_expanded)
|
|
{
|
|
this.name = name;
|
|
this.type = type;
|
|
this.multiSelect = false;
|
|
this.inLeftRightBox = false;
|
|
this.iconCollapsed = icon_collapsed;
|
|
this.iconExpanded = icon_expanded;
|
|
this.selectedItem = -1;
|
|
selection_boxes[this.name] = this;
|
|
|
|
this.addEventListener = function(e, handler)
|
|
{
|
|
addEventHandler(getElement(this.name + ":select"), e, handler);
|
|
}
|
|
|
|
// Copy the selected value (in the list) to the edit box
|
|
this.CopyValue = function()
|
|
{
|
|
var form_data = getElement(this.name);
|
|
form_data.value = getElement(this.name + ':editbox').value;
|
|
}
|
|
|
|
this.isDropDown = function()
|
|
{
|
|
return (this.type == 'dropdown' || this.type == 'dropdownlist');
|
|
}
|
|
|
|
this.ShowDropDown = function()
|
|
{
|
|
if( selection_box_open && selection_box_open.name != this.name ) {
|
|
selection_box_open.HideDropDown();
|
|
}
|
|
|
|
getElement(this.name + ':dropdownlist').style.display = 'block';
|
|
getElement(this.name + ':button').src = this.iconExpanded;
|
|
selection_box_open = this;
|
|
}
|
|
|
|
this.HideDropDown = function()
|
|
{
|
|
getElement(this.name + ':dropdownlist').style.display = 'none';
|
|
getElement(this.name + ':button').src = this.iconCollapsed;
|
|
selection_box_open = null;
|
|
}
|
|
|
|
this.OnClickDropDown = function(e)
|
|
{
|
|
if( getElement(this.name + ':dropdownlist').style.display == 'block' ) {
|
|
this.HideDropDown();
|
|
}
|
|
else {
|
|
this.ShowDropDown();
|
|
this.ShowSelected();
|
|
}
|
|
}
|
|
|
|
this.OnClickEditBox = function()
|
|
{
|
|
getElement(this.name + ':button').onclick();
|
|
}
|
|
|
|
// Show which list is focussed
|
|
this.FocusList = function(set_focus)
|
|
{
|
|
if( !this.isDropDown() ) {
|
|
var list = getElement(this.name + ':select');
|
|
|
|
if( list ) changeClass(list, "focus", set_focus);
|
|
}
|
|
}
|
|
|
|
// Get index of item at Y position 'p'
|
|
this.ItemAt = function(p)
|
|
{
|
|
var list = getElement(this.name + ':select');
|
|
var r_list = list.getBoundingClientRect();
|
|
|
|
for( var i = 0, index = 0; i < list.childNodes.length; i++ ) {
|
|
var list_item = list.childNodes[i];
|
|
|
|
if( list_item.tagName ) {
|
|
if( list_item.tagName.toLowerCase() == 'li' && list_item.id ) {
|
|
var r = list_item.getBoundingClientRect();
|
|
|
|
if( p >= (r.top - r_list.top) && p <= (r.bottom - r_list.top) )
|
|
return index;
|
|
|
|
index++;
|
|
}
|
|
else if( list_item.tagName.toLowerCase() == 'a' ) {
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Get first selected item
|
|
this.GetFirstSelected = function()
|
|
{
|
|
var list = getElement(this.name + ':select');
|
|
for( var i = 0, index = 0; i < list.childNodes.length; i++ ) {
|
|
var list_item = list.childNodes[i];
|
|
|
|
if( list_item.tagName ) {
|
|
if( list_item.tagName.toLowerCase() == 'li' && list_item.id ) {
|
|
if( list_item.className == 'selected' )
|
|
return index;
|
|
index++;
|
|
}
|
|
else if( list_item.tagName.toLowerCase() == 'a' ) {
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Get a listbox item by index
|
|
this.GetItem = function(item_index)
|
|
{
|
|
var list = getElement(this.name + ':select');
|
|
for( var i = 0, index = 0; i < list.childNodes.length; i++ ) {
|
|
var list_item = list.childNodes[i];
|
|
|
|
if( list_item.tagName && ((list_item.tagName.toLowerCase() == 'li' && list_item.id) || list_item.tagName.toLowerCase() == 'a') ) {
|
|
if( index == item_index )
|
|
return list_item;
|
|
index++;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Get the index for a listbox item
|
|
this.GetItemIndex = function(item)
|
|
{
|
|
var list = getElement(this.name + ':select');
|
|
for( var i = 0, index = 0; i < list.childNodes.length; i++ ) {
|
|
var list_item = list.childNodes[i];
|
|
|
|
if( list_item.tagName && ((list_item.tagName.toLowerCase() == 'li' && list_item.id) || list_item.tagName.toLowerCase() == 'a') ) {
|
|
if( list_item.id == item.id )
|
|
return index;
|
|
index++;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Get the total number of (visible) items
|
|
this.GetItemCount = function(visible_only)
|
|
{
|
|
var list = getElement(this.name + ':select');
|
|
for( var i = 0, count = 0; i < list.childNodes.length; i++ ) {
|
|
var list_item = list.childNodes[i];
|
|
|
|
if( list_item.tagName && ((list_item.tagName.toLowerCase() == 'li' && list_item.id) || list_item.tagName.toLowerCase() == 'a') ) {
|
|
// update selected items string
|
|
if( !visible_only || list_item.style.display != 'none' ) {
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
// Get all (visible) items
|
|
this.GetItems = function(visible_only)
|
|
{
|
|
var list = getElement(this.name + ':select');
|
|
var items = [];
|
|
for( var i = 0, index = 0; i < list.childNodes.length; i++ ) {
|
|
var list_item = list.childNodes[i];
|
|
|
|
// NB: fillers don't have an 'id', real items do
|
|
if( list_item.tagName && ((list_item.tagName.toLowerCase() == 'li' && list_item.id) || list_item.tagName.toLowerCase() == 'a') ) {
|
|
// update selected items string
|
|
if( (!visible_only || list_item.style.display != 'none') && list_item.className != 'filler' ) {
|
|
items.push(index);
|
|
}
|
|
|
|
index++;
|
|
}
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
// Get the selected items
|
|
this.GetSelectedItems = function()
|
|
{
|
|
var list = getElement(this.name + ':select');
|
|
var selected_items = [];
|
|
for( var i = 0, index = 0; i < list.childNodes.length; i++ ) {
|
|
var list_item = list.childNodes[i];
|
|
|
|
if( list_item.tagName && ((list_item.tagName.toLowerCase() == 'li' && list_item.id) || list_item.tagName.toLowerCase() == 'a') ) {
|
|
// update selected items string
|
|
if( list_item.className == 'selected' && list_item.style.display != 'none' ) {
|
|
selected_items.push(index);
|
|
}
|
|
|
|
index++;
|
|
}
|
|
}
|
|
|
|
return selected_items;
|
|
}
|
|
|
|
// Get the value for an item
|
|
this.GetItemValue = function(list_item)
|
|
{
|
|
if( typeof(list_item) == 'number' ) {
|
|
list_item = this.GetItem(list_item);
|
|
}
|
|
if( !list_item ) return false;
|
|
|
|
var prefix = this.name.length + 6; // format: 'name:item:value'
|
|
return list_item.id.substr(prefix);
|
|
}
|
|
|
|
// Get the display string for an item
|
|
this.GetItemString = function(list_item)
|
|
{
|
|
if( typeof(list_item) == 'number' ) {
|
|
list_item = this.GetItem(list_item);
|
|
}
|
|
if( !list_item ) return false;
|
|
|
|
var s = list_item.innerHTML;
|
|
if( s == " " ) {
|
|
// special marker for an empty <li>; the browser won't show empty <li>'s,
|
|
// but copying it to the edit box will show the text " " instead
|
|
// of an empty string
|
|
return "";
|
|
}
|
|
else return s;
|
|
}
|
|
|
|
// Select a item(s) that match 'search_str'
|
|
this.SelectValue = function(search_str)
|
|
{
|
|
var list = getElement(this.name + ':select');
|
|
var done = false;
|
|
for( var i = 0, index = 0; i < list.childNodes.length; i++ ) {
|
|
var list_item = list.childNodes[i];
|
|
|
|
if( list_item.tagName && ((list_item.tagName.toLowerCase() == 'li' && list_item.id) || list_item.tagName.toLowerCase() == 'a') ) {
|
|
// NB: to fix; should look at the inner 'li' of an 'a' element
|
|
if( !list_item.style || list_item.style.display != 'none' ) {
|
|
if( !done && this.GetItemString(list_item).substr(0, search_str.length) == search_str ) {
|
|
this.SelectItem(list_item, true);
|
|
// select one (this) or multiple items
|
|
done = !this.multiSelect;
|
|
}
|
|
else {
|
|
// no match; unselect
|
|
this.SelectItem(list_item, false);
|
|
}
|
|
}
|
|
index++;
|
|
}
|
|
}
|
|
|
|
//. not found
|
|
return false;
|
|
}
|
|
|
|
// Select a single item
|
|
this.SelectItem = function(list_item, select_not_deselect)
|
|
{
|
|
if( typeof(list_item) == 'number' ) {
|
|
list_item = this.GetItem(list_item);
|
|
}
|
|
if( !list_item ) return false;
|
|
|
|
if( (!list_item.style || list_item.style.display != 'none') && list_item.className != 'disabled' ) {
|
|
if( select_not_deselect )
|
|
list_item.className = 'selected';
|
|
else
|
|
list_item.className = null;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
this.isSelected = function(list_item)
|
|
{
|
|
if( typeof(list_item) == 'number' ) {
|
|
list_item = this.GetItem(list_item);
|
|
}
|
|
if( !list_item ) return false;
|
|
|
|
return list_item.className == 'selected' && (!list_item.style || list_item.style.display != 'none');
|
|
}
|
|
|
|
this.ShowItem = function(list_item, show)
|
|
{
|
|
if( typeof(list_item) == 'number' ) {
|
|
list_item = this.GetItem(list_item);
|
|
}
|
|
if( !list_item ) return false;
|
|
|
|
list_item.style.display = (show ? 'block' : 'none');
|
|
|
|
return true;
|
|
}
|
|
|
|
this.isVisible = function(list_item)
|
|
{
|
|
if( typeof(list_item) == 'number' ) {
|
|
list_item = this.GetItem(list_item);
|
|
}
|
|
if( !list_item ) return false;
|
|
|
|
return list_item.style.display != 'none';
|
|
}
|
|
|
|
this.Select = function(selected_item, update_only)
|
|
{
|
|
var form_data = getElement(this.name);
|
|
var list = getElement(this.name + ':select');
|
|
|
|
// select the selected item or augment the current selection for a multi-select box
|
|
var start, end;
|
|
|
|
// grow the selection up or down?
|
|
if( !this.multiSelect || !shiftKeyDown ) {
|
|
start = end = selected_item;
|
|
}
|
|
else if( selected_item < this.selectedItem ) {
|
|
start = selected_item;
|
|
end = this.selectedItem;
|
|
}
|
|
else {
|
|
start = this.selectedItem;
|
|
end = selected_item;
|
|
}
|
|
|
|
// select the items in the listbox; create a list (string) of the selected
|
|
// items on the fly
|
|
var selected_items = '', sep = '';
|
|
for( var i = 0, index = 0; i < list.childNodes.length; i++ ) {
|
|
var list_item = list.childNodes[i];
|
|
|
|
if( list_item.tagName ) {
|
|
if( list_item.tagName.toLowerCase() == 'li' && list_item.id ) {
|
|
// hidden items can't be selected
|
|
if( list_item.style.display != 'none' ) {
|
|
if( index >= start && index <= end ) {
|
|
if( this.multiSelect && ctrlKeyDown && !shiftKeyDown && list_item.className == 'selected' )
|
|
list_item.className = null;
|
|
else if( list_item.className != 'disabled' )
|
|
list_item.className = 'selected';
|
|
}
|
|
else if( (!this.multiSelect || !ctrlKeyDown) && list_item.className != 'disabled' ) {
|
|
list_item.className = null;
|
|
}
|
|
|
|
// update selected items string
|
|
if( list_item.className == 'selected' ) {
|
|
selected_items += sep + this.GetItemValue(list_item);
|
|
sep = ',';
|
|
}
|
|
}
|
|
else if( list_item.className != 'disabled' ) {
|
|
list_item.className = null;
|
|
}
|
|
|
|
index++;
|
|
}
|
|
else if( list_item.tagName.toLowerCase() == 'a' ) {
|
|
// direct jump, but we must count it
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// copy the string for the selected value to the edit box
|
|
// (a dropdown list should never be a multi-select box and we can't bother to check)
|
|
if( !update_only ) {
|
|
if( this.isDropDown() ) {
|
|
var editbox = getElement(this.name + ':editbox');
|
|
|
|
if( editbox ) {
|
|
editbox.value = this.GetItemString(selected_item);
|
|
}
|
|
}
|
|
|
|
// copy the selected value to the hidden control (the value of the emulated 'select' element)
|
|
if( this.type == 'dropdown' ) {
|
|
// combobox; copy the selected string and not the item values
|
|
form_data.value = this.GetItemString(selected_item);
|
|
}
|
|
else {
|
|
form_data.value = selected_items;
|
|
}
|
|
|
|
// save the selected item
|
|
this.selectedItem = selected_item;
|
|
|
|
if( this.isDropDown() ) this.HideDropDown();
|
|
}
|
|
}
|
|
|
|
this.SelectFrom = function(start, end)
|
|
{
|
|
var form_data = getElement(this.name);
|
|
var list = getElement(this.name + ':select');
|
|
|
|
// swap start/end?
|
|
if( !this.multiSelect ) {
|
|
// not a multi-select box; only select the first item
|
|
end = start;
|
|
}
|
|
else if( end != -1 && end < start ) {
|
|
var p = start;
|
|
start = end;
|
|
end = p;
|
|
}
|
|
|
|
// select all items
|
|
var selected_items = '', sep = '';
|
|
for( var i = 0, index = 0; i < list.childNodes.length; i++ ) {
|
|
var list_item = list.childNodes[i];
|
|
|
|
if( list_item.tagName ) {
|
|
if( list_item.tagName.toLowerCase() == 'li' && list_item.id ) {
|
|
// hidden items can't be selected
|
|
if( (!list_item.style || list_item.style.display != 'none') && index >= start && (index <= end || end == -1) ) {
|
|
list_item.className = 'selected';
|
|
|
|
// update selected items string
|
|
selected_items += sep + this.GetItemValue(list_item);
|
|
sep = ',';
|
|
}
|
|
else if( list_item.className != 'disabled' && !ctrlKeyDown ) {
|
|
list_item.className = null;
|
|
}
|
|
|
|
index++;
|
|
}
|
|
else if( list_item.tagName.toLowerCase() == 'a' ) {
|
|
// direct jump, but we must count it
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.ShowSelected = function()
|
|
{
|
|
var list = getElement(this.name + ':select');
|
|
if(
|
|
list &&
|
|
(!this.isDropDown() || getElement(this.name + ':dropdownlist').style.display != 'none')
|
|
) {
|
|
for( var i = 0; i < list.childNodes.length; i++ ) {
|
|
var list_item = list.childNodes[i];
|
|
|
|
if( list_item.className == 'selected' ) {
|
|
// scroll to the item, minding the offset from the list ('ul' element)
|
|
// to its parent (the 'div' element)
|
|
var offset = list_item.offsetTop;
|
|
offset -= list.offsetTop;
|
|
offset -= (list.clientHeight / 2);
|
|
if( offset < list.parentNode.scrollTop || offset > list.parentNode.scrollTop + list.clientHeight ) {
|
|
list.parentNode.scrollTop = offset;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.Initialize = function()
|
|
{
|
|
this.ShowSelected();
|
|
if( selection_box_focus && selection_box_focus.name == this.name ) {
|
|
this.FocusList(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Left/right selection box (support for 'print_left_right_control()')
|
|
*/
|
|
function LeftRightBox(name, options, left_box, right_box)
|
|
{
|
|
this.name = name;
|
|
this.options = options.split(',');
|
|
this.leftBox = left_box;
|
|
this.rightBox = right_box;
|
|
left_right_boxes[this.name] = this;
|
|
|
|
// indicate that the selection boxes are part of a left/right box
|
|
this.leftBox.inLeftRightBox = true;
|
|
this.rightBox.inLeftRightBox = true;
|
|
|
|
this.OnClickLtr = function(e) {
|
|
if( this.options && this.options.indexOf('all') != -1 ) {
|
|
this.DeselectAll();
|
|
}
|
|
else {
|
|
this.DeselectItems();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
this.OnClickRtl = function(e) {
|
|
if( this.options && this.options.indexOf('all') != -1 ) {
|
|
this.SelectAll();
|
|
}
|
|
else {
|
|
this.SelectItems();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Filter items in the right listbox
|
|
this.UpdateFilter = function()
|
|
{
|
|
var filter_str = getElement(name + ":filter").value;
|
|
var search_regex = new RegExp(filter_str, "i");
|
|
|
|
for( var i = 0; i < this.rightBox.GetItemCount(); i++ ) {
|
|
this.rightBox.ShowItem(i, this.rightBox.GetItemString(i).search(search_regex) != -1 && !this.leftBox.isVisible(i));
|
|
}
|
|
}
|
|
|
|
// Update the selection in the hidden control
|
|
this.UpdateSelection = function(event)
|
|
{
|
|
// get the visible items
|
|
var selections = this.leftBox.GetItems(true);
|
|
// and build a comma separated string from their id's
|
|
var selection_str = '', sep = '';
|
|
for( var i = 0; i < selections.length; i++ ) {
|
|
selection_str += sep + this.leftBox.GetItemValue(selections[i]);
|
|
sep = ',';
|
|
}
|
|
getElement(name + '_left_ids').value = selection_str;
|
|
|
|
// get the "invisible" items
|
|
var selections = this.rightBox.GetItems(true);
|
|
// and build a comma separated string from their id's
|
|
var selection_str = '', sep = '';
|
|
for( var i = 0; i < selections.length; i++ ) {
|
|
selection_str += sep + this.rightBox.GetItemValue(selections[i]);
|
|
sep = ',';
|
|
}
|
|
getElement(name + '_right_ids').value = selection_str;
|
|
|
|
// remove selections from the listboxes
|
|
this.leftBox.Select(-1);
|
|
this.rightBox.Select(-1);
|
|
|
|
// only one of the events is fired
|
|
if( event == 'ltr' && this.onltr ) eval(this.onltr);
|
|
else if( event == 'rtl' && this.onrtl ) eval(this.onrtl);
|
|
else if( this.onchange && event ) eval(this.onchange);
|
|
}
|
|
|
|
// Move items from the right to the left listbox (i.e., select them)
|
|
this.SelectItems = function() {
|
|
var selections = this.rightBox.GetSelectedItems();
|
|
|
|
for( var i = 0; i < selections.length; i++ ) {
|
|
// unselect item
|
|
this.rightBox.SelectItem(selections[i], false);
|
|
// "move" it from right to left
|
|
this.rightBox.ShowItem(selections[i], false);
|
|
this.leftBox.ShowItem(selections[i], true);
|
|
}
|
|
|
|
// update the hidden input that contains the "left" selections
|
|
this.UpdateSelection('rtl');
|
|
}
|
|
|
|
// Move items from the left to the right listbox (i.e. deselect them)
|
|
this.DeselectItems = function() {
|
|
var selections = this.leftBox.GetSelectedItems();
|
|
|
|
for( var i = 0; i < selections.length; i++ ) {
|
|
// unselect item
|
|
this.leftBox.SelectItem(selections[i], false);
|
|
// "move" it from left to right
|
|
this.leftBox.ShowItem(selections[i], false);
|
|
this.rightBox.ShowItem(selections[i], true);
|
|
}
|
|
|
|
// update the hidden input that contains the "left" selections
|
|
this.UpdateSelection('ltr');
|
|
}
|
|
|
|
this.SelectAll = function() {
|
|
for( var i = 0; i < this.rightBox.GetItemCount(); i++ ) {
|
|
this.leftBox.SelectItem(i, true);
|
|
this.rightBox.SelectItem(i, false);
|
|
}
|
|
|
|
// update the hidden input that contains the "left" selections
|
|
this.UpdateSelection('rtl');
|
|
}
|
|
|
|
this.DeselectAll = function() {
|
|
for( var i = 0; i < this.rightBox.GetItemCount(); i++ ) {
|
|
this.leftBox.SelectItem(i, false);
|
|
this.rightBox.SelectItem(i, true);
|
|
}
|
|
|
|
// update the hidden input that contains the "left" selections
|
|
this.UpdateSelection('ltr');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tree view (support for 'print_treeview()')
|
|
*/
|
|
function TreeView(name, icon_collapsed, icon_expanded, icon_empty)
|
|
{
|
|
this.name = name;
|
|
this.iconCollapsed = icon_collapsed;
|
|
this.iconExpanded = icon_expanded;
|
|
this.iconEmtpy = icon_empty;
|
|
|
|
this.HideSubtree = function(id)
|
|
{
|
|
var list_element = getElement(this.name + "[" + id + "][list]");
|
|
var image_element = getElement(this.name + "[" + id + "][img]");
|
|
var state_element = getElement(this.name + "[" + id + "]");
|
|
|
|
image_element.src = this.iconCollapsed;
|
|
list_element.style.display = 'none';
|
|
state_element.value = 0;
|
|
}
|
|
|
|
this.ShowSubtree = function(id)
|
|
{
|
|
var list_element = getElement(this.name + "[" + id + "][list]");
|
|
var image_element = getElement(this.name + "[" + id + "][img]");
|
|
var state_element = getElement(this.name + "[" + id + "]");
|
|
|
|
image_element.src = this.iconExpanded;
|
|
list_element.style.display = 'block';
|
|
state_element.value = 1;
|
|
}
|
|
|
|
this.OnClickExpand = function(id)
|
|
{
|
|
var list_element = getElement(this.name + "[" + id + "][list]");
|
|
var state = list_element.style.display;
|
|
|
|
// the state is "nothing" by default
|
|
if( !state || state == 'block' ) {
|
|
this.HideSubtree(id);
|
|
}
|
|
else {
|
|
this.ShowSubtree(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Global event handlers
|
|
*/
|
|
|
|
// Global click events
|
|
addEventHandler(document, 'click', function(e) {
|
|
if( !e ) e = window.event;
|
|
var target = (e.target ? e.target : e.srcElement);
|
|
var target_name = (target ? (target.name ? target.name : target.id) : false);
|
|
|
|
// click outside a 'SelectionBox'? hide its dropdown list
|
|
if( selection_box_open ) {
|
|
if(
|
|
!target_name ||
|
|
target_name.substr(0, selection_box_open.name.length) != selection_box_open.name
|
|
) {
|
|
selection_box_open.HideDropDown();
|
|
}
|
|
}
|
|
|
|
// selection box clicked? pointless on a mobile device...
|
|
if( !isMobile ) {
|
|
if( selection_box_focus ) {
|
|
// unhighlight the previously focused box
|
|
selection_box_focus.FocusList(false);
|
|
}
|
|
if( (selection_box_focus = selection_boxes[target_name.substr(0, target_name.indexOf(':'))]) ) {
|
|
// show focus for the newly selected box
|
|
selection_box_focus.FocusList(true);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
// Monitor the state of the shift and ctrl keys
|
|
addEventHandler(document, 'keydown', function(e) {
|
|
if( !e ) e = window.event;
|
|
|
|
shiftKeyDown = e.shiftKey;
|
|
ctrlKeyDown = e.ctrlKey;
|
|
|
|
return true;
|
|
});
|
|
|
|
addEventHandler(document, 'keyup', function(e) {
|
|
if( !e ) e = window.event;
|
|
|
|
shiftKeyDown = e.shiftKey;
|
|
ctrlKeyDown = e.ctrlKey;
|
|
var keyCode = (e.which ? e.which : (e.key ? e.key.charCodeAt(0) : -1));
|
|
|
|
if( selection_box_focus ) {
|
|
// the selection list is a UL element and those don't receive keyboard focus,
|
|
// so the hocus pocus below (and in the global 'click' event handler) is
|
|
// needed emulate the keyboard focus
|
|
// Ctrl+A handler
|
|
if( ctrlKeyDown && keyCode == 65 /* 'a' */ ) {
|
|
selection_box_focus.SelectFrom(0, -1);
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
/**
|
|
* Event handlers and helpers for drag and drop functionality
|
|
*/
|
|
|
|
// Highlight the drop target; primarily used for Safari (which has a bug) and
|
|
// mobile browsers, but it gives a nice effect in other browsers as well
|
|
function dragShowAction(target, css_action, highlight)
|
|
{
|
|
var target_name = (target ? (target.name ? target.name : target.id) : false);
|
|
if( target_name ) {
|
|
if( (p = target_name.indexOf(':')) != -1 ) target_name = target_name.substr(0, p);
|
|
var list = getElement(target_name + ':select');
|
|
|
|
if( list ) changeClass(list, css_action, highlight);
|
|
}
|
|
}
|
|
|
|
// De-highlight the drop source
|
|
function dragHighlight(target, highlight)
|
|
{
|
|
return dragShowAction(target, "dragdrop_target", highlight);
|
|
}
|
|
|
|
// De-highlight the drop source
|
|
function dragShowDrag(target, highlight)
|
|
{
|
|
return dragShowAction(target, "dragdrop_source", highlight);
|
|
}
|
|
|
|
// Start a drag and drop operation
|
|
// Returns the selection box where the drag and drop starts on success,
|
|
// true if the event is handled or false if not
|
|
function dragStart(source)
|
|
{
|
|
var source_name = (source ? (source.name ? source.name : source.id) : false);
|
|
|
|
if( source_name ) {
|
|
var selection_box = selection_boxes[source_name.substr(0, source_name.indexOf(':'))];;
|
|
|
|
if( selection_box && selection_box.multiSelect ) {
|
|
// initialize the drag s/m
|
|
if( selection_box.isSelected(source) && selection_box.inLeftRightBox ) {
|
|
// true drag and drop between the left and right boxes
|
|
|
|
// in Google Chrome, the drag&drop is broken (the e.dataTransfer
|
|
// is invalid after the "dragstart")
|
|
selection_move = { source: selection_box, started: false };
|
|
dragShowDrag(selection_move.source, true);
|
|
|
|
return selection_box;
|
|
}
|
|
else {
|
|
selection_drag = { selection_box: selection_box, list_item: source, start: selection_box.GetItemIndex(source), started: false };
|
|
|
|
// select this item as well
|
|
selection_drag.selection_box.Select(selection_drag.start, true);
|
|
|
|
// let the "default behaviour" proceed; this "bug" is what actually makes
|
|
// the selection-by-dragging work :-)
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// not handled
|
|
return false;
|
|
}
|
|
|
|
function dragDrop(source, target)
|
|
{
|
|
var source_name = source.name;
|
|
var target_name = (target ? (target.name ? target.name : target.id) : false);
|
|
if( (p = target_name.indexOf(':')) != -1 ) target_name = target_name.substr(0, p);
|
|
var t_offset, s_offset;
|
|
|
|
// we only allow drag&drop for the left/right controls and the user can only drag a
|
|
// selection from the left to right box or v.v. of the same left/right control
|
|
if(
|
|
(target_name.slice((t_offset = -5)) == '_left' || target_name.slice((t_offset = -6)) == '_right') &&
|
|
(source_name.slice((s_offset = -5)) == '_left' || source_name.slice((s_offset = -6)) == '_right') &&
|
|
(target_name.slice(0, t_offset) == source_name.slice(0, s_offset)) &&
|
|
t_offset != s_offset // left->right or right->left
|
|
) {
|
|
dragHighlight(target, false);
|
|
dragShowDrag(source, false);
|
|
|
|
var left_right_box = left_right_boxes[target_name.slice(0, t_offset)];
|
|
if( left_right_box ) {
|
|
if( t_offset == -5 ) {
|
|
//right to left
|
|
left_right_box.OnClickRtl(null);
|
|
}
|
|
else {
|
|
// left to right
|
|
left_right_box.OnClickLtr(null);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// not handled
|
|
return false;
|
|
}
|
|
|
|
function dragCancel(source)
|
|
{
|
|
dragShowDrag(source, false);
|
|
}
|
|
|
|
function dragTest(source, target)
|
|
{
|
|
var source_name = source.name;
|
|
var target_name = (target ? (target.name ? target.name : target.id) : false);
|
|
if( (p = target_name.indexOf(':')) != -1 ) target_name = target_name.substr(0, p);
|
|
var t_offset, s_offset;
|
|
|
|
if( selection_move && selection_move.target && selection_move.target.name != target_name ) {
|
|
dragHighlight(selection_move.target, false);
|
|
}
|
|
|
|
// we only allow drag&drop for the left/right controls and the user can only drag a
|
|
// selection from the left to right box or v.v. of the same left/right control
|
|
if(
|
|
(target_name.slice((t_offset = -5)) == '_left' || target_name.slice((t_offset = -6)) == '_right') &&
|
|
(source_name.slice((s_offset = -5)) == '_left' || source_name.slice((s_offset = -6)) == '_right') &&
|
|
(target_name.slice(0, t_offset) == source_name.slice(0, s_offset)) &&
|
|
t_offset != s_offset // left->right or right->left
|
|
) {
|
|
dragHighlight(target, true);
|
|
|
|
// fix for Safari (see the 'dragend' handler below); save off the drop target
|
|
selection_move.target = { name: target_name };
|
|
|
|
return true;
|
|
}
|
|
else {
|
|
dragHighlight(selection_move.target, false);
|
|
|
|
selection_move.target = null;
|
|
}
|
|
|
|
// invalid target
|
|
return false;
|
|
}
|
|
|
|
// Scroll the selection list, when needed, when the user is selecting
|
|
// list items by dragging
|
|
function dragScroll(selection_box, mouse_x, mouse_y)
|
|
{
|
|
var list = getElement(selection_box.name + ':select');
|
|
var r = list.parentNode.getBoundingClientRect();
|
|
var index;
|
|
|
|
if( mouse_y < r.top ) {
|
|
var pos = list.parentNode.scrollTop - (1 + ((r.top - mouse_y) / 8));
|
|
if( pos < 0 ) list.parentNode.scrollTop = 0;
|
|
else list.parentNode.scrollTop = pos;
|
|
|
|
index = selection_box.ItemAt(list.parentNode.scrollTop);
|
|
}
|
|
else if( mouse_y > r.bottom ) {
|
|
var pos = list.parentNode.scrollTop + (1 + ((mouse_y - r.bottom) / 8));
|
|
var max_pos = list.parentNode.scrollTop + r.height;
|
|
if( pos > max_pos ) list.parentNode.scrollTop = max_pos;
|
|
else list.parentNode.scrollTop = pos;
|
|
|
|
index = selection_box.ItemAt(list.parentNode.scrollTop + r.height);
|
|
}
|
|
|
|
if( index >= 0 ) selection_box.SelectFrom(selection_drag.start, index);
|
|
}
|
|
|
|
addEventHandler(document, 'mouseup', function(e) {
|
|
// selection by dragging active? if so, clear the "select by drag" state
|
|
if( selection_drag ) {
|
|
selection_drag = null;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
addEventHandler(document, "mouseover", function(e) {
|
|
// selection by dragging active?
|
|
if( selection_drag ) {
|
|
if( !e ) e = window.event;
|
|
var target = (e.target ? e.target : e.srcElement);
|
|
|
|
// select all items from the "start" to this one
|
|
var index = selection_drag.selection_box.GetItemIndex(target);
|
|
if( index != -1 ) selection_drag.selection_box.SelectFrom(selection_drag.start, index);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
addEventHandler(document, 'mousemove', function(e) {
|
|
if( selection_drag ) {
|
|
dragScroll(selection_drag.selection_box, e.clientX, e.clientY);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
addEventHandler(document, 'dragstart', function(e) {
|
|
if( !e ) e = window.event;
|
|
var target = (e.target ? e.target : e.srcElement);
|
|
|
|
var drag_started = dragStart(target);
|
|
if( drag_started ) {
|
|
if( typeof drag_started == 'object' ) {
|
|
// normal HTML5 drag-and-drop; default behaviour wanted
|
|
e.dataTransfer.setData('Text', drag_started.name);
|
|
return true;
|
|
}
|
|
else {
|
|
// select-by-drag; do _not_ set 'e.dataTransfer' and prevent the default
|
|
// behaviour of HTML5 drag-and-drop
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
}
|
|
// old behavour: return 'true' when the default behaviour must proceed
|
|
return !drag_started;
|
|
});
|
|
|
|
addEventHandler(document, 'dragover', function(e) {
|
|
if( !e ) e = window.event;
|
|
var target = (e.target ? e.target : e.srcElement);
|
|
var source = { name: e.dataTransfer.getData('Text') };
|
|
// fix for Google Chrome (the e.dataTransfer is lost after 'dragstrart')
|
|
if( !source.name && selection_move && selection_move.source ) source = selection_move.source;
|
|
|
|
var drag_over = dragTest(source, target);
|
|
if( drag_over ) {
|
|
// funnily enough, we must _prevent_ the drag-and-drop default behaviour
|
|
// to show the "you can drop here" cursor
|
|
e.preventDefault();
|
|
}
|
|
// old behavour: return 'true' when the default behaviour must proceed
|
|
return !drag_over;
|
|
});
|
|
|
|
addEventHandler(document, 'drop', function(e) {
|
|
if( !e ) e = window.event;
|
|
var target = (e.target ? e.target : e.srcElement);
|
|
var source = { name: e.dataTransfer.getData('Text') };
|
|
// fix for Google Chrome (the e.dataTransfer is lost after 'dragstrart')
|
|
if( !source.name && selection_move && selection_move.source ) source = selection_move.source;
|
|
|
|
var drag_dropped = dragDrop(source, target);
|
|
if( drag_dropped ) e.preventDefault();
|
|
// old behavour: return 'true' when the default behaviour must proceed
|
|
return !drag_dropped;
|
|
});
|
|
|
|
addEventHandler(document, 'dragend', function(e) {
|
|
if( !e ) e = window.event;
|
|
var source = { name: e.dataTransfer.getData('Text') };
|
|
// fix for Google Chrome (the e.dataTransfer is lost after 'dragstrart')
|
|
if( !source.name && selection_move && selection_move.source ) source = selection_move.source;
|
|
|
|
// bug in Safari: it doesn't receive the 'ondrop' event
|
|
// however, it does receive the 'ondragend' event, _but_ the "target" of this event
|
|
// is actually the source, so we need to remember the actual target in the 'ondragover'
|
|
// event handler
|
|
if( safariDragDropBug && selection_move && selection_move.target ) {
|
|
dragDrop(source, selection_move.target);
|
|
dragHighlight(selection_move.target, false);
|
|
}
|
|
else if( source ) {
|
|
dragCancel(source);
|
|
}
|
|
|
|
// clear "move" state
|
|
selection_move = null;
|
|
|
|
return true;
|
|
});
|
|
|
|
/**
|
|
* Touch handlers (Android, iPad)
|
|
*/
|
|
|
|
// State
|
|
var touch_state = null;
|
|
|
|
function touchKillTimer()
|
|
{
|
|
if( touch_state ) {
|
|
if( touch_state.timer ) {
|
|
clearTimeout(touch_state.timer);
|
|
touch_state.timer = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
function touchStartTimer(source, touch)
|
|
{
|
|
var source_name = (source ? (source.name ? source.name : source.id) : false);
|
|
|
|
if( source_name ) {
|
|
var selection_box = selection_boxes[source_name.substr(0, source_name.indexOf(':item:'))];;
|
|
|
|
if( selection_box ) {
|
|
// kill previous timer, when present
|
|
touchKillTimer();
|
|
|
|
touch_state = {
|
|
target: source,
|
|
touch: { clientX: touch.clientX, clientY: touch.clientY },
|
|
timer: setTimeout(
|
|
// start dragging after a "tap and hold" time-out of 500ms
|
|
function() {
|
|
var drag_started = dragStart(touch_state.target);
|
|
if( drag_started ) {
|
|
// fat finger detection to prevent that multiple items are selected, even
|
|
// without actual dragging
|
|
if( selection_drag ) {
|
|
selection_drag.dikke_vinger = { clientX: touch_state.touch.clientX, clientY: touch_state.touch.clientY };
|
|
}
|
|
}
|
|
},
|
|
500
|
|
)
|
|
};
|
|
|
|
return touch_state;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
addEventHandler(document, 'touchstart', function(e) {
|
|
if( !e ) e = window.event;
|
|
var target = (e.target ? e.target : e.srcElement);
|
|
var target_name = (target ? (target.name ? target.name : target.id) : false);
|
|
var selection_box = false;
|
|
if( target_name ) selection_box = selection_boxes[target_name.substr(0, target_name.indexOf(':'))];
|
|
|
|
// selection box selected and not a multi-touch event
|
|
if( e.changedTouches.length == 1 && selection_box ) {
|
|
var touch = e.changedTouches[0];
|
|
|
|
if(
|
|
selection_box.inLeftRightBox && // true drag and drop is only supported for the left/right boxes
|
|
selection_box.isSelected(target)
|
|
) {
|
|
dragStart(target);
|
|
|
|
touch_state = {
|
|
target: target,
|
|
touch: { clientX: touch.clientX, clientY: touch.clientY },
|
|
timer: null
|
|
};
|
|
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
else {
|
|
// do _not_ cancel the event, as this will disable the default scroll behaviour
|
|
// NB: on an iPad, not cancelling the "touchstart" event will trigger a "click"
|
|
// event after "touchend"; the PHP code will therefore not install the default
|
|
// "onclick" handler to select an item (a click is handled in the "touchend"
|
|
// handler)
|
|
touch_state = touchStartTimer(target, touch);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
addEventHandler(document, 'touchmove', function(e) {
|
|
if( !e ) e = window.event;
|
|
var touch = e.changedTouches[0];
|
|
// iPad doesn't have a 'radius' in its touch evnet
|
|
var radiusX = useDefault(touch.radiusX, 16);
|
|
var radiusY = useDefault(touch.radiusX, 16);
|
|
|
|
// is the user scrolling the list? if so, clear the drag/touch state
|
|
if(
|
|
touch_state &&
|
|
(
|
|
Math.abs(touch_state.touch.clientY - touch.clientY) >= radiusY ||
|
|
Math.abs(touch_state.touch.clientX - touch.clientX) >= radiusX
|
|
)
|
|
) {
|
|
if( touch_state.timer ) touchKillTimer();
|
|
|
|
if( selection_move ) {
|
|
selection_move.started = true;
|
|
}
|
|
else if( selection_drag ) {
|
|
selection_drag.started = true;
|
|
}
|
|
}
|
|
|
|
if( selection_move ) {
|
|
var target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
var source = selection_move.source;
|
|
|
|
dragTest(source, target);
|
|
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
else if( selection_drag ) {
|
|
var list = getElement(selection_drag.selection_box.name + ':select');
|
|
var r = list.parentNode.getBoundingClientRect();
|
|
|
|
if(
|
|
touch.clientY >= r.top && touch.clientY <= r.bottom &&
|
|
(
|
|
!selection_drag.dikke_vinger ||
|
|
Math.abs(selection_drag.dikke_vinger.clientY - touch.clientY) >= radiusY ||
|
|
Math.abs(selection_drag.dikke_vinger.clientX - touch.clientX) >= radiusX
|
|
)
|
|
) {
|
|
// clear fat finger state
|
|
selection_drag.dikke_vinger = null;
|
|
|
|
// select all items from the "start" to this one
|
|
var r_list = list.getBoundingClientRect();
|
|
var index = selection_drag.selection_box.ItemAt(touch.clientY - r_list.top);
|
|
|
|
selection_drag.selection_box.SelectFrom(selection_drag.start, index);
|
|
}
|
|
else {
|
|
// scroll the list when the mouse pointer is outside this list
|
|
dragScroll(selection_drag.selection_box, touch.clientX, touch.clientY);
|
|
}
|
|
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
addEventHandler(document, 'touchend', function(e) {
|
|
if( !e ) e = window.event;
|
|
var touch = e.changedTouches[0];
|
|
|
|
if( touch_state && touch_state.timer ) {
|
|
// iPad doesn't have a 'radius' in its touch event
|
|
var radiusX = useDefault(touch.radiusX, 16);
|
|
var radiusY = useDefault(touch.radiusX, 16);
|
|
|
|
// is it actually a click? if so, pretend that the drag was started
|
|
// and handle the click event below
|
|
// NB: mostly to avoid so hitches of the iPad
|
|
if(
|
|
Math.abs(touch_state.touch.clientY - touch.clientY) <= radiusY ||
|
|
Math.abs(touch_state.touch.clientX - touch.clientX) <= radiusX
|
|
) {
|
|
dragStart(touch_state.target);
|
|
// 'selection_drag' is initialized above, with 'started' set to false
|
|
}
|
|
|
|
touchKillTimer();
|
|
}
|
|
|
|
if( selection_move ) {
|
|
// drop if the target is valid
|
|
if( selection_move.target ) {
|
|
dragDrop(selection_move.source, selection_move.target);
|
|
}
|
|
else {
|
|
dragCancel(selection_move.source);
|
|
|
|
|
|
if( !selection_move.started ) {
|
|
// a "click" really
|
|
var list = getElement(selection_move.source.name + ':select');
|
|
var r_list = list.getBoundingClientRect();
|
|
var index = selection_move.source.ItemAt(touch.clientY - r_list.top);
|
|
|
|
selection_move.source.Select(index, false);
|
|
}
|
|
}
|
|
|
|
e.preventDefault();
|
|
}
|
|
else if( selection_drag ) {
|
|
var list = getElement(selection_drag.selection_box.name + ':select');
|
|
var r_list = list.getBoundingClientRect();
|
|
var index = selection_drag.selection_box.ItemAt(touch.clientY - r_list.top);
|
|
|
|
if( !selection_drag.started ) {
|
|
// a "click" really...
|
|
selection_drag.selection_box.Select(index, false);
|
|
}
|
|
|
|
e.preventDefault();
|
|
}
|
|
|
|
// clear drag-and-drop state
|
|
if( selection_move && selection_move.target ) {
|
|
dragHighlight(selection_move.target, false);
|
|
}
|
|
selection_drag = null;
|
|
selection_move = null;
|
|
|
|
return true;
|
|
});
|
|
|
|
addEventHandler(document, 'touchcancel', function(e) {
|
|
if( touch_state ) touchKillTimer();
|
|
|
|
if( selection_move ) {
|
|
if( selection_move.target ) dragHighlight(selection_move.target, false);
|
|
if( selection_move.source ) dragShowDrag(selection_move.source, true);
|
|
}
|
|
selection_drag = null;
|
|
selection_move = null;
|
|
|
|
return true;
|
|
});
|