(function(){ /** * XWiki namespace */ if (typeof XWiki == "undefined") { XWiki = new Object(); } /** * widgets namespace */ if (typeof XWiki.widgets == "undefined") { XWiki.widgets = new Object(); } /** * The class representing an AJAX-populated live table. * It is (almost) independent of the underlying HTML markup, a function passed as an argument being * responsible with displaying the content corresponding to a row. Uses JSON for the response * encoding. */ XWiki.widgets.LiveTable = Class.create({ /** * @param url The base address for requesting the table data. * @param domNodeName The node supposed to hold the data rows, should be a . DOM element or * identifier. * @param handler A javascript function called for displaying fetched rows. The function * accepts a JSON-parsed object and returns a DOM node * @param options An object with options for the live table. Supported options: * * @todo Make this a valid ARIA table: http://www.w3.org/TR/aria-role/#structural */ initialize: function(url, domNodeName, handler, options) { if (!options) { var options = {}; } // id of the root element that encloses this livetable this.domNodeName = domNodeName; // id of the display element (the inner dynamic table) of this livetable // defined by convention as the root node id on which is appenned "-display". // fallback on the unique "display1" id for backward compatibility. this.displayNode = $(domNodeName + "-display") || $('display1'); // Node under which all forms controls (input, selects, etc.) will be filters for this table this.filtersNode = options.filtersNode || $(domNodeName).down(".xwiki-livetable-display-filters"); // Array of nodes under which pagination for this livetable will be displayed. this.paginationNodes = options.paginationNodes || $(this.domNodeName).select(".xwiki-livetable-pagination"); if (typeof options == "undefined") { options = {}; } this.limit = options.limit || 10; this.action = options.action || "view"; // FIXME check if this can be removed safely. this.permalinks = options.permalinks || true; // Initialize pagination if (typeof this.paginationNodes != "undefined") { this.paginator = new LiveTablePagination(this, this.paginationNodes, options.maxPages || 10); } // Initialize filters if (this.filtersNode) { var initialFilters = this.permalinks ? this.getFiltersFromHash() : new Object(); this.filter = new LiveTableFilter(this, this.filtersNode, initialFilters); } if ($(domNodeName + "-tagcloud")) { this.tagCloud = new LiveTableTagCloud(this, domNodeName + "-tagcloud"); } this.loadingStatus = $(this.domNodeName + '-ajax-loader') || $('ajax-loader'); this.limitsDisplay = $(this.domNodeName + '-limits') || new Element("div"); this.filters = ""; this.handler = handler || function(){}; this.totalRows = -1; this.fetchedRows = new Array(); this.getUrl = url; this.lastoffset = 1; this.sendReqNo = 0; this.recvReqNo = 0; this.observeSortableColumns(); var initialPage = this.permalinks ? this.getPageFromHash() : 1; this.currentOffset = (initialPage - 1) * this.limit + 1; // Show initial rows this.showRows(this.currentOffset, this.limit); }, /** * Re-write location hash with current page and filters values */ updateLocationHash: function() { var currentHash = window.location.hash.substring(1); var filterString = this.filter.serializeFilters(); var shouldUpdate = this.lastoffset != 1 || !currentHash.blank() || !filterString.blank(); if (shouldUpdate) { var tables = currentHash.split("|"), newHash = ""; for (var i=0;i this.totalRows) f = this.totalRows; var off = (this.totalRows > 0) ? offset : 0; var msg = "" + off + " - " + f + " $msg.get('xe.pagination.results.of') " + this.totalRows + ""; var msg = msg.toLowerCase(); this.limitsDisplay.innerHTML = "$msg.get('xe.pagination.results') " + msg; this.clearDisplay(); for (var i = off; i <= f; i++) { if (this.fetchedRows[i]) { var elem = this.handler(this.fetchedRows[i], i, this); this.displayNode.appendChild(elem); document.fire("xwiki:livetable:newrow", { "row":elem, "table":this }); } } if (this.paginator) this.paginator.refreshPagination(); }, /** * Fetch and display rows. This method checks the existing fetched data to determine which (if * any) rows should be fetched from the server, then forwards the call to {@link #displayRows}. * @param offset Starting offset; the index of the first row that should be displayed. * @param limit Maximum number of rows to display. */ showRows: function(offset, limit) { this.lastoffset = offset; if (this.permalinks) { this.updateLocationHash(); } // This is some debugging string. var buff = 'request to display rows '+offset+' to '+(offset+limit)+'
\n'; // If no rows fetched yet, get all we need if (this.totalRows == -1) { this.getRows(offset, limit, offset, limit); buff += 'table is empty so we get all rows'; return buff; } // Make a range of required rows var min = -1; var max = -1; for (var i = offset; i < (offset + limit); ++i) { if (this.fetchedRows[i] == undefined) { if (min == -1) { min = i; } max = i; } } // If we don't need any new row if (min == -1) { buff += 'no need to get new rows
\n'; this.displayRows(offset, limit); } else { // We need to get new rows buff += 'we need to get rows '+min+' to '+ (max+1) +'
\n'; this.getRows(min, max - min + 1, offset, limit); } if(this.paginator) this.paginator.refreshPagination(); return buff; }, /** * Delete a row from the fetch cache, shifting the remaining rows accordingly. */ deleteAndShiftRows: function(indx) { for(i in this.fetchedRows) { if(i >= indx) this.fetchedRows[i] = this.fetchedRows[''+(parseInt(i)+1)]; } }, /** * Debug method. Dumps the content of the fetch cache (row indexes only). */ debugFetchedRows: function() { var buf = ''; for (i in this.fetchedRows) { if (this.fetchedRows[i] != undefined) { buf += i+' '; } } return buf; }, /** * Delete a row and redisplay the table. * @param indx The index of the row that must be deleted. */ deleteRow: function(indx) { this.deleteAndShiftRows(indx); // Compute new refresh offset var newoffset = this.lastoffset; if(indx > this.totalRows - this.limit - 1) { newoffset -= 1; } if(newoffset <= 0) { newoffset = 1; } this.totalRows -= 1; if(this.totalRows < this.limit) { this.showRows(newoffset, this.totalRows); } else { this.showRows(newoffset, this.limit); } if (this.paginator) this.paginator.refreshPagination(); }, /** * Return the URL fragment with sort parameters depending on the state of the table. */ getSortURLFragment:function() { var fragment = "&sort="; if (typeof $(this.domNodeName).down("th.selected a") != "undefined") { fragment += $(this.domNodeName).down("th.selected a").getAttribute('rel'); } fragment += "&dir="; if (typeof $(this.domNodeName).down("th.selected") != "undefined") { fragment += ($(this.domNodeName).down("th.selected").hasClassName('desc') ? 'desc' : 'asc'); } return fragment; }, /** * Remove all the fetched data from the cache. */ clearCache: function() { this.fetchedRows.clear(); this.totalRows = -1; }, /** * Iterate over the column headers that have the sortable class to observe sort changes when user clicks the column header. */ observeSortableColumns: function(){ var self = this; $(this.domNodeName).select('th.sortable').each(function(el) { if (el.hasClassName('selected')) { self.selectedColumn = el; } if(!el.hasClassName('desc') && !el.hasClassName('asc')) { // no order set in the HTML. Force desc el.addClassName('desc'); } Event.observe(el, "click", function(event) { var elem = event.element(); if (!elem.hasClassName('sortable')) { elem = elem.up('th.sortable'); } if (elem == null) { // This should never happen in real life, but better safe than sorry... return; } if (elem.hasClassName("selected")) { // Sort column already selected. Change direction var direction = elem.hasClassName("asc") ? "asc" : "desc"; var newDirection = direction == "asc" ? "desc" : "asc"; elem.removeClassName(direction); elem.addClassName(newDirection); } else { // sort column was not selected, do not change direction, just column if (self.selectedColumn){ self.selectedColumn.removeClassName("selected"); } elem.addClassName("selected"); self.selectedColumn = elem; } self.clearCache(); //reset self.showRows(1, self.limit); }); }); } }); /** * Helper class to display pagination */ var LiveTablePagination = Class.create({ initialize: function(table, domNodes, max) { this.table = table; var self = this; this.pagesNodes = []; domNodes.each(function(elem){ self.pagesNodes.push(elem.down(".xwiki-livetable-pagination-content")); }); this.max = max; $(this.table.domNodeName).select("span.prevPagination").invoke("observe", "click", this.gotoPrevPage.bind(this)); $(this.table.domNodeName).select("span.nextPagination").invoke("observe", "click", this.gotoNextPage.bind(this)); }, refreshPagination: function() { var self = this; this.pagesNodes.each(function(elem){ elem.innerHTML = ""; }); var pages = Math.ceil(this.table.totalRows / this.table.limit); var currentMax = (!this.max) ? pages : this.max; var currentPage = Math.floor( this.table.lastoffset / this.table.limit) + 1; var startPage = Math.floor(currentPage / currentMax) * currentMax - 1; // always display the first page if (startPage>1) { this.pagesNodes.each(function(elem){ elem.insert(self.createPageLink(1, false)); }); if (startPage>2) { this.pagesNodes.invoke("insert", " ... "); } } // display pages var i; for (i=(startPage<=0) ? 1 : startPage;i<=Math.min(startPage + currentMax + 1, pages);i++) { var selected = (currentPage == i) ? true : false this.pagesNodes.each(function(elem){ elem.insert(self.createPageLink(i, selected)); }); this.pagesNodes.invoke("insert", " "); } // alwyas display the last page. if (i 0) { this.table.showRows(((parseInt(prevPage) - 1) * this.table.limit) + 1, this.table.limit); } }, gotoNextPage: function() { var currentPage = Math.floor( this.table.lastoffset / this.table.limit) + 1; var pages = Math.ceil(this.table.totalRows / this.table.limit); var nextPage = currentPage + 1; if (nextPage <= pages) { this.table.showRows(((parseInt(nextPage) - 1) * this.table.limit) + 1, this.table.limit); } } }); /* * The class that deals with the filtering in a table */ var LiveTableFilter = Class.create({ initialize: function(table, filterNode, filters) { this.table = table; this.filterNode = $(filterNode); this.filters = new Object(); this.filters = filters; this.initializeFilters(); this.attachEventHandlers(); }, makeRefreshHandler: function(self) { return function() { self.refreshContent(); } }, /** * Initialize DOM values of the filters elements based on the passed map of name/value. * TODO: rewrite this method the other way around (iterate on the map, not on the filters). */ initializeFilters: function() { var inputs = this.filterNode.select("input"); for(var i=0;i 0) { this.updateTagCloud(tags); } } }, /** * Tags cardinality map. Empty at first. */ tags: [], /** * Tags matching the current filters */ matchingTags: [], /** * Tags selected as filters */ selectedTags: {}, /** * Default popularity levels. Used as CSS class on the tag list items. */ popularityLevels: ["notPopular", "notVeryPopular", "somewhatPopular", "popular", "veryPopular", "ultraPopular"], /** * Update the tag cloud with new tags values. * This is the only hook the table will call us from. */ updateTagCloud: function(tags, matchingTags) { if (!this.hasTags && tags.length > 0) { this.tags = tags; this.map = this.buildPopularityMap(this.tags); this.hasTags = true; this.domNode.removeClassName("hidden"); } this.matchingTags = matchingTags; this.displayTagCloud(); }, displayTagCloud: function(){ this.domNode.down('.xwiki-livetable-tagcloud').innerHTML = ""; var cloud = new Element("ol", {'class':'tagCloud'}); var liClass; for (var i=0;i= levels[j] || (j == (levels.length - 1))) { liClass = this.map.get(levels[j]); break; } } var tagLabel = this.tags[i].tag; var tagSpan = new Element("span").update(tagLabel.escapeHTML()); var tag = new Element("li", {'class':liClass}).update(tagSpan); if (typeof this.matchingTags[tagLabel] != "undefined") { tag.addClassName("selectable"); Event.observe(tagSpan, "click", function(event) { var tag = event.element().up("li").down("span").innerHTML.unescapeHTML(); event.element().up("li").toggleClassName("selected"); if (event.element().up("li").hasClassName("selected")) { self.selectedTags[tag] = {}; } else { delete self.selectedTags[tag]; } self.table.tags = self.getSelectedTags(); self.table.totalRows = -1; self.table.fetchedRows = new Array(); self.table.showRows(1, self.table.limit); }); } if (this.selectedTags[tagLabel] != undefined) { tag.addClassName("selected"); } var self = this; tag.appendChild(document.createTextNode(" ")); cloud.appendChild(tag); } this.domNode.down('.xwiki-livetable-tagcloud').appendChild(cloud); }, getSelectedTags: function() { var result = new Array(); this.domNode.select("li.selected").each(function(tag) { result.push(tag.down("span").innerHTML); }); return result; }, /** * Transform the cardinality map of tags in a map of CSS classes. * Ported to JS from XWiki.TagCloud for consistency. */ buildPopularityMap:function(tags){ var totalCount = 0; var minCount = 0; var maxCount = -1; tags.each(function(tag){ totalCount += tag.count; if(tag.count < minCount || minCount === 0) { minCount = tag.count; } if(tag.count > maxCount || maxCount === -1) { maxCount = tag.count; } }); var countAverage = totalCount / tags.length; var levelsHalf = this.popularityLevels.length / 2; var firstHalfCountDelta = countAverage - minCount; var secondHalfCountDelta = maxCount - countAverage; var firstHalfIntervalSize = firstHalfCountDelta / levelsHalf; var secondHalfIntervalSize = secondHalfCountDelta / levelsHalf; var previousPopularityMax = minCount; var intervalSize = firstHalfIntervalSize; var halfPassed = false; var count = 0; var currentPopularityMax; var popularityMap = new Hash(); this.popularityLevels.each(function(level){ count++; if(count > levelsHalf && !halfPassed) { intervalSize = secondHalfIntervalSize; halfPassed = true; } currentPopularityMax = previousPopularityMax + intervalSize; popularityMap.set(currentPopularityMax, level); previousPopularityMax = currentPopularityMax; }); return popularityMap; } }); /** * The Ugly: Fix IE6 * Add specific classes when mouse is over table rows, since it cannot be handled in CSS. */ if(browser.isIE6x) { // get notified of all new rows created by live tables. document.observe("xwiki:livetable:newrow", function(ev) { // Add events listeners to mouse over/out on the Event.observe(ev.memo.row, "mouseover", function(event){ event.element().up("tr").addClassName("rowHover"); }); Event.observe(ev.memo.row, "mouseout", function(event){ event.element().up("tr").removeClassName("rowHover"); }); }); } })();