oxjs/source/Ox.UI/js/List/Ox.TextList.js
2011-06-15 16:36:01 +02:00

872 lines
29 KiB
JavaScript

// vim: et:ts=4:sw=4:sts=4:ft=js
/*@
Ox.TextList <f:Ox.Element> TextList Object
() -> <f> TextList Object
(options) -> <f> TextList Object
(options, self) -> <f> TextList Object
options <o> Options object
columns <[o]|[]>
Fixme: There's probably more...
addable <b>
editable <b>
format <f>
id <s>
removable <b>
map <f> function that maps values to sort values
operator <s> default sort operator
title <s>
unique <b> If true, this column acts as unique id
visible <b>
width <n>
columnsMovable <b|false>
columnsRemovable <b|false>
columnsResizable <b|false>
columnsVisible <b|false>
columnWidth <a|[40, 800]>
id <s|''>
items <f|null> function() {} {sort, range, keys, callback} or array
max <n|-1>
min <n|0>
pageLength <n|100>
scrollbarVisible <b|false>
selected <a|[]>
sort <a|[]>
self <o> shared private variable
@*/
Ox.TextList = function(options, self) {
// fixme: rename to TableList
var self = self || {},
that = new Ox.Element({}, self)
.defaults({
columns: [],
columnsMovable: false,
columnsRemovable: false,
columnsResizable: false,
columnsVisible: false,
columnWidth: [40, 800],
id: '',
items: null, // function() {} {sort, range, keys, callback} or array
max: -1,
min: 0,
pageLength: 100,
scrollbarVisible: false,
selected: [],
sort: []
})
.options(options || {})
.addClass('OxTextList')
.bindEvent({
key_left: function() {
var $element = that.$body.$element,
scrollLeft = $element[0].scrollLeft - $element.width();
$element.animate({scrollLeft: scrollLeft}, 250);
},
key_right: function() {
var $element = that.$body.$element,
scrollLeft = $element[0].scrollLeft + $element.width();
$element.animate({scrollLeft: scrollLeft}, 250);
},
keys: find
});
self.options.columns.forEach(function(v) { // fixme: can this go into a generic ox.js function?
// fixme: and can't these just remain undefined?
if (Ox.isUndefined(v.align)) {
v.align = 'left';
}
if (Ox.isUndefined(v.clickable)) {
v.clickable = false;
}
if (Ox.isUndefined(v.editable)) {
v.editable = false;
}
if (Ox.isUndefined(v.unique)) {
v.unique = false;
}
if (Ox.isUndefined(v.visible)) {
v.visible = false;
}
if (v.unique) {
self.unique = v.id;
}
});
$.extend(self, {
columnPositions: [],
defaultColumnWidths: $.map(self.options.columns, function(v) {
return v.defaultWidth || v.width;
}),
itemHeight: 16,
page: 0,
pageLength: 100,
scrollLeft: 0,
selectedColumn: getColumnIndexById(self.options.sort[0].key),
visibleColumns: $.map(self.options.columns, function(v) {
return v.visible ? v : null;
})
});
// fixme: there might be a better way than passing both visible and position
self.options.columns.forEach(function(v) {
if (!Ox.isUndefined(v.position)) {
self.visibleColumns[v.position] = v;
}
})
$.extend(self, {
columnWidths: $.map(self.visibleColumns, function(v, i) {
return v.width;
}),
pageHeight: self.options.pageLength * self.itemHeight
});
self.format = {};
self.options.columns.forEach(function(v, i) {
if (v.format) {
self.format[v.id] = v.format;
}
});
// Head
if (self.options.columnsVisible) {
that.$bar = new Ox.Bar({
orientation: 'horizontal',
size: 16
}).appendTo(that);
that.$head = new Ox.Container()
.addClass('OxHead')
.css({
right: self.options.scrollbarVisible ? Ox.UI.SCROLLBAR_SIZE + 'px' : 0
})
.appendTo(that.$bar);
that.$head.$content.addClass('OxTitles');
constructHead();
if (self.options.columnsRemovable) {
that.$select = new Ox.Select({
id: self.options.id + 'SelectColumns',
items: $.map(self.options.columns, function(v, i) {
return {
checked: v.visible,
disabled: v.removable === false,
id: v.id,
title: v.title
}
}),
max: -1,
min: 1,
type: 'image'
})
.bindEvent('change', changeColumns)
.appendTo(that.$bar.$element);
}
}
// Body
that.$body = new Ox.List({
construct: constructItem,
id: self.options.id,
items: self.options.items,
itemHeight: 16,
items: self.options.items,
itemWidth: getItemWidth(),
format: self.format, // fixme: not needed, happens in TextList
keys: $.map(self.visibleColumns, function(v) {
return v.id;
}),
max: self.options.max,
min: self.options.min,
pageLength: self.options.pageLength,
paste: self.options.paste,
orientation: 'vertical',
selected: self.options.selected,
sort: self.options.sort,
sortable: self.options.sortable,
type: 'text',
unique: self.unique
}, $.extend({}, self)) // pass event handler
.addClass('OxBody')
.css({
top: (self.options.columnsVisible ? 16 : 0) + 'px',
overflowY: (self.options.scrollbarVisible ? 'scroll' : 'hidden')
})
.scroll(function() {
var scrollLeft = $(this).scrollLeft();
if (scrollLeft != self.scrollLeft) {
self.scrollLeft = scrollLeft;
that.$head && that.$head.scrollLeft(scrollLeft);
}
})
.bindEvent({
edit: function(data) {
that.editCell(data.id, data.key);
},
cancel: function(data) {
Ox.print('cancel edit', data);
},
init: function(data) {
// fixme: why does this never reach?
//Ox.print('INIT????')
//that.triggerEvent('init', data);
},
select: function(data) {
Ox.print('SELECT????')
self.options.selected = data.ids;
}
})
.appendTo(that);
that.$body.$content.css({
width: getItemWidth() + 'px'
});
//Ox.print('s.vC', self.visibleColumns)
function addColumn(id) {
//Ox.print('addColumn', id);
var column, ids,
index = 0;
Ox.forEach(self.options.columns, function(v) {
if (v.visible) {
index++;
} else if (v.id == id) {
column = v;
return false;
}
});
column.visible = true;
self.visibleColumns.splice(index, 0, column);
self.columnWidths.splice(index, 0, column.width);
that.$head.$content.empty();
constructHead();
that.$body.options({
keys: $.map(self.visibleColumns, function(v, i) {
return v.id;
})
});
that.$body.reloadPages();
}
function changeColumns(event, data) {
var add,
ids = [];
Ox.forEach(data.selected, function(column) {
var index = getColumnIndexById(column.id);
if (!self.options.columns[index].visible) {
addColumn(column.id);
add = true;
return false;
}
ids.push(column.id);
});
if (!add) {
Ox.forEach(self.visibleColumns, function(column) {
if (ids.indexOf(column.id) == -1) {
removeColumn(column.id);
return false;
}
});
}
triggerColumnChangeEvent();
}
function clickColumn(id) {
Ox.print('clickColumn', id);
var i = getColumnIndexById(id),
isSelected = self.options.sort[0].key == self.options.columns[i].id;
self.options.sort = [{
key: self.options.columns[i].id,
operator: isSelected ?
(self.options.sort[0].operator == '+' ? '-' : '+') :
self.options.columns[i].operator,
map: self.options.columns[i].map
}]
updateColumn();
// fixme: strangely, sorting the list blocks updating the column,
// so we use a timeout for now
setTimeout(function() {
that.$body.options({sort: self.options.sort});
}, 10)
}
function constructHead() {
var offset = 0;
that.$titles = [];
self.columnOffsets = [];
self.visibleColumns.forEach(function(v, i) {
var $order, $resize, $left, $center, $right;
offset += self.columnWidths[i];
self.columnOffsets[i] = offset - self.columnWidths[i] / 2;
that.$titles[i] = new Ox.Element()
.addClass('OxTitle OxColumn' + Ox.toTitleCase(v.id))
.css({
width: (self.columnWidths[i] - 9) + 'px',
textAlign: v.align
})
.html(v.title)
.appendTo(that.$head.$content.$element);
// if sort operator is set, bind click event
if (v.operator) {
that.$titles[i].bindEvent({
anyclick: function(event, e) {
clickColumn(v.id);
}
});
}
// if columns are movable, bind drag events
if (self.options.columnsMovable) {
that.$titles[i].bindEvent({
dragstart: function(event, e) {
dragstartColumn(v.id, e);
},
drag: function(event, e) {
dragColumn(v.id, e);
},
dragend: function(event, e) {
dragendColumn(v.id, e);
}
})
}
$order = $('<div>')
.addClass('OxOrder')
.html(Ox.UI.symbols['triangle_' + (
v.operator == '+' ? 'up' : 'down'
)])
.click(function() {
$(this).prev().trigger('click')
})
.appendTo(that.$head.$content.$element);
$resize = new Ox.Element()
.addClass('OxResize')
.appendTo(that.$head.$content.$element);
// if columns are resizable, bind click and drag events
if (self.options.columnsResizable) {
$resize.addClass('OxResizable')
.bindEvent({
doubleclick: function(event, e) {
resetColumn(v.id, e);
},
dragstart: function(event, e) {
dragstartResize(v.id, e);
},
drag: function(event, e) {
dragResize(v.id, e);
},
dragend: function(event, e) {
dragendResize(v.id, e);
}
});
}
$left = $('<div>').addClass('OxLeft').appendTo($resize.$element);
$center = $('<div>').addClass('OxCenter').appendTo($resize.$element);
$right = $('<div>').addClass('OxRight').appendTo($resize.$element);
});
that.$head.$content.css({
width: (Ox.sum(self.columnWidths) + 2) + 'px'
});
if (getColumnPositionById(self.options.columns[self.selectedColumn].id) > -1) { // fixme: save in var
toggleSelected(self.options.columns[self.selectedColumn].id);
that.$titles[getColumnPositionById(self.options.columns[self.selectedColumn].id)].css({
width: (self.options.columns[self.selectedColumn].width - 25) + 'px'
});
}
}
function constructItem(data) {
var $item = $('<div>')
.addClass('OxTarget')
.css({
width: getItemWidth(true) + 'px'
});
self.visibleColumns.forEach(function(v, i) {
//Ox.print(data[v.id], '(--value--)')
var clickable = Ox.isBoolean(v.clickable) ? v.clickable : v.clickable(data),
editable = Ox.isBoolean(v.editable) ? v.editable : v.editable(data),
$cell = $('<div>')
.addClass(
'OxCell OxColumn' + Ox.toTitleCase(v.id) +
(clickable ? ' OxClickable' : '') +
(editable ? ' OxEditable' : '')
)
.css({
width: (self.columnWidths[i] - (self.options.columnsVisible ? 9 : 8)) + 'px',
borderRightWidth: (self.options.columnsVisible ? 1 : 0) + 'px',
textAlign: v.align
})
.html(v.id in data ? formatValue(v.id, data[v.id]) : '')
.appendTo($item);
});
//Math.random() < 0.01 && Ox.print('item', data, $item);
return $item;
}
function formatValue(key, value) {
var format = self.format[key];
if (value === null) {
value = '';
} else if (format) {
value = Ox.isObject(format) ?
Ox['format' + Ox.toTitleCase(format.type)]
.apply(this, $.merge([value], format.args)) :
format(value);
} else if (Ox.isArray(value)) {
value = value.join(', ');
}
return value;
}
function dragstartColumn(id, e) {
self.drag = {
startX: e.clientX,
startPos: getColumnPositionById(id)
}
$.extend(self.drag, {
stopPos: self.drag.startPos,
offsets: $.map(self.visibleColumns, function(v, i) {
return self.columnOffsets[i] - self.columnOffsets[self.drag.startPos]
})
});
$('.OxColumn' + Ox.toTitleCase(id)).css({
opacity: 0.25
});
that.$titles[self.drag.startPos].addClass('OxDrag').css({ // fixme: why does the class not work?
cursor: 'move'
});
}
function dragColumn(id, e) {
var d = e.clientX - self.drag.startX,
pos = self.drag.stopPos;
Ox.forEach(self.drag.offsets, function(v, i) {
if (d < 0 && d < v) {
self.drag.stopPos = i;
return false;
} else if (d > 0 && d > v) {
self.drag.stopPos = i;
}
});
if (self.drag.stopPos != pos) {
moveColumn(id, self.drag.stopPos);
}
}
function dragendColumn(id, e) {
var column = self.visibleColumns.splice(self.drag.stopPos, 1)[0],
width = self.columnWidths.splice(self.drag.stopPos, 1)[0];
self.visibleColumns.splice(self.drag.stopPos, 0, column);
self.columnWidths.splice(self.drag.stopPos, 0, width);
that.$head.$content.empty();
constructHead();
$('.OxColumn' + Ox.toTitleCase(id)).css({
opacity: 1
});
that.$titles[self.drag.stopPos].removeClass('OxDrag').css({
cursor: 'pointer'
});
that.$body.clearCache();
triggerColumnChangeEvent();
}
function dragstartResize(id, e) {
var pos = getColumnPositionById(id);
self.drag = {
startX: e.clientX,
startWidth: self.columnWidths[pos]
};
}
function dragResize(id, e) {
var width = Ox.limit(
self.drag.startWidth - self.drag.startX + e.clientX,
self.options.columnWidth[0],
self.options.columnWidth[1]
);
resizeColumn(id, width);
}
function dragendResize(id, e) {
var pos = getColumnPositionById(id);
that.triggerEvent('columnresize', {
id: id,
width: self.columnWidths[pos]
});
}
function find(data) {
// fixme: works only if items are an array
var query = data.keys,
sort = self.options.sort[0];
Ox.print('QUERY', query)
Ox.forEach(self.options.items, function(item, i) {
var value = (
sort.map ? sort.map(item[sort.key]) : item[sort.key]
).toString().toLowerCase();
if (Ox.startsWith(value, query)) {
that.$body.options({selected: [item[self.unique]]});
Ox.print('QUERY', query, 'VALUE', value)
return false;
}
});
}
function getCell(id, key) {
Ox.print('getCell', id, key)
var $item = getItem(id);
key = key || '';
return $($item.find('.OxCell.OxColumn' + Ox.toTitleCase(key))[0]);
}
function getColumnIndexById(id) {
return Ox.getPositionById(self.options.columns, id);
}
function getColumnPositionById(id) {
return Ox.getPositionById(self.visibleColumns, id);
}
function getItem(id) {
//Ox.print('getItem', id)
var $item = null;
$.each(that.find('.OxItem'), function(i, v) {
$v = $(v);
if ($v.data('id') == id) {
$item = $v;
return false;
}
});
return $item;
}
function getItemWidth(cached) {
// fixme: this gets called for every constructItem and is slooow
// the proper way to fix this would be to find out how and when
// that.$element.width() might change... which would probably
// mean binding to every SplitPanel and window resize...
// for now, use a cached value
if (!cached) {
self.cachedWidth = that.$element.width();
} else if (!self.cachedWidth || self.cachedWidthTime < +new Date() - 5000) {
self.cachedWidth = that.$element.width();
self.cachedWidthTime = +new Date();
}
return Math.max(
Ox.sum(self.columnWidths),
self.cachedWidth -
(self.options.scrollbarVisible ? Ox.UI.SCROLLBAR_SIZE : 0)
);
}
function moveColumn(id, pos) {
// fixme: column head should be one element, not three
//Ox.print('moveColumn', id, pos)
var startPos = getColumnPositionById(id),
stopPos = pos,
startClassName = '.OxColumn' + Ox.toTitleCase(id),
stopClassName = '.OxColumn' + Ox.toTitleCase(self.visibleColumns[stopPos].id),
insert = startPos < stopPos ? 'insertAfter' : 'insertBefore'
$column = $('.OxTitle' + startClassName),
$order = $column.next(),
$resize = $order.next();
//Ox.print(startClassName, insert, stopClassName)
$column.detach()[insert](insert == 'insertAfter' ? $('.OxTitle' + stopClassName).next().next() : $('.OxTitle' + stopClassName));
$order.detach().insertAfter($column);
$resize.detach().insertAfter($order);
$.each(that.$body.find('.OxItem'), function(i, v) {
var $v = $(v);
$v.children(startClassName).detach()[insert]($v.children(stopClassName));
});
var column = self.visibleColumns.splice(startPos, 1)[0],
width = self.columnWidths.splice(startPos, 1)[0];
self.visibleColumns.splice(stopPos, 0, column);
self.columnWidths.splice(stopPos, 0, width);
}
function removeColumn(id) {
//Ox.print('removeColumn', id);
var className = '.OxColumn' + Ox.toTitleCase(id),
index = getColumnIndexById(id),
itemWidth,
position = getColumnPositionById(id),
$column = $('.OxTitle' + className),
$order = $column.next(),
$resize = $order.next();
self.options.columns[index].visible = false;
self.visibleColumns.splice(position, 1);
self.columnWidths.splice(position, 1);
that.$head.$content.empty();
constructHead();
itemWidth = getItemWidth();
$.each(that.$body.find('.OxItem'), function(i, v) {
var $v = $(v);
$v.children(className).remove();
$v.css({
width: itemWidth + 'px'
});
});
that.$body.$content.css({
width: itemWidth + 'px'
});
that.$body.options({
keys: $.map(self.visibleColumns, function(v, i) {
return v.id;
})
});
//that.$body.clearCache();
}
function resetColumn(id) {
var width = self.defaultColumnWidths[getColumnIndexById(id)];
resizeColumn(id, width);
that.triggerEvent('columnresize', {
id: id,
width: width
});
}
function resizeColumn(id, width) {
var i = getColumnIndexById(id),
pos = getColumnPositionById(id);
self.options.columns[i].width = width;
self.columnWidths[pos] = width;
if (self.options.columnsVisible) {
that.$head.$content.css({
width: (Ox.sum(self.columnWidths) + 2) + 'px'
});
that.$titles[pos].css({
width: (width - 9 - (i == self.selectedColumn ? 16 : 0)) + 'px'
});
}
that.find('.OxCell.OxColumn' + Ox.toTitleCase(self.options.columns[i].id)).css({
width: (width - (self.options.columnsVisible ? 9 : 8)) + 'px'
});
setWidth();
}
function setWidth() {
var width = getItemWidth();
that.$body.$content.find('.OxItem').css({ // fixme: can we avoid this lookup?
width: width + 'px'
});
that.$body.$content.css({
width: width + 'px' // fixme: check if scrollbar visible, and listen to resize/toggle event
});
}
function toggleSelected(id) {
var pos = getColumnPositionById(id);
if (pos > -1) {
updateOrder(id);
pos > 0 && that.$titles[pos].prev().children().eq(2).toggleClass('OxSelected');
that.$titles[pos].toggleClass('OxSelected');
that.$titles[pos].next().toggleClass('OxSelected');
that.$titles[pos].next().next().children().eq(0).toggleClass('OxSelected');
that.$titles[pos].css({
width: (
that.$titles[pos].width() + (that.$titles[pos].hasClass('OxSelected') ? -16 : 16)
) + 'px'
});
}
}
function triggerColumnChangeEvent() {
that.triggerEvent('columnchange', {
ids: $.map(self.visibleColumns, function(v, i) {
return v.id;
})
});
}
function updateColumn() {
var columnId = self.options.columns[self.selectedColumn].id
isSelected = columnId == self.options.sort[0].key;
if (self.options.columnsVisible) {
if (isSelected) {
updateOrder(columnId);
} else {
toggleSelected(columnId);
self.selectedColumn = getColumnIndexById(self.options.sort[0].key);
toggleSelected(self.options.columns[self.selectedColumn].id);
}
}
}
function updateOrder(id) {
var pos = getColumnPositionById(id);
//Ox.print(id, pos)
that.$titles[pos].next().html(Ox.UI.symbols[
'triangle_' + (self.options.sort[0].operator == '+' ? 'up' : 'down')
]);
}
self.setOption = function(key, value) {
//Ox.print('---------------------------- TextList setOption', key, value)
if (key == 'items') {
that.$body.options(key, value);
} else if (key == 'paste') {
that.$body.options(key, value);
} else if (key == 'selected') {
that.$body.options(key, value);
} else if (key == 'sort') {
updateColumn();
that.$body.options(key, value);
}
};
that.closePreview = function() {
that.$body.closePreview();
return that;
};
that.addItem = function(item) {
/*
self.options.items.push(item);
that.$body.options({items: self.options.items});
//that.$body.options({selected: [item.id]});
*/
}
that.editCell = function(id, key) {
Ox.print('editCell', id, key)
var $item = getItem(id),
$cell = getCell(id, key),
$input,
html = $cell.html(),
index = getColumnIndexById(key),
column = self.options.columns[index],
width = column.width - self.options.columnsVisible;
$cell.empty()
.addClass('OxEdit')
.css({
width: width + 'px'
});
$input = new Ox.Input({
autovalidate: column.input ? column.input.autovalidate : null,
style: 'square',
value: html,
width: width
})
.bind({
mousedown: function(e) {
// keep mousedown from reaching list
e.stopPropagation();
}
})
.bindEvent({
blur: submit,
})
.appendTo($cell);
//.focusInput();
setTimeout($input.focusInput, 0); // fixme: strange
function submit() {
var value = $input.value();
//$input.loseFocus().remove();
// fixme: leaky, inputs remain in focus stack
$cell.removeClass('OxEdit')
.css({
width: (width - 8) + 'px'
})
.html(value)
that.triggerEvent('submit', {
id: id,
key: key,
value: value
});
}
}
that.gainFocus = function() {
that.$body.gainFocus();
return that;
};
that.loseFocus = function() {
that.$body.loseFocus();
return that;
}
that.paste = function(data) {
that.$body.paste();
return that;
};
that.reloadList = function(stayAtPosition) {
that.$body.reloadList(stayAtPosition);
return that;
};
that.resizeColumn = function(id, width) {
resizeColumn(id, width);
return that;
}
that.size = function() {
Ox.print('SIZE FUNCTION CALLED')
setWidth();
that.$body.size();
}
// fixme: deprecated
that.sortList = function(key, operator) {
Ox.print('$$$$ DEPRECATED $$$$')
var isSelected = key == self.options.sort[0].key;
self.options.sort = [{
key: key,
operator: operator,
map: self.options.columns[self.selectedColumn].sort
}];
if (self.options.columnsVisible) {
if (isSelected) {
updateOrder(self.options.columns[self.selectedColumn].id);
} else {
toggleSelected(self.options.columns[self.selectedColumn].id);
self.selectedColumn = getColumnIndexById(key);
toggleSelected(self.options.columns[self.selectedColumn].id);
}
}
// fixme: strangely, sorting the list blocks toggling the selection,
// so we use a timeout for now
setTimeout(function() {
that.$body.options({sort: self.options.sort});
/*
that.$body.sortList(
self.options.sort[0].key,
self.options.sort[0].operator,
self.options.sort[0].map
);
*/
}, 10);
return that;
};
that.value = function(id, key, value) {
// fixme: make this accept id, {k: v, ...}
Ox.print('value', id, key, value)
var $item = getItem(id),
$cell = getCell(id, key),
column = self.options.columns[getColumnIndexById(key)];
if (arguments.length == 1) {
return that.$body.value(id);
} else if (arguments.length == 2) {
return that.$body.value(id, key);
} else {
that.$body.value(id, key, value);
Ox.print('? ? ?', column, column.format)
$cell && $cell.html(formatValue(key, value));
if (key == self.options.sort[0].key) {
that.$body.sort();
}
/* fixme: something like this is needed:
if (column.unique) {
that.$body.setId($item.data('id'), value);
$item.data({id: value});
}
*/
return that;
}
}
return that;
};