/** TableView.js - implementation of the TableView class * * Creates a YUI dataTable view with data from a FirstClass DataSource object * Defaults to row-based selection with highlighting on mouseover; provides * column sorting and display. * * Config: * dataSource - (object) a FIRSTCLASS.util.DataSource object * domEl - DOM element that will contain the view * width - (numeric) initial width in pixels - defaults to domEl dimensions * height - (numeric) initial height in pixels * columns - (array) list of column names for DS query schema (not needed if passed-in DS is non-empty) * colDefs [ - array of column definitions: * label - column heading string * key - dataSource column to get data * pcWidth - (numeric) percent of total width * formatter - function(elCell, oRow, oColumn, oData) to format cells * className - CSS class name to display the cell * sortable - can be used as a sort key * ] * totalPad - sum of column paddings * defaultSort { - initial sort * key - column key value dir - sort direction; either YAHOO.widget.DataTable.CLASS_ASC or YAHOO.widget.DataTable.CLASS_DESC } * objtypes[] - list of firstclass obj types to display/fetch * callback { - callback function object * onNewRow(row) - new row received (post-filter, pre-display) * onRowUpdate(oldRow, row) - existing row updated (post-filter, pre-display) * onRowDelete(row) - row deleted * onRowClick(row) - handle mouse click on passed-in DS row * rowFilter(row) - return true if row should be displayed in table * fillComplete() - data fill completed * } **/ FIRSTCLASS.layout.tableViewPadding = 34; FIRSTCLASS.layout.TableView = function(config) { var that = this; this._dt = false; this._hidden = false; this._pds = []; this._rowsToDelete = false; this._nItems = 0; this._filterStr = false; // row filter value this._flushTimer = false; this._highlightRow = false; // last highlighted row this._config = config; this._dataSource = config.dataSource; this._domEl = config.domEl; this._defaultSort = config.defaultSort || { key:"parsedDate", dir: YAHOO.widget.DataTable.CLASS_DESC }; // YAHOO data source for table this._yds = new YAHOO.util.FunctionDataSource(function() { return that._pds; }); var cols = this._config.columns ? this._config.columns : this._config.dataSource.getColumns(); cols.push("bytesize"); cols.push("suffix"); this._yds.responseSchema = { fields: cols }; this._nCols = config.colDefs.length; this._dataSource.addRowListener(this, true); this._totalPadding = config.totalPad ? config.totalPad : FIRSTCLASS.layout.tableViewPadding; this._active = false; this._nQueued = 0; }; FIRSTCLASS.layout.TableView.prototype.filter = function(row, isNewDelivery) { return !this._config.callback.rowFilter || this._config.callback.rowFilter(row); }; FIRSTCLASS.layout.TableView.prototype.onRow = function(row, dataSource, atEnd, hasMore) { var oldRow; // add item to list keyed by id if (this._pds[row.threadid]) { // already have a version of this item oldRow = this._pds[row.threadid]; if (this.rowIsAnUpdate(oldRow, row)) { // replace old with new this.deleteRow(oldRow); this._pds[oldRow.threadid] = row; if (this._config.callback && this._config.callback.onRowUpdate) { this._config.callback.onRowUpdate(oldRow, row); } this.queueRow(row); } } else { // just add the new row this._pds[row.threadid] = row; if (this._config.callback && this._config.callback.onNewRow) { this._config.callback.onNewRow(row); } this.queueRow(row); this._nItems++; } }; FIRSTCLASS.layout.TableView.prototype.onRows = function(rows, dataSource, atEnd, hasMore) { var oldRow, row; // add item to list keyed by id for (row in rows) { if (this._pds[rows[row].threadid]) { // already have a version of this item oldRow = this._pds[rows[row].threadid]; if (this.rowIsAnUpdate(oldRow, rows[row])) { // replace old with new this.deleteRow(oldRow); this._pds[oldRow.threadid] = rows[row]; if (this._config.callback && this._config.callback.onRowUpdate) { this._config.callback.onRowUpdate(oldRow, rows[row]); } this.queueRow(rows[row]); } } else { // just add the new row this._pds[rows[row].threadid] = rows[row]; if (this._config.callback && this._config.callback.onNewRow) { this._config.callback.onNewRow(rows[row]); } this.queueRow(rows[row]); this._nItems++; } } }; FIRSTCLASS.layout.TableView.prototype.onRowChanged = function(row, dataSource) { this.onRow(row, dataSource, true, false); }; FIRSTCLASS.layout.TableView.prototype.onRowDeleted = function(row) { var nRows = 0, i; for (i in this._pds) { if (this._pds[i].threadid) { nRows++; } } // delete row from list; if it was visible, update the display if (this._pds[row.threadid]) { if (nRows > 1) { this.deleteRow(this._pds[row.threadid]); } else { this.removeListView(); } if (this._config.callback && this._config.callback.onRowDelete) { this._config.callback.onRowDelete(row); } this._pds.splice(row.threadid, 1); if (this._nItems > 0) { this._nItems--; } } }; // test if received row is an update to an existing one; since entire threads are received // on update, must reject older items or same item previously unread, and pass same item with updated list // info FIRSTCLASS.layout.TableView.prototype.rowIsAnUpdate = function(oldRow, newRow) { var isNewer = false; if (newRow.typedef.objtype === oldRow.typedef.objtype) { if (newRow.lastmods > oldRow.lastmods) { isNewer = true; } else if (newRow.lastmods === oldRow.lastmods) { if (newRow.col8090 && oldRow.col8090 && newRow.col8090 > oldRow.col8090) { isNewer = true; } else if (newRow.wasUnread) { isNewer = true; } else if (newRow.col8101 && !oldRow.col8101) { isNewer = true; } else if ((newRow.tags && !oldRow.tags) || (!newRow.tags && oldRow.tags) || (newRow.tags && oldRow.tags && newRow.tags !== oldRow.tags)) { isNewer = true; } } } return isNewer; }; // queue row to add to table FIRSTCLASS.layout.TableView.prototype.queueRow = function(row) { var that = this; var suffix = ""; var bytes = 0; if (row.name) { suffix = row.name.slice(row.name.lastIndexOf(".")+1); } row.suffix = suffix; if (row.col6) { bytes = Number(row.col6); } row.bytesize = bytes; var queueIt = true; if (this._filterStr) { queueIt = row.name.toLowerCase().indexOf(this._filterStr) >= 0; } if (queueIt) { row.inQueue = true; if (this._nQueued++ > 50) { this.flushRowsToTable(); } } if (this._flushTimer) { window.clearTimeout(this._flushTimer); } this._flushTimer = window.setTimeout(function(){ that.flushRowsToTable(); }, 5000); }; // flush queued rows to table and sort them in according to the current sort FIRSTCLASS.layout.TableView.prototype.flushRowsToTable = function() { var i; if (!this._hidden && (this._nQueued > 0)) { if (!this._dt) { this.installListView(); } this._flushRows = []; // fetch the queued rows for (i in this._pds) { if (this._pds[i].inQueue) { delete this._pds[i].inQueue; this._flushRows.push(this._pds[i]); } } if (this._dt && this._flushRows.length > 0) { this._nQueued = 0; this._dt.addRows(this._flushRows); var sort = this._defaultSort; sort.column = false; var state = this._dt.getState(); if (state.sortedBy) { sort = state.sortedBy; } if (!sort.column) { sort.column = this._dt.getColumn(sort.key); } this._dt.sortColumn(sort.column, sort.dir); } this.fixRightHeaderBorder(); } }; FIRSTCLASS.layout.TableView.prototype.fixRightHeaderBorder = function() { // remove right-side border from header bar if (this._dt && this._nCols > 0) { var col = this._dt.getColumn(this._config.colDefs[this._nCols - 1].key); if (col) { var colHead = this._dt.getThEl(col); if (colHead) { YAHOO.util.Dom.setStyle(colHead, 'border-right', 'none'); } } } }; FIRSTCLASS.layout.TableView.prototype.deleteRowFromTable = function(row) { if (this._dt) { var rec = this._dt.getRecord(row.yId); this._dt.deleteRow(rec); } }; FIRSTCLASS.layout.TableView.prototype.activate = function() { this._dataSource.activate(); if (!this._active) { this._dataSource.fetchRowsByObjType(this._config.objtypes, true); this._active = true; } }; FIRSTCLASS.layout.TableView.prototype.deactivate = function() { this._dataSource.deactivate(); this._active = false; }; FIRSTCLASS.layout.TableView.prototype.dispose = function() { this._dt.destroy(); }; FIRSTCLASS.layout.TableView.prototype.installListView = function() { var that = this, width = this._config.width || this._config.domEl.offsetWidth, height = this._config.height || this._config.domEl.offsetHeight; this.setSize(width, height, height); var dtConfig = { height: height + "px", selectionMode: "single", initialLoad: false }; YAHOO.util.Dom.setStyle(this._domEl,'background-color',""); this._dt = new YAHOO.widget.ScrollingDataTable(this._domEl, this._config.colDefs, this._yds, dtConfig); this._dt.subscribe("rowsAddEvent", function(oRecords) { var i; if (oRecords.records.length === that._flushRows.length) { for (i in oRecords.records) { if (typeof oRecords.records[i] === "object") { if (typeof oRecords.records[i]._oData === "object") { that._flushRows[i].yId = oRecords.records[i]._sId; } } } that._flushRows = []; } }); this._dt.subscribe("rowClickEvent", function(oArgs) { that.selectClickedItem(oArgs); }); this._dt.subscribe("rowMouseoverEvent", function(oArgs) { if (that._highlightRow) { if (oArgs.target.id !== that._highlightRow) { var oldRow = that._dt.getRecord(that._highlightRow); that._dt.unhighlightRow(oldRow); } } that._highlightRow = oArgs.target.id; if (!YAHOO.util.Dom.isAncestor(that._selPanel, oArgs.target)) { that._dt.onEventHighlightRow(oArgs); } }); // reset list scroll position prior to resorting columns this._dt.subscribe('theadCellMousedownEvent', function(event) { var body = this.getBdContainerEl(); if (body) { body.scrollTop = 0; } }, this._dt); this._dt.subscribe("rowMouseoutEvent", this._dt.onEventUnhighlightRow); // crappy workaround for YUI 2.7.0 column sort bug YAHOO.widget.DataTable.prototype.getTdEl = function(cell) { var Dom = YAHOO.util.Dom, lang = YAHOO.lang, elCell, el = Dom.get(cell); // Validate HTML element if (el && (el.ownerDocument === document)) { // Validate TD element if(el.nodeName.toLowerCase() !== "td") { // Traverse up the DOM to find the corresponding TR element elCell = Dom.getAncestorByTagName(el, "td"); } else { elCell = el; } // Make sure the TD is in this TBODY if (elCell && (elCell.parentNode.parentNode === this._elTbody)) { // Now we can return the TD element return elCell; } } else if(cell) { var oRecord, nColKeyIndex; if (lang.isString(cell.columnKey) && lang.isString(cell.recordId)) { oRecord = this.getRecord(cell.recordId); var oColumn = this.getColumn(cell.columnKey); if (oColumn) { nColKeyIndex = oColumn.getKeyIndex(); } } if (cell.record && cell.column && cell.column.getKeyIndex) { oRecord = cell.record; nColKeyIndex = cell.column.getKeyIndex(); } var elRow = this.getTrEl(oRecord); if ((nColKeyIndex !== null) && elRow && elRow.cells && elRow.cells.length > 0) { return elRow.cells[nColKeyIndex]; } } return null; }; this.setSize(width, height, height); return; }; FIRSTCLASS.layout.TableView.prototype.removeListView = function() { this._domEl.innerHTML = ""; YAHOO.util.Dom.setStyle(this._domEl,'background-color','#FFFFFF'); if (this._dt) { this._dt.destroy(); this._dt = false; } }; FIRSTCLASS.layout.TableView.prototype.fillFinished = function() { this.flushRowsToTable(); this.onFillCompleted(); }; FIRSTCLASS.layout.TableView.prototype.onFillCompleted = function() { if (this._config.callback && this._config.callback.fillComplete) { this._config.callback.fillComplete(); } }; FIRSTCLASS.layout.TableView.prototype.show = function() { if (this._dt) { YAHOO.util.Dom.removeClass(this._domEl, 'fcHidden'); this._hidden = false; this.flushDeletesToTable(); this.flushRowsToTable(); } }; FIRSTCLASS.layout.TableView.prototype.hide = function() { if (this._dt) { YAHOO.util.Dom.addClass(this._domEl, 'fcHidden'); this._hidden = true; } }; FIRSTCLASS.layout.TableView.prototype.scrollSelectionIntoView = function() { // scroll selected item to top of list if (this._dt) { var bdContEl = this._dt.getBdContainerEl(); var selectedTR = this._dt.getSelectedTrEls(); if (selectedTR.length > 0) { var row = selectedTR[0]; var target = row.offsetTop - 2; // scroll if required if (target > bdContEl.scrollHeight - bdContEl.clientHeight) { target = bdContEl.scrollHeight - bdContEl.clientHeight; } if (target < 0) { target = 0; } bdContEl.scrollTop = target; } } }; FIRSTCLASS.layout.TableView.prototype.setSize = function(width, listHeight, totalHeight, lockScroll) { var calcColumnWidths = function(totalWidth, tableView) { var pSum = 0, cSum = 0, currBound = 0; var pad = tableView._totalPadding, i; for (i = 0; i < tableView._nCols; i++) { if (typeof tableView._config.colDefs[i].pcWidth === "number") { cSum += tableView._config.colDefs[i].pcWidth; currBound = (totalWidth - pad) * (cSum / 100.0); tableView._config.colDefs[i].width = Math.floor(currBound - pSum); pSum = currBound; } } }; var setColumnWidths = function(totalWidth, tableView) { var tblWidth = totalWidth, col, i, bdEl = tableView._dt.getBdContainerEl(); if (bdEl) { if (bdEl.scrollHeight > bdEl.clientHeight) { tblWidth -= 16; } } calcColumnWidths(tblWidth, tableView); col = null; for (i = 0; i < tableView._config.colDefs.length; i++) { col = tableView._dt.getColumn(tableView._config.colDefs[i].key); tableView._dt.setColumnWidth(col,tableView._config.colDefs[i].width); } }; calcColumnWidths(width, this); YAHOO.util.Dom.setStyle(this._domEl, "height", totalHeight + "px"); if (this._dt && !this._hidden) { var el = this._dt.getBdContainerEl(); listHeight -= this._dt.getHdContainerEl().offsetHeight; if (listHeight < 0) { listHeight = 0; } var preOrg = this._domEl.scrollTop; YAHOO.util.Dom.setStyle(el, "height", listHeight + "px"); if (lockScroll === 'lockScroll') { this._domEl.scrollTop = preOrg; } setColumnWidths(width, this); YAHOO.util.Dom.setStyle(el, "width", width + "px"); el = this._dt.getContainerEl(); YAHOO.util.Dom.setStyle(el, "width", width + "px"); this.fixRightHeaderBorder(); } }; FIRSTCLASS.layout.TableView.prototype.selectClickedItem = function(oArgs) { var tagName = ""; if (YAHOO.env.ua.gecko) { tagName = oArgs.event.originalTarget.tagName; } else { tagName = oArgs.event.srcElement.tagName; } if ((tagName === 'A') || (tagName === 'IMG')) { return; } // this._dt.onEventSelectRow(oArgs); this._dt.unhighlightRow(oArgs.target.id); // get the row var rec = this._dt.getRecord(oArgs.target.id); var row = rec._oData; if (!row.yId) { row.yId = oArgs.target.id; } if (this._config.callback && this._config.callback.onRowClick) { this._config.callback.onRowClick(row); } }; FIRSTCLASS.layout.TableView.prototype.quickFilterRows = function(queryStr) { // save filter string this._filterStr = queryStr; if (this._dt) { // mark/accumulate rows to display if (this._pds && this._nItems > 0) { var field = "", colname=""; var cols = ["name","col8082","tags"]; // search these var row, col; for (row in this._pds) { if (this._pds[row].threadid) { for (col=0; col < cols.length; col++) { colname = cols[col]; field = this._pds[row][colname]; if (field && field.toLowerCase().indexOf(this._filterStr) >= 0) { this._pds[row].inQueue = true; this._nQueued++; } } } } } // clear data table and re-add filtered rows this.clearTable(); this.flushRowsToTable(); } }; FIRSTCLASS.layout.TableView.prototype.unQuickFilter = function() { var i; this._filterStr = false; if (this._dt) { // mark/accumulate rows to display if (this._pds && this._nItems > 0) { for (i in this._pds) { if (!this._pds[i].inQueue) { this._pds[i].inQueue = true; this._nQueued++; } } } // clear data table if any this.clearTable(); // re-add the rows this.flushRowsToTable(); } }; FIRSTCLASS.layout.TableView.prototype.clearTable = function() { // clear data table var nRows = this._dt.getRecordSet().getLength(); if (nRows > 0) { this._dt.deleteRows(0,nRows); } }; // mark row for deletion; flush deletes if table is visible FIRSTCLASS.layout.TableView.prototype.deleteRow = function(row) { if (!this._rowsToDelete) { this._rowsToDelete = []; } this._rowsToDelete.push(row); if (!this._hidden) { this.flushDeletesToTable(); } }; FIRSTCLASS.layout.TableView.prototype.flushDeletesToTable = function() { var row; if (!this._hidden) { if (this._rowsToDelete && this._dt) { for (row in this._rowsToDelete) { if (this._rowsToDelete[row].threadid) { this.deleteRowFromTable(this._rowsToDelete[row]); } } } this._rowsToDelete = false; } }; // pds query FIRSTCLASS.layout.TableView.prototype.query = function(column, query, caseIns, compare) { var rows = [], i; var search = (caseIns) ? query.toLowerCase() : query; if (this._pds) { for (i in this._pds) { if (this._pds[i][column]) { var key = this._pds[i][column]; if (key) { if(caseIns) { key = key.toLowerCase(); } var comp = false; if (compare) { comp = compare(key, search); } else if (typeof key === "string") { comp = (key.indexOf(search) >= 0); } else { comp = (key.toString() === search); } if (comp) { rows.push([this._pds[i][column],this._pds[i]]); } } } } } return rows; }; // this is the end