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