Index: dojo-release-1.3.2-src/dojox/iconGrid/DataIconGrid.js =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ dojo-release-1.3.2-src/dojox/iconGrid/DataIconGrid.js 2010-03-23 05:29:16.000000000 -0700 @@ -0,0 +1,1209 @@ +dojo.provide('dojox.iconGrid.DataIconGrid'); + +dojo.require('dijit._Widget'); +dojo.require('dijit._Templated'); +dojo.require('dijit._Container'); +dojo.require('dojox.image.Loader'); +dojo.require('dojox.html.metrics'); +dojo.require('dojox.xmpp.util'); + + +dojo.declare('dojox.iconGrid.DataIconGridRow', [dijit._Widget, dijit._Templated, dijit._Container, dijit._Contained], { + templateString: '
' +}); + +dojo.declare('dojox.iconGrid.DataIconGridItem', [dijit._Widget, dijit._Templated, dijit._Contained], { + // item: dojo.data.Item + item: null, + // store: dojo.data.Store + store: null, + + // grid: dojox.iconGrid.DataIconGrid + grid: null, + + // layout: Object + // Map of the store fields to use for getting the values. + layout: { + thumbnail: { + field: 'thumbnail_s' + }, + captions: [ + {field: 'title'}, + {field: 'sizeHumanReadable'}, + {field: ''}, + {field: ''} + ] + }, + + focus: function(){ + //console.debug(this.id+'::focus'); + this.caption1node.setAttribute('tabIndex', '0'); + dijit.focus(this.caption1node); + if(dojo.isIE == 6 || dojo.isIE == 7){ + this.domNode.scrollLeft = 0; + } + }, + + onClick: function(){ + //console.debug(this.id+'::onClick'); + }, + + onBlur: function(){ + //console.debug(this.id+'::onBlur'); + }, + + onFocus: function(){ + //console.debug(this.id+'::onFocus'); + }, + + // + // + // + // + // + // + // + + + // + // + // + // + // + // + + templatePath: dojo.moduleUrl("dojox.iconGrid","resources/DataIconGridItem.html"), + postCreate: function(){ + if(this.item === null || this.store === null){ + //console.debug(this.id+'::postCreate does not have a store item or a store.'); + this._updateWithSampleData(); + return; + } + this._update(); + }, + + _update: function(){ + dojo.forEach([1, 2, 3, 4], function(i){ + var node = this['caption'+i+'node']; + var field = this.layout.captions[i-1].field; + if(field){ + node.innerHTML = dojox.xmpp.util.xmlEncode(this.store.getValue(this.item, field)); + dojo.removeClass(node, 'dijitHidden'); + } + else{ + dojo.addClass(node, 'dijitHidden'); + } + }, this); + }, + + _updateWithSampleData: function(){ + dojo.forEach([1, 2, 3, 4], function(i){ + var node = this['caption'+i+'node']; + var field = this.layout.captions[i-1].field; + if(field){ + dojo.removeClass(node, 'dijitHidden'); + node.innerHTML = "sample" + } + else{ + dojo.addClass(node, 'dijitHidden'); + } + }, this); + }, + + _setSelectedAttr: function(value){ + //console.debug(this.id+"::_setSelectedAttr", value); + dojo.toggleClass(this.domNode, 'dojoxDataIconGridItemSelected', value); + }, + + _setContextMenuOpenedAttr: function(value){ + //console.debug(this.id+"::_setContextMenuOpenedAttr", value); + dojo.toggleClass(this.domNode, 'dojoxDataIconGridItemContextMenuOpened', value); + }, + + loadThumbnail: function(){ + var src = this.store.getValue(this.item, this.layout.thumbnail.field); + this.grid._imageLoader.add({ + src: src, + onComplete: dojo.hitch(this, function(item){ + dojo.style( + this.thumbnailNode, + 'backgroundImage', + 'url("'+src+'")' + ); + }) + }); + }, + + _onClick: function(){ + //console.debug(this.id+'::_onClick'); + this.onClick(); + }, + + _onBlur: function(){ + //console.debug(this.id+'::_onBlur'); + this.caption1node.setAttribute('tabIndex', '-1'); + this.onBlur(); + }, + + _onFocus: function(){ + //console.debug(this.id+'::_onFocus'); + this.onFocus(); + }, + + _onMouseOver: function(){ + dojo.toggleClass(this.domNode, "dojoxDataIconGridItemHovered", true); + }, + + _onMouseOut: function(){ + //console.debug(this.id+"::_onMouseOut"); + dojo.toggleClass(this.domNode, "dojoxDataIconGridItemHovered", false); + } + +}); + +dojo.declare('dojox.iconGrid.DataIconGrid', [dijit._Widget, dijit._Templated, dijit._Container], { + + // query: String|Object + query: '', + + // store: dojo.data.api.Notification + // Store that is used to obtain its data + store: null, + + // sort: String + // Sort string to pass to the store when fetching + sort: "", + + // threshold: + // Number of rows before reaching the virtual zone that triggers the fetch. + threshold: 3, + + // buffer: + // Number of rows to fetch ahead/behind. Should be greater than + // (visibleRows + 2*threshold) to prevent excessive fetching. + buffer: 20, + + // onBegin: Callback + // Called before store fetch. + onBegin: function(){}, + + // onComplete: Callback + // Called when store fetch complete and items rendered. + onComplete: function(/*dojo.data.Item[]*/ items, /*Integer*/size){}, + + // onError: Callback + // Called when store fetch fails. + onError: function(/*Error*/ e){}, + + // onScroll: Callback + // Called when a scroll occurs. + onScroll: function(/*Integer*/start, /*Integer*/size){}, + + // onSelect: Callback + // Called when an item was selected via clicking or navigating via + // keyboard. Also called with null when selection is cleared by + // clicking a blank area in the grid. + onSelect: function(/*dojo.data.Item*/item){}, + + // onDoubleClick: Callback + // Called when item is double clicked while selected + onDoubleClick: function(/*dojo.data.Item*/item){}, + + // onContextMenu: Callback + // Called when right clicks on an item + onContextMenu: function(/*dojo.data.Item*/item){}, + + // onContextMenuClose: Callback + // Called when the context menu item loses focus. + onContextMenuClose: function(/*dojo.data.Item*/item){}, + + // onMouseOver: Callback + // Called when mouse over an item. + onMouseOver: function(/*Object*/kwArgs){}, + + // onMouseOut: Callback + // Called when mouse out an item. + onMouseOut: function(/*Object*/kwArgs){}, + + + // resize: Function + // Quack like a _LayoutWidget. Call when the domNode has been resized. + resize: function(changeSize, resultSize){ + //console.debug(this.id+'::resize', arguments); + // delay this method till 'timeout' occurs without getting called + var timeout = 250; + if(this._resizeHandle){ + //console.debug('canceling prior resize.'); + clearTimeout(this._resizeHandle); + } + + if(changeSize){ + dojo.marginBox(this.domNode, changeSize); + } + + marginBox = dojo.mixin({ + w: 0, + h: 0 + }, changeSize||resultSize); + + //console.debug(this.id+'::resize', marginBox.h, marginBox.w, this._state.size, this._state.isLoading); + + if(marginBox.w <= 0 || marginBox.h <= 0){ + //console.debug("must be visible to render, canceling resize."); + return; + } + + if((this._state.size || this._state.isLoading) + && this._state.marginBox.w == marginBox.w + && this._state.marginBox.h == marginBox.h){ + //console.debug("no change in marginBox, canceling resize."); + return; + } + + this._resizeHandle = setTimeout(dojo.hitch(this, function(){ + //console.debug(this.id+'::resize: ', marginBox); + this._resizeHandle = null; + var cb = this._getContentBox(dojo.marginBox(this.domNode)); + if(cb.w <= 0 || cb.h <= 0){ + //console.debug(this.id+'::resize cannot resize, not visible.'); + return; + } + + // find top left visible cell (or at least something close to it) + var currentState = dojo.clone(this._state); + this._state.start = Math.max(0, (this._getFirstVisibleRow() - this.threshold - 1) * this._state.columns); + this._onResize(marginBox); + + if((this._state.size || this._state.isLoading) + && currentState.count == this._state.count){ + //console.debug('resized but no change in count, canceling resize.'); + return; + } + + if(currentState.columns != this._state.columns){ + //console.debug('columns changed, blank out grid to prevent misaligned view'); + var selectedWidget = this._getSelectedCellAttr(); + if(selectedWidget){ + // slide window to fit around the selected item + for(var index in this._cacheByIndex){ + if(selectedWidget === this._cacheByIndex[index]){ + var newRow = Math.floor(index / this._state.columns); + var newStart = Math.max(0, ((newRow - this.threshold - Math.floor(this._state.visibleRows / 2) - 1) * this._state.columns)) + //console.debug(this.threshold + Math.floor(this._state.visibleRows / 2) + 1); + //console.debug('sliding window:', this._state.start, '->', newStart); + this._state.start = newStart; + break; + } + } + } + } + + this._render(); + + }), timeout); + }, + + _setQueryAttr: function(query){ + // summary: + // updates query and re-renders + //console.debug(this.id+'::_setQueryAttr'); + this._clear(); + this.query = query; + this._state.start = 0; + this.domNode.scrollTop = 0; + + this._render(); + }, + + _getSelectedCellAttr: function(){ + + var id = null; + + // currently only single select is supported + for(var id in this._state.selected){ + return this._cacheById[id] || null; + } + return null; + }, + + _setSelectedCellAttr: function(widget){ + + // currently only single select is supported + for(var id in this._state.selected){ + if(id in this._cacheById){ + //console.debug('deselecting ', this._cacheById[id]); + this._cacheById[id].attr("selected", false); + } + } + this._state.selected = {}; + + if(widget !== null){ + //console.debug('selecting cell ', this.store.getValue(widget.item, 'title'), this.store.getIdentity(widget.item)); + this._state.selected[this.store.getIdentity(widget.item)] = true; + widget.attr("selected", true); + this.onSelect({item: widget.item, widget: widget}); + } + else{ + //console.debug('deselecting cell'); + // maybe make a separate callback for this? + this.onSelect({item: null, widget: null}); + } + }, + + _setSelectedItemAttr: function(item){ + if(item === null){ + this._setSelectedCellAttr(null); + return; + } + var widget = this._cacheById[this.store.getIdentity(item)] || null; + //console.debug('selecting item ', this.store.getIdentity(item)); + this._setSelectedCellAttr(widget); + }, + + _setSelectedIndexAttr: function(index){ + //console.debug(this.id+"::_setSelectedIndexAttr", index); + if(index < 0 || this._state.size-1 < index){ + //console.debug(this.id+'::_setSelectedIndexAttr invalid index argument'); + return; + } + + var selectAndScrollToIndex = dojo.hitch(this, function(){ + var cell = this._cacheByIndex[index]; + this._setSelectedCellAttr(cell); + this.scrollToView({ + widget: cell, + animate: false + }); + }); + + if(index in this._cacheByIndex){ + selectAndScrollToIndex(); + return; + } + + // index not in cache, must scroll to area + var row = Math.floor(index / this._state.columns); + this._state.start = Math.max(0, ((row - this.threshold - Math.floor(this._state.visibleRows / 2) - 1) * this._state.columns)) + //console.debug('new start index:', this._state.start); + this._clear(); + this._render({ onComplete: selectAndScrollToIndex }); + + }, + + _getFocusedCellAttr: function(){ + if(!dijit._curFocus){ + return null; + } + return this._getEnclosedWidget(dijit._curFocus); + }, + + + scrollToView: function(kwArgs){ + kwArgs = dojo.mixin({ + widget: this._item2cell(kwArgs.item), + animate: true, + onEnd: function(){} + }, kwArgs || {}); + + if(kwArgs.widget === null){ + //console.debug("cannot scroll to item because it isn't in the cache."); + kwArgs.onEnd(); + return; + } + if(this._scrollingAnimation){ + this._scrollingAnimation.stop(); + } + + var mb = this._getCellMarginBox(kwArgs.widget); + kwArgs.start = this.domNode.scrollTop; + + if(kwArgs.start > mb.t){ + //console.debug('scrolling up.'); + kwArgs.stop = mb.t; + } + else if(kwArgs.start + this._state.contentBox.h < mb.t+mb.h) { + //console.debug('scrolling down.'); + kwArgs.stop = mb.t - this._state.contentBox.h + mb.h; + } + else{ + //console.debug('cell is completely visible'); + kwArgs.onEnd(); + return; + } + + if(kwArgs.animate){ + this._animateScrollTop(kwArgs); + } + else{ + this.domNode.scrollTop = kwArgs.stop; + kwArgs.onEnd(); + } + }, + + scrollToCenter: function(kwArgs){ + //console.debug(this.id+"::_scrollToItem", kwArgs); + kwArgs = dojo.mixin({ + widget: this._item2cell(kwArgs.item), + animate: true, + onEnd: function(){} + }, kwArgs || {}); + + if(kwArgs.widget === null){ + //console.debug("cannot scroll to item because it isn't in the cache."); + kwArgs.onEnd(); + return; + } + if(this._scrollingAnimation){ + this._scrollingAnimation.stop(); + } + var mb = this._getCellMarginBox(kwArgs.widget); + kwArgs.start = this.domNode.scrollTop; + kwArgs.stop = mb.t - (this._state.contentBox.h / 2 - mb.h / 2); + + if(kwArgs.animate){ + this._animateScrollTop(kwArgs); + } + else{ + this.domNode.scrollTop = kwArgs.stop; + kwArgs.onEnd(); + } + }, + + focus: function(){ + //console.debug(this.id+'::focus'); + var widget = this._getSelectedCellAttr(); + if(widget === null){ + //console.debug('focusing first visible cell(1)'); + this._focusFirstVisibleCell(); + return; + } + + // is selected node in view? + var mb = this._getCellMarginBox(widget); + if(mb.t > this.domNode.scrollTop + && mb.t+mb.h < this.domNode.scrollTop + this._state.contentBox.h){ + //console.debug('focusing selected cell'); + widget.focus(); + return; + } + else{ + //console.debug('focusing first visible cell(2)'); + this._focusFirstVisibleCell(); + } + }, + + destroy: function(){ + if(this._resizeHandle){ + clearTimeout(this._resizeHandle); + } + if(this._scrollHandle){ + clearTimeout(this._scrollHandle); + } + if(this._renderHandle){ + clearTimeout(this._renderHandle); + } + this.inherited(arguments); + }, + + + // private + // + // + // + // + + templatePath: dojo.moduleUrl("dojox.iconGrid","resources/DataIconGrid.html"), + + // _state: Object + // state variables used to determine how to render the grid + _state: null, + + // _cacheByIndex: [private] Object + // Widgets cached by their display index. + _cacheByIndex: null, + + // _cacheById: [private] Object + // Widgets cached by their store identity + _cacheById: null, + + // _imageLoader: [protected] dojox.image.Loader + // Sequential image request manager + _imageLoader: null, + + postMixInProperties: function(){ + //console.debug(this.id+"::postMixInProperties"); + + // TODO:: check the store features + this._cacheByIndex = {}; + this._cacheById = {}; + this._imageLoader = new dojox.image.Loader({ + concurrentRequests: 8 + }); + + this._state = { + size: 0, // number of items in the query result + rows: 0, // rows + start: 0, + count: 0, // items to fetch + columns: 0, + virtualTopRows: 0, + virtualBottomRows: 0, + lastScrollTop: 0, // used to deter man the scrolling direction + isScrollingDown: true, // the scrolling direction + isLoading: false, + contentBox: {w: 0, h: 0}, + marginBox: {w: 0, h: 0}, + cellMarginBox: {w: 0, h: 0}, + selected: {} + }; + + }, + + postCreate: function(){ + //console.debug(this.id+"::postCreate"); + // override the mouse wheel events to prevent smooth scrolling when enabled + var eventName = ''; + if (dojo.isSafari || dojo.isOpera || dojo.isIE) { + eventName = 'onmousewheel'; + } + else{ + eventName = 'DOMMouseScroll'; + } + this.connect(this.domNode, eventName, "_onScrollWheel"); + }, + + + + + _onResize: function(marginBox){ + // summary: + // Sets up measurements relating to the container size. + this._state.marginBox = marginBox; + + // we need to find our own contentBox that always accounts for the scrollbar + var contentBox = this._getContentBox(marginBox); + this._state.contentBox = contentBox; + + var cellMarginBox = this._getSampleCellMarginBox(); + this._state.cellMarginBox = cellMarginBox; + //console.debug('cell dimensions measured: ', this._state.cellMarginBox); + + this._state.visibleRows = Math.ceil(contentBox.h / this._state.cellMarginBox.h); + this._state.columns = Math.floor(contentBox.w / this._state.cellMarginBox.w); + + this._state.count = (this._state.visibleRows + this.buffer) * this._state.columns; + }, + _clear: function(){ + //console.debug(this.id, "::_clear"); + + this._cacheByIndex = {}; + this._cacheById = {}; + + dojo.forEach(this.getChildren(), function(child, i){ + child.destroyRecursive(); + }, this); + }, + + _render: function(kwArgs){ + //console.debug(this.id+"::_render"); + var cb = dojo.contentBox(this.domNode); + if(cb.w <= 0 || cb.h <= 0){ + // Cannot render unless it has visibility + // User must call resize with a cb. + //console.debug(this.id+'::_render cannot render, not visible.'); + return; + } + + kwArgs = dojo.mixin({ + onComplete: function(){} + }, kwArgs || {}); + + var timeout = 250; + + if(this._renderHandle){ + //console.debug('canceling prior _render'); + clearTimeout(this._renderHandle); + } + + if(this._state.isLoading){ + // TODO:: abort the fetch instead of deferring execution. + //console.debug("store is still loading, deferring _render."); + this._renderHandle = setTimeout(dojo.hitch(this, function(){ + this._renderHandle = null; + this._render(kwArgs); + }), timeout); + return; + } + this._state.isLoading = true; + + + var firstVisibleCell = this._getFirstVisibleCell(); + var firstVisibleCellId = ''; + if(firstVisibleCell){ + firstVisibleCellId = this.store.getIdentity(firstVisibleCell.item); + var scrollOffset = this.domNode.scrollTop - this._getCellMarginBox(firstVisibleCell).t; + //console.debug('first visible cell offset: ', scrollOffset); + } + else{ + //console.debug('first visible cell not available.'); + + } + + this.onBegin(); + + this._imageLoader.cancel(); + + this.store.fetch({ + query: this.query, + sort: this.sort, + start: this._state.start, + count: this._state.count, + onBegin: dojo.hitch(this, function(size){ + this._clear(); + //console.debug('query returned ', size, ' items.'); + this._state.size = size; + }), + onComplete: dojo.hitch(this, function(items){ + var row = []; + dojo.forEach(items, function(item, index){ + //this.store.setValue(item, 'index', this._state.start + index); + var widget = new dojox.iconGrid.DataIconGridItem({ + item: item, + store: this.store, + grid: this + }); + if(this.store.getIdentity(item) in this._state.selected){ + //console.debug('reselecting item ', this.store.getValue(item, 'title'), this.store.getIdentity(item)); + widget.attr("selected", true); + } + row.push(widget); + if(row.length == this._state.columns){ + var rowWidget = new dojox.iconGrid.DataIconGridRow(); + dojo.forEach(row, function(w){ + rowWidget.addChild(w); + }); + this.addChild(rowWidget); + row = []; + } + this._cacheByIndex[this._state.start + index] = widget; + this._cacheById[this.store.getIdentity(item)] = widget; + }, this); + + if(row.length){ + // last row + var rowWidget = new dojox.iconGrid.DataIconGridRow(); + dojo.forEach(row, function(w){ + rowWidget.addChild(w); + }); + this.addChild(rowWidget); + } + + var contentBox = this._state.contentBox; + var cellMarginBox = this._state.cellMarginBox; + + this._state.rows = Math.ceil(this._state.size / this._state.columns); + + var virtualTopRows = Math.floor(this._state.start / this._state.columns); + dojo.marginBox(this.virtualTopNode, {h: virtualTopRows * cellMarginBox.h, w: contentBox.w}); + + this._state.virtualTopRows = virtualTopRows; + + var trailingColumns = (this._state.columns - ((this._state.start + items.length) % this._state.columns)) % this._state.columns; + //console.debug('trailing columns: ', trailingColumns); + var remainingCells = (this._state.size - this._state.start - items.length - trailingColumns); + //console.debug(remainingCells); + + var virtualBottomRows = Math.ceil(remainingCells / this._state.columns); + this._state.virtualBottomRows = virtualBottomRows; + //console.debug(virtualBottomRows); + dojo.marginBox(this.virtualBottomNode, {h: virtualBottomRows * cellMarginBox.h, w: contentBox.w}); + + // eliminate the scrollbar resizing + dojo.marginBox(this.containerNode, { + w: contentBox.w, + h: (this._state.rows - this._state.virtualTopRows - this._state.virtualBottomRows) * cellMarginBox.h + }); + + // fix scrollTop if needed + var topVisibleRow = this._getFirstVisibleRow(); + if(firstVisibleCellId in this._cacheById){ + //console.debug('fixing scroll from offset'); + var mb = this._getCellMarginBox(this._cacheById[firstVisibleCellId]); + this.domNode.scrollTop = mb.t + scrollOffset; + } + else if(this._isScrollTopAboveThreshold(topVisibleRow)){ + //console.debug('rendered but not visible. scroll up to visible spot'); + this.domNode.scrollTop = ( + this._state.start && ((virtualTopRows+this.threshold+1) * this._state.cellMarginBox.h) + ); + } + else if(this._isScrollTopBelowThreshold(topVisibleRow)){ + //console.debug('rendered but not visible. scroll down to visible spot'); + this.domNode.scrollTop = ((virtualTopRows+this.threshold+1) * this._state.cellMarginBox.h); + } + else{ + //console.debug('no scroll fix needed, within visible range.'); + } + + this._loadThumbnails(); + kwArgs.onComplete(); + this.onComplete({ + items: items, + size: this._state.size, + start: this._state.start, + count: this._state.count + }); + this._state.isLoading = false; + }), + onError: dojo.hitch(this, function(e){ + this._state.isLoading = false; + this.onError(e); + }) + }); + }, + + _item2cell: function(item){ + if(item && this.store.getIdentity(item) in this._cacheById){ + return this._cacheById[this.store.getIdentity(item)]; + } + return null; + }, + + _animateScrollTop: function(kwArgs){ + kwArgs = dojo.mixin({ + start: 0, + stop: 0, + onEnd: function(){} + }, kwArgs || {}); + this._scrollingAnimation = new dojo._Animation({ + curve: [kwArgs.start, kwArgs.stop], + rate: 5, + easing: dojo._defaultEasing, + onAnimate: dojo.hitch(this, function(value){ + this.domNode.scrollTop = value; + }), + onEnd: dojo.hitch(this, function(){ + this._scrollingAnimation = null; + kwArgs.onEnd(); + }) + }).play(); + }, + + _onScrollWheel: function(e){ + //console.debug(this.id+"::_onScrollWheel", e); + + dojo.stopEvent(e); + + if(this._scrollingAnimation){ + this._scrollingAnimation.stop(); + this._scrollingAnimation = null; + } + + if(this._state.isLoading){ + return; + } + + // normalize scroll amount + var delta; + if (e.wheelDelta) { + // Safari || IE || Opera + delta = e.wheelDelta / 120; + } + else if (e.detail) { // W3C + delta = -e.detail / 3; + } + //console.assert(Math.abs(delta) == 3); + var scrollRate = 20; + this.domNode.scrollTop -= delta*scrollRate; + }, + + _onScroll: function(e){ + //console.debug(this.id+"::_onScroll", e, this.domNode.scrollTop); + // summary: + // Delay this callback until 'wait' seconds of inactivity. + + var wait = 250; + + if(this._scrollHandle){ + //console.debug('canceling prior scroll request'); + clearTimeout(this._scrollHandle); + } + + this._scrollHandle = setTimeout(dojo.hitch(this, function(){ + this._scrollHandle = null; + this._onDelayedScroll(); + }), wait); + }, + + _onDelayedScroll: function(){ + + // Wait to fetch if still loading. Probably should just cancel the request, + // but server supposedly doesn't like that. + if(this._state.isLoading){ + this._onScroll(); + return; + } + //console.debug(this.id+"::_onScroll", this.domNode.scrollTop); + + + + this._state.isScrollingDown = (this.domNode.scrollTop > this._state.lastScrollTop); + this._state.lastScrollTop = this.domNode.scrollTop; + + + var topVisibleRow = this._getFirstVisibleRow(); + this.onScroll({start: Math.max(0, (topVisibleRow-1)) * this._state.columns, size: this._state.size}); + + if(this._isScrollTopAboveThreshold(topVisibleRow)){ + //console.debug('buffering up...'); + this._state.start = Math.max( + // slide window upwards... + topVisibleRow - this.buffer + this.threshold, + // ...or at the very top + 0 + ) * this._state.columns; + this._render({onComplete: dojo.hitch(this, "focus")}); + } + + else if(this._isScrollTopBelowThreshold(topVisibleRow)){ + //console.debug('buffering down...'); + this._state.start = Math.min( + // slide window downwards... + (topVisibleRow - this.threshold - 1), + // ...or when at the very bottom + (this._state.rows - Math.floor(this.threshold/2) - this.buffer - 1) + ) * this._state.columns; + this._render({onComplete: dojo.hitch(this, "focus")}); + return; + } + + }, + + _isScrollTopAboveThreshold: function(topVisibleRow){ + var topBoundRow = this._state.virtualTopRows + this.threshold; + //console.debug('_isScrollTopAboveThreshold:', (this._state.virtualTopRows != 0 && topBoundRow > topVisibleRow)); + return (this._state.virtualTopRows != 0 && topBoundRow > topVisibleRow); + }, + + _isScrollTopBelowThreshold: function(topVisibleRow){ + var bottomVisibleRow = topVisibleRow + this._state.visibleRows; + var bottomBoundRow = this._state.rows - this._state.virtualBottomRows - this.threshold; + //console.debug(this.id+'::_isScrollTopBelowThreshold:', (this._state.virtualBottomRows != 0 && bottomBoundRow < bottomVisibleRow)); + return (this._state.virtualBottomRows != 0 && bottomBoundRow < bottomVisibleRow); + }, + + _onClick: function(e){ + //console.debug(this.id+'::_onClick', e.target); + if(this._state.isLoading){ + return; + } + if(e.target === this.virtualTopNode || e.target === this.virtualBottomNode){ + //console.debug('not allowing deselection by clicking a virtual node'); + return; + } + + var targetWidget = this._getEnclosedWidget(e.target); + + if(targetWidget === null){ + this._setSelectedCellAttr(null); + return; + } + this.scrollToView({ + widget: targetWidget, + onEnd: function(){ + targetWidget.focus(); + } + }); + this._setSelectedCellAttr(targetWidget); + }, + + _onContextMenu: function(e){ + //console.debug(this.id+"::_onContextMenu"); + dojo.stopEvent(e); + + var targetWidget = this._getEnclosedWidget(e.target); + if(targetWidget === null){ + return; + } + + this.onContextMenu({item: targetWidget.item, widget: targetWidget, event: e}); + targetWidget.attr("contextMenuOpened", true); + var _handles = []; + function closeContextMenu(){ + //console.debug('closing context menu'); + dojo.forEach(_handles, dojo.disconnect); + targetWidget.attr("contextMenuOpened", false); + var selectedWidget = this._getSelectedCellAttr(); + setTimeout(dojo.hitch(this, "focus"), 0); + this.onContextMenuClose({item: targetWidget.item, widget: targetWidget}); + } + _handles.push(dojo.connect(targetWidget, 'onClick', this, closeContextMenu)); + _handles.push(dojo.connect(targetWidget, 'onBlur', this, closeContextMenu)); + }, + + _onKeyDown: function(e){ + //console.debug(this.id+"::_onKeyDown ", e); + if(this._state.isLoading){ + //console.debug('is loading from store, stopping key event ', e); + dojo.stopEvent(e); + return; + } + if(e.keyCode == dojo.keys.ENTER){ + this.onDoubleClick({event: e}); + return; + } + + if(dojo.indexOf([ + dojo.keys.UP_ARROW, + dojo.keys.DOWN_ARROW, + dojo.keys.LEFT_ARROW, + dojo.keys.RIGHT_ARROW + ], e.keyCode) == -1) { + //console.debug('non navigational key entered: ', e); + return; + } + + var selectedWidget = this._getSelectedCellAttr() || this._getFocusedCellAttr(); + if(selectedWidget === null){ + return; + } + var selectedNode = selectedWidget.domNode; + var selectedRow = selectedNode.parentNode; + var selectedIndex = dojo.indexOf(selectedRow.childNodes, selectedNode); + var upRow = selectedRow.previousSibling; + var downRow = selectedRow.nextSibling; + + var nextWidget = null; + + switch(e.keyCode){ + case dojo.keys.UP_ARROW: + if(upRow !== null){ + nextWidget = this._getEnclosedWidget(upRow.childNodes[selectedIndex]); + } + break; + case dojo.keys.DOWN_ARROW: + if(downRow !== null){ + nextWidget = this._getEnclosedWidget(downRow.childNodes[selectedIndex]); + if(nextWidget === null){ + nextWidget = this._getEnclosedWidget(downRow.childNodes[0]); + } + } + break; + case dojo.keys.LEFT_ARROW: + if(selectedIndex == 0){ + if(upRow !== null){ + nextWidget = this._getEnclosedWidget(upRow.lastChild); + } + } + else{ + nextWidget = this._getEnclosedWidget(selectedNode.previousSibling); + } + break; + case dojo.keys.RIGHT_ARROW: + if(selectedIndex == selectedRow.childNodes.length -1){ + if(downRow !== null){ + nextWidget = this._getEnclosedWidget(downRow.firstChild); + } + } + else{ + nextWidget = this._getEnclosedWidget(selectedNode.nextSibling); + } + break; + } + + if(nextWidget !== null){ + this.scrollToView({ + widget: nextWidget, + onEnd: function(){ + nextWidget.focus(); + } + }); + this._setSelectedCellAttr(nextWidget); + dojo.stopEvent(e); + } + }, + + _onDoubleClick: function(e){ + var targetWidget = this._getEnclosedWidget(e.target); + this.onDoubleClick(targetWidget); + }, + + _onMouseMove: function(e){ + var targetWidget = this._getEnclosedWidget(e.target); + if(targetWidget === null){ + this.onMouseOut(); + return; + } + this.onMouseOver({item: targetWidget.item, widget: targetWidget, event: e}); + }, + + _onMouseOut: function(e){ + this.onMouseOut(); + }, + + _loadThumbnails: function(){ + + // First, the visible thumbnails are loaded. + // + // If scrolling downwards, the thumbnails below the visible area are loaded + // next then the thumbnails above the visible area are loaded. + // + // If scrolling upwards, the thumbnails above the visible area are loaded + // next, then the thumbnails below the visible area are loaded. + + + var items = []; + + var topRow = Math.floor(this.domNode.scrollTop / this._state.cellMarginBox.h); + var bottomRow = topRow + this._state.visibleRows; + + function loadVisibleThumbnails(){ + //console.debug('loading visible thumbnails...'); + var start = (topRow * this._state.columns); + var stop = bottomRow * this._state.columns; + for(var i = start; this._cacheByIndex[i] && i < stop; ++i){ + this._cacheByIndex[i].loadThumbnail(); + } + } + + function loadSucceedingThumbnails(){ + //console.debug('loading succeeding thumbnails...'); + var start = (bottomRow * this._state.columns); + var stop = this._state.start + this._state.count; + for(var i = start; this._cacheByIndex[i] && i < stop; ++i){ + this._cacheByIndex[i].loadThumbnail(); + } + } + + function loadPrecedingThumbnails(){ + //console.debug('loading preceding thumbnails...'); + var start = (topRow * this._state.columns) - 1; + var stop = this._state.start; + for(var i = start; this._cacheByIndex[i] && i >= stop; --i){ + this._cacheByIndex[i].loadThumbnail(); + } + } + + dojo.hitch(this, loadVisibleThumbnails)(); + if(this._state.isScrollingDown){ + dojo.hitch(this, loadSucceedingThumbnails)(); + dojo.hitch(this, loadPrecedingThumbnails)(); + } + else{ + dojo.hitch(this, loadPrecedingThumbnails)(); + dojo.hitch(this, loadSucceedingThumbnails)(); + } + }, + + _getFirstVisibleRow: function(){ + //console.debug(this.id+"::_getFirstVisibleRow"); + + // scan downwards + // TODO:: change to a binary search instead of a linear search. + var index, mb; + for(index in this._cacheByIndex){ break; } + //console.debug('start index:', index, (index in this._cacheByIndex)); + while(index in this._cacheByIndex){ + mb = this._getCellMarginBox(this._cacheByIndex[index]); + if(mb.t + mb.h < this.domNode.scrollTop){ + index = Number(index) + Number(this._state.columns); + } + else if(mb.t > this.domNode.scrollTop + this._state.contentBox.h){ + //console.debug('probed cache but out of visible range, using default...'); + break; + } + else{ + //console.debug('determined top visible row from cache'); + return Math.floor(index / this._state.columns); + } + } + + //console.debug('default top visible row', this._cacheByIndex[this._state.start]); + return Math.floor(this.domNode.scrollTop / this._state.cellMarginBox.h) || 0; + + }, + + _getFirstVisibleCell: function(){ + //console.debug(this.id+'::_getFirstVisibleCell'); + var startRow = this._getFirstVisibleRow(); + var index = startRow * this._state.columns; + + if(index in this._cacheByIndex){ + return this._cacheByIndex[index]; + } + else{ + return null; + } + }, + + _focusFirstVisibleCell: function(){ + // skip first visible row because it might be partially covered + var startRow = this._getFirstVisibleRow()+1; + var index = startRow * this._state.columns; + if(index in this._cacheByIndex){ + //console.debug('focusing top left visible cell'); + this._cacheByIndex[index].focus(); + } + }, + + + _getContentBox: function(marginBox){ + var contentBox = dojo.clone(marginBox); + contentBox.w -= ( + parseInt(dojo.style(this.domNode, 'borderLeftWidth')) + + parseInt(dojo.style(this.domNode, 'borderRightWidth')) + + parseInt(dojo.style(this.domNode, 'marginLeft')) + + parseInt(dojo.style(this.domNode, 'marginRight')) + + dojox.html.metrics.getScrollbar().w + ); + contentBox.h -= ( + parseInt(dojo.style(this.domNode, 'borderTopWidth')) + + parseInt(dojo.style(this.domNode, 'borderBottomWidth')) + + parseInt(dojo.style(this.domNode, 'marginTop')) + + parseInt(dojo.style(this.domNode, 'marginBottom')) + ); + return contentBox; + }, + + _getEnclosedWidget: function(node){ + var targetWidget = dijit.getEnclosingWidget(node); + if(targetWidget === null + || targetWidget.item === undefined + || this._cacheById[this.store.getIdentity(targetWidget.item)] === undefined){ + return null; + } + return targetWidget; + + }, + + _getCellMarginBox: function(cell){ + var mb = dojo.marginBox(cell.domNode); + mb.t += (this._state.virtualTopRows * this._state.cellMarginBox.h); + return mb; + }, + + _getSampleCellMarginBox: function(){ + // summary: + // Make a fake child to measure its dimensions. + + var widget = new dojox.iconGrid.DataIconGridItem({ + item: null, + store: null + }); + + dojo.style(widget.domNode, 'position', 'absolute'); + dojo.style(widget.domNode, 'visibility', 'hidden'); + dojo.style(widget.domNode, 'top', '0px'); + dojo.style(widget.domNode, 'left', '0px'); + + this.domNode.appendChild(widget.domNode); + + var mb = dojo.marginBox(widget.domNode); + this.domNode.removeChild(widget.domNode); + widget.destroy(); + + return mb; + } + +}); + Index: dojo-release-1.3.2-src/dojox/iconGrid/resources/DataIconGrid.css =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ dojo-release-1.3.2-src/dojox/iconGrid/resources/DataIconGrid.css 2010-03-02 04:35:01.000000000 -0800 @@ -0,0 +1,124 @@ +/* not in 0.9 */ +.dijitHidden { + display: none; +} + +.dojoxDataIconGrid { + overflow: auto; + margin: 0; + border: 0; +} + +.dojoxDataIconGrid .dojoxDataIconGridContainerNode{ + position: relative; /* required to setup context */ +} + +.dj_ie6 .dojoxDataIconGrid .dojoxDataIconGridVirtualTopNode, +.dj_ie6 .dojoxDataIconGrid .dojoxDataIconGridContainerNode, +.dj_ie6 .dojoxDataIconGrid .dojoxDataIconGridVirtualBottomNode { + margin: 0; + border: 0; + padding: 0; + overflow: hidden; +} + +.dojoxDataIconGrid .dojoxDataIconGridItem { + float:left; +} + +.dojoxDataIconGrid .dojoxDataIconGridRow, +.dojoxDataIconGrid .dojoxDataIconGridVirtualBottomNode { + clear: left; +} + +.dojoxDataIconGrid .dojoxDataIconGridItem { + margin: 5px; + width: 100px; + overflow: hidden; + /* + -moz-border-radius: 5px; + */ +} + +.dojoxDataIconGrid .dojoxDataIconGridItemTop { + background-image: url("images/rounded_corner_top.png"); + background-repeat: no-repeat; + background-position: top left; +} +.dj_ie6 .dojoxDataIconGrid .dojoxDataIconGridItemTop { + background-image: url("images/rounded_corner_top.gif"); +} + +.dojoxDataIconGrid .dojoxDataIconGridItemBottom { + background-image: url("images/rounded_corner_bottom.png"); + background-repeat: no-repeat; + background-position: bottom left; + padding: 5px; +} +.dj_ie6 .dojoxDataIconGrid .dojoxDataIconGridItemBottom { + background-image: url("images/rounded_corner_bottom.gif"); +} + +.dojoxDataIconGrid .dojoxDataIconGridItemHovered { + background-color: #eeeeee; +} + +.dojoxDataIconGrid .dojoxDataIconGridItemSelected { + /* + border: 3px solid #586878; + margin: 2px; + */ + background-color: #8097AD; +} + +.dojoxDataIconGrid .dojoxDataIconGridItemContextMenuOpened { + margin: 4px; + border: 1px dotted black; +} + + +.dojoxDataIconGrid .dojoxDataIconGridItem .dojoxDataIconGridItemThumbnail { + /* + margin-top: -1.3em; + */ + background-image: url("../../image/resources/images/loading.gif"); + height: 90px; + background-repeat: no-repeat; + background-position: center center; + cursor: pointer; +} + +.dojoxDataIconGrid .dojoxDataIconGridItem .dojoxDataIconGridItemCaption1, +.dojoxDataIconGrid .dojoxDataIconGridItem .dojoxDataIconGridItemCaption2, +.dojoxDataIconGrid .dojoxDataIconGridItem .dojoxDataIconGridItemCaption3 { + text-align: center; + font-size: 1em; + line-height: 1.3em; + cursor: pointer; + max-height: 4em; + overflow: hidden; +} + +.dojoxDataIconGrid .dojoxDataIconGridItem .dojoxDataIconGridItemCaption2 { + color: #999999; + font-size: 0.8em; +} + +.dojoxDataIconGrid .dojoxDataIconGridItem .dojoxDataIconGridItemCaption3 { + color: #cccccc; + font-size: 0.8em; +} + +.dojoxDataIconGrid .dojoxDataIconGridItem .dojoxDataIconGridItemCaption4 { + color: #cccccc; +} + +.dojoxDataIconGrid .dojoxDataIconGridItemSelected .dojoxDataIconGridItemCaption1 { + color: #ffffff; +} +.dojoxDataIconGrid .dojoxDataIconGridItemSelected .dojoxDataIconGridItemCaption2 { + color: #f9f9f9; +} +.dojoxDataIconGrid .dojoxDataIconGridItemSelected .dojoxDataIconGridItemCaption3 { + color: #f7f7f7; +} Index: dojo-release-1.3.2-src/dojox/iconGrid/resources/DataIconGrid.html =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ dojo-release-1.3.2-src/dojox/iconGrid/resources/DataIconGrid.html 2010-03-01 17:16:33.000000000 -0800 @@ -0,0 +1,13 @@ +
Index: dojo-release-1.3.2-src/dojox/iconGrid/resources/DataIconGridItem.html =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ dojo-release-1.3.2-src/dojox/iconGrid/resources/DataIconGridItem.html 2010-03-02 04:35:01.000000000 -0800 @@ -0,0 +1,15 @@ +
Index: dojo-release-1.3.2-src/dojox/iconGrid/tests/test_data_icon_grid.html =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ dojo-release-1.3.2-src/dojox/iconGrid/tests/test_data_icon_grid.html 2010-03-01 17:16:33.000000000 -0800 @@ -0,0 +1,180 @@ + + + + Test dojox.iconGrid.DataIconGrid + + + + + + + +
dojox.iconGrid.DataIconGrid Basic Test
+
+ + + + + + + Index: dojo-release-1.3.2-src/dojox/image/Loader.js =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ dojo-release-1.3.2-src/dojox/image/Loader.js 2010-03-26 11:37:18.000000000 -0700 @@ -0,0 +1,186 @@ +dojo.provide("dojox.image.Loader"); + + +dojo.declare('dojox.image.LoaderItem', null, { + // src: String + // URL of image to request + src: '', + + // onComplete: Function + // overridable function to call when image has been loaded + onComplete: function(image){}, + + // onError: Function + // overridable function to call when image load fails + onError: function(image){}, + + + constructor: function(kwArgs){ + //console.log('constructing:', kwArgs.src); + this._image = new Image(); + kwArgs = dojo.mixin({ + src: '', + onComplete: function(){}, + onError: function(){} + }, kwArgs||{}); + + this.src = kwArgs.src; + this.onComplete = kwArgs.onComplete; + this.onError = kwArgs.onError; + + this._image.onload = dojo.hitch(this, function(){ + //console.debug(this.src, ' loaded.'); + this.onComplete(this.src); + }); + + this._image.onerror = dojo.hitch(this, function(){ + //console.debug(this.src, ' loaded.'); + this.onError(this.src); + }); + }, + + // _image: [private] Image + // reference to Image element that actually loads the image + _image: null, + + request: function(){ + this._image.src = this.src; + }, + + isRequested: function(){ + // has the request been made? + return (this._image.src.length != 0); + }, + + isComplete: function(){ + // has the request been completed? + return (this.isRequested() && this._image.complete); + }, + + isLoading: function(){ + // is the request still loading? + return (this.isRequested() && !this.isComplete()); + }, + + cancel: function(){ + //console.debug('canceling ', this.src); + this._image.onload = function(){}; + this._image.onerror = function(){}; + this._image.src = ''; + } + +}); + +dojo.declare('dojox.image.Loader', null, { + // summary: + // Class that can sequentially load images to optimize the loading order + // of larger sized images. + // description: + // This prevents from too many requests being sent out to the webserver at + // once, causing the server to get stuck scaling a ton of thumbnails from + // a particular client. Loading a whole directory at once can cause further + // requests to hang until scaling is complete. + concurrentRequests: 2, + + constructor: function(kwArgs){ + kwArgs = dojo.mixin({ + concurrentRequests: this.concurrentRequests + }, kwArgs || {}); + this.concurrentRequests = kwArgs.concurrentRequests; + this._queue = []; + this._runningRequests = 0; + }, + + add: function(kwArgs){ + //console.debug(this.id+'::add', kwArgs); + kwArgs.position = 'end'; + this._add(kwArgs); + }, + + // same as add() but inserts right after current loading image + addNext: function(kwArgs){ + kwArgs.position = 'next'; + this._add(kwArgs); + }, + + cancel: function(){ + dojo.forEach(this._queue, function(item){ + item.cancel(); + }); + this._queue = []; + this._runningRequests = 0; + }, + + + // private + // + // + // + // + // + // + // + + + _add: function(kwArgs){ + kwArgs = dojo.mixin({ + position: 'end', // next|end + src: '', + onComplete: function(src){}, + onError: function(src){} + }, kwArgs||{}); + + var imageLoaderItem = new dojox.image.LoaderItem({ + src: kwArgs.src, + onComplete: dojo.hitch(this, function(src){ + kwArgs.onComplete(src); + this._onLoad(imageLoaderItem); + }), + onError: dojo.hitch(this, function(src){ + kwArgs.onError(src); + this._onLoad(imageLoaderItem); + }) + }); + + if(kwArgs.position == 'next'){ + for(var i = 0; i < this._queue.length; i++){ + if(!this._queue[i].isRequested()){ + this._queue.splice(i, 0, imageLoaderItem); + break; + } + } + } + else{ + this._queue.push(imageLoaderItem); + } + + this._makeRequest(); + }, + + _makeRequest: function(){ + // see if any images have not been loaded yet and load them + + //console.debug("::_makeRequest concurrent requests: ", this._runningRequests); + for(i = 0; i < this._queue.length; i++){ + if(this._runningRequests < this.concurrentRequests + && !this._queue[i].isRequested()){ + //console.debug('requesting ', this._queue[i].src); + this._runningRequests++; + this._queue[i].request(); + } + } + }, + + _onLoad: function(imageLoaderItem){ + //console.debug('::_onLoad'); + this._queue = dojo.filter(this._queue, function(item){ + return (item !== imageLoaderItem); + }); + + this._runningRequests--; + //console.debug("runningRequests: ", this._runningRequests); + + // following timeout is required for IE not to run out of stack space. + setTimeout(dojo.hitch(this, this._makeRequest), 0); + } +});