// vim: et:ts=4:sw=4:sts=4:ft=javascript /*@ Ox.TextList TextList Object () -> TextList Object (options) -> TextList Object (options, self) -> TextList Object options Options object columns <[o]|[]> Fixme: There's probably more... addable editable format id removable map function that maps values to sort values operator default sort operator title unique If true, this column acts as unique id visible width columnsMovable columnsRemovable columnsResizable columnsVisible columnWidth draggable If true, items can be dragged id items function() {} {sort, range, keys, callback} or array max Maximum number of items that can be selected (-1 for all) min Minimum number of items that must be selected pageLength scrollbarVisible selected sort sortable If true, elements can be re-ordered self shared private variable @*/ Ox.TextList = function(options, self) { // fixme: rename to TableList self = self || {}; var that = Ox.Element({}, self) .defaults({ columns: [], columnsMovable: false, columnsRemovable: false, columnsResizable: false, columnsVisible: false, columnWidth: [40, 800], draggable: false, id: '', items: null, // function() {} {sort, range, keys, callback} or array max: -1, min: 0, pageLength: 100, scrollbarVisible: false, selected: [], sort: [], sortable: false }) .options(options || {}) .addClass('OxTextList') .bindEvent({ key_left: function() { var $element = that.$body.$element, scrollLeft = $element[0].scrollLeft - $element.width(); $element.animate({scrollLeft: scrollLeft}, 250); }, key_right: function() { var $element = that.$body.$element, scrollLeft = $element[0].scrollLeft + $element.width(); $element.animate({scrollLeft: scrollLeft}, 250); }, keys: find }); self.options.columns.forEach(function(v) { // fixme: can this go into a generic ox.js function? // fixme: and can't these just remain undefined? if (Ox.isUndefined(v.align)) { v.align = 'left'; } if (Ox.isUndefined(v.clickable)) { v.clickable = false; } if (Ox.isUndefined(v.editable)) { v.editable = false; } if (Ox.isUndefined(v.unique)) { v.unique = false; } if (Ox.isUndefined(v.visible)) { v.visible = false; } if (v.unique) { self.unique = v.id; } }); $.extend(self, { columnPositions: [], defaultColumnWidths: $.map(self.options.columns, function(v) { return v.defaultWidth || v.width; }), itemHeight: 16, page: 0, pageLength: 100, scrollLeft: 0, selectedColumn: getColumnIndexById(self.options.sort[0].key), visibleColumns: $.map(self.options.columns, function(v) { return v.visible ? v : null; }) }); // fixme: there might be a better way than passing both visible and position self.options.columns.forEach(function(v) { if (!Ox.isUndefined(v.position)) { self.visibleColumns[v.position] = v; } }); $.extend(self, { columnWidths: $.map(self.visibleColumns, function(v, i) { return v.width; }), pageHeight: self.options.pageLength * self.itemHeight }); self.format = {}; self.options.columns.forEach(function(v, i) { if (v.format) { self.format[v.id] = v.format; } }); // Head if (self.options.columnsVisible) { that.$bar = Ox.Bar({ orientation: 'horizontal', size: 16 }).appendTo(that); that.$head = Ox.Container() .addClass('OxHead') .css({ right: self.options.scrollbarVisible ? Ox.UI.SCROLLBAR_SIZE + 'px' : 0 }) .appendTo(that.$bar); that.$head.$content.addClass('OxTitles'); constructHead(); if (self.options.columnsRemovable) { that.$select = Ox.Select({ id: self.options.id + 'SelectColumns', items: $.map(self.options.columns, function(v, i) { return { checked: v.visible, disabled: v.removable === false, id: v.id, title: v.title } }), max: -1, min: 1, type: 'image' }) .bindEvent('change', changeColumns) .appendTo(that.$bar.$element); } } // Body that.$body = Ox.List({ construct: constructItem, draggable: self.options.draggable, id: self.options.id, items: self.options.items, itemHeight: 16, items: self.options.items, itemWidth: getItemWidth(), format: self.format, // fixme: not needed, happens in TextList keys: $.map(self.visibleColumns, function(v) { return v.id; }), max: self.options.max, min: self.options.min, pageLength: self.options.pageLength, paste: self.options.paste, orientation: 'vertical', selected: self.options.selected, sort: self.options.sort, sortable: self.options.sortable, type: 'text', unique: self.unique }, $.extend({}, self)) // pass event handler .addClass('OxBody') .css({ top: (self.options.columnsVisible ? 16 : 0) + 'px', overflowY: (self.options.scrollbarVisible ? 'scroll' : 'hidden') }) .scroll(function() { var scrollLeft = $(this).scrollLeft(); if (scrollLeft != self.scrollLeft) { self.scrollLeft = scrollLeft; that.$head && that.$head.scrollLeft(scrollLeft); } }) .bindEvent({ edit: function(data) { that.editCell(data.id, data.key); }, cancel: function(data) { Ox.print('cancel edit', data); }, init: function(data) { // fixme: why does this never reach? //Ox.print('INIT????') //that.triggerEvent('init', data); }, select: function() { Ox.print('SELECT????') self.options.selected = that.$body.options('selected'); } }) .appendTo(that); that.$body.$content.css({ width: getItemWidth() + 'px' }); //Ox.print('s.vC', self.visibleColumns) function addColumn(id) { //Ox.print('addColumn', id); var column, ids, index = 0; Ox.forEach(self.options.columns, function(v) { if (v.visible) { index++; } else if (v.id == id) { column = v; return false; } }); column.visible = true; self.visibleColumns.splice(index, 0, column); self.columnWidths.splice(index, 0, column.width); that.$head.$content.empty(); constructHead(); that.$body.options({ keys: $.map(self.visibleColumns, function(v, i) { return v.id; }) }); that.$body.reloadPages(); } function changeColumns(event, data) { var add, ids = []; Ox.forEach(data.selected, function(column) { var index = getColumnIndexById(column.id); if (!self.options.columns[index].visible) { addColumn(column.id); add = true; return false; } ids.push(column.id); }); if (!add) { Ox.forEach(self.visibleColumns, function(column) { if (ids.indexOf(column.id) == -1) { removeColumn(column.id); return false; } }); } triggerColumnChangeEvent(); } function clickColumn(id) { Ox.print('clickColumn', id); var i = getColumnIndexById(id), isSelected = self.options.sort[0].key == self.options.columns[i].id; self.options.sort = [{ key: self.options.columns[i].id, operator: isSelected ? (self.options.sort[0].operator == '+' ? '-' : '+') : self.options.columns[i].operator, map: self.options.columns[i].map }] updateColumn(); // fixme: strangely, sorting the list blocks updating the column, // so we use a timeout for now setTimeout(function() { that.$body.options({sort: self.options.sort}); }, 10); that.triggerEvent('sort', { key: self.options.sort[0].key, operator: self.options.sort[0].operator }); } function constructHead() { var offset = 0; that.$titles = []; self.columnOffsets = []; self.visibleColumns.forEach(function(v, i) { var $order, $resize, $left, $center, $right; offset += self.columnWidths[i]; self.columnOffsets[i] = offset - self.columnWidths[i] / 2; that.$titles[i] = Ox.Element() .addClass('OxTitle OxColumn' + Ox.toTitleCase(v.id)) .css({ width: (self.columnWidths[i] - 9) + 'px', textAlign: v.align }) .html(v.title) .appendTo(that.$head.$content.$element); // if sort operator is set, bind click event if (v.operator) { that.$titles[i].bindEvent({ anyclick: function(event, e) { clickColumn(v.id); } }); } // if columns are movable, bind drag events if (self.options.columnsMovable) { that.$titles[i].bindEvent({ dragstart: function(event, e) { dragstartColumn(v.id, e); }, drag: function(event, e) { dragColumn(v.id, e); }, dragend: function(event, e) { dragendColumn(v.id, e); } }) } $order = $('
') .addClass('OxOrder') .html(Ox.UI.symbols['triangle_' + ( v.operator == '+' ? 'up' : 'down' )]) .click(function() { $(this).prev().trigger('click') }) .appendTo(that.$head.$content.$element); $resize = Ox.Element() .addClass('OxResize') .appendTo(that.$head.$content.$element); // if columns are resizable, bind click and drag events if (self.options.columnsResizable) { $resize.addClass('OxResizable') .bindEvent({ doubleclick: function(event, e) { resetColumn(v.id, e); }, dragstart: function(event, e) { dragstartResize(v.id, e); }, drag: function(event, e) { dragResize(v.id, e); }, dragend: function(event, e) { dragendResize(v.id, e); } }); } $left = $('
').addClass('OxLeft').appendTo($resize.$element); $center = $('
').addClass('OxCenter').appendTo($resize.$element); $right = $('
').addClass('OxRight').appendTo($resize.$element); }); that.$head.$content.css({ width: (Ox.sum(self.columnWidths) + 2) + 'px' }); if (getColumnPositionById(self.options.columns[self.selectedColumn].id) > -1) { // fixme: save in var toggleSelected(self.options.columns[self.selectedColumn].id); that.$titles[getColumnPositionById(self.options.columns[self.selectedColumn].id)].css({ width: (self.options.columns[self.selectedColumn].width - 25) + 'px' }); } } function constructItem(data) { var $item = $('
') .addClass('OxTarget') .css({ width: getItemWidth(true) + 'px' }); self.visibleColumns.forEach(function(v, i) { var clickable = Ox.isBoolean(v.clickable) ? v.clickable : v.clickable(data), editable = Ox.isBoolean(v.editable) ? v.editable : v.editable(data), $cell = Ox.Element({ tooltip: v.tooltip ? function() { return self.options.selected.indexOf(data[self.unique]) > -1 ? (Ox.isString(v.tooltip) ? v.tooltip : v.tooltip(data)) : ''; } : null }) .addClass( 'OxCell OxColumn' + Ox.toTitleCase(v.id) + (clickable ? ' OxClickable' : '') + (editable ? ' OxEditable' : '') ) .css({ width: (self.columnWidths[i] - (self.options.columnsVisible ? 9 : 8)) + 'px', borderRightWidth: (self.options.columnsVisible ? 1 : 0) + 'px', textAlign: v.align }) .html(v.id in data ? formatValue(v.id, data[v.id], data) : '') .appendTo($item); }); //Math.random() < 0.01 && Ox.print('item', data, $item); return $item; } function formatValue(key, value, data) { // fixme: this may be obscure... // since the format of a value may depend on another value, // we pass all data as a second parameter to the supplied format function var format = self.format[key]; if (value === null) { value = ''; } else if (format) { value = Ox.isObject(format) ? Ox['format' + Ox.toTitleCase(format.type)] .apply(this, Ox.merge([value], format.args || [])) : format(value, data); } else if (Ox.isArray(value)) { value = value.join(', '); } return value; } function dragstartColumn(id, e) { self.drag = { startX: e.clientX, startPos: getColumnPositionById(id) } $.extend(self.drag, { stopPos: self.drag.startPos, offsets: $.map(self.visibleColumns, function(v, i) { return self.columnOffsets[i] - self.columnOffsets[self.drag.startPos] }) }); $('.OxColumn' + Ox.toTitleCase(id)).css({ opacity: 0.25 }); that.$titles[self.drag.startPos].addClass('OxDrag').css({ // fixme: why does the class not work? cursor: 'move' }); } function dragColumn(id, e) { var d = e.clientX - self.drag.startX, pos = self.drag.stopPos; Ox.forEach(self.drag.offsets, function(v, i) { if (d < 0 && d < v) { self.drag.stopPos = i; return false; } else if (d > 0 && d > v) { self.drag.stopPos = i; } }); if (self.drag.stopPos != pos) { moveColumn(id, self.drag.stopPos); } } function dragendColumn(id, e) { var column = self.visibleColumns.splice(self.drag.stopPos, 1)[0], width = self.columnWidths.splice(self.drag.stopPos, 1)[0]; self.visibleColumns.splice(self.drag.stopPos, 0, column); self.columnWidths.splice(self.drag.stopPos, 0, width); that.$head.$content.empty(); constructHead(); $('.OxColumn' + Ox.toTitleCase(id)).css({ opacity: 1 }); that.$titles[self.drag.stopPos].removeClass('OxDrag').css({ cursor: 'pointer' }); that.$body.clearCache(); triggerColumnChangeEvent(); } function dragstartResize(id, e) { var pos = getColumnPositionById(id); self.drag = { startX: e.clientX, startWidth: self.columnWidths[pos] }; } function dragResize(id, e) { var width = Ox.limit( self.drag.startWidth - self.drag.startX + e.clientX, self.options.columnWidth[0], self.options.columnWidth[1] ); resizeColumn(id, width); } function dragendResize(id, e) { var pos = getColumnPositionById(id); that.triggerEvent('columnresize', { id: id, width: self.columnWidths[pos] }); } function find(data) { // fixme: works only if items are an array var query = data.keys, sort = self.options.sort[0]; Ox.print('QUERY', query) Ox.forEach(self.options.items, function(item, i) { var value = ( sort.map ? sort.map(item[sort.key]) : item[sort.key] ).toString().toLowerCase(); if (Ox.startsWith(value, query)) { that.$body.options({selected: [item[self.unique]]}); Ox.print('QUERY', query, 'VALUE', value) return false; } }); } function getCell(id, key) { Ox.print('getCell', id, key) var $item = getItem(id); key = key || ''; return $($item.find('.OxCell.OxColumn' + Ox.toTitleCase(key))[0]); } function getColumnIndexById(id) { return Ox.getPositionById(self.options.columns, id); } function getColumnPositionById(id) { return Ox.getPositionById(self.visibleColumns, id); } function getItem(id) { //Ox.print('getItem', id) var $item = null; $.each(that.find('.OxItem'), function(i, v) { $v = $(v); if ($v.data('id') == id) { $item = $v; return false; } }); return $item; } function getItemWidth(cached) { // fixme: this gets called for every constructItem and is slooow // the proper way to fix this would be to find out how and when // that.$element.width() might change... which would probably // mean binding to every SplitPanel and window resize... // for now, use a cached value if (!cached) { self.cachedWidth = that.$element.width(); } else if (!self.cachedWidth || self.cachedWidthTime < +new Date() - 5000) { self.cachedWidth = that.$element.width(); self.cachedWidthTime = +new Date(); } return Math.max( Ox.sum(self.columnWidths), self.cachedWidth - (self.options.scrollbarVisible ? Ox.UI.SCROLLBAR_SIZE : 0) ); } function moveColumn(id, pos) { // fixme: column head should be one element, not three //Ox.print('moveColumn', id, pos) var startPos = getColumnPositionById(id), stopPos = pos, startClassName = '.OxColumn' + Ox.toTitleCase(id), stopClassName = '.OxColumn' + Ox.toTitleCase(self.visibleColumns[stopPos].id), insert = startPos < stopPos ? 'insertAfter' : 'insertBefore' $column = $('.OxTitle' + startClassName), $order = $column.next(), $resize = $order.next(); //Ox.print(startClassName, insert, stopClassName) $column.detach()[insert](insert == 'insertAfter' ? $('.OxTitle' + stopClassName).next().next() : $('.OxTitle' + stopClassName)); $order.detach().insertAfter($column); $resize.detach().insertAfter($order); $.each(that.$body.find('.OxItem'), function(i, v) { var $v = $(v); $v.children(startClassName).detach()[insert]($v.children(stopClassName)); }); var column = self.visibleColumns.splice(startPos, 1)[0], width = self.columnWidths.splice(startPos, 1)[0]; self.visibleColumns.splice(stopPos, 0, column); self.columnWidths.splice(stopPos, 0, width); } function removeColumn(id) { //Ox.print('removeColumn', id); var className = '.OxColumn' + Ox.toTitleCase(id), index = getColumnIndexById(id), itemWidth, position = getColumnPositionById(id), $column = $('.OxTitle' + className), $order = $column.next(), $resize = $order.next(); self.options.columns[index].visible = false; self.visibleColumns.splice(position, 1); self.columnWidths.splice(position, 1); that.$head.$content.empty(); constructHead(); itemWidth = getItemWidth(); $.each(that.$body.find('.OxItem'), function(i, v) { var $v = $(v); $v.children(className).remove(); $v.css({ width: itemWidth + 'px' }); }); that.$body.$content.css({ width: itemWidth + 'px' }); that.$body.options({ keys: $.map(self.visibleColumns, function(v, i) { return v.id; }) }); //that.$body.clearCache(); } function resetColumn(id) { var width = self.defaultColumnWidths[getColumnIndexById(id)]; resizeColumn(id, width); that.triggerEvent('columnresize', { id: id, width: width }); } function resizeColumn(id, width) { var i = getColumnIndexById(id), pos = getColumnPositionById(id); self.options.columns[i].width = width; self.columnWidths[pos] = width; if (self.options.columnsVisible) { that.$head.$content.css({ width: (Ox.sum(self.columnWidths) + 2) + 'px' }); that.$titles[pos].css({ width: (width - 9 - (i == self.selectedColumn ? 16 : 0)) + 'px' }); } that.$element.find('.OxCell.OxColumn' + Ox.toTitleCase(self.options.columns[i].id)).css({ width: (width - (self.options.columnsVisible ? 9 : 8)) + 'px' }); setWidth(); } function setWidth() { var width = getItemWidth(); that.$body.$content.find('.OxItem').css({ // fixme: can we avoid this lookup? width: width + 'px' }); that.$body.$content.css({ width: width + 'px' // fixme: check if scrollbar visible, and listen to resize/toggle event }); } function toggleSelected(id) { var pos = getColumnPositionById(id); if (pos > -1) { updateOrder(id); pos > 0 && that.$titles[pos].prev().children().eq(2).toggleClass('OxSelected'); that.$titles[pos].toggleClass('OxSelected'); that.$titles[pos].next().toggleClass('OxSelected'); that.$titles[pos].next().next().children().eq(0).toggleClass('OxSelected'); that.$titles[pos].css({ width: ( that.$titles[pos].width() + (that.$titles[pos].hasClass('OxSelected') ? -16 : 16) ) + 'px' }); } } function triggerColumnChangeEvent() { that.triggerEvent('columnchange', { ids: $.map(self.visibleColumns, function(v, i) { return v.id; }) }); } function updateColumn() { var columnId = self.options.columns[self.selectedColumn].id isSelected = columnId == self.options.sort[0].key; if (self.options.columnsVisible) { if (isSelected) { updateOrder(columnId); } else { toggleSelected(columnId); self.selectedColumn = getColumnIndexById(self.options.sort[0].key); toggleSelected(self.options.columns[self.selectedColumn].id); } } } function updateOrder(id) { var pos = getColumnPositionById(id); if (pos > -1) { that.$titles[pos].next().html(Ox.UI.symbols[ 'triangle_' + (self.options.sort[0].operator == '+' ? 'up' : 'down') ]); } } self.setOption = function(key, value) { //Ox.print('---------------------------- TextList setOption', key, value) if (key == 'items') { that.$body.options(key, value); } else if (key == 'paste') { that.$body.options(key, value); } else if (key == 'selected') { that.$body.options(key, value); } else if (key == 'sort') { updateColumn(); that.$body.options(key, value); } }; that.closePreview = function() { that.$body.closePreview(); return that; }; that.addItem = function(item) { /* self.options.items.push(item); that.$body.options({items: self.options.items}); //that.$body.options({selected: [item.id]}); */ } that.editCell = function(id, key) { Ox.print('editCell', id, key) var $item = getItem(id), $cell = getCell(id, key), $input, html = $cell.html(), index = getColumnIndexById(key), column = self.options.columns[index], width = column.width - self.options.columnsVisible; $cell.empty() .addClass('OxEdit') .css({ width: width + 'px' }); $input = Ox.Input({ autovalidate: column.input ? column.input.autovalidate : null, style: 'square', value: html, width: width }) .bind({ mousedown: function(e) { // keep mousedown from reaching list e.stopPropagation(); } }) .bindEvent({ blur: submit, }) .appendTo($cell); //.focusInput(); setTimeout($input.focusInput, 0); // fixme: strange function submit() { var value = $input.value(); //$input.loseFocus().remove(); // fixme: leaky, inputs remain in focus stack $cell.removeClass('OxEdit') .css({ // account for padding width: (width - 8) + 'px' }) .html(value); that.triggerEvent('submit', { id: id, key: key, value: value }); } } that.gainFocus = function() { that.$body.gainFocus(); return that; }; that.loseFocus = function() { that.$body.loseFocus(); return that; } that.paste = function(data) { that.$body.paste(); return that; }; that.reloadList = function(stayAtPosition) { that.$body.reloadList(stayAtPosition); return that; }; that.resizeColumn = function(id, width) { resizeColumn(id, width); return that; } that.size = function() { Ox.print('SIZE FUNCTION CALLED') setWidth(); that.$body.size(); } // fixme: deprecated that.sortList = function(key, operator) { Ox.print('$$$$ DEPRECATED $$$$') var isSelected = key == self.options.sort[0].key; self.options.sort = [{ key: key, operator: operator, map: self.options.columns[self.selectedColumn].sort }]; if (self.options.columnsVisible) { if (isSelected) { updateOrder(self.options.columns[self.selectedColumn].id); } else { toggleSelected(self.options.columns[self.selectedColumn].id); self.selectedColumn = getColumnIndexById(key); toggleSelected(self.options.columns[self.selectedColumn].id); } } // fixme: strangely, sorting the list blocks toggling the selection, // so we use a timeout for now setTimeout(function() { that.$body.options({sort: self.options.sort}); /* that.$body.sortList( self.options.sort[0].key, self.options.sort[0].operator, self.options.sort[0].map ); */ }, 10); return that; }; that.value = function(id, key, value) { // fixme: make this accept id, {k: v, ...} //Ox.print('value', id, key, value) var $cell, $item = getItem(id); //column = self.options.columns[getColumnIndexById(key)]; if (arguments.length == 1) { return that.$body.value(id); } else if (arguments.length == 2) { return that.$body.value(id, key); } else { that.$body.value(id, key, value); //Ox.print('? ? ?', column, column.format) $cell = getCell(id, key); $cell && $cell.html(formatValue(key, value)); if (key == self.options.sort[0].key) { that.$body.sort(); } /* fixme: something like this is needed: if (column.unique) { that.$body.setId($item.data('id'), value); $item.data({id: value}); } */ return that; } } return that; };