// vim: et:ts=4:sw=4:sts=4:ft=js /*@ Basic list object (options) -> that (options, self) -> that options the list's options self shared private variable @*/ Ox.List = function(options, self) { /*** basic list object Options centered boolean if true, and orientation is 'horizontal', then keep the selected item centered construct function function(data), returns the list item HTML items function function(callback) returns {items, size, ...} function(data, callback) returns [items] or array of items Methods Events ***/ var self = self || {}, that = new Ox.Container({}, self) .defaults({ centered: false, //@ if true, and orientation is 'horizontal', //@ then keep the selected item centered construct: null, //@ (data) returns the list item HTML draggable: false, //@ true if the items can be reordered format: [], //@ ??? itemHeight: 16, //@ item height items: null, //@ list items //@ (data) returns {items, size, ...} //@ (data, callback) returns [items] itemWidth: 16, //@ item width keys: [], //@ keys of the list items max: -1, //@ max number of items that can be selected min: 0, //@ min number of items that must be selected orientation: 'vertical', //@ 'horizontal' or 'vertical' pageLength: 100, //@ number of items per page selected: [], //@ ids of the selected elements sort: [], //@ sortable: false, //@ type: 'text', //@ unique: '' //@ name of the key that acts as unique id }) .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: {}, 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 (Ox.isArray(self.options.items)) { self.listLength = self.options.items.length; loadItems(); } else { updateQuery(self.options.selected); } 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 = new Ox.ListPage().css(getPageCSS(page)); for (i = 0; i < getPageLength(page); i++ ) { // fixme: why does chainging fail here? new Ox.ListItem({ construct: self.options.construct }).appendTo($page); } //Ox.print('cEP done') return $page; } function copyItems() { var ids = getSelectedIds(); ids.length && that.triggerEvent('copy', { ids: ids }); /* 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() { var ids = getSelectedIds(); ids.length && that.triggerEvent('delete', { ids: ids }); } 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').removeElement(); } 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 = new 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 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(ids) { Ox.print('getPositions', ids) ids = ids || getSelectedIds(); Ox.print('getPositions', ids) // fixme: optimize: send non-selected ids if more than half of the items are selected if (ids.length /*&& ids.length < self.listLength*/) { /*Ox.print('-------- request', { ids: ids, sort: self.options.sort });*/ self.requests.push(self.options.items({ ids: ids, sort: self.options.sort }, getPositionsCallback)); } else { getPositionsCallback(); } } function getPositionsCallback(result) { Ox.print('getPositionsCallback', result) var pos = 0; if (result) { $.extend(self, { ids: {}, 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); } // that.scrollTop(0); that.$content.empty(); //Ox.print('self.selected', self.selected, 'self.page', self.page); loadPages(self.page, function() { scrollToPosition(pos, true); }); } 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) return $.map(self.selected, function(pos) { Ox.print('....', pos, self.options.unique, self.$items[pos].options('data')[self.options.unique]) //Ox.print('2222', self.$items, 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() { that.$content.empty(); self.options.items.forEach(function(item, pos) { // fixme: duplicated self.$items[pos] = new 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); }); } 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] = new Ox.ListPage().css(getPageCSS(page)); result.data.items.forEach(function(v, i) { var pos = offset + i; self.$items[pos] = new 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() { var ids = getSelectedIds(); ids.length && that.triggerEvent('open', { ids: ids }); } function pasteItems() { that.triggerEvent('paste', Ox.Clipboard.paste()); } function preview() { var ids = getSelectedIds(); if (ids.length) { self.preview = !self.preview; if (self.preview) { that.triggerEvent('openpreview', { ids: getSelectedIds() }); } else { that.triggerEvent('closepreview'); } } } function scroll() { if (Ox.isFunction(self.options.items)) { 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) { // fixme: can't use selectNone here, // since it'd trigger a select event 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); self.selected.push(pos); !Ox.isUndefined(self.$items[pos]) && self.$items[pos].addClass('OxSelected'); }); } 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_ = 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: getSelectedIds() }); } 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(ids) { // fixme: shouldn't this be setQuery? // ids are the selcected ids // (in case list is loaded with selection) Ox.print('updateQuery', self.options) clear(); self.requests.push(self.options.items({}, function(result) { var keys = {}; 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(ids); })); } function updateSort() { var key = self.options.sort[0].key, operator = self.options.sort[0].operator; if (self.listLength > 1) { if (Ox.isArray(self.options.items)) { self.options.items.sort(function(a, b) { var ret = 0 if (a[key] < b[key]) { return operator == '+' ? -1 : 1 } else if (a[key] > b[key]) { return operator == '+' ? 1 : -1; } return ret; }); loadItems(); } else { clear(); // fixme: bad function name getPositions(); } } } self.setOption = function(key, value) { //Ox.print('list onChange', key, value); if (key == 'items') { updateQuery(); } else if (key == 'selected') { Ox.print('onChange selected', value) setSelected(value); } }; 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 = new 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(); } 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 = new Ox.ItemInput({ type: 'textarea', value: item.value, height: height, width: width }).bindEvent({ cancel: cancel, save: submit }).appendTo($item.$element); /* setTimeout(function() { $input.gainFocus(); $input.focus(); }); */ function cancel() { $item.options('data', item); //fixme: trigger event to reset i/o points } 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); } } that.clearCache = function() { // fixme: was used by TextList resizeColumn, now probably no longer necessary self.$pages = []; return that; }; that.closePreview = function() { self.preview = false; return that; }; that.paste = function(data) { pasteItems(data); return that; }; that.reloadList = function() { updateQuery(); return that; }; 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; }; that.removeItems = function(pos, length) { /* removeItems(ids) or removeItems(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(); } } that.scrollToSelection = function() { self.selected.length && scrollToPosition(self.selected[0]); return that; }; 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; } that.sortList = function(key, operator) { Ox.print('sortList', key, operator) if (key != self.options.sort[0].key || operator != self.options.sort[0].operator) { self.options.sort[0] = {key: key, operator: operator}; updateSort(); that.triggerEvent('sort', self.options.sort[0]); } return that; } 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; };