// vim: et:ts=4:sw=4:sts=4:ft=js /*@ Ox.List List Element () -> List Object (options) -> List Object (options, self) -> List Object options Options object centered if true, and orientation is 'horizontal', then keep the selected item centered construct (data) returns the list item HTML draggable true if the items can be reordered format <[]> ??? itemHeight item height items list of items, (data) returns {items, size, ...} (data, callback) returns [items] itemWidth item width keys keys of the list items max max number of items that can be selected min min number of items that must be selected orientation 'horizontal' or 'vertical' pageLength number of items per page selected ids of the selected elements sort sort order sortable type unique name of the key that acts as unique id self 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: [] }); 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 add item to list (pos, items) -> add items to list at position pos position to add items items 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 turn item into edit form (pos) -> edit item at position pos 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 empy list cache () -> empy cache, returns List Element @*/ that.clearCache = function() { // fixme: was used by TextList resizeColumn, now probably no longer necessary self.$pages = []; return that; }; /*@ closePreview close preview () -> close preview, returns List Element @*/ that.closePreview = function() { self.preview = false; return that; }; /*@ paste paste data (data) -> paste data into list data paste object @*/ that.paste = function(data) { pasteItems(data); return that; }; /*@ reloadList reload list contents () -> returns List Element @*/ that.reloadList = function(stayAtPosition) { if (stayAtPosition) { var scrollTop = that.scrollTop(); updateQuery(function() { that.scrollTop(scrollTop); }); } else { updateQuery(); } return that; }; /*@ reloadPages reload list pages () -> returns List Element @*/ 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 remove items from list (ids) -> remove items (pos, length) -> remove items ids array of item ids pos delete items starting at this position length 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 scroll list to current selection () -> returns List Element @*/ that.scrollToSelection = function() { self.selected.length && scrollToPosition(self.selected[0]); return that; }; /*@ size fixme: not a good function name () -> 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 sort list (key, operator) -> returns List Element key key to sort list by operator +/- sort ascending or descending map 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 get/set list value (id, key, value) -> sets value, returns List Element (id, key) -> returns value (id) -> returns all values of id id id of item key key if item property value 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; };