| /* |
| Copyright (c) 2004-2006, The Dojo Foundation |
| All Rights Reserved. |
| |
| Licensed under the Academic Free License version 2.1 or above OR the |
| modified BSD license. For more information on Dojo licensing, see: |
| |
| http://dojotoolkit.org/community/licensing.shtml |
| */ |
| |
| dojo.provide("dojo.widget.SortableTable"); |
| |
| dojo.deprecated("SortableTable will be removed in favor of FilteringTable.", "0.5"); |
| |
| dojo.require("dojo.lang.common"); |
| dojo.require("dojo.date.format"); |
| dojo.require("dojo.html.*"); |
| dojo.require("dojo.html.selection"); |
| dojo.require("dojo.html.util"); |
| dojo.require("dojo.html.style"); |
| dojo.require("dojo.event.*"); |
| dojo.require("dojo.widget.*"); |
| dojo.require("dojo.widget.HtmlWidget"); |
| |
| dojo.widget.defineWidget( |
| "dojo.widget.SortableTable", |
| dojo.widget.HtmlWidget, |
| function(){ |
| this.data=[]; |
| this.selected=[]; // always an array to handle multiple selections. |
| this.columns=[]; |
| }, |
| { |
| // custom properties |
| enableMultipleSelect: false, |
| maximumNumberOfSelections: 0, // 0 for unlimited, is the default. |
| enableAlternateRows: false, |
| minRows: 0, // 0 means ignore. |
| defaultDateFormat: "%D", |
| sortIndex: 0, // index of the column sorted on, first is the default. |
| sortDirection: 0, // 0==asc, 1==desc |
| valueField: "Id", // if a JSON structure is parsed and there is a field of this name, |
| // a value attribute will be added to the row (tr value="{Id}") |
| |
| headClass: "", |
| tbodyClass: "", |
| headerClass: "", |
| headerSortUpClass: "selected", |
| headerSortDownClass: "selected", |
| rowClass: "", |
| rowAlternateClass: "alt", |
| rowSelectedClass: "selected", |
| columnSelected: "sorted-column", |
| |
| isContainer: false, |
| templatePath:null, |
| templateCssPath:null, |
| |
| getTypeFromString:function(/* string */ s){ |
| // summary |
| // Find the constructor that matches param s by searching through the entire object tree. |
| var parts=s.split("."),i=0,obj=dj_global; |
| do{obj=obj[parts[i++]];}while(i<parts.length&&obj); |
| return(obj!=dj_global)?obj:null; // function |
| }, |
| compare:function(/* object */ o1, /* object */ o2){ |
| // summary |
| // Compare two objects using a shallow property compare |
| for(var p in o1){ |
| if(!(p in o2)) return false; // boolean |
| if(o1[p].valueOf()!=o2[p].valueOf()) return false; // boolean |
| } |
| return true; // boolean |
| }, |
| isSelected:function(/* object */ o){ |
| // summary |
| // checked to see if the passed object is in the current selection. |
| for(var i=0;i<this.selected.length;i++){ |
| if(this.compare(this.selected[i],o)){ |
| return true; // boolean |
| } |
| } |
| return false; // boolean |
| }, |
| removeFromSelected:function(/* object */ o){ |
| // summary |
| // remove the passed object from the current selection. |
| var idx=-1; |
| for(var i=0;i<this.selected.length;i++){ |
| if(this.compare(this.selected[i],o)){ |
| idx=i; |
| break; |
| } |
| } |
| if(idx>=0){ |
| this.selected.splice(idx,1); |
| } |
| }, |
| getSelection:function(){ |
| // summary |
| // return the array of currently selected objects (JSON format) |
| return this.selected; // array |
| }, |
| getValue:function(){ |
| // summary |
| // return a comma-delimited list of selected valueFields. |
| var a=[]; |
| for(var i=0;i<this.selected.length;i++){ |
| if (this.selected[i][this.valueField]){ |
| a.push(this.selected[i][this.valueField]); |
| } |
| } |
| return a.join(); // string |
| }, |
| reset:function(){ |
| // summary |
| // completely resets the internal representations. |
| this.columns=[]; |
| this.data=[]; |
| this.resetSelections(this.domNode.getElementsByTagName("tbody")[0]); |
| }, |
| resetSelections:function(/* HTMLTableBodyElement */ body){ |
| this.selected=[]; |
| var idx=0; |
| var rows=body.getElementsByTagName("tr"); |
| for(var i=0; i<rows.length; i++){ |
| if(rows[i].parentNode==body){ |
| rows[i].removeAttribute("selected"); |
| if(this.enableAlternateRows&&idx%2==1){ |
| rows[i].className=this.rowAlternateClass; |
| }else{ |
| rows[i].className=""; |
| } |
| idx++; |
| } |
| } |
| }, |
| |
| getObjectFromRow:function(/* HTMLTableRowElement */ row){ |
| // summary |
| // creates a JSON object based on the passed row |
| var cells=row.getElementsByTagName("td"); |
| var o={}; |
| for(var i=0; i<this.columns.length;i++){ |
| if(this.columns[i].sortType=="__markup__"){ |
| // FIXME: should we parse this instead? Because if the user may not get back the markup they put in... |
| o[this.columns[i].getField()]=cells[i].innerHTML; |
| }else{ |
| var text=dojo.html.renderedTextContent(cells[i]); |
| var val=text; |
| if (this.columns[i].getType() != String){ |
| var val=new (this.columns[i].getType())(text); |
| } |
| o[this.columns[i].getField()]=val; |
| } |
| } |
| if(dojo.html.hasAttribute(row,"value")){ |
| o[this.valueField]=dojo.html.getAttribute(row,"value"); |
| } |
| return o; // object |
| }, |
| setSelectionByRow:function(/* HTMLTableElementRow */ row){ |
| // summary |
| // create the selection object based on the passed row, makes sure it's unique. |
| // note that you need to call render manually (because of multi-select operations) |
| var o=this.getObjectFromRow(row); |
| var b=false; |
| for(var i=0;i<this.selected.length;i++){ |
| if(this.compare(this.selected[i], o)){ |
| b=true; |
| break; |
| } |
| } |
| if(!b){ |
| this.selected.push(o); |
| } |
| }, |
| |
| parseColumns:function(/* HTMLTableHeadElement */ node){ |
| // summary |
| // parses the passed element to create column objects |
| this.reset(); |
| var row=node.getElementsByTagName("tr")[0]; |
| var cells=row.getElementsByTagName("td"); |
| if (cells.length==0) cells=row.getElementsByTagName("th"); |
| for(var i=0; i<cells.length; i++){ |
| var o={ |
| field:null, |
| format:null, |
| noSort:false, |
| sortType:"String", |
| dataType:String, |
| sortFunction:null, |
| label:null, |
| align:"left", |
| valign:"middle", |
| getField:function(){ return this.field||this.label; }, |
| getType:function(){ return this.dataType; } |
| }; |
| // presentation attributes |
| if(dojo.html.hasAttribute(cells[i], "align")){ |
| o.align=dojo.html.getAttribute(cells[i],"align"); |
| } |
| if(dojo.html.hasAttribute(cells[i], "valign")){ |
| o.valign=dojo.html.getAttribute(cells[i],"valign"); |
| } |
| |
| // sorting features. |
| if(dojo.html.hasAttribute(cells[i], "nosort")){ |
| o.noSort=dojo.html.getAttribute(cells[i],"nosort")=="true"; |
| } |
| if(dojo.html.hasAttribute(cells[i], "sortusing")){ |
| var trans=dojo.html.getAttribute(cells[i],"sortusing"); |
| var f=this.getTypeFromString(trans); |
| if (f!=null && f!=window && typeof(f)=="function") |
| o.sortFunction=f; |
| } |
| |
| if(dojo.html.hasAttribute(cells[i], "field")){ |
| o.field=dojo.html.getAttribute(cells[i],"field"); |
| } |
| if(dojo.html.hasAttribute(cells[i], "format")){ |
| o.format=dojo.html.getAttribute(cells[i],"format"); |
| } |
| if(dojo.html.hasAttribute(cells[i], "dataType")){ |
| var sortType=dojo.html.getAttribute(cells[i],"dataType"); |
| if(sortType.toLowerCase()=="html"||sortType.toLowerCase()=="markup"){ |
| o.sortType="__markup__"; // always convert to "__markup__" |
| o.noSort=true; |
| }else{ |
| var type=this.getTypeFromString(sortType); |
| if(type){ |
| o.sortType=sortType; |
| o.dataType=type; |
| } |
| } |
| } |
| o.label=dojo.html.renderedTextContent(cells[i]); |
| this.columns.push(o); |
| |
| // check to see if there's a default sort, and set the properties necessary |
| if(dojo.html.hasAttribute(cells[i], "sort")){ |
| this.sortIndex=i; |
| var dir=dojo.html.getAttribute(cells[i], "sort"); |
| if(!isNaN(parseInt(dir))){ |
| dir=parseInt(dir); |
| this.sortDirection=(dir!=0)?1:0; |
| }else{ |
| this.sortDirection=(dir.toLowerCase()=="desc")?1:0; |
| } |
| } |
| } |
| }, |
| |
| parseData:function(/* array */ data){ |
| // summary |
| // Parse the passed JSON data structure, and cast based on columns. |
| this.data=[]; |
| this.selected=[]; |
| for(var i=0; i<data.length; i++){ |
| var o={}; // new data object. |
| for(var j=0; j<this.columns.length; j++){ |
| var field=this.columns[j].getField(); |
| if(this.columns[j].sortType=="__markup__"){ |
| o[field]=String(data[i][field]); |
| }else{ |
| var type=this.columns[j].getType(); |
| var val=data[i][field]; |
| var t=this.columns[j].sortType.toLowerCase(); |
| if(type == String) { |
| o[field]=val; |
| } else { |
| if(val!=null){ |
| o[field]=new type(val); |
| }else{ |
| o[field]=new type(); // let it use the default. |
| } |
| } |
| } |
| } |
| // check for the valueField if not already parsed. |
| if(data[i][this.valueField]&&!o[this.valueField]){ |
| o[this.valueField]=data[i][this.valueField]; |
| } |
| this.data.push(o); |
| } |
| }, |
| |
| parseDataFromTable:function(/* HTMLTableBodyElement */ tbody){ |
| // summary |
| // parses the data in the tbody of a table to create a set of objects. |
| // Will add objects to this.selected if an attribute 'selected="true"' is present on the row. |
| this.data=[]; |
| this.selected=[]; |
| var rows=tbody.getElementsByTagName("tr"); |
| for(var i=0; i<rows.length; i++){ |
| if(dojo.html.getAttribute(rows[i],"ignoreIfParsed")=="true"){ |
| continue; |
| } |
| var o={}; // new data object. |
| var cells=rows[i].getElementsByTagName("td"); |
| for(var j=0; j<this.columns.length; j++){ |
| var field=this.columns[j].getField(); |
| if(this.columns[j].sortType=="__markup__"){ |
| // FIXME: parse this? |
| o[field]=cells[j].innerHTML; |
| }else{ |
| var type=this.columns[j].getType(); |
| var val=dojo.html.renderedTextContent(cells[j]); // should be the same index as the column. |
| if(type == String){ |
| o[field]=val; |
| } else { |
| if (val!=null){ |
| o[field]=new type(val); |
| } else { |
| o[field]=new type(); // let it use the default. |
| } |
| } |
| } |
| } |
| if(dojo.html.hasAttribute(rows[i],"value")&&!o[this.valueField]){ |
| o[this.valueField]=dojo.html.getAttribute(rows[i],"value"); |
| } |
| // FIXME: add code to preserve row attributes in __metadata__ field? |
| this.data.push(o); |
| |
| // add it to the selections if selected="true" is present. |
| if(dojo.html.getAttribute(rows[i],"selected")=="true"){ |
| this.selected.push(o); |
| } |
| } |
| }, |
| |
| showSelections:function(){ |
| var body=this.domNode.getElementsByTagName("tbody")[0]; |
| var rows=body.getElementsByTagName("tr"); |
| var idx=0; |
| for(var i=0; i<rows.length; i++){ |
| if(rows[i].parentNode==body){ |
| if(dojo.html.getAttribute(rows[i],"selected")=="true"){ |
| rows[i].className=this.rowSelectedClass; |
| } else { |
| if(this.enableAlternateRows&&idx%2==1){ |
| rows[i].className=this.rowAlternateClass; |
| }else{ |
| rows[i].className=""; |
| } |
| } |
| idx++; |
| } |
| } |
| }, |
| render:function(bDontPreserve){ |
| // summary |
| // renders the table to the browser |
| var data=[]; |
| var body=this.domNode.getElementsByTagName("tbody")[0]; |
| |
| if(!bDontPreserve){ |
| // rebuild data and selection |
| this.parseDataFromTable(body); |
| } |
| |
| // clone this.data for sorting purposes. |
| for(var i=0; i<this.data.length; i++){ |
| data.push(this.data[i]); |
| } |
| |
| var col=this.columns[this.sortIndex]; |
| if(!col.noSort){ |
| var field=col.getField(); |
| if(col.sortFunction){ |
| var sort=col.sortFunction; |
| }else{ |
| var sort=function(a,b){ |
| if (a[field]>b[field]) return 1; |
| if (a[field]<b[field]) return -1; |
| return 0; |
| } |
| } |
| data.sort(sort); |
| if(this.sortDirection!=0) data.reverse(); |
| } |
| |
| // build the table and pop it in. |
| while(body.childNodes.length>0) body.removeChild(body.childNodes[0]); |
| for(var i=0; i<data.length;i++){ |
| var row=document.createElement("tr"); |
| dojo.html.disableSelection(row); |
| if (data[i][this.valueField]){ |
| row.setAttribute("value",data[i][this.valueField]); |
| } |
| if(this.isSelected(data[i])){ |
| row.className=this.rowSelectedClass; |
| row.setAttribute("selected","true"); |
| } else { |
| if(this.enableAlternateRows&&i%2==1){ |
| row.className=this.rowAlternateClass; |
| } |
| } |
| for(var j=0;j<this.columns.length;j++){ |
| var cell=document.createElement("td"); |
| cell.setAttribute("align", this.columns[j].align); |
| cell.setAttribute("valign", this.columns[j].valign); |
| dojo.html.disableSelection(cell); |
| if(this.sortIndex==j){ |
| cell.className=this.columnSelected; |
| } |
| if(this.columns[j].sortType=="__markup__"){ |
| cell.innerHTML=data[i][this.columns[j].getField()]; |
| for(var k=0; k<cell.childNodes.length; k++){ |
| var node=cell.childNodes[k]; |
| if(node&&node.nodeType==dojo.html.ELEMENT_NODE){ |
| dojo.html.disableSelection(node); |
| } |
| } |
| }else{ |
| if(this.columns[j].getType()==Date){ |
| var format=this.defaultDateFormat; |
| if(this.columns[j].format) format=this.columns[j].format; |
| cell.appendChild(document.createTextNode(dojo.date.strftime(data[i][this.columns[j].getField()], format))); |
| }else{ |
| cell.appendChild(document.createTextNode(data[i][this.columns[j].getField()])); |
| } |
| } |
| row.appendChild(cell); |
| } |
| body.appendChild(row); |
| dojo.event.connect(row, "onclick", this, "onUISelect"); |
| } |
| |
| // if minRows exist. |
| var minRows=parseInt(this.minRows); |
| if (!isNaN(minRows) && minRows>0 && data.length<minRows){ |
| var mod=0; |
| if(data.length%2==0) mod=1; |
| var nRows=minRows-data.length; |
| for(var i=0; i<nRows; i++){ |
| var row=document.createElement("tr"); |
| row.setAttribute("ignoreIfParsed","true"); |
| if(this.enableAlternateRows&&i%2==mod){ |
| row.className=this.rowAlternateClass; |
| } |
| for(var j=0;j<this.columns.length;j++){ |
| var cell=document.createElement("td"); |
| cell.appendChild(document.createTextNode("\u00A0")); |
| row.appendChild(cell); |
| } |
| body.appendChild(row); |
| } |
| } |
| }, |
| |
| // the following the user can override. |
| onSelect:function(/* DomEvent */ e){ |
| // summary |
| // empty function for the user to attach code to, fired by onUISelect |
| }, |
| onUISelect:function(/* DomEvent */ e){ |
| // summary |
| // fired when a user selects a row |
| var row=dojo.html.getParentByType(e.target,"tr"); |
| var body=dojo.html.getParentByType(row,"tbody"); |
| if(this.enableMultipleSelect){ |
| if(e.metaKey||e.ctrlKey){ |
| if(this.isSelected(this.getObjectFromRow(row))){ |
| this.removeFromSelected(this.getObjectFromRow(row)); |
| row.removeAttribute("selected"); |
| }else{ |
| // push onto the selection stack. |
| this.setSelectionByRow(row); |
| row.setAttribute("selected","true"); |
| } |
| }else if(e.shiftKey){ |
| // the tricky one. We need to figure out the *last* selected row above, |
| // and select all the rows in between. |
| var startRow; |
| var rows=body.getElementsByTagName("tr"); |
| // if there's a selection above, we go with that first. |
| for(var i=0;i<rows.length;i++){ |
| if(rows[i].parentNode==body){ |
| if(rows[i]==row) break; |
| if(dojo.html.getAttribute(rows[i],"selected")=="true"){ |
| startRow=rows[i]; |
| } |
| } |
| } |
| // if there isn't a selection above, we continue with a selection below. |
| if(!startRow){ |
| startRow=row; |
| for(;i<rows.length;i++){ |
| if(dojo.html.getAttribute(rows[i],"selected")=="true"){ |
| row=rows[i]; |
| break; |
| } |
| } |
| } |
| this.resetSelections(body); |
| if(startRow==row){ |
| // this is the only selection |
| row.setAttribute("selected","true"); |
| this.setSelectionByRow(row); |
| }else{ |
| var doSelect=false; |
| for(var i=0; i<rows.length; i++){ |
| if(rows[i].parentNode==body){ |
| rows[i].removeAttribute("selected"); |
| if(rows[i]==startRow){ |
| doSelect=true; |
| } |
| if(doSelect){ |
| this.setSelectionByRow(rows[i]); |
| rows[i].setAttribute("selected","true"); |
| } |
| if(rows[i]==row){ |
| doSelect=false; |
| } |
| } |
| } |
| } |
| }else{ |
| // reset the selection |
| this.resetSelections(body); |
| row.setAttribute("selected","true"); |
| this.setSelectionByRow(row); |
| } |
| }else{ |
| // reset the data selection and go. |
| this.resetSelections(body); |
| row.setAttribute("selected","true"); |
| this.setSelectionByRow(row); |
| } |
| this.showSelections(); |
| this.onSelect(e); |
| e.stopPropagation(); |
| e.preventDefault(); |
| }, |
| onHeaderClick:function(/* DomEvent */ e){ |
| // summary |
| // Main handler function for each header column click. |
| var oldIndex=this.sortIndex; |
| var oldDirection=this.sortDirection; |
| var source=e.target; |
| var row=dojo.html.getParentByType(source,"tr"); |
| var cellTag="td"; |
| if(row.getElementsByTagName(cellTag).length==0) cellTag="th"; |
| |
| var headers=row.getElementsByTagName(cellTag); |
| var header=dojo.html.getParentByType(source,cellTag); |
| |
| for(var i=0; i<headers.length; i++){ |
| if(headers[i]==header){ |
| if(i!=oldIndex){ |
| // new col. |
| this.sortIndex=i; |
| this.sortDirection=0; |
| headers[i].className=this.headerSortDownClass |
| }else{ |
| this.sortDirection=(oldDirection==0)?1:0; |
| if(this.sortDirection==0){ |
| headers[i].className=this.headerSortDownClass; |
| }else{ |
| headers[i].className=this.headerSortUpClass; |
| } |
| } |
| }else{ |
| // reset the header class. |
| headers[i].className=this.headerClass; |
| } |
| } |
| this.render(); |
| }, |
| |
| postCreate:function(){ |
| // summary |
| // overridden from HtmlWidget, initializes and renders the widget. |
| var thead=this.domNode.getElementsByTagName("thead")[0]; |
| if(this.headClass.length>0){ |
| thead.className=this.headClass; |
| } |
| |
| // disable selections |
| dojo.html.disableSelection(this.domNode); |
| |
| // parse the columns. |
| this.parseColumns(thead); |
| |
| // attach header handlers. |
| var header="td"; |
| if(thead.getElementsByTagName(header).length==0) header="th"; |
| var headers=thead.getElementsByTagName(header); |
| for(var i=0; i<headers.length; i++){ |
| if(!this.columns[i].noSort){ |
| dojo.event.connect(headers[i], "onclick", this, "onHeaderClick"); |
| } |
| if(this.sortIndex==i){ |
| if(this.sortDirection==0){ |
| headers[i].className=this.headerSortDownClass; |
| }else{ |
| headers[i].className=this.headerSortUpClass; |
| } |
| } |
| } |
| |
| // parse the tbody element and re-render it. |
| var tbody=this.domNode.getElementsByTagName("tbody")[0]; |
| if (this.tbodyClass.length>0) { |
| tbody.className=this.tbodyClass; |
| } |
| |
| this.parseDataFromTable(tbody); |
| this.render(true); |
| } |
| } |
| ); |