// vim: et:ts=4:sw=4:sts=4:ft=js /*@ 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 operator default sort operator sort function that maps values to sort values title unique If true, this column acts as unique id visible width columnsMovable columnsRemovable columnsResizable columnsVisible columnWidth id items function() {} {sort, range, keys, callback} or array max min pageLength scrollbarVisible selected sort self shared private variable @*/ Ox.TextList = function(options, self) { // fixme: rename to TableList var self = self || {}, that = new Ox.Element({}, self) .defaults({ columns: [], columnsMovable: false, columnsRemovable: false, columnsResizable: false, columnsVisible: false, columnWidth: [40, 800], id: '', items: null, // function() {} {sort, range, keys, callback} or array max: -1, min: 0, pageLength: 100, scrollbarVisible: false, selected: [], sort: [] }) .options(options || {}) .addClass('OxTextList'); 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 = new Ox.Bar({ orientation: 'horizontal', size: 16 }).appendTo(that); that.$head = new 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 = new 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 = new Ox.List({ construct: constructItem, 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(event, data) { that.editCell(data.id, data.key); }, select: function(event, data) { self.options.selected = data.ids; } }) .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; that.sortList( self.options.columns[i].id, isSelected ? (self.options.sort[0].operator == '+' ? '-' : '+') : self.options.columns[i].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] = new 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 = new 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() + '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 = $('
') .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(data[v.id], v.format) : '') .appendTo($item); }); function formatValue(value, format) { if (value === null) { value = ''; } else if (format) { value = Ox.isObject(format) ? Ox['format' + Ox.toTitleCase(format.type)] .apply(this, $.merge([value], format.args)) : format(value); } else if (Ox.isArray(value)) { value = value.join(', '); } return value; } //Math.random() < 0.01 && Ox.print('item', data, $item); return $item; } 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 getCell(id, key) { Ox.print('getCell', id, key) var $item = getItem(id); 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() { return Math.max( Ox.sum(self.columnWidths), that.$element.width() - (self.options.scrollbarVisible ? Ox.UI.SCROLLBAR_SIZE : 0) ); //return Ox.sum(self.columnWidths) } 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.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 updateOrder(id) { var pos = getColumnPositionById(id); //Ox.print(id, pos) that.$titles[pos].next().html(Ox.UI.symbols[ 'triangle_' + (self.options.sort[0].operator == '+' ? 'up' : 'down') ]); } self.setOption = function(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); } }; // fixme: doesn't work, doesn't return that that.closePreview = that.$body.closePreview; 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 = new 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({ 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() { that.$body.reloadList(); return that; }; that.resizeColumn = function(id, width) { resizeColumn(id, width); return that; } that.size = function() { setWidth(); that.$body.size(); } that.sortList = function(key, operator) { var isSelected = key == self.options.sort[0].key; self.options.sort = [{key: key, operator: operator}]; 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.sortList( self.options.sort[0].key, self.options.sort[0].operator, self.options.columns[self.selectedColumn].sort ); }, 10); return that; }; that.value = function(id, key, value) { // fixme: make this accept id, {k: v, ...} Ox.print('value', id, key, value) var $item = getItem(id), $cell = getCell(id, key), 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); $cell && $cell.html(column.format ? column.format(value) : value); /* if (column.unique) { that.$body.setId($item.data('id'), value); $item.data({id: value}); } */ return that; } } return that; };