oxjs/source/Ox.UI/js/List/Ox.List.js
2011-08-07 23:43:37 +00:00

1619 lines
54 KiB
JavaScript

// vim: et:ts=4:sw=4:sts=4:ft=javascript
/*@
Ox.List <f:Ox.Element> List Element
() -> <f> List Object
(options) -> <f> List Object
(options, self) -> <f> List Object
options <o> Options object
centered <b|false> if true, and orientation is 'horizontal',
then keep the selected item centered
construct <f|null> (data) returns the list item HTML
draggable <b|false> true if the items can be reordered
format <[]> ???
itemHeight <n|16> item height
items <a|f|null> <a> list of items,
<f> (data) returns {items, size, ...}
(data, callback) returns [items]
itemWidth <n|16> item width
keys <a|[]> keys of the list items
max <n|-1> max number of items that can be selected
min <n|0> min number of items that must be selected
orientation <s|vertical> 'horizontal' or 'vertical'
pageLength <n|100> number of items per page
selected <a|[]> ids of the selected elements
sort <a|[]> sort order
sortable <b|false>
type <s|text>
unique <s|''> name of the key that acts as unique id
self <o> shared private variable
add <!> item added
delete <!> item removed
copy <!> copy
paste <!> paste
movie <!> move item
load <!> list loaded
openpreview <!> preview of selected item opened
closepreview <!> preview closed
select <!> select item
@*/
Ox.List = function(options, self) {
self = self || {};
var that = Ox.Container({}, self)
.defaults({
centered: false,
construct: null,
draggable: false,
format: [],
itemHeight: 16,
items: null,
itemWidth: 16,
keys: [],
max: -1,
min: 0,
orientation: 'vertical',
pageLength: 100,
selected: [],
sort: [],
sortable: false,
type: 'text',
unique: ''
})
.options(options || {})
.scroll(scroll);
that.$content.mousedown(_mousedown);
//that.bindEvent('doubleclick', function() {alert('d')})
/*
that.$content.bindEvent({ // fixme: port to new Ox mouse events
mousedown: mousedown,
singleclick: singleclick,
doubleclick: doubleclick,
dragstart: dragstart,
drag: drag,
dragend: dragend
});
*/
// fixme: without this, horizontal lists don't get their full width
self.options.orientation == 'horizontal' && that.$content.css({height: '1px'});
$.extend(self, {
$items: [],
$pages: [],
clickTimeout: 0,
dragTimeout: 0,
format: {},
isAsync: Ox.isFunction(self.options.items),
itemMargin: self.options.type == 'text' ? 0 : 8, // 2 x 4 px margin ... fixme: the 2x should be computed later
keyboardEvents: {
key_control_c: copyItems,
key_control_n: addItem,
key_control_v: pasteItems,
key_control_x: cutItems,
key_delete: deleteItems,
key_end: scrollToFirst,
key_enter: open,
key_home: scrollToLast,
key_pagedown: scrollPageDown,
key_pageup: scrollPageUp,
key_section: preview, // fixme: firefox gets keyCode 0 when pressing space
key_space: preview
},
listMargin: self.options.type == 'text' ? 0 : 8, // 2 x 4 px padding
page: 0,
preview: false,
requests: [],
scrollTimeout: 0,
selected: []
});
if (!self.isAsync) {
self.selected = self.options.items.map(function(item, i) {
return Ox.extend(item, {_index: i})
}).filter(function(item) {
return self.options.selected.indexOf(item[self.options.unique]) > -1;
}).map(function(item) {
return item['_index'];
});
}
self.options.max == -1 && $.extend(self.keyboardEvents, {
key_alt_control_a: invertSelection,
key_control_a: selectAll
});
self.options.min == 0 && $.extend(self.keyboardEvents, {
key_control_shift_a: selectNone
});
self.keyboardEvents[
'key_' + (self.options.orientation == 'vertical' ? 'up' : 'left')
] = selectPrevious;
self.keyboardEvents[
'key_' + (self.options.orientation == 'vertical' ? 'down' : 'right')
] = selectNext;
if (self.options.max == -1) {
self.keyboardEvents[
'key_' + (self.options.orientation == 'vertical' ? 'shift_up' : 'shift_left')
] = addPreviousToSelection;
self.keyboardEvents[
'key_' + (self.options.orientation == 'vertical' ? 'shift_down' : 'shift_right')
] = addNextToSelection;
}
if (self.options.orientation == 'vertical') {
$.extend(self.keyboardEvents, {
key_left: function() {
triggerToggleEvent(false);
},
key_right: function() {
triggerToggleEvent(true);
}
});
} else if (self.options.orientation == 'both') {
$.extend(self.keyboardEvents, {
key_down: selectBelow,
key_up: selectAbove
});
if (self.options.max == -1) {
$.extend(self.keyboardEvents, {
key_shift_down: addBelowToSelection,
key_shift_up: addAboveToSelection
});
}
self.pageLengthByRowLength = [
0, 60, 60, 60, 60, 60, 60, 63, 64, 63, 60, 66, 60, 65, 70, 60, 64, 68, 72, 76, 60
];
}
if (self.options.draggable) {
that.bind({
dragstart: function(e) {
//alert('DRAGSTART')
Ox.print('DRAGSTART', e);
}
});
}
if (!self.isAsync) {
self.listLength = self.options.items.length;
loadItems();
} else {
updateQuery();
}
that.bindEvent(self.keyboardEvents);
//Ox.UI.$window.resize(that.size); // fixme: this is not the widget's job
function addAboveToSelection() {
var pos = getAbove();
if (pos > -1) {
addToSelection(pos);
scrollToPosition(pos);
}
}
function addAllToSelection(pos) {
var arr,
len = self.$items.length;
if (!isSelected(pos)) {
if (self.selected.length == 0) {
addToSelection(pos);
} else {
if (Ox.min(self.selected) < pos) {
var arr = [pos];
for (var i = pos - 1; i >= 0; i--) {
if (isSelected(i)) {
arr.forEach(function(v) {
addToSelection(v);
});
break;
}
arr.push(i);
}
}
if (Ox.max(self.selected) > pos) {
var arr = [pos];
for (var i = pos + 1; i < len; i++) {
if (isSelected(i)) {
arr.forEach(function(v) {
addToSelection(v);
});
break;
}
arr.push(i);
}
}
}
}
}
function addBelowToSelection() {
var pos = getBelow();
if (pos > -1) {
addToSelection(pos);
scrollToPosition(pos);
}
}
function addItem() {
that.triggerEvent('add', {});
}
function addNextToSelection() {
var pos = getNext();
if (pos > -1) {
addToSelection(pos);
scrollToPosition(pos);
}
}
function addPreviousToSelection() {
var pos = getPrevious();
if (pos > -1) {
addToSelection(pos);
scrollToPosition(pos);
}
}
function addToSelection(pos) {
if (!isSelected(pos)) {
self.selected.push(pos);
!Ox.isUndefined(self.$items[pos]) &&
self.$items[pos].addClass('OxSelected');
//Ox.print('addToSelection')
triggerSelectEvent();
} else {
// allow for 'cursor navigation' if orientation == 'both'
self.selected.splice(self.selected.indexOf(pos), 1);
self.selected.push(pos);
//Ox.print('self.selected', self.selected)
}
}
function clear() {
self.requests.forEach(function(v) {
//Ox.print('Ox.Request.cancel', v);
Ox.Request.cancel(v);
});
$.extend(self, {
//$items: [],
$pages: [],
page: 0,
requests: []
});
}
function constructEmptyPage(page) {
//Ox.print('cEP', page)
var i, $page = Ox.ListPage().css(getPageCSS(page));
for (i = 0; i < getPageLength(page); i++
) {
// fixme: why does chainging fail here?
Ox.ListItem({
construct: self.options.construct
}).appendTo($page);
}
//Ox.print('cEP done')
return $page;
}
function copyItems() {
self.options.selected.length && that.triggerEvent('copy', {
ids: self.options.selected
});
/*
ids.length && self.options.copy && Ox.Clipboard.copy(
self.options.copy(
$.map(ids, function(id) {
return that.value(id);
})
)
);
*/
}
function cutItems() {
copyItems();
deleteItems();
}
function deleteItems() {
self.options.selected.length && that.triggerEvent('delete', {
ids: self.options.selected
});
}
function deselect(pos) {
if (isSelected(pos)) {
self.selected.splice(self.selected.indexOf(pos), 1);
!Ox.isUndefined(self.$items[pos]) &&
self.$items[pos].removeClass('OxSelected');
triggerSelectEvent();
}
}
function dragstart(event, e) { // fixme: doesn't work yet
self.drag = {
pos: findItemPosition(e)
};
$.extend(self.drag, {
id: self.$items[self.drag.pos].options('data')[self.options.unique],
startPos: self.drag.pos,
startY: e.clientY,
stopPos: self.drag.pos
});
self.$items[pos].addClass('OxDrag') // fixme: why does the class not work?
.css({
cursor: 'move',
});
}
function drag(event, e) { // fixme: doesn't work yet
var clientY = e.clientY - that.offset()['top'],
offset = clientY % 16,
position = Ox.limit(parseInt(clientY / 16), 0, self.$items.length - 1);
if (position < self.drag.pos) {
self.drag.stopPos = position + (offset > 8 ? 1 : 0);
} else if (position > self.drag.pos) {
self.drag.stopPos = position - (offset <= 8 ? 1 : 0);
}
if (self.drag.stopPos != self.drag.pos) {
moveItem(self.drag.pos, self.drag.stopPos);
self.drag.pos = self.drag.stopPos;
}
}
function dragend(event, e) { // fixme: doesn't work yet
var $item = self.$items[self.drag.pos];
$item.removeClass('OxDrag')
.css({
cursor: 'default',
});
that.triggerEvent('move', {
//id: id,
ids: $.map(self.$items, function($item) {
return $item.options('data')[self.options.unique];
})
//position: pos
});
}
function dragItem(pos, e) {
var $item = self.$items[pos],
id = self.$items[pos].options('data')[self.options.unique],
startPos = pos,
startY = e.clientY,
stopPos = startPos,
offsets = $.map(self.$items, function($item, pos) {
return (pos - startPos) * 16 - e.offsetY + 8;
});
//Ox.print('dragItem', e);
//Ox.print(e.offsetY, offsets)
$item.addClass('OxDrag');
Ox.UI.$window.mousemove(function(e) {
var clientY = e.clientY - that.offset()['top'],
offset = clientY % 16,
position = Ox.limit(parseInt(clientY / 16), 0, self.$items.length - 1);
if (position < pos) {
stopPos = position + (offset > 8 ? 1 : 0);
} else if (position > pos) {
stopPos = position - (offset <= 8 ? 1 : 0);
}
if (stopPos != pos) {
moveItem(pos, stopPos);
pos = stopPos;
}
});
Ox.UI.$window.one('mouseup', function() {
dropItem(id, pos);
Ox.UI.$window.unbind('mousemove');
});
}
function dropItem(id, pos) {
var $item = self.$items[pos];
$item.removeClass('OxDrag')
.css({
cursor: 'default',
});
that.triggerEvent('move', {
//id: id,
ids: $.map(self.$items, function($item) {
return $item.options('data')[self.options.unique];
})
//position: pos
});
}
function emptyFirstPage() {
//Ox.print('emptyFirstPage', self.$pages);
self.$pages[0] && self.$pages[0].find('.OxEmpty').remove();
}
function fillFirstPage() {
Ox.print('fillFirstPage')
if (self.$pages[0]) {
var height = getHeight(),
lastItemHeight = height % self.options.itemHeight || self.options.itemHeight,
visibleItems = Math.ceil(height / self.options.itemHeight);
if (self.listLength < visibleItems) {
Ox.range(self.listLength, visibleItems).forEach(function(v) {
var $item = Ox.ListItem({
construct: self.options.construct,
});
$item.addClass('OxEmpty').removeClass('OxTarget');
if (v == visibleItems - 1) {
$item.$element.css({
height: lastItemHeight + 'px',
overflowY: 'hidden'
});
}
$item.appendTo(self.$pages[0]);
});
}
}
}
function findCell(e) {
var $element = $(e.target);
while (!$element.hasClass('OxCell') && !$element.hasClass('OxPage') && !$element.is('body')) {
$element = $element.parent();
}
return $element.hasClass('OxCell') ? $element : null;
}
function findItemPosition(e) {
//Ox.print('---- findItem', e.target)
var $element = $(e.target),
position = -1;
while (!$element.hasClass('OxTarget') && !$element.hasClass('OxPage') && !$element.is('body')) {
$element = $element.parent();
}
if ($element.hasClass('OxTarget')) {
while (!$element.hasClass('OxItem') && !$element.hasClass('OxPage') && !$element.is('body')) {
$element = $element.parent();
}
if ($element.hasClass('OxItem')) {
position = $element.data('position');
}
}
return position;
}
function getAbove() {
var pos = -1;
if (self.selected.length) {
pos = self.selected[self.selected.length - 1] - self.rowLength;
if (pos < 0) {
pos = -1;
}
}
return pos;
}
function getBelow() {
var pos = -1;
if (self.selected.length) {
pos = self.selected[self.selected.length - 1] + self.rowLength;
if (pos >= self.$items.length) {
pos = -1;
}
}
return pos;
}
function getHeight() {
return that.height() - (that.$content.width() > that.width() ? Ox.UI.SCROLLBAR_SIZE : 0);
}
function getListSize() {
return Math.ceil(self.listLength *
(self.options[self.options.orientation == 'horizontal' ?
'itemWidth' : 'itemHeight'] + self.itemMargin) / self.rowLength);
}
function getNext() {
var pos = -1;
if (self.selected.length) {
pos = (self.options.orientation == 'both' ?
self.selected[self.selected.length - 1] :
Ox.max(self.selected)) + 1;
if (pos == self.$items.length) {
pos = -1;
}
}
return pos;
}
function getPage() {
return Math.max(
Math.floor(self.options.orientation == 'horizontal' ?
(that.scrollLeft() - self.listMargin / 2) / self.pageWidth :
(that.scrollTop() - self.listMargin / 2) / self.pageHeight
), 0);
}
function getPageByPosition(pos) {
return parseInt(pos / self.options.pageLength);
}
function getPageByScrollPosition(pos) {
return getPageByPosition(pos / (
self.options.orientation == 'vertical'
? self.options.itemHeight
: self.options.itemWidth
));
}
function getPageCSS(page) {
return self.options.orientation == 'horizontal' ? {
left: (page * self.pageWidth + self.listMargin / 2) + 'px',
top: (self.listMargin / 2) + 'px',
width: (page < self.pages - 1 ? self.pageWidth :
getPageLength(page) * (self.options.itemWidth + self.itemMargin)) + 'px'
} : {
top: (page * self.pageHeight + self.listMargin / 2) + 'px',
width: self.pageWidth + 'px'
};
}
function getPageHeight() {
return Math.ceil(self.pageLength * (self.options.itemHeight + self.itemMargin) / self.rowLength);
}
function getPositionById(id) {
// fixme: is this really needed?
var pos = -1;
Ox.forEach(self.$items, function($item, i) {
if ($item.options('data')[self.options.unique] == id) {
pos = i;
return false;
}
});
return pos;
}
function getPositions(callback) {
Ox.print('getPositions', self.options.selected);
// fixme: optimize: send non-selected ids if more than half of the items are selected
if (self.options.selected.length /*&& ids.length < self.listLength*/) {
/*Ox.print('-------- request', {
positions: ids,
sort: self.options.sort
});*/
self.requests.push(self.options.items({
positions: self.options.selected,
sort: self.options.sort
}, function(result) {
getPositionsCallback(result, callback);
}));
} else {
getPositionsCallback(null, callback);
}
}
function getPositionsCallback(result, callback) {
Ox.print('getPositionsCallback', result);
var pos = 0;
if (result) {
$.extend(self, {
positions: {},
selected: []
});
Ox.forEach(result.data.positions, function(pos, id) {
//Ox.print('id', id, 'pos', pos)
self.selected.push(pos);
});
pos = Ox.min(self.selected);
self.page = getPageByPosition(pos);
} else if (self.stayAtPosition) {
self.page = getPageByScrollPosition(self.stayAtPosition);
}
that.$content.empty();
loadPages(self.page, function() {
scrollToPosition(pos, true);
callback && callback();
});
}
function getPrevious() {
var pos = -1;
if (self.selected.length) {
pos = (self.options.orientation == 'both' ?
self.selected[self.selected.length - 1] :
Ox.min(self.selected)) - 1;
}
return pos;
}
function getRow(pos) {
return Math.floor(pos / self.rowLength);
}
function getRowLength() {
return self.options.orientation == 'both' ?
Math.floor((getWidth() - self.listMargin) /
(self.options.itemWidth + self.itemMargin)) : 1;
}
function getScrollPosition() {
// if orientation is both, this returns the
// element position at the current scroll position
return parseInt(
that.scrollTop() / (self.options.itemHeight + self.itemMargin)
) * self.rowLength;
}
function getSelectedIds() {
//Ox.print('gSI', self.selected, self.$items)
if (self.$items.length == 0) {
return self.options.selected;
} else {
return $.map(self.selected, function(pos) {
//Ox.print('....', pos, self.options.unique, self.$items[pos].options('data')[self.options.unique])
Ox.print('gsI', pos)
return self.$items[pos].options('data')[self.options.unique];
});
}
}
function getWidth() {
return that.width() - (that.$content.height() > that.height() ? Ox.UI.SCROLLBAR_SIZE : 0);
}
function invertSelection() {
Ox.range(self.listLength).forEach(function(v) {
toggleSelection(v);
});
}
function isSelected(pos) {
return self.selected.indexOf(pos) > -1;
}
function loadItems() {
//Ox.print('start loadItems')
that.$content.empty();
self.options.items.forEach(function(item, pos) {
// fixme: duplicated
self.$items[pos] = Ox.ListItem({
construct: self.options.construct,
data: item,
draggable: self.options.draggable,
position: pos,
unique: self.options.unique
});
isSelected(pos) && self.$items[pos].addClass('OxSelected');
self.$items[pos].appendTo(that.$content);
});
self.selected.length && scrollToPosition(self.selected[0]);
//Ox.print('stop loadItems')
}
function getPageLength(page) {
var mod = self.listLength % self.pageLength;
return page < self.pages - 1 || mod == 0 ? self.pageLength : mod;
}
function loadPage(page, callback) {
if (page < 0 || page >= self.pages) {
!Ox.isUndefined(callback) && callback();
return;
}
//Ox.print('loadPage', page);
var keys = $.merge(self.options.keys.indexOf(self.options.unique) == -1 ? [self.options.unique] : [], self.options.keys),
offset = page * self.pageLength,
range = [offset, offset + getPageLength(page)];
if (Ox.isUndefined(self.$pages[page])) { // fixme: unload will have made this undefined already
self.$pages[page] = constructEmptyPage(page);
self.options.type == 'text' && page == 0 && fillFirstPage();
self.$pages[page].appendTo(that.$content);
self.requests.push(self.options.items({
keys: keys,
range: range,
sort: self.options.sort
}, function(result) {
var $emptyPage = Ox.clone(self.$pages[page]);
self.$pages[page] = Ox.ListPage().css(getPageCSS(page));
result.data.items.forEach(function(v, i) {
var pos = offset + i;
self.$items[pos] = Ox.ListItem({
construct: self.options.construct,
data: v,
draggable: self.options.draggable,
//format: self.options.format,
position: pos,
unique: self.options.unique
});
isSelected(pos) && self.$items[pos].addClass('OxSelected');
self.$items[pos].appendTo(self.$pages[page]);
});
self.options.type == 'text' && page == 0 && fillFirstPage();
// fixme: why does emptyPage sometimes have no methods?
Ox.print('emptyPage', $emptyPage)
$emptyPage.removeElement && $emptyPage.removeElement();
self.$pages[page].appendTo(that.$content);
!Ox.isUndefined(callback) && callback(); // fixme: callback necessary? why not bind to event?
}));
} else {
//Ox.print('loading a page from cache, this should probably not happen -----------')
self.$pages[page].appendTo(that.$content);
}
}
function loadPages(page, callback) {
var counter = 0,
fn = function() {
if (++counter == 3) {
!Ox.isUndefined(callback) && callback();
that.triggerEvent('load');
}
};
// fixme: find out which option is better
/*
loadPage(page, function() {
loadPage(page - 1, fn);
loadPage(page + 1, fn);
});
*/
loadPage(page, fn);
loadPage(page - 1, fn);
loadPage(page + 1, fn);
}
function mousedown(event, e) { // fixme: doesn't work yet
var pos = findItemPosition(e);
self.hadFocus = that.hasFocus();
that.gainFocus();
if (pos > -1) {
if (e.metaKey) {
if (!isSelected(pos) && (self.options.max == -1 || self.options.max > self.selected.length)) {
// meta-click on unselected item
addToSelection(pos);
} else if (isSelected(pos) && self.options.min < self.selected.length) {
// meta-click on selected item
deselect(pos);
}
} else if (e.shiftKey) {
if (self.options.max == -1) {
// shift-click on item
addAllToSelection(pos);
}
} else if (!isSelected(pos)) {
// click on unselected item
select(pos);
}
} else if (self.options.min == 0) {
// click on empty area
selectNone();
}
}
function singleclick(event, e) { // fixme: doesn't work yet
// these can't trigger on mousedown,
// since it could be a doubleclick
var pos = findItemPosition(e),
clickable, editable;
alert('singleclick')
if (pos > -1) {
if (!e.metaKey && !e.shiftKey && isSelected(pos)) {
alert('??')
if (self.selected.length > 1) {
// click on one of multiple selected items
alert('!!')
select(pos);
} else if (self.options.type == 'text' && self.hadFocus) {
$cell = findCell(e);
if ($cell) {
clickable = $cell.hasClass('OxClickable');
editable = $cell.hasClass('OxEditable') && !$cell.hasClass('OxEdit');
if (clickable || editable) {
// click on a clickable or editable cell
triggerClickEvent(clickable ? 'click' : 'edit', self.$items[pos], $cell);
}
}
}
}
}
}
function doubleclick(event, e) { // fixme: doesn't work yet
alert('doubleclick')
open();
}
function _mousedown(e) {
var pos = findItemPosition(e),
clickable, editable,
clickTimeout = false,
selectTimeout = false,
$element,
hadFocus = that.hasFocus();
//Ox.print('mousedown', pos)
that.gainFocus();
if (pos > -1) {
if (!self.clickTimeout) {
// click
if (e.metaKey) {
if (!isSelected(pos) && (self.options.max == -1 || self.options.max > self.selected.length)) {
addToSelection(pos);
} else if (isSelected(pos) && self.options.min < self.selected.length) {
deselect(pos);
}
} else if (e.shiftKey) {
if (self.options.max == -1) {
addAllToSelection(pos);
}
} else if (!isSelected(pos)) {
Ox.print('select', pos)
select(pos);
} else if (self.selected.length > 1) {
// this could be the first click
// of a double click on multiple items
selectTimeout = true;
} else if (self.options.type == 'text' && hadFocus) {
var $cell = findCell(e),
$element = $cell || self.$items[pos];
clickable = $element.hasClass('OxClickable');
editable = $element.hasClass('OxEditable') && !$element.hasClass('OxEdit');
if (clickable || editable) {
if (self.options.sortable && self.listLength > 1) {
clickTimeout = true;
} else {
!$cell && that.editItem(pos);
triggerClickEvent(clickable ? 'click' : 'edit', self.$items[pos], $cell);
}
}
}
self.clickTimeout = setTimeout(function() {
self.clickTimeout = 0;
if (selectTimeout) {
select(pos);
}
}, 250);
if (self.options.sortable && self.listLength > 1) {
self.dragTimeout = setTimeout(function() {
if (self.dragTimeout) {
dragItem(pos, e);
self.dragTimeout = 0;
}
}, 250);
Ox.UI.$window.one('mouseup', function(e) {
if (self.dragTimeout) {
clearTimeout(self.dragTimeout);
self.dragTimeout = 0;
if (clickTimeout) {
triggerClickEvent(clickable ? 'click' : 'edit', self.$items[pos], $cell);
}
}
});
}
} else {
// dblclick
clearTimeout(self.clickTimeout);
self.clickTimeout = 0;
open();
}
} else if (!$(e.target).hasClass('OxToggle') && self.options.min == 0) {
selectNone();
}
}
function moveItem(startPos, stopPos) {
var $item = self.$items[startPos],
insert = startPos < stopPos ? 'insertAfter' : 'insertBefore';
$item.detach()[insert](self.$items[stopPos].$element); // fixme: why do we need .$element here?
//Ox.print('moveItem', startPos, stopPos, insert, self.ids);
var $item = self.$items.splice(startPos, 1)[0];
self.$items.splice(stopPos, 0, $item);
self.$items.forEach(function($item, pos) {
$item.data({position: pos});
});
self.selected = [stopPos];
//Ox.print('ids', self.ids, $.map(self.$items, function(v, i) { return v.data('id'); }));
}
function open() {
self.options.selected.length && that.triggerEvent('open', {
ids: self.options.selected
});
}
function pasteItems() {
that.triggerEvent('paste', Ox.Clipboard.paste());
}
function preview() {
if (self.options.selected.length) {
self.preview = !self.preview;
if (self.preview) {
that.triggerEvent('openpreview', {
ids: self.options.selected
});
} else {
that.triggerEvent('closepreview');
}
}
}
function scroll() {
if (self.isAsync) {
var page = self.page;
self.scrollTimeout && clearTimeout(self.scrollTimeout);
self.scrollTimeout = setTimeout(function() {
self.scrollTimeout = 0;
self.page = getPage();
if (self.page != page) {
//Ox.print('page', page, '-->', self.page);
}
if (self.page == page - 1) {
unloadPage(self.page + 2);
loadPage(self.page - 1);
} else if (self.page == page + 1) {
unloadPage(self.page - 2);
loadPage(self.page + 1);
} else if (self.page == page - 2) {
unloadPage(self.page + 3);
unloadPage(self.page + 2);
loadPage(self.page);
loadPage(self.page - 1);
} else if (self.page == page + 2) {
unloadPage(self.page - 3);
unloadPage(self.page - 2);
loadPage(self.page);
loadPage(self.page + 1);
} else if (self.page != page) {
unloadPages(page);
loadPages(self.page);
}
}, 250);
}
//that.gainFocus();
}
function scrollPageDown() {
that.scrollBy(getHeight());
}
function scrollPageUp() {
that.scrollBy(-getHeight());
}
function scrollTo(value) {
that.animate(self.options.orientation == 'horizontal' ? {
scrollLeft: (self.listSize * value) + 'px'
} : {
scrollTop: (self.listSize * value) + 'px'
}, 0);
}
function scrollToFirst() {
that[self.options.orientation == 'horizontal' ? 'scrollLeft' : 'scrollTop'](0);
}
function scrollToLast() {
that[self.options.orientation == 'horizontal' ? 'scrollLeft' : 'scrollTop'](self.listSize);
}
function scrollToPosition(pos, leftOrTopAlign) {
var itemHeight = self.options.itemHeight + self.itemMargin,
itemWidth = self.options.itemWidth + self.itemMargin,
positions = [],
scroll,
size;
if (self.options.orientation == 'horizontal') {
if (self.options.centered) {
that.animate({
scrollLeft: (self.listMargin / 2 + (pos + 0.5) * itemWidth - that.width() / 2) + 'px'
}, 0);
} else {
positions[0] = pos * itemWidth + self.listMargin / 2;
positions[1] = positions[0] + itemWidth + self.itemMargin / 2;
scroll = that.scrollLeft();
size = getWidth();
if (positions[0] < scroll || leftOrTopAlign) {
that.animate({
scrollLeft: positions[0] + 'px'
}, 0);
} else if (positions[1] > scroll + size) {
that.animate({
scrollLeft: (positions[1] - size) + 'px'
}, 0);
}
}
} else {
positions[0] = (self.options.orientation == 'vertical' ? pos : getRow(pos)) * itemHeight;
positions[1] = positions[0] + itemHeight + (self.options.orientation == 'vertical' ? 0 : self.itemMargin);
scroll = that.scrollTop();
size = getHeight();
if (positions[0] < scroll || leftOrTopAlign) {
that.animate({
scrollTop: positions[0] + 'px'
}, 0);
} else if (positions[1] > scroll + size) {
that.animate({
scrollTop: (positions[1] - size) + 'px'
}, 0);
}
}
}
function select(pos) {
if (!isSelected(pos) || self.selected.length > 1) {
selectNone();
addToSelection(pos);
self.options.centered && scrollToPosition(pos);
}
}
function selectAbove() {
var pos = getAbove();
if (pos > -1) {
select(pos);
scrollToPosition(pos);
}
}
function selectAll() {
Ox.range(self.listLength).forEach(function(pos) {
addToSelection(pos);
});
}
function selectBelow() {
var pos = getBelow();
if (pos > -1) {
select(pos);
scrollToPosition(pos);
}
}
function selectNext() {
var pos = getNext();
if (pos > -1) {
select(pos);
scrollToPosition(pos);
}
}
function selectNone() {
self.$items.forEach(function(v, i) {
deselect(i);
});
}
function selectPrevious() {
var pos = getPrevious();
if (pos > -1) {
select(pos);
scrollToPosition(pos);
}
}
function selectQuery(str) {
Ox.forEach(self.$items, function(v, i) {
if (Ox.toLatin(v.title).toUpperCase().indexOf(str) == 0) {
select(i);
scrollToPosition(i);
return false;
}
});
}
function setSelected(ids, callback) {
// fixme: no case where callback is set
// fixme: can't use selectNone here,
// since it'd trigger a select event
Ox.print('setSelected', ids)
var counter = 0;
self.$items.forEach(function($item, pos) {
if (isSelected(pos)) {
self.selected.splice(self.selected.indexOf(pos), 1);
!Ox.isUndefined(self.$items[pos]) &&
self.$items[pos].removeClass('OxSelected');
}
});
ids.forEach(function(id, i) {
var pos = getPositionById(id);
if (pos > -1) {
select(pos, i);
} else {
// async and id not in current view
self.options.items({
positions: [id],
sort: self.options.sort
}, function(result) {
pos = result.data.positions[id];
select(pos, i);
});
}
});
function select(pos, i) {
Ox.print('pushing', pos, 'onto self.selected')
self.selected.push(pos);
!Ox.isUndefined(self.$items[pos]) &&
self.$items[pos].addClass('OxSelected');
i == 0 && scrollToPosition(pos);
++counter == ids.length && callback && callback();
}
}
function toggleSelection(pos) {
if (!isSelected(pos)) {
addToSelection(pos);
} else {
deselect(pos);
}
}
function triggerClickEvent(event, $item, $cell) {
// event can be 'click' or 'edit'
that.triggerEvent(event, $.extend({
id: $item.data('id')
}, $cell ? {
key: $cell.attr('class').split('OxColumn')[1].split(' ')[0].toLowerCase()
} : {}));
}
function triggerSelectEvent() {
var ids = self.options.selected = getSelectedIds();
setTimeout(function() {
var ids_ = self.options.selected = getSelectedIds();
Ox.print('ids', ids, 'ids after 100 msec', ids_, Ox.isEqual(ids, ids_))
if (Ox.isEqual(ids, ids_)) {
that.triggerEvent('select', {
ids: ids
});
self.preview && that.triggerEvent('openpreview', {
ids: ids
});
} else {
Ox.print('select event not triggered after timeout');
}
}, 100);
}
function triggerToggleEvent(expanded) {
that.triggerEvent('toggle', {
expanded: expanded,
ids: self.options.selected
});
}
function unloadPage(page) {
if (page < 0 || page >= self.pages) {
return;
}
//Ox.print('unloadPage', page)
//Ox.print('self.$pages', self.$pages)
//Ox.print('page not undefined', !Ox.isUndefined(self.$pages[page]))
if (!Ox.isUndefined(self.$pages[page])) {
self.$pages[page].removeElement();
delete self.$pages[page];
}
}
function unloadPages(page) {
unloadPage(page);
unloadPage(page - 1);
unloadPage(page + 1)
}
function updatePages(pos, scroll) {
// only used if orientation is both
clear();
self.pageLength = self.pageLengthByRowLength[self.rowLength]
$.extend(self, {
listSize: getListSize(),
pages: Math.ceil(self.listLength / self.pageLength),
pageWidth: (self.options.itemWidth + self.itemMargin) * self.rowLength,
pageHeight: getPageHeight()
});
that.$content.css({
height: self.listSize + 'px'
});
self.page = getPageByPosition(pos);
//that.scrollTop(0);
that.$content.empty();
loadPages(self.page, function() {
scrollTo(scroll);
});
}
function updatePositions() {
self.$items.forEach(function(item, pos) {
item.data('position', pos);
});
}
function updateQuery(callback) { // fixme: shouldn't this be setQuery?
Ox.print('updateQuery', self.options)
clear(); // fixme: bad function name ... clear what?
self.requests.push(self.options.items({}, function(result) {
var keys = {};
Ox.print('INIT!!!', result.data)
that.triggerEvent('init', result.data);
self.rowLength = getRowLength();
self.pageLength = self.options.orientation == 'both' ?
self.pageLengthByRowLength[self.rowLength] :
self.options.pageLength;
$.extend(self, {
listLength: result.data.items,
pages: Math.max(Math.ceil(result.data.items / self.pageLength), 1),
pageWidth: self.options.orientation == 'vertical' ? 0 :
(self.options.itemWidth + self.itemMargin) *
(self.options.orientation == 'horizontal' ?
self.pageLength : self.rowLength),
pageHeight: self.options.orientation == 'horizontal' ? 0 :
Math.ceil(self.pageLength * (self.options.itemHeight +
self.itemMargin) / self.rowLength)
});
self.listSize = getListSize();
that.$content.css(
self.options.orientation == 'horizontal' ? 'width' : 'height',
self.listSize + 'px'
);
getPositions(callback);
}));
}
function updateSelected() {
Ox.print('updateSelected')
var oldSelectedIds = getSelectedIds(),
newSelectedIds = [];
Ox.forEach(self.options.items, function(item) {
if (oldSelectedIds.indexOf(item.id) > -1) {
newSelectedIds.push(item.id);
}
return newSelectedIds.length < oldSelectedIds.length;
});
setSelected(newSelectedIds);
}
function updateSort() {
var key = self.options.sort[0].key,
map = self.options.sort[0].map,
operator = self.options.sort[0].operator,
selectedIds,
sort = {};
if (self.listLength > 1) {
if (!self.isAsync) {
selectedIds = getSelectedIds();
self.options.items.forEach(function(item) {
sort[item.id] = map ? map(item[key]) : item[key];
});
Ox.print('start sort')
self.options.items.sort(function(a, b) {
var aValue = sort[a.id],
bValue = sort[b.id],
ret = 0
if (aValue < bValue) {
ret = operator == '+' ? -1 : 1
} else if (aValue > bValue) {
ret = operator == '+' ? 1 : -1;
}
return ret;
});
Ox.print('end sort')
if (selectedIds.length) {
self.selected = [];
self.options.items.forEach(function(item, i) {
if (selectedIds.indexOf(item.id) > -1) {
self.selected.push(i);
}
});
}
loadItems();
} else {
clear(); // fixme: bad function name
getPositions();
}
}
}
self.setOption = function(key, value) {
//Ox.print('list setOption', key, value);
var selectedIds;
if (key == 'items') {
// fixme: this could be used to change the list
// from sync to async or vice versa, which wouldn't work
if (Ox.isArray(value)) {
updateSelected();
updateSort();
loadItems();
} else {
updateQuery();
}
} else if (key == 'selected') {
Ox.print('setOption selected', value)
setSelected(value);
// fixme: next line added to make text list find-as-you-type work,
// may break other things
!self.isAsync && triggerSelectEvent(value);
} else if (key == 'sort') {
Ox.print('---sort---')
updateSort();
}
};
/*@
addItems <f> add item to list
(pos, items) -> <u> add items to list at position
pos <n> position to add items
items <a> array of items to add
@*/
that.addItems = function(pos, items) {
var $items = [],
length = items.length
//first = self.$items.length == 0;
self.selected.forEach(function(v, i) {
if (v >= pos) {
self.selected[i] += length;
}
});
items.forEach(function(item, i) {
var $item;
$items.push($item = Ox.ListItem({
construct: self.options.construct,
data: item,
draggable: self.options.draggable,
position: pos + i,
unique: self.options.unique
}));
if (i == 0) {
if (pos == 0) {
$item.insertBefore(self.$items[0]);
} else {
$item.insertAfter(self.$items[pos - 1]);
}
} else {
$item.insertAfter($items[i - 1]);
}
});
self.options.items.splice.apply(self.options.items, $.merge([pos, 0], items));
self.$items.splice.apply(self.$items, $.merge([pos, 0], $items));
//if(first)
loadItems();
updatePositions();
}
/*@
editItem <f> turn item into edit form
(pos) -> <u> edit item at position
pos <n> position of item to edit
@*/
that.editItem = function(pos) {
var $input,
item = self.options.items[pos],
$item = self.$items[pos],
width = $item.width(), // fixme: don't lookup in DOM
height = $item.height();
$item
.height(height + 8 + 16)
.empty()
.addClass('OxEdit');
$input = Ox.ItemInput({
type: 'textarea',
value: item.value,
height: height,
width: width
}).bindEvent({
cancel: cancel,
remove: remove,
save: submit
}).appendTo($item.$element);
/*
setTimeout(function() {
$input.gainFocus();
$input.focus();
});
*/
function cancel() {
$item.options('data', item);
that.triggerEvent('cancel', item);
loadItems();
}
function remove() {
that.triggerEvent('remove', item.id);
}
function submit(event, data) {
item.value = data.value;
//$input.loseFocus().remove();
// fixme: leaky, inputs remain in focus stack
$item.options('data', item);
that.triggerEvent('submit', item);
loadItems();
}
}
/*@
clearCache <f> empty list cache
() -> <o> the list
@*/
that.clearCache = function() { // fixme: was used by TextList resizeColumn, now probably no longer necessary
self.$pages = [];
return that;
};
/*@
closePreview <f> close preview
() -> <o> the list
@*/
that.closePreview = function() {
self.preview = false;
return that;
};
/*@
paste <f> paste data
(data) -> <o> the list
data <o> paste object
@*/
that.paste = function(data) {
pasteItems(data);
return that;
};
/*@
reloadList <f> reload list contents
() -> <o> the list
@*/
that.reloadList = function(stayAtPosition) {
var scrollTop = that.scrollTop();
if (!self.isAsync) {
loadItems();
scrollList();
} else {
updateQuery(scrollList);
}
function scrollList() {
stayAtPosition && that.scrollTop(scrollTop);
}
return that;
};
/*@
reloadPages <f> reload list pages
() -> <o> the list
@*/
that.reloadPages = function() {
//Ox.print('---------------- list reload, page', self.page)
var page = self.page;
clear();
self.page = page
that.$content.empty();
loadPages(self.page);
return that;
};
/*@
removeItems <f> remove items from list
(ids) -> <u> remove items
(pos, length) -> <u> remove items
ids <a> array of item ids
pos <n> delete items starting at this position
length <n> number of items to remove
@*/
that.removeItems = function(pos, length) {
if(!length) { //pos is list of ids
pos.forEach(function(id) {
var p = getPositionById(id);
that.removeItems(p, 1);
});
} else { //remove items from pos to pos+length
Ox.range(pos, pos + length).forEach(function(i) {
self.selected.indexOf(i) > -1 && deselect(i);
self.$items[i].removeElement();
});
self.options.items.splice(pos, length);
self.$items.splice(pos, length);
self.selected.forEach(function(v, i) {
if (v >= pos + length) {
self.selected[i] -= length;
}
});
updatePositions();
}
}
/*@
scrollToSelection <f> scroll list to current selection
() -> <f> returns List Element
@*/
that.scrollToSelection = function() {
self.selected.length && scrollToPosition(self.selected[0]);
return that;
};
/*@
size <f> fixme: not a good function name
() -> <f> returns List Element
@*/
that.size = function() { // fixme: not a good function name
if (self.options.orientation == 'both') {
var rowLength = getRowLength(),
pageLength = self.pageLengthByRowLength[rowLength],
pos = getScrollPosition(),
scroll = that.scrollTop() / self.listSize;
if (pageLength != self.pageLength) {
self.pageLength = pageLength;
self.rowLength = rowLength;
updatePages(pos, scroll);
} else if (rowLength != self.rowLength) {
self.rowLength = rowLength;
self.pageWidth = (self.options.itemWidth + self.itemMargin) * self.rowLength; // fixme: make function
self.listSize = getListSize();
self.pageHeight = getPageHeight();
self.$pages.forEach(function($page, i) {
!Ox.isUndefined($page) && $page.css({
width: self.pageWidth + 'px',
top: (i * self.pageHeight + self.listMargin / 2) + 'px'
});
});
that.$content.css({
height: self.listSize + 'px'
});
//Ox.print('scrolling to', scroll)
scrollTo(scroll);
}
} else if (self.options.type == 'text') {
//Ox.print('that.size, type==text')
emptyFirstPage();
fillFirstPage();
}
return that;
}
// needed when a value has changed
// but, fixme: better function name
that.sort = function() {
updateSort();
}
/*@
sortList <f> sort list
(key, operator) -> <f> returns List Element
key <s> key to sort list by
operator <s> +/- sort ascending or descending
map <f> function that maps values to sort values
@*/
// fixme: this (and others) should be deprecated,
// one should set options instead
that.sortList = function(key, operator, map) {
Ox.print('sortList', key, operator, map)
if (key != self.options.sort[0].key || operator != self.options.sort[0].operator) {
self.options.sort[0] = {key: key, operator: operator, map: map};
updateSort();
that.triggerEvent('sort', self.options.sort[0]);
}
return that;
}
/*@
value <f> get/set list value
(id, key, value) -> <f> sets value, returns List Element
(id, key) -> <a> returns value
(id) -> <o> returns all values of id
id <s> id of item
key <s> key if item property
value <s> value, can be whatever that property is
@*/
that.value = function(id, key, value) {
var pos = getPositionById(id),
$item = self.$items[pos],
data = $item.options('data'),
oldValue;
if (arguments.length == 1) {
return data;
} else if (arguments.length == 2) {
return data[key];
} else {
oldValue = data[key];
data[key] = value;
$item.options({data: data});
return that;
}
};
return that;
};