// vim: et:ts=4:sw=4:sts=4:ft=javascript /*@ 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 If true, items can be dragged 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 Maximum number of items that can be selected (-1 for all) min Minimum 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 If true, items can be re-ordered type unique name of the key that acts as unique id self shared private variable add item added delete item removed draganddrop Fires during drag draganddropend Fires on drop draganddropenter Fires when entering an item during drag draganddropleave Fires when leaving an item during drag draganddroppause Fires when the mouse stops during drag draganddropstart Fires when drag starts copy copy paste paste move move item load list loaded openpreview preview of selected item opened closepreview preview closed select select item @*/ // fixme: rename the add event to new, or the delete event to remove 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.bindEvent({ mousedown: mousedown, singleclick: singleclick, doubleclick: doubleclick }); if (self.options.draggable) { that.$content.bindEvent({ dragstart: dragstart, drag: drag, dragpause: dragpause, dragenter: dragenter, dragleave: dragleave, dragend: dragend }); } else if (self.options.sortable) { that.$content.bindEvent({ dragstart: movestart, drag: move, dragend: moveend }); } // fixme: without this, horizontal lists don't get their full width self.options.orientation == 'horizontal' && that.$content.css({height: '1px'}); Ox.extend(self, { $items: [], $pages: [], 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: function() { addItem(''); }, key_alt_control_n: function() { addItem('alt'); }, key_alt_shift_control_n: function() { addItem('alt_shift'); }, key_shift_control_n: function() { addItem('shift'); }, key_control_v: pasteItems, key_control_x: cutItems, key_delete: deleteItems, key_end: scrollToFirst, key_enter: open, key_home: scrollToLast, key_pagedown: scrollPageDown, key_pageup: scrollPageUp, key_section: preview, // fixme: firefox gets keyCode 0 when pressing space key_space: preview }, listMargin: self.options.type == 'text' ? 0 : 8, // 2 x 4 px padding page: 0, preview: false, requests: [], scrollTimeout: 0, selected: [] }); if (!self.isAsync) { self.selected = self.options.items.map(function(item, i) { return Ox.extend(item, {_index: i}) }).filter(function(item) { return self.options.selected.indexOf(item[self.options.unique]) > -1; }).map(function(item) { return item['_index']; }); } self.options.max == -1 && Ox.extend(self.keyboardEvents, { key_alt_control_a: invertSelection, key_control_a: selectAll }); self.options.min == 0 && Ox.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') { Ox.extend(self.keyboardEvents, { key_left: function() { triggerToggleEvent(false); }, key_right: function() { triggerToggleEvent(true); } }); } else if (self.options.orientation == 'both') { Ox.extend(self.keyboardEvents, { key_down: selectBelow, key_up: selectAbove }); if (self.options.max == -1) { Ox.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.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(keys) { that.triggerEvent('add', { keys: keys }); } 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'); triggerSelectEvent(); } else { // allow for 'cursor navigation' if orientation == 'both' self.selected.splice(self.selected.indexOf(pos), 1); self.selected.push(pos); } } function clear() { self.requests.forEach(function(v) { Ox.Request.cancel(v); }); Ox.extend(self, { $items: [], $pages: [], page: 0, requests: [] }); } function constructEmptyPage(page) { var i, $page = Ox.ListPage().css(getPageCSS(page)); Ox.loop(getPageLength(page), function() { Ox.ListItem({ construct: self.options.construct }).appendTo($page); }); return $page; } function copyItems() { // fixme: both copy and paste should just deal with Ox.Clipboard, // and use a "type" self.options.selected.length && that.triggerEvent('copy', { ids: self.options.selected }); /* ids.length && self.options.copy && Ox.Clipboard.copy( self.options.copy( ids.map(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(data) { var $target = $(data.target), $parent = $target.parent(); if ( $target.is('.OxTarget') // icon lists || $parent.is('.OxTarget') // text lists || $parent.parent().is('.OxTarget') // text lists with div inside cell ) { self.drag = { ids: self.options.selected }; // fixme: shouldn't the target have been // automatically passed already, somewhere? that.triggerEvent('draganddropstart', { ids: self.drag.ids, _event: data }); } } function drag(data) { self.drag && that.triggerEvent('draganddrop', { ids: self.drag.ids, _event: data }); } function dragpause(data) { self.drag && that.triggerEvent('draganddroppause', { ids: self.drag.ids, _event: data }); } function dragenter(data) { self.drag && that.triggerEvent('draganddropenter', { ids: self.drag.ids, _event: data }); } function dragleave(data) { self.drag && that.triggerEvent('draganddropleave', { ids: self.drag.ids, _event: data }); } function dragend(data) { if (self.drag) { that.triggerEvent('draganddropend', { ids: self.drag.ids, _event: data }); delete self.drag; } } function emptyFirstPage() { if (self.$pages[0]) { if (self.options.type == 'text') { self.$pages[0].find('.OxEmpty').remove(); } else if (self.options.orientation == 'both') { that.$content.css({height: getListSize() + 'px'}); } } } function fillFirstPage() { if (self.$pages[0]) { if (self.options.type == 'text') { 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]); }); } } else if (self.options.orientation == 'both') { var height = getHeight(), listSize = getListSize(); if (listSize < height) { that.$element.css({overflowY: 'hidden'}); that.$content.css({height: height + 'px'}); } else { that.$element.css({overflowY: 'auto'}); } } } } function findCell(e) { var $element = $(e.target); while (!$element.is('.OxCell') && !$element.is('.OxPage') && !$element.is('body')) { $element = $element.parent(); } return $element.is('.OxCell') ? $element : null; } function findItemPosition(e) { var $element = $(e.target), $parent, position = -1; while ( !$element.is('.OxTarget') && !$element.is('.OxPage') && ($parent = $element.parent()).length ) { $element = $parent; } if ($element.is('.OxTarget')) { while ( !$element.is('.OxItem') && !$element.is('.OxPage') && ($parent = $element.parent()).length ) { $element = $parent; } if ($element.is('.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.rowLength) * (self.options[ self.options.orientation == 'horizontal' ? 'itemWidth' : 'itemHeight' ] + self.itemMargin); } 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 getPageLength(page) { var mod = self.listLength % self.pageLength; return page < self.pages - 1 || (self.listLength && mod == 0) ? self.pageLength : mod; } 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) { self.options.selected = []; self.positions = {}; self.selected = []; Ox.forEach(result.data.positions, function(pos, id) { // fixme: in case the order of self.options.selected // is important - it may get lost here self.options.selected.push(id); self.selected.push(pos); }); if (self.selected.length) { 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() { if (self.$items.length == 0) { return self.options.selected; } else { return self.selected.map(function(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.$items = []; self.options.items.forEach(function(item, pos) { // fixme: duplicated self.$items[pos] = Ox.ListItem({ construct: self.options.construct, data: item, 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]); // that.triggerEvent('init', {items: self.options.items.length}); // fixme: do async lists need to trigger init? // will this only be reached in async lists? } function loadPage(page, callback) { if (page < 0 || page >= self.pages) { !Ox.isUndefined(callback) && callback(); return; } Ox.print(that.id, 'loadPage', page); var keys = Ox.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); 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, //format: self.options.format, position: pos, unique: self.options.unique }); isSelected(pos) && self.$items[pos].addClass('OxSelected'); self.$items[pos].appendTo(self.$pages[page]); }); 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) { Ox.print('loadPages', page) 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(data) { var pos = findItemPosition(data); //self.hadFocus = that.hasFocus(); that.gainFocus(); if (pos > -1) { if (data.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 (data.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.type == 'text') { $cell = findCell(data); if ($cell) { clickable = $cell.is('.OxClickable'); editable = $cell.is('.OxEditable') && !$cell.is('.OxEdit'); if (clickable || editable) { // click on a clickable or editable cell triggerClickEvent(clickable ? 'click' : 'edit', self.$items[pos], $cell); } } } } else if (!$(data.target).is('.OxToggle') && self.options.min == 0) { // click on empty area selectNone(); } } function movestart(data) { self.drag = { pos: findItemPosition(data) }; Ox.extend(self.drag, { id: self.$items[self.drag.pos].options('data')[self.options.unique], startPos: self.drag.pos, startY: data.clientY, stopPos: self.drag.pos }); self.$items[self.drag.pos] .addClass('OxDrag') .css({ cursor: 'move', }); } function move(data) { var clientY = data.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 moveend(data) { var $item = self.$items[self.drag.pos]; $item.removeClass('OxDrag') .css({ cursor: 'default', }); that.triggerEvent('move', { //id: id, ids: self.$items.map(function($item) { return $item.options('data')[self.options.unique]; }) //position: pos }); delete self.drag; } function singleclick(data) { // this can't trigger on mousedown, // since it could be a doubleclick var pos = findItemPosition(data), clickable, editable; if (pos > -1) { if ( !data.metaKey && !data.shiftKey && isSelected(pos) && self.selected.length > 1 ) { // click on one of multiple selected items select(pos); } } } function doubleclick(data) { open(); } function _mousedown(e) { // fixme: no longer used, remove 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 // or the mousedown of a drag of multiple items selectTimeout = true; that.$content.one({ mousemove: function() { self.clickTimeout && clearTimeout(self.clickTimeout); } }); } else if (self.options.type == 'text' && hadFocus) { var $cell = findCell(e), $element = $cell || self.$items[pos]; clickable = $element.is('.OxClickable'); editable = $element.is('.OxEditable') && !$element.is('.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).is('.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]; } 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) { //Ox.print('scrollToPosition', pos) 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' }, 250); } 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 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) { 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, Ox.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] Ox.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; Ox.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]; }); 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; }); 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, 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, Ox.merge([pos, 0], items)); self.$items.splice.apply(self.$items, Ox.merge([pos, 0], $items)); //if(first) loadItems(); updatePositions(); } /*@ closePreview to be called when preview is closed externally () -> the list @*/ that.closePreview = function() { self.preview = false; return that; }; /*@ clearCache empty list cache () -> the list @*/ that.clearCache = function() { // fixme: was used by TextList resizeColumn, now probably no longer necessary self.$pages = []; return that; }; /*@ 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(data) { item.value = data.value; //$input.loseFocus().remove(); // fixme: leaky, inputs remain in focus stack $item.options('data', item); that.triggerEvent('submit', item); loadItems(); } } /*@ paste paste data (data) -> the list data paste object @*/ that.paste = function(data) { pasteItems(data); return that; }; /*@ reloadList reload list contents () -> the list @*/ that.reloadList = function(stayAtPosition) { var scrollTop = that.scrollTop(); if (!self.isAsync) { loadItems(); scrollList(); } else { updateQuery(scrollList); } function scrollList() { stayAtPosition && that.scrollTop(scrollTop); } return that; }; /*@ reloadPages reload list pages () -> the list @*/ that.reloadPages = function() { 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' }); scrollTo(scroll); } emptyFirstPage(); fillFirstPage(); } else if (self.options.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'); if (arguments.length == 1) { return data; } else if (arguments.length == 2) { return data[key]; } else { if (key == self.options.unique) { // unique id has changed self.options.selected = self.options.selected.map(function(id_) { return id_ == data[key] ? value : id_ }); } data[key] = value; $item.options({data: data}); return that; } }; return that; };