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

     1  /*
     2  Copyright 2011 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.PermanodePage');
    18  
    19  goog.require('goog.dom');
    20  goog.require('goog.string');
    21  goog.require('goog.events.EventHandler');
    22  goog.require('goog.events.EventType');
    23  goog.require('goog.events.FileDropHandler');
    24  goog.require('goog.ui.Component');
    25  
    26  goog.require('cam.BlobItem');
    27  goog.require('cam.BlobItemContainer');
    28  goog.require('cam.ServerConnection');
    29  
    30  // @param {cam.ServerType.DiscoveryDocument} config Global config of the current server this page is being rendered for.
    31  // @param {goog.dom.DomHelper=} opt_domHelper DOM helper to use.
    32  // @extends {goog.ui.Component}
    33  // @constructor
    34  cam.PermanodePage = function(config, opt_domHelper) {
    35  	goog.base(this, opt_domHelper);
    36  
    37  	this.config_ = config;
    38  
    39  	this.connection_ = new cam.ServerConnection(config);
    40  
    41  	this.blobItemContainer_ = new cam.BlobItemContainer(this.connection_, opt_domHelper);
    42  	this.blobItemContainer_.thumbnailSize_ = cam.BlobItemContainer.THUMBNAIL_SIZES_[3];
    43  
    44  	this.describeResponse_ = null;
    45  };
    46  goog.inherits(cam.PermanodePage, goog.ui.Component);
    47  
    48  cam.PermanodePage.prototype.decorateInternal = function(element) {
    49  	cam.PermanodePage.superClass_.decorateInternal.call(this, element);
    50  
    51  	var el = this.getElement();
    52  	goog.dom.classes.add(el, 'cam-permanode-page');
    53  
    54  };
    55  
    56  cam.PermanodePage.prototype.disposeInternal = function() {
    57  	cam.PermanodePage.superClass_.disposeInternal.call(this);
    58  	this.eh_.dispose();
    59  };
    60  
    61  cam.PermanodePage.prototype.enterDocument = function() {
    62  	cam.PermanodePage.superClass_.enterDocument.call(this);
    63  	var permanode = getPermanodeParam();
    64  	if (permanode) {
    65  		goog.dom.getElement('permanode').innerHTML = "<a href='./?p=" + permanode + "'>" + permanode + "</a>";
    66  		goog.dom.getElement('permanodeBlob').innerHTML = "<a href='./?b=" + permanode + "'>view blob</a>";
    67  		goog.dom.getElement('detailLink').innerHTML = "<a href='./detail.html?" + permanode + "'>new hotness</a>"
    68  	}
    69  
    70  	// TODO(mpl): use this.eh_ instead?
    71  	// set up listeners
    72  	goog.events.listen(goog.dom.getElement('formTitle'), goog.events.EventType.SUBMIT, this.handleFormTitleSubmit_, false, this);
    73  	goog.events.listen(goog.dom.getElement('formTags'), goog.events.EventType.SUBMIT, this.handleFormTagsSubmit_, false, this);
    74  	goog.events.listen(goog.dom.getElement('formAccess'), goog.events.EventType.SUBMIT, this.handleFormAccessSubmit_, false, this);
    75  	goog.events.listen(goog.dom.getElement('btnGallery'), goog.events.EventType.CLICK, function() {
    76  		var btnGallery = goog.dom.getElement('btnGallery');
    77  		if (btnGallery.value == "list") {
    78  			goog.dom.setTextContent(btnGallery, "List");
    79  			btnGallery.value = "thumbnails";
    80  		} else {
    81  			goog.dom.setTextContent(btnGallery, "Thumbnails");
    82  			btnGallery.value = "list";
    83  		}
    84  		this.reloadMembers_();
    85  	}, false, this);
    86  
    87  	// set publish roots
    88  	this.setupRootsDropdown_();
    89  
    90  	// set dnd and form for file upload
    91  	this.setupFilesHandlers_();
    92  
    93  	this.describeBlob_()
    94  
    95  	this.buildPathsList_()
    96  
    97  	this.blobItemContainer_.render(goog.dom.getElement('membersThumbs'));
    98  };
    99  
   100  // Gets the |p| query parameter, assuming that it looks like a blobref.
   101  function getPermanodeParam() {
   102  	var blobRef = getQueryParam('p');
   103  	return (blobRef && isPlausibleBlobRef(blobRef)) ? blobRef : null;
   104  };
   105  
   106  cam.PermanodePage.prototype.exitDocument = function() {
   107  	cam.PermanodePage.superClass_.exitDocument.call(this);
   108  };
   109  
   110  // @param {string} blobRef BlobRef for the uploaded file.
   111  // @param {string} permanode Permanode this blobRef is now the content of.
   112  cam.PermanodePage.prototype.describeBlob_ = function() {
   113  	var permanode = getPermanodeParam();
   114  	this.connection_.describeWithThumbnails(permanode, this.blobItemContainer_.thumbnailSize_,
   115  		goog.bind(this.handleDescribeBlob_, this, permanode),
   116  		function(msg) {
   117  			alert("failed to get blob description: " + msg);
   118  		}
   119  	);
   120  };
   121  
   122  // @param {string} permanode Node to describe.
   123  // @param {Object} describeResult Object of properties for the node.
   124  cam.PermanodePage.prototype.handleDescribeBlob_ = function(permanode, describeResult) {
   125  	var meta = describeResult.meta;
   126  	if (!meta[permanode]) {
   127  		alert("didn't get blob " + permanode);
   128  		return;
   129  	}
   130  	var permObj = meta[permanode].permanode;
   131  	if (!permObj) {
   132  		alert("blob " + permanode + " isn't a permanode");
   133  		return;
   134  	}
   135  	this.describeResponse_ = describeResult;
   136  
   137  	// title form
   138  	var permTitleValue = permAttr(permObj, "title") ? permAttr(permObj, "title") : "";
   139  	var inputTitle = goog.dom.getElement("inputTitle");
   140  	inputTitle.value = permTitleValue;
   141  	inputTitle.disabled = false;
   142  	var btnSaveTitle = goog.dom.getElement("btnSaveTitle");
   143  	btnSaveTitle.disabled = false;
   144  
   145  	// tags form
   146  	this.reloadTags_(permanode, describeResult);
   147  	var inputNewTag = goog.dom.getElement("inputNewTag");
   148  	inputNewTag.disabled = false;
   149  	var btnAddTag = goog.dom.getElement("btnAddTag");
   150  	btnAddTag.disabled = false;
   151  
   152  	// access form
   153  	var selectAccess = goog.dom.getElement("selectAccess");
   154  	var accessValue = permAttr(permObj,"camliAccess") ? permAttr(permObj,"camliAccess") : "private";
   155  	selectAccess.value = accessValue;
   156  	selectAccess.disabled = false;
   157  	var btnSaveAccess = goog.dom.getElement("btnSaveAccess");
   158  	btnSaveAccess.disabled = false;
   159  
   160  	// handle type detection
   161  	handleType(permObj);
   162  
   163  	// TODO(mpl): add a line showing something like
   164  	// "Content: file (blobref)" or
   165  	// "Content: directory (blobref)" or
   166  	// "Content: None (has members)".
   167  
   168  	// members
   169  	this.reloadMembers_();
   170  
   171  	// TODO(mpl): use a permanent blobItemContainer instead?
   172  	/* blob content */
   173  	var camliContent = permObj.attr.camliContent;
   174  	if (camliContent && camliContent.length > 0) {
   175  		var content = goog.dom.getElement('content');
   176  		content.innerHTML = '';
   177  		var useFileBlobrefAsLink = "true";
   178  		var blobItem = new cam.BlobItem(permanode, meta, useFileBlobrefAsLink);
   179  		blobItem.decorate(content);
   180  		blobItem.setSize(300, 300);
   181  		// TODO(mpl): ideally this should be done by handleType, but it's easier
   182  		// to do it now that we have a blobItem object to work with.
   183  		var isdir = blobItem.getDirBlobref_()
   184  		var mountTip = goog.dom.getElement("cammountTip");
   185  		goog.dom.removeChildren(mountTip);
   186  		if (isdir != "") {
   187  			var tip = "Mount with:";
   188  			goog.dom.setTextContent(mountTip, tip);
   189  			goog.dom.appendChild(mountTip, goog.dom.createDom("br"));
   190  			var codeTip = goog.dom.createDom("code");
   191  			goog.dom.setTextContent(codeTip, "$ cammount /some/mountpoint " + isdir);
   192  			goog.dom.appendChild(mountTip, codeTip);
   193  		}
   194  	}
   195  
   196  	// debug attrs
   197  	goog.dom.setTextContent(goog.dom.getElement("debugattrs"), JSON.stringify(permObj.attr, null, 2));
   198  };
   199  
   200  // TODO(mpl): pass directly the permanode object
   201  // @param {string} permanode Node to describe.
   202  // @param {Object} describeResult Object of properties for the node.
   203  cam.PermanodePage.prototype.reloadTags_ = function(permanode, describeResult) {
   204  	var permanodeObject = describeResult.meta[permanode].permanode;
   205  	var spanTags = document.getElementById("spanTags");
   206  	while (spanTags.firstChild) {
   207  		spanTags.removeChild(spanTags.firstChild);
   208  	}
   209  	var tags = permanodeObject.attr.tag;
   210  	for (idx in tags) {
   211  		var tag = tags[idx];
   212  
   213  		var tagSpan = goog.dom.createDom("span");
   214  		tagSpan.className = 'cam-permanode-tag-c';
   215  		var tagTextEl = goog.dom.createDom("span");
   216  		tagTextEl.className = 'cam-permanode-tag';
   217  		goog.dom.setTextContent(tagTextEl, tag);
   218  		goog.dom.appendChild(tagSpan, tagTextEl);
   219  
   220  		var tagDel = goog.dom.createDom("span");
   221  		tagDel.className = 'cam-permanode-del';
   222  		goog.dom.setTextContent(tagDel, "x");
   223  		goog.events.listen(tagDel, goog.events.EventType.CLICK, this.deleteTagFunc_(tag, tagTextEl, tagSpan), false, this);
   224  
   225  		goog.dom.appendChild(tagSpan, tagDel);
   226  		goog.dom.appendChild(spanTags, tagSpan);
   227  	}
   228  };
   229  
   230  // @param {Object} tag tag value to remove.
   231  // @param {Object} strikeEle text element to strike while we wait for the removal to take effect.
   232  // @param {Object} removeEle element to remove.
   233  // @return {Function}
   234  cam.PermanodePage.prototype.deleteTagFunc_ = function(tag, strikeEle, removeEle) {
   235  	var delFunc = function(e) {
   236  		strikeEle.innerHTML = "<del>" + strikeEle.innerHTML + "</del>";
   237  		this.connection_.newDelAttributeClaim(getPermanodeParam(), "tag", tag,
   238  			function() { removeEle.parentNode.removeChild(removeEle); },
   239  			function(msg) { alert(msg); }
   240  		);
   241  	};
   242  	return goog.bind(delFunc, this);
   243  };
   244  
   245  cam.PermanodePage.prototype.isCamliPathAttribute_ = function(name) {
   246  	return goog.string.startsWith(name, "camliPath:");
   247  };
   248  
   249  cam.PermanodePage.prototype.reloadMembers_ = function() {
   250  	var membersList = goog.dom.getElement('membersList');
   251  	var membersThumbs = goog.dom.getElement('membersThumbs');
   252  	membersList.innerHTML = '';
   253  
   254  	var meta = this.describeResponse_.meta;
   255  	var permanode = meta[getPermanodeParam()].permanode;
   256  	var attrs = permanode.attr;
   257  	var hasMembers = false;
   258  	var btnGallery = goog.dom.getElement('btnGallery');
   259  	var doThumbnails = (btnGallery.value == "thumbnails");
   260  
   261  	if (attrs.camliMember) {
   262  		attrs.camliMember.forEach(function(m) {
   263  			this.addMember_(m, "camliMember", meta, doThumbnails);
   264  			hasMembers = true;
   265  		}.bind(this));
   266  	}
   267  
   268  	for (var name in attrs) {
   269  		if (this.isCamliPathAttribute_(name)) {
   270  			var attr = permAttr(permanode, name);
   271  			if (attr) {
   272  				this.addMember_(attr, name, meta, doThumbnails);
   273  				hasMembers = true;
   274  			}
   275  		}
   276  	}
   277  
   278  	if (hasMembers) {
   279  		if (doThumbnails) {
   280  			this.blobItemContainer_.show_();
   281  		} else {
   282  			this.blobItemContainer_.hide_();
   283  			this.blobItemContainer_.resetChildren_();
   284  		}
   285  	}
   286  };
   287  
   288  // @param {string} pn child permanode.
   289  // @param {Object} meta meta in describe response.
   290  // @param {boolean} thumbnails whether to display thumbnails or a list
   291  cam.PermanodePage.prototype.addMember_ = function(pn, path, meta, thumbnails) {
   292  	var blobItem = new cam.BlobItem(pn, meta);
   293  	if (thumbnails) {
   294  		this.blobItemContainer_.addChild(blobItem, true)
   295  	} else {
   296  		var membersList = goog.dom.getElement("membersList");
   297  		var ul;
   298  		if (membersList.innerHTML == "") {
   299  			ul = goog.dom.createDom("ul");
   300  			goog.dom.appendChild(membersList, ul);
   301  		} else {
   302  			ul = membersList.firstChild;
   303  		}
   304  		var li = goog.dom.createDom("li");
   305  		var a = goog.dom.createDom("a");
   306  		a.href = "./?p=" + pn;
   307  		goog.dom.setTextContent(a, blobItem.getTitle_());
   308  
   309  		var del = goog.dom.createDom("span");
   310  		del.className = 'cam-permanode-del';
   311  		goog.dom.setTextContent(del, "x");
   312  		goog.events.listen(del, goog.events.EventType.CLICK, this.deleteMemberFunc_(pn, path, a, li), false, this);
   313  
   314  		goog.dom.appendChild(li, a);
   315  		goog.dom.appendChild(li, del);
   316  		goog.dom.appendChild(ul, li);
   317  	}
   318  };
   319  
   320  // @param {string} member child permanode
   321  // @param {Object} strikeEle text element to strike while we wait for the removal to take effect.
   322  // @param {Object} removeEle element to remove.
   323  // @return {Function}
   324  cam.PermanodePage.prototype.deleteMemberFunc_ = function(member, path, strikeEle, removeEle) {
   325  	var delFunc = function(e) {
   326  		strikeEle.innerHTML = "<del>" + strikeEle.innerHTML + "</del>";
   327  		this.connection_.newDelAttributeClaim(getPermanodeParam(), path, member,
   328  			goog.bind(function() {
   329  				removeEle.parentNode.removeChild(removeEle);
   330  				// TODO(mpl): refreshing the whole thing is kindof heavy, maybe?
   331  				this.describeBlob_();
   332  			}, this),
   333  			function(msg) {
   334  				alert(msg);
   335  			}
   336  		);
   337  	};
   338  	return goog.bind(delFunc, this);
   339  };
   340  
   341  // @param {string} sourcePermanode permanode pointed by the path.
   342  // @param {string} path path to remove.
   343  // @param {Object} strikeEle element to remove.
   344  // @return {Function}
   345  cam.PermanodePage.prototype.deletePathFunc_ = function(sourcePermanode, path, strikeEle) {
   346  	var delFunc = function(e) {
   347  		strikeEle.innerHTML = "<del>" + strikeEle.innerHTML + "</del>";
   348  		this.connection_.newDelAttributeClaim(
   349  			sourcePermanode,
   350  			"camliPath:" + path,
   351  			getPermanodeParam(),
   352  			goog.bind(function() {
   353  				this.buildPathsList_();
   354  			}, this),
   355  			function(msg) {
   356  				alert(msg);
   357  			}
   358  		);
   359  	};
   360  	return goog.bind(delFunc, this);
   361  };
   362  
   363  cam.PermanodePage.prototype.handleFormTitleSubmit_ = function(e) {
   364  	e.stopPropagation();
   365  	e.preventDefault();
   366  
   367  	var inputTitle = goog.dom.getElement("inputTitle");
   368  	inputTitle.disabled = true;
   369  	var btnSaveTitle = goog.dom.getElement("btnSaveTitle");
   370  	btnSaveTitle.disabled = true;
   371  
   372  	var startTime = new Date();
   373  	this.connection_.newSetAttributeClaim(
   374  		getPermanodeParam(), "title", inputTitle.value,
   375  		goog.bind(function() {
   376  			var elapsedMs = new Date().getTime() - startTime.getTime();
   377  			setTimeout(goog.bind(function() {
   378  				inputTitle.disabled = false;
   379  				btnSaveTitle.disabled = false;
   380  				this.describeBlob_();
   381  			},this), Math.max(250 - elapsedMs, 0));
   382  		}, this),
   383  		function(msg) {
   384  			alert(msg);
   385  			inputTitle.disabled = false;
   386  			btnSaveTitle.disabled = false;
   387  		}
   388  	);
   389  };
   390  
   391  cam.PermanodePage.prototype.handleFormTagsSubmit_ = function(e) {
   392  	e.stopPropagation();
   393  	e.preventDefault();
   394  
   395  	var input = goog.dom.getElement("inputNewTag");
   396  	var btn = goog.dom.getElement("btnAddTag");
   397  	if (input.value == "") {
   398  		return;
   399  	}
   400  	input.disabled = true;
   401  	btn.disabled = true;
   402  
   403  	var startTime = new Date();
   404  	var tags = input.value.split(/\s*,\s*/);
   405  	var nRemain = tags.length;
   406  	var oneDone = goog.bind(function() {
   407  		nRemain--;
   408  		if (nRemain == 0) {
   409  			var elapsedMs = new Date().getTime() - startTime.getTime();
   410  			setTimeout(goog.bind(function() {
   411  				input.value = '';
   412  				input.disabled = false;
   413  				btn.disabled = false;
   414  				this.describeBlob_();
   415  			}, this), Math.max(250 - elapsedMs, 0));
   416  		}
   417  	}, this);
   418  	for (idx in tags) {
   419  		var tag = tags[idx];
   420  		this.connection_.newAddAttributeClaim(
   421  			getPermanodeParam(), "tag", tag, oneDone,
   422  			function(msg) {
   423  				alert(msg);
   424  				oneDone();
   425  			}
   426  		);
   427  	}
   428  };
   429  
   430  cam.PermanodePage.prototype.handleFormAccessSubmit_ = function(e) {
   431  	e.stopPropagation();
   432  	e.preventDefault();
   433  
   434  	var selectAccess = goog.dom.getElement("selectAccess");
   435  	selectAccess.disabled = true;
   436  	var btnSaveAccess = goog.dom.getElement("btnSaveAccess");
   437  	btnSaveAccess.disabled = true;
   438  
   439  	var operation = this.connection_.newDelAttributeClaim;
   440  	var value = "";
   441  	if (selectAccess.value != "private") {
   442  		operation = this.connection_.newSetAttributeClaim;
   443  		value = selectAccess.value;
   444  	}
   445  
   446  	var startTime = new Date();
   447  	operation = goog.bind(operation, this.connection_);
   448  	operation(
   449  		getPermanodeParam(),
   450  		"camliAccess",
   451  		value,
   452  		function() {
   453  			var elapsedMs = new Date().getTime() - startTime.getTime();
   454  			setTimeout(function() {
   455  				selectAccess.disabled = false;
   456  				btnSaveAccess.disabled = false;
   457  			}, Math.max(250 - elapsedMs, 0));
   458  		},
   459  		function(msg) {
   460  			alert(msg);
   461  			selectAccess.disabled = false;
   462  			btnSaveAccess.disabled = false;
   463  		}
   464  	);
   465  };
   466  
   467  cam.PermanodePage.prototype.setupRootsDropdown_ = function() {
   468  	var selRoots = goog.dom.getElement("selectPublishRoot");
   469  	if (!this.config_.publishRoots) {
   470  		console.log("no publish roots");
   471  		return;
   472  	}
   473  	for (var rootName in this.config_.publishRoots) {
   474  		var opt = goog.dom.createElement("option");
   475  		opt.setAttribute("value", rootName);
   476  		goog.dom.appendChild(opt, goog.dom.createTextNode(this.config_.publishRoots[rootName].prefix[0]));
   477  		goog.dom.appendChild(selRoots, opt);
   478  	}
   479  	goog.events.listen(goog.dom.getElement("btnSavePublish"), goog.events.EventType.CLICK, this.handleSavePublish_, false, this);
   480  };
   481  
   482  cam.PermanodePage.prototype.handleSavePublish_ = function(e) {
   483  	var selRoots = goog.dom.getElement("selectPublishRoot");
   484  	var suffix = goog.dom.getElement("publishSuffix");
   485  
   486  	var ourPermanode = getPermanodeParam();
   487  	if (!ourPermanode) {
   488  		return;
   489  	}
   490  
   491  	var publishRoot = selRoots.value;
   492  	if (!publishRoot) {
   493  		alert("no publish root selected");
   494  		return;
   495  	}
   496  	var pathSuffix = suffix.value;
   497  	if (!pathSuffix) {
   498  		alert("no path suffix specified");
   499  		return;
   500  	}
   501  
   502  	selRoots.disabled = true;
   503  	suffix.disabled = true;
   504  
   505  	var enabled = function() {
   506  		selRoots.disabled = false;
   507  		suffix.disabled = false;
   508  	};
   509  
   510  	// Step 1: resolve selRoots.value -> blobref of the root's permanode.
   511  	// Step 2: set attribute on the root's permanode, or a sub-permanode
   512  	// if multiple path components in suffix:
   513  	// "camliPath:<suffix>" => permanode-of-ourselves
   514  
   515  	var sigconf = this.config_.signing;
   516  	var handleFindCamliRoot = function(pnres) {
   517  		if (!pnres.permanode) {
   518  			alert("failed to publish root's permanode");
   519  			enabled();
   520  			return;
   521  		}
   522  		var handleSetCamliPath = function() {
   523  			console.log("success.");
   524  			enabled();
   525  			selRoots.value = "";
   526  			suffix.value = "";
   527  			this.buildPathsList_();
   528  		};
   529  		var handleFailCamliPath = function() {
   530  			alert("failed to set attribute");
   531  			enabled();
   532  		};
   533  		this.connection_.newSetAttributeClaim(
   534  			pnres.permanode, "camliPath:" + pathSuffix, ourPermanode,
   535  			goog.bind(handleSetCamliPath, this), handleFailCamliPath
   536  		);
   537  	};
   538  	var handleFailFindCamliRoot = function() {
   539  		alert("failed to find publish root's permanode");
   540  		enabled();
   541  	};
   542  	this.connection_.permanodeOfSignerAttrValue(
   543  		sigconf.publicKeyBlobRef, "camliRoot", publishRoot,
   544  		goog.bind(handleFindCamliRoot, this), handleFailFindCamliRoot
   545  	);
   546  };
   547  
   548  cam.PermanodePage.prototype.buildPathsList_ = function() {
   549  	var ourPermanode = getPermanodeParam();
   550  	if (!ourPermanode) {
   551  		return;
   552  	}
   553  	var sigconf = this.config_.signing;
   554  
   555  	var handleFindPath = function(jres) {
   556  		var div = goog.dom.getElement("existingPaths");
   557  
   558  		// TODO: there can be multiple paths in this list, but the HTML
   559  		// UI only shows one.	The UI should show all, and when adding a new one
   560  		// prompt users whether they want to add to or replace the existing one.
   561  		// For now we just update the UI to show one.
   562  		// alert(JSON.stringify(jres, null, 2));
   563  		if (jres.paths && jres.paths.length > 0) {
   564  			div.innerHTML = "Existing paths for this permanode:";
   565  			var ul = goog.dom.createElement("ul");
   566  			goog.dom.appendChild(div,ul);
   567  			for (var idx in jres.paths) {
   568  				var path = jres.paths[idx];
   569  				var li = goog.dom.createElement("li");
   570  				var span = goog.dom.createElement("span");
   571  				goog.dom.appendChild(li,span);
   572  
   573  				var blobLink = goog.dom.createElement("a");
   574  				blobLink.href = ".?p=" + path.baseRef;
   575  				goog.dom.setTextContent(blobLink, path.baseRef);
   576  				goog.dom.appendChild(span,blobLink);
   577  
   578  				goog.dom.appendChild(span,goog.dom.createTextNode(" - "));
   579  
   580  				var pathLink = goog.dom.createElement("a");
   581  				pathLink.href = "";
   582  				goog.dom.setTextContent(pathLink, path.suffix);
   583  				for (var key in this.config_.publishRoots) {
   584  					var root = this.config_.publishRoots[key];
   585  					if (root.currentPermanode == path.baseRef) {
   586  						// Prefix should include a trailing slash.
   587  						pathLink.href = root.prefix[0] + path.suffix;
   588  						// TODO: Check if we're the latest permanode
   589  						// for this path and display some "old" notice
   590  						// if not.
   591  						break;
   592  					}
   593  				}
   594  				goog.dom.appendChild(span,pathLink);
   595  
   596  				var del = goog.dom.createElement("span");
   597  				del.className = "cam-permanode-del";
   598  				goog.dom.setTextContent(del, "x");
   599  				goog.events.listen(del, goog.events.EventType.CLICK, this.deletePathFunc_(path.baseRef, path.suffix, span), false, this);
   600  				goog.dom.appendChild(span,del);
   601  
   602  				goog.dom.appendChild(ul,li);
   603  			}
   604  		} else {
   605  			div.innerHTML = "";
   606  		}
   607  	};
   608  	this.connection_.pathsOfSignerTarget(sigconf.publicKeyBlobRef, ourPermanode, goog.bind(handleFindPath, this), alert);
   609  };
   610  
   611  // TODO(mpl): reuse blobitem code for dnd?
   612  cam.PermanodePage.prototype.setupFilesHandlers_ = function() {
   613  	var dnd = goog.dom.getElement("dnd");
   614  	goog.events.listen(goog.dom.getElement("fileForm"), goog.events.EventType.SUBMIT, this.handleFilesSubmit_, false, this);
   615  	goog.events.listen(goog.dom.getElement("fileInput"), goog.events.EventType.CHANGE, onFileInputChange, false, this);
   616  
   617  	var stop = function(e) {
   618  		this.classList &&
   619  			goog.dom.classes.add(this, 'cam-permanode-dnd-over');
   620  		e.stopPropagation();
   621  		e.preventDefault();
   622  	};
   623  	goog.events.listen(dnd, goog.events.EventType.DRAGENTER, stop, false, this);
   624  	goog.events.listen(dnd, goog.events.EventType.DRAGOVER, stop, false, this);
   625  	goog.events.listen(dnd, goog.events.EventType.DRAGLEAVE,
   626  		goog.bind(function() {
   627  			goog.dom.classes.remove(this, 'cam-permanode-dnd-over');
   628  		}, this), false, this);
   629  
   630  	var drop = function(e) {
   631  		goog.dom.classes.remove(this, 'cam-permanode-dnd-over');
   632  		stop(e);
   633  		var dt = e.getBrowserEvent().dataTransfer;
   634  		var files = dt.files;
   635  		goog.dom.getElement("info").innerHTML = "";
   636  		this.handleFiles_(files);
   637  	};
   638  	goog.events.listen(dnd, goog.events.FileDropHandler.EventType.DROP, goog.bind(drop, this), false, this);
   639  };
   640  
   641  cam.PermanodePage.prototype.handleFilesSubmit_ = function(e) {
   642  	e.stopPropagation();
   643  	e.preventDefault();
   644  	this.handleFiles_(document.getElementById("fileInput").files);
   645  };
   646  
   647  // @param {Array<files>} files the files to upload.
   648  cam.PermanodePage.prototype.handleFiles_ = function(files) {
   649  	for (var i = 0; i < files.length; i++) {
   650  		var file = files[i];
   651  		this.startFileUpload_(file);
   652  	}
   653  };
   654  
   655  cam.PermanodePage.prototype.startFileUpload_ = function(file) {
   656  	var dnd = goog.dom.getElement("dnd");
   657  	var up = goog.dom.createElement("div");
   658  	up.className= 'cam-permanode-dnd-item';
   659  	goog.dom.appendChild(dnd, up);
   660  	var info = "name=" + file.name + " size=" + file.size + "; type=" + file.type;
   661  
   662  	var setStatus = function(status) {
   663  		up.innerHTML = info + " " + status;
   664  	};
   665  	setStatus("(scanning)");
   666  
   667  	var onFail = function(msg) {
   668  		up.innerHTML = info + " <strong>fail:</strong> ";
   669  		goog.dom.appendChild(up, goog.dom.createTextNode(msg));
   670  	};
   671  
   672  	var onGotFileSchemaRef = function(fileref) {
   673  		setStatus(" <strong>fileref: " + fileref + "</strong>");
   674  		this.connection_.createPermanode(
   675  			goog.bind(function(filepn) {
   676  				var doneWithAll = goog.bind(function() {
   677  					setStatus("- done");
   678  					this.describeBlob_();
   679  				}, this);
   680  				var addMemberToParent = function() {
   681  					setStatus("adding member");
   682  					this.connection_.newAddAttributeClaim(
   683  						getPermanodeParam(), "camliMember", filepn,
   684  						doneWithAll, onFail
   685  					);
   686  				};
   687  				var makePermanode = goog.bind(function() {
   688  					setStatus("making permanode");
   689  					this.connection_.newSetAttributeClaim(
   690  						filepn, "camliContent", fileref,
   691  						goog.bind(addMemberToParent, this), onFail
   692  					);
   693  				}, this);
   694  				makePermanode();
   695  			}, this),
   696  			onFail
   697  		);
   698  	};
   699  
   700  	this.connection_.uploadFile(file, goog.bind(onGotFileSchemaRef, this), onFail,
   701  	function(contentsRef) {
   702  		setStatus("(checking for dup of " + contentsRef + ")");
   703  	});
   704  };
   705  
   706  // Returns the first value from the query string corresponding to |key|.
   707  // Returns null if the key isn't present.
   708  getQueryParam = function(key) {
   709  	var params = document.location.search.substring(1).split('&');
   710  	for (var i = 0; i < params.length; ++i) {
   711  		var parts = params[i].split('=');
   712  		if (parts.length == 2 && decodeURIComponent(parts[0]) == key)
   713  			return decodeURIComponent(parts[1]);
   714  	}
   715  	return null;
   716  };
   717  
   718  // Returns true if the passed-in string might be a blobref.
   719  isPlausibleBlobRef = function(blobRef) {
   720  	return /^\w+-[a-f0-9]+$/.test(blobRef);
   721  };
   722  
   723  function hasNamedMembers(permanode) {
   724  	for (var name in permanode.attr) {
   725  		if (/^camliPath:/.test(name)) {
   726  			return Boolean(permAttr(permanode, name));
   727  		}
   728  	}
   729  	return false;
   730  }
   731  
   732  function hasUnnamedMembers(permanode) {
   733  	return permAttr(permanode, "camliMember");
   734  }
   735  
   736  function permAttr(permanodeObject, name) {
   737  	if (!(name in permanodeObject.attr)) {
   738  		return null;
   739  	}
   740  	if (permanodeObject.attr[name].length == 0) {
   741  		return null;
   742  	}
   743  	return permanodeObject.attr[name][0];
   744  };
   745  
   746  function handleType(permObj) {
   747  	var disablePublish = false;
   748  	var selType = goog.dom.getElement("type");
   749  	var dnd = goog.dom.getElement("dnd");
   750  	var btnGallery = goog.dom.getElement("btnGallery");
   751  	var membersDiv = goog.dom.getElement("members");
   752  	dnd.style.display = "none";
   753  	btnGallery.style.visibility = 'hidden';
   754  	goog.dom.setTextContent(membersDiv, "");
   755  	if (permAttr(permObj, "camliRoot")) {
   756  		disablePublish = true;	// can't give a URL to a root with a claim
   757  	} else if (hasNamedMembers(permObj) || hasUnnamedMembers(permObj)) {
   758  		dnd.style.display = "block";
   759  		btnGallery.style.visibility = 'visible';
   760  		goog.dom.setTextContent(membersDiv, "Members:");
   761  	}
   762  
   763  	goog.dom.getElement("selectPublishRoot").disabled = disablePublish;
   764  	goog.dom.getElement("publishSuffix").disabled = disablePublish;
   765  	goog.dom.getElement("btnSavePublish").disabled = disablePublish;
   766  };
   767  
   768  function $(id) { return goog.dom.getElement(id) }
   769  
   770  function onFileInputChange(e) {
   771  	var s = "";
   772  	var files = $("fileInput").files;
   773  	for (var i = 0; i < files.length; i++) {
   774  		var file = files[i];
   775  		s += "<p>" + file.name + "</p>";
   776  	}
   777  	var fl = $("filelist");
   778  	fl.innerHTML = s;
   779  }