github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/server/camlistored/ui/blob_item_container.js (about)

     1  /*
     2  Copyright 2013 Google Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  goog.provide('cam.BlobItemContainer');
    18  
    19  goog.require('goog.dom');
    20  goog.require('goog.dom.classes');
    21  goog.require('goog.events.Event');
    22  goog.require('goog.events.EventHandler');
    23  goog.require('goog.events.EventType');
    24  goog.require('goog.events.FileDropHandler');
    25  goog.require('goog.ui.Container');
    26  
    27  goog.require('cam.BlobItem');
    28  goog.require('cam.SearchSession');
    29  goog.require('cam.ServerConnection');
    30  
    31  // An infinite scrolling list of BlobItem. The heights of rows and clip of individual items is adjusted to get a fully justified appearance.
    32  cam.BlobItemContainer = function(connection, opt_domHelper) {
    33  	goog.base(this, opt_domHelper);
    34  
    35  	this.checkedBlobItems_ = [];
    36  
    37  	this.connection_ = connection;
    38  
    39  	this.searchSession_ = null;
    40  
    41  	this.eh_ = new goog.events.EventHandler(this);
    42  
    43  	// BlobRef of the permanode defined as the current collection/set. Selected blobitems will be added as members of that collection upon relevant actions (e.g click on the 'Add to Set' toolbar button).
    44  	this.currentCollec_ = "";
    45  
    46  	// Whether our content has changed since last layout.
    47  	this.isLayoutDirty_ = false;
    48  
    49  	// An id for a timer we use to know when the drag has ended.
    50  	this.dragEndTimer_ = 0;
    51  
    52  	// Whether the blobItems within can be selected.
    53  	this.isSelectionEnabled = false;
    54  
    55  	// Whether users can drag files onto the container to upload.
    56  	this.isFileDragEnabled = false;
    57  
    58  	// A lookup of blobRef->cam.BlobItem. This allows us to quickly find and reuse existing controls when we're updating the UI in response to a server push.
    59  	this.itemCache_ = {};
    60  
    61  	this.setFocusable(false);
    62  };
    63  goog.inherits(cam.BlobItemContainer, goog.ui.Container);
    64  
    65  // Margin between items in the layout.
    66  cam.BlobItemContainer.BLOB_ITEM_MARGIN = 7;
    67  
    68  // If the last row uses at least this much of the available width before adjustments, we'll call it "close enough" and adjust things so that it fills the entire row. Less than this, and we'll leave the last row unaligned.
    69  cam.BlobItemContainer.LAST_ROW_CLOSE_ENOUGH_TO_FULL = 0.85;
    70  
    71  cam.BlobItemContainer.THUMBNAIL_SIZES_ = [75, 100, 150, 200, 250];
    72  
    73  // Distance from the bottom of the page at which we will trigger loading more data.
    74  cam.BlobItemContainer.INFINITE_SCROLL_THRESHOLD_PX_ = 100;
    75  
    76  cam.BlobItemContainer.NUM_ITEMS_PER_PAGE = 50;
    77  
    78  cam.BlobItemContainer.prototype.fileDropHandler_ = null;
    79  
    80  cam.BlobItemContainer.prototype.dragActiveElement_ = null;
    81  
    82  // Constants for events fired by BlobItemContainer
    83  cam.BlobItemContainer.EventType = {
    84  	SELECTION_CHANGED: 'Camlistore_BlobItemContainer_SelectionChanged',
    85  };
    86  
    87  cam.BlobItemContainer.prototype.thumbnailSize_ = 200;
    88  
    89  cam.BlobItemContainer.prototype.smaller = function() {
    90  	var index = cam.BlobItemContainer.THUMBNAIL_SIZES_.indexOf(this.thumbnailSize_);
    91  	if (index == 0) {
    92  		return false;
    93  	}
    94  	var el = this.getElement();
    95  	goog.dom.classes.remove(el, 'cam-blobitemcontainer-' + this.thumbnailSize_);
    96  	this.thumbnailSize_ = cam.BlobItemContainer.THUMBNAIL_SIZES_[index-1];
    97  	goog.dom.classes.add(el, 'cam-blobitemcontainer-' + this.thumbnailSize_);
    98  	return true;
    99  };
   100  
   101  cam.BlobItemContainer.prototype.bigger = function() {
   102  	var index = cam.BlobItemContainer.THUMBNAIL_SIZES_.indexOf(
   103  			this.thumbnailSize_);
   104  	if (index == cam.BlobItemContainer.THUMBNAIL_SIZES_.length - 1) {
   105  		return false;
   106  	}
   107  	var el = this.getElement();
   108  	goog.dom.classes.remove(el, 'cam-blobitemcontainer-' + this.thumbnailSize_);
   109  	this.thumbnailSize_ = cam.BlobItemContainer.THUMBNAIL_SIZES_[index+1];
   110  	goog.dom.classes.add(el, 'cam-blobitemcontainer-' + this.thumbnailSize_);
   111  	return true;
   112  };
   113  
   114  cam.BlobItemContainer.prototype.createDom = function() {
   115  	this.decorateInternal(this.dom_.createElement('div'));
   116  };
   117  
   118  cam.BlobItemContainer.prototype.decorateInternal = function(element) {
   119  	cam.BlobItemContainer.superClass_.decorateInternal.call(this, element);
   120  	this.layout_();
   121  
   122  	var el = this.getElement();
   123  	el.style.marginLeft = '36px';
   124  	goog.dom.classes.add(el, 'cam-blobitemcontainer');
   125  	goog.dom.classes.add(el, 'cam-blobitemcontainer-' + this.thumbnailSize_);
   126  };
   127  
   128  cam.BlobItemContainer.prototype.disposeInternal = function() {
   129  	cam.BlobItemContainer.superClass_.disposeInternal.call(this);
   130  	this.eh_.dispose();
   131  };
   132  
   133  cam.BlobItemContainer.prototype.addChildAt = function(child, index, opt_render) {
   134  	goog.base(this, "addChildAt", child, index, opt_render);
   135  	child.setEnabled(this.isSelectionEnabled);
   136  	if (!this.isLayoutDirty_) {
   137  		var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
   138  		// It's OK if raf not supported, the timer loop we have going will pick up the layout a little later.
   139  		if (raf) {
   140  			raf(goog.bind(this.layout_, this, false));
   141  		}
   142  
   143  		this.isLayoutDirty_ = true;
   144  	}
   145  };
   146  
   147  cam.BlobItemContainer.prototype.removeChildAt = function(index, opt_render) {
   148  	goog.base(this, "removeChildAt", index, opt_render);
   149  	this.isLayoutDirty_ = true;
   150  };
   151  
   152  cam.BlobItemContainer.prototype.enterDocument = function() {
   153  	cam.BlobItemContainer.superClass_.enterDocument.call(this);
   154  
   155  	this.resetChildren_();
   156  	this.listenToBlobItemEvents_();
   157  
   158  	if (this.isFileDragEnabled) {
   159  		this.fileDragListener_ = goog.bind(this.handleFileDrag_, this);
   160  		this.eh_.listen(document, goog.events.EventType.DRAGOVER, this.fileDragListener_);
   161  		this.eh_.listen(document, goog.events.EventType.DRAGENTER, this.fileDragListener_);
   162  
   163  		this.fileDropHandler_ = new goog.events.FileDropHandler(document);
   164  		this.registerDisposable(this.fileDropHandler_);
   165  		this.eh_.listen(this.fileDropHandler_, goog.events.FileDropHandler.EventType.DROP, this.handleFileDrop_);
   166  	}
   167  
   168  	this.eh_.listen(document, goog.events.EventType.SCROLL, this.handleScroll_);
   169  
   170  	// We can't catch everything that could cause us to need to relayout. Instead, be lazy and just poll every second.
   171  	window.setInterval(goog.bind(this.layout_, this, false), 1000);
   172  };
   173  
   174  cam.BlobItemContainer.prototype.exitDocument = function() {
   175  	cam.BlobItemContainer.superClass_.exitDocument.call(this);
   176  	this.eh_.removeAll();
   177  };
   178  
   179  cam.BlobItemContainer.prototype.showSearchSession = function(session) {
   180  	var changeType = cam.SearchSession.SEARCH_SESSION_CHANGE_TYPE.APPEND;
   181  
   182  	if (this.searchSession_ != session) {
   183  		if (this.searchSession_) {
   184  			this.eh_.unlisten(this.searchSession_, cam.SearchSession.SEARCH_SESSION_CHANGED, this.searchDone_);
   185  		}
   186  		this.resetChildren_();
   187  		this.itemCache_ = {};
   188  		this.layout_();
   189  		this.searchSession_ = session;
   190  		this.eh_.listen(session, cam.SearchSession.SEARCH_SESSION_CHANGED, this.searchDone_);
   191  		changeType = cam.SearchSession.SEARCH_SESSION_CHANGE_TYPE.NEW;
   192  	}
   193  
   194  	this.searchDone_({changeType:changeType});
   195  };
   196  
   197  cam.BlobItemContainer.prototype.getSearchSession = function() {
   198  	return this.searchSession_;
   199  };
   200  
   201  cam.BlobItemContainer.prototype.searchDone_ = function(e) {
   202  	if (e.changeType == cam.SearchSession.SEARCH_SESSION_CHANGE_TYPE.NEW) {
   203  		this.resetChildren_();
   204  		this.itemCache_ = {};
   205  	}
   206  
   207  	this.populateChildren_(this.searchSession_.getCurrentResults(), e.changeType == cam.SearchSession.SEARCH_SESSION_CHANGE_TYPE.APPEND);
   208  
   209  	if (this.searchSession_.isComplete()) {
   210  		return;
   211  	}
   212  
   213  	// If we haven't filled the window with results, add some more.
   214  	this.layout_();
   215  	var docHeight = goog.dom.getDocumentHeight();
   216  	var viewportHeight = goog.dom.getViewportSize().height;
   217  	if (docHeight < (viewportHeight * 1.5)) {
   218  		this.searchSession_.loadMoreResults();
   219  	}
   220  };
   221  
   222  cam.BlobItemContainer.prototype.findByBlobref_ = function(blobref) {
   223  	this.connection_.describeWithThumbnails(
   224  		blobref, this.thumbnailSize_,
   225  		goog.bind(this.findByBlobrefDone_, this, blobref),
   226  		function(msg) { alert(msg); });
   227  };
   228  
   229  cam.BlobItemContainer.prototype.getCheckedBlobItems = function() {
   230  	return this.checkedBlobItems_;
   231  };
   232  
   233  cam.BlobItemContainer.prototype.listenToBlobItemEvents_ = function() {
   234  	var doc = goog.dom.getOwnerDocument(this.element_);
   235  	this.eh_.listen(this, goog.ui.Component.EventType.CHECK, this.handleBlobItemChecked_);
   236  	this.eh_.listen(this, goog.ui.Component.EventType.UNCHECK, this.handleBlobItemChecked_);
   237  	this.eh_.listen(doc, goog.events.EventType.KEYDOWN, this.handleKeyDownEvent_);
   238  	this.eh_.listen(doc, goog.events.EventType.KEYUP, this.handleKeyUpEvent_);
   239  };
   240  
   241  cam.BlobItemContainer.prototype.isShiftKeyDown_ = false;
   242  
   243  cam.BlobItemContainer.prototype.isCtrlKeyDown_ = false;
   244  
   245  // Sets state for whether or not the shift or ctrl key is down.
   246  cam.BlobItemContainer.prototype.handleKeyDownEvent_ = function(e) {
   247  	if (e.keyCode == goog.events.KeyCodes.SHIFT) {
   248  		this.isShiftKeyDown_ = true;
   249  		this.isCtrlKeyDown_ = false;
   250  		return;
   251  	}
   252  	if (e.keyCode == goog.events.KeyCodes.CTRL) {
   253  		this.isCtrlKeyDown_ = true;
   254  		this.isShiftKeyDown_ = false;
   255  		return;
   256  	}
   257  };
   258  
   259  // Sets state for whether or not the shift or ctrl key is up.
   260  cam.BlobItemContainer.prototype.handleKeyUpEvent_ = function(e) {
   261  	this.isShiftKeyDown_ = false;
   262  	this.isCtrlKeyDown_ = false;
   263  };
   264  
   265  cam.BlobItemContainer.prototype.handleBlobItemChecked_ = function(e) {
   266  	// Because the CHECK/UNCHECK event dispatches before isChecked is set.
   267  	// We stop the default behaviour because want to control manually here whether
   268  	// the source blobitem gets checked or not. See http://cam.org/issue/134
   269  	e.preventDefault();
   270  	var blobItem = e.target;
   271  	var isCheckingItem = !blobItem.isChecked();
   272  	var isShiftMultiSelect = this.isShiftKeyDown_;
   273  	var isCtrlMultiSelect = this.isCtrlKeyDown_;
   274  
   275  	if (isShiftMultiSelect || isCtrlMultiSelect) {
   276  		var lastChildSelected = this.checkedBlobItems_[this.checkedBlobItems_.length - 1];
   277  		var firstChildSelected = this.checkedBlobItems_[0];
   278  		var lastChosenIndex = this.indexOfChild(lastChildSelected);
   279  		var firstChosenIndex = this.indexOfChild(firstChildSelected);
   280  		var thisIndex = this.indexOfChild(blobItem);
   281  	}
   282  
   283  	if (isShiftMultiSelect) {
   284  		// deselect all items after the chosen one
   285  		for (var i = lastChosenIndex; i > thisIndex; i--) {
   286  			var item = this.getChildAt(i);
   287  			item.setState(goog.ui.Component.State.CHECKED, false);
   288  			if (goog.array.contains(this.checkedBlobItems_, item)) {
   289  				goog.array.remove(this.checkedBlobItems_, item);
   290  			}
   291  		}
   292  		// make sure all the others are selected.
   293  		for (var i = firstChosenIndex; i <= thisIndex; i++) {
   294  			var item = this.getChildAt(i);
   295  			item.setState(goog.ui.Component.State.CHECKED, true);
   296  			if (!goog.array.contains(this.checkedBlobItems_, item)) {
   297  				this.checkedBlobItems_.push(item);
   298  			}
   299  		}
   300  	} else if (isCtrlMultiSelect) {
   301  		if (isCheckingItem) {
   302  			blobItem.setState(goog.ui.Component.State.CHECKED, true);
   303  			if (!goog.array.contains(this.checkedBlobItems_, blobItem)) {
   304  				var pos = -1;
   305  				for (var i = 0; i <= this.checkedBlobItems_.length; i++) {
   306  					var idx = this.indexOfChild(this.checkedBlobItems_[i]);
   307  					if (idx > thisIndex) {
   308  						pos = i;
   309  						break;
   310  					}
   311  				}
   312  				if (pos != -1) {
   313  					goog.array.insertAt(this.checkedBlobItems_, blobItem, pos)
   314  				} else {
   315  					this.checkedBlobItems_.push(blobItem);
   316  				}
   317  			}
   318  		} else {
   319  			blobItem.setState(goog.ui.Component.State.CHECKED, false);
   320  			if (goog.array.contains(this.checkedBlobItems_, blobItem)) {
   321  				var done = goog.array.remove(this.checkedBlobItems_, blobItem);
   322  				if (!done) {
   323  					alert("Failed to remove item from selection");
   324  				}
   325  			}
   326  		}
   327  	} else {
   328  		blobItem.setState(goog.ui.Component.State.CHECKED, isCheckingItem);
   329  		if (isCheckingItem) {
   330  			this.checkedBlobItems_.push(blobItem);
   331  		} else {
   332  			goog.array.remove(this.checkedBlobItems_, blobItem);
   333  		}
   334  	}
   335  	this.dispatchEvent(cam.BlobItemContainer.EventType.SELECTION_CHANGED);
   336  };
   337  
   338  cam.BlobItemContainer.prototype.unselectAll = function() {
   339  	goog.array.forEach(this.checkedBlobItems_, function(item) {
   340  		item.setState(goog.ui.Component.State.CHECKED, false);
   341  	});
   342  	this.checkedBlobItems_ = [];
   343  	this.dispatchEvent(cam.BlobItemContainer.EventType.SELECTION_CHANGED);
   344  };
   345  
   346  cam.BlobItemContainer.prototype.populateChildren_ = function(result, append) {
   347  	var i = append ? this.getChildCount() : 0;
   348  	for (var blob; blob = result.blobs[i]; i++) {
   349  		var blobRef = blob.blob;
   350  		var item = this.itemCache_[blobRef];
   351  		var render = true;
   352  
   353  		// If there's already an item for this blob, reuse it so that we don't lose any of the UI state (like whether it is selected).
   354  		if (item) {
   355  			item.update(blobRef, result.description.meta);
   356  			item.updateDom();
   357  			render = false;
   358  		} else {
   359  			item = new cam.BlobItem(blobRef, result.description.meta);
   360  			this.itemCache_[blobRef] = item;
   361  		}
   362  
   363  		if (append) {
   364  			this.addChild(item, render);
   365  		} else {
   366  			this.addChildAt(item, i, render);
   367  		}
   368  	}
   369  
   370  	// Remove any children we don't need anymore.
   371  	if (!append) {
   372  		var numBlobs = result.blobs.length;
   373  		while (this.getChildCount() > numBlobs) {
   374  			this.itemCache_[this.getChildAt(numBlobs).getBlobRef()] = null;
   375  			this.removeChildAt(numBlobs, true);
   376  		}
   377  	}
   378  };
   379  
   380  cam.BlobItemContainer.prototype.layout_ = function(force) {
   381  	var el = this.getElement();
   382  	var availWidth = el.clientWidth;
   383  
   384  	if (!this.isVisible()) {
   385  		return;
   386  	}
   387  
   388  	if (!force && !this.isLayoutDirty_ && availWidth == this.lastClientWidth_) {
   389  		return;
   390  	}
   391  
   392  	this.isLayoutDirty_ = false;
   393  	this.lastClientWidth_ = availWidth;
   394  
   395  	var currentTop = this.constructor.BLOB_ITEM_MARGIN;
   396  	var currentWidth = this.constructor.BLOB_ITEM_MARGIN;
   397  	var rowStart = 0;
   398  	var lastItem = this.getChildCount() - 1;
   399  
   400  	for (var i = rowStart; i <= lastItem; i++) {
   401  		var item = this.getChildAt(i);
   402  
   403  		var nextWidth = currentWidth + this.thumbnailSize_ * item.getThumbAspect() + this.constructor.BLOB_ITEM_MARGIN;
   404  		if (i != lastItem && nextWidth < availWidth) {
   405  			currentWidth = nextWidth;
   406  			continue;
   407  		}
   408  
   409  		// Decide how many items are going to be in this row. We choose the number that will result in the smallest adjustment to the image sizes having to be done.
   410  		var rowEnd, rowWidth;
   411  		if (i == lastItem) {
   412  			rowEnd = lastItem;
   413  			rowWidth = nextWidth;
   414  			if (nextWidth / availWidth <
   415  					this.constructor.LAST_ROW_CLOSE_ENOUGH_TO_FULL) {
   416  				availWidth = nextWidth;
   417  			}
   418  		} else if (availWidth - currentWidth <= nextWidth - availWidth) {
   419  			rowEnd = i - 1;
   420  			rowWidth = currentWidth;
   421  		} else {
   422  			rowEnd = i;
   423  			rowWidth = nextWidth;
   424  		}
   425  
   426  		currentTop += this.layoutRow_(rowStart, rowEnd, availWidth, rowWidth, currentTop) + this.constructor.BLOB_ITEM_MARGIN;
   427  
   428  		currentWidth = this.constructor.BLOB_ITEM_MARGIN;
   429  		rowStart = rowEnd + 1;
   430  		i = rowEnd;
   431  	}
   432  
   433  	el.style.height = currentTop + this.constructor.BLOB_ITEM_MARGIN + 'px';
   434  };
   435  
   436  // @param {Number} startIndex The index of the first item in the row.
   437  // @param {Number} endIndex The index of the last item in the row.
   438  // @param {Number} availWidth The width available to the row for layout.
   439  // @param {Number} usedWidth The width that the contents of the row consume
   440  // using their initial dimensions, before any scaling or clipping.
   441  // @param {Number} top The position of the top of the row.
   442  // @return {Number} The height of the row after layout.
   443  cam.BlobItemContainer.prototype.layoutRow_ = function(startIndex, endIndex, availWidth, usedWidth, top) {
   444  	var currentLeft = 0;
   445  	var rowHeight = Number.POSITIVE_INFINITY;
   446  
   447  	var numItems = endIndex - startIndex + 1;
   448  	var availThumbWidth = availWidth - (this.constructor.BLOB_ITEM_MARGIN * (numItems + 1));
   449  	var usedThumbWidth = usedWidth - (this.constructor.BLOB_ITEM_MARGIN * (numItems + 1));
   450  
   451  	for (var i = startIndex; i <= endIndex; i++) {
   452  		var item = this.getChildAt(i);
   453  
   454  		// We figure out the amount to adjust each item in this slightly non- intuitive way so that the adjustment is split up as fairly as possible. Figuring out a ratio up front and applying it to all items uniformly can end up with a large amount left over because of rounding.
   455  		var numItemsLeft = (endIndex + 1) - i;
   456  		var delta = Math.round((availThumbWidth - usedThumbWidth) / numItemsLeft);
   457  		var originalWidth = this.thumbnailSize_ * item.getThumbAspect();
   458  		var width = originalWidth + delta;
   459  		var ratio = width / originalWidth;
   460  		var height = Math.round(this.thumbnailSize_ * ratio);
   461  
   462  		var elm = item.getElement();
   463  		elm.style.left = currentLeft + this.constructor.BLOB_ITEM_MARGIN + 'px';
   464  		elm.style.top = top + 'px';
   465  		item.setSize(width, height);
   466  
   467  		currentLeft += width + this.constructor.BLOB_ITEM_MARGIN;
   468  		usedThumbWidth += delta;
   469  		rowHeight = Math.min(rowHeight, height);
   470  	}
   471  
   472  	for (var i = startIndex; i <= endIndex; i++) {
   473  		this.getChildAt(i).setHeight(rowHeight);
   474  	}
   475  
   476  	return rowHeight;
   477  };
   478  
   479  cam.BlobItemContainer.prototype.handleScroll_ = function() {
   480  	if (!this.isVisible()) {
   481  		return;
   482  	}
   483  
   484  	var docHeight = goog.dom.getDocumentHeight();
   485  	var scroll = goog.dom.getDocumentScroll();
   486  	var viewportSize = goog.dom.getViewportSize();
   487  
   488  	if ((docHeight - scroll.y - viewportSize.height) >
   489  			this.constructor.INFINITE_SCROLL_THRESHOLD_PX_) {
   490  		return;
   491  	}
   492  
   493  	if (this.searchSession_) {
   494  		this.searchSession_.loadMoreResults();
   495  	}
   496  };
   497  
   498  cam.BlobItemContainer.prototype.findByBlobrefDone_ = function(permanode, result) {
   499  	this.resetChildren_();
   500  	if (!result) {
   501  		return;
   502  	}
   503  	var meta = result.meta;
   504  	if (!meta || !meta[permanode]) {
   505  		return;
   506  	}
   507  	var item = new cam.BlobItem(permanode, meta);
   508  	this.addChild(item, true);
   509  };
   510  
   511  // Clears all children from this container, reseting to the default state.
   512  cam.BlobItemContainer.prototype.resetChildren_ = function() {
   513  	this.removeChildren(true);
   514  };
   515  
   516  cam.BlobItemContainer.prototype.handleFileDrop_ = function(e) {
   517  	var recipient = this.dragActiveElement_;
   518  	if (!recipient) {
   519  		console.log("No valid target to drag and drop on.");
   520  		return;
   521  	}
   522  
   523  	goog.dom.classes.remove(recipient.getElement(), 'cam-dropactive');
   524  	this.dragActiveElement_ = null;
   525  
   526  	var files = e.getBrowserEvent().dataTransfer.files;
   527  	for (var i = 0, n = files.length; i < n; i++) {
   528  		var file = files[i];
   529  		// TODO(bslatkin): Add an uploading item placeholder while the upload is in progress. Somehow pipe through the POST progress.
   530  		this.connection_.uploadFile(file, goog.bind(this.handleUploadSuccess_, this, file, recipient.blobRef_));
   531  	}
   532  };
   533  
   534  cam.BlobItemContainer.prototype.handleUploadSuccess_ = function(file, recipient, blobRef) {
   535  	this.connection_.createPermanode(
   536  		goog.bind(this.handleCreatePermanodeSuccess_, this, file, recipient, blobRef));
   537  };
   538  
   539  cam.BlobItemContainer.prototype.handleCreatePermanodeSuccess_ = function(file, recipient, blobRef, permanode) {
   540  	this.connection_.newSetAttributeClaim(permanode, 'camliContent', blobRef,
   541  		goog.bind(this.handleSetAttributeSuccess_, this, file, recipient, blobRef, permanode));
   542  };
   543  
   544  cam.BlobItemContainer.prototype.handleSetAttributeSuccess_ = function(file, recipient, blobRef, permanode) {
   545  	this.connection_.describeWithThumbnails(permanode, this.thumbnailSize_,
   546  		goog.bind(this.handleDescribeSuccess_, this, recipient, permanode));
   547  };
   548  
   549  cam.BlobItemContainer.prototype.handleDescribeSuccess_ = function(recipient, permanode, describeResult) {
   550  	if (recipient) {
   551  		this.connection_.newAddAttributeClaim(recipient, 'camliMember', permanode);
   552  	}
   553  
   554  	if (this.searchSession_ && this.searchSession_.supportsChangeNotifications()) {
   555  		// We'll find this when we reload.
   556  		return;
   557  	}
   558  
   559  	var item = new cam.BlobItem(permanode, describeResult.meta);
   560  	this.addChildAt(item, 0, true);
   561  	if (!recipient) {
   562  		return;
   563  	}
   564  };
   565  
   566  cam.BlobItemContainer.prototype.handleFileDrag_ = function(e) {
   567  	if (this.dragEndTimer_) {
   568  		this.dragEndTimer_ = window.clearTimeout(this.dragEndTimer_);
   569  	}
   570  	this.dragEndTimer_ = window.setTimeout(this.fileDragListener_, 2000);
   571  
   572  	var activeElement = e ? this.getOwnerControl(e.target) : e;
   573  	if (activeElement) {
   574  		if (!activeElement.isCollection()) {
   575  			activeElement = this;
   576  		}
   577  	} else if (e) {
   578  		activeElement = this;
   579  	}
   580  
   581  	if (activeElement == this.dragActiveElement_) {
   582  		return;
   583  	}
   584  
   585  	if (this.dragActiveElement_) {
   586  		goog.dom.classes.remove(this.dragActiveElement_.getElement(), 'cam-dropactive');
   587  	}
   588  
   589  	this.dragActiveElement_ = activeElement;
   590  
   591  	if (this.dragActiveElement_) {
   592  		goog.dom.classes.add(this.dragActiveElement_.getElement(), 'cam-dropactive');
   593  	}
   594  };
   595  
   596  cam.BlobItemContainer.prototype.hide_ = function() {
   597  	goog.dom.classes.remove(this.getElement(), 'cam-blobitemcontainer-' + this.thumbnailSize_);
   598  	goog.dom.classes.add(this.getElement(), 'cam-blobitemcontainer-hidden');
   599  };
   600  
   601  cam.BlobItemContainer.prototype.show_ = function() {
   602  	goog.dom.classes.remove(this.getElement(), 'cam-blobitemcontainer-hidden');
   603  	goog.dom.classes.add(this.getElement(), 'cam-blobitemcontainer-' + this.thumbnailSize_);
   604  	this.layout_(true);
   605  };