// 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) { var self = self || {}, that = new 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: {}, 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(map) { 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 aValue = map ? map(a[key]) : a[key], bValue = map ? map(b[key]) : b[key], ret = 0 if (aValue < bValue) { return operator == '+' ? -1 : 1 } else if (aValue > bValue) { 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); } }; /*@ addItems add item to list (pos, items) -> add items to list at position pos position to add items items array of items ot 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 = 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(); } /*@ 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 = 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); } } /*@ 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() { 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; } /*@ 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 @*/ that.sortList = function(key, operator, map) { 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(map); 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; };