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

     1  /*
     2  Copyright 2012 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.IndexPage');
    18  
    19  goog.require('goog.array');
    20  goog.require('goog.dom');
    21  goog.require('goog.dom.classes');
    22  goog.require('goog.events.EventHandler');
    23  goog.require('goog.events.EventType');
    24  goog.require('goog.events.KeyCodes');
    25  goog.require('goog.string');
    26  goog.require('goog.Uri');
    27  goog.require('goog.ui.Component');
    28  goog.require('goog.ui.Textarea');
    29  
    30  goog.require('cam.AnimationLoop');
    31  goog.require('cam.BlobItemContainer');
    32  goog.require('cam.DetailView');
    33  goog.require('cam.object');
    34  goog.require('cam.Nav');
    35  goog.require('cam.Navigator');
    36  goog.require('cam.SearchSession');
    37  goog.require('cam.ServerConnection');
    38  goog.require('cam.ServerType');
    39  
    40  cam.IndexPage = function(config, opt_domHelper) {
    41  	goog.base(this, opt_domHelper);
    42  
    43  	this.config_ = config;
    44  
    45  	this.connection_ = new cam.ServerConnection(config);
    46  
    47  	this.eh_ = new goog.events.EventHandler(this);
    48  
    49  	this.baseURL_ = goog.Uri.resolve(location.href, this.config_.uiRoot);
    50  	this.currentURL_ = null;
    51  
    52  	this.navigator_ = new cam.Navigator(window, location, history, true);
    53  	this.navigator_.onNavigate = this.handleURL_.bind(this);
    54  
    55  	this.nav_ = new cam.Nav(opt_domHelper, this);
    56  
    57  	this.searchNavItem_ = new cam.Nav.SearchItem(this.dom_, 'magnifying_glass.svg', 'Search');
    58  	this.newPermanodeNavItem_ = new cam.Nav.Item(this.dom_, 'new_permanode.svg', 'New permanode');
    59  	this.searchRootsNavItem_ = new cam.Nav.Item(this.dom_, 'icon_27307.svg', 'Search roots');
    60  	this.selectAsCurrentSetNavItem_ = new cam.Nav.Item(this.dom_, 'target.svg', 'Select as current set');
    61  	this.selectAsCurrentSetNavItem_.setVisible(false);
    62  	this.addToSetNavItem_ = new cam.Nav.Item(this.dom_, 'icon_16716.svg', 'Add to set');
    63  	this.addToSetNavItem_.setVisible(false);
    64  	this.createSetWithSelectionNavItem_ = new cam.Nav.Item(this.dom_, 'circled_plus.svg', 'Create set with 5 items');
    65  	this.createSetWithSelectionNavItem_.setVisible(false);
    66  	this.clearSelectionNavItem_ = new cam.Nav.Item(this.dom_, 'clear.svg', 'Clear selection');
    67  	this.clearSelectionNavItem_.setVisible(false);
    68  	this.embiggenNavItem_ = new cam.Nav.Item(this.dom_, 'up.svg', 'Moar bigger');
    69  	this.ensmallenNavItem_ = new cam.Nav.Item(this.dom_, 'down.svg', 'Less bigger');
    70  	this.logoNavItem_ = new cam.Nav.LinkItem(this.dom_, '/favicon.ico', 'Camlistore', '/ui/');
    71  	this.logoNavItem_.addClassName('cam-logo');
    72  
    73  	this.searchSession_ = null;
    74  
    75  	this.blobItemContainer_ = new cam.BlobItemContainer(this.connection_, opt_domHelper);
    76  	this.blobItemContainer_.isSelectionEnabled = true;
    77  	this.blobItemContainer_.isFileDragEnabled = true;
    78  
    79  	// TODO(aa): This is a quick hack to make the scroll position restore in the case where you go to detail view, then press back to search page.
    80  	// To make the reload case work we need to save the scroll position in window.history. That needs more thought though, we might want to store something more abstract that the scroll position.
    81  	this.savedScrollPosition_ = 0;
    82  
    83  	this.inDetailMode_ = false;
    84  	this.inSearchMode_ = false;
    85  	this.detail_ = null;
    86  	this.detailLoop_ = null;
    87  	this.detailViewHost_ = null;
    88  };
    89  goog.inherits(cam.IndexPage, goog.ui.Component);
    90  
    91  cam.IndexPage.prototype.onNavOpen = function() {
    92  	this.setTransform_();
    93  };
    94  
    95  cam.IndexPage.prototype.setTransform_ = function() {
    96  	var currentWidth = this.getElement().offsetWidth - 36;
    97  	var desiredWidth = currentWidth - (275 - 36);
    98  	var scale = desiredWidth / currentWidth;
    99  
   100  	var currentHeight = goog.dom.getDocumentHeight();
   101  	var currentScroll = goog.dom.getDocumentScroll().y;
   102  	var potentialScroll = currentHeight - goog.dom.getViewportSize().height;
   103  	var originY = currentHeight * currentScroll / potentialScroll;
   104  
   105  	goog.style.setStyle(this.blobItemContainer_.getElement(),
   106  		// The 3d transform is important. See: https://code.google.com/p/camlistore/issues/detail?id=284.
   107  		{'transform': goog.string.subs('scale3d(%s, %s, 1)', scale, scale),
   108  			'transform-origin': goog.string.subs('right %spx 0', originY)});
   109  };
   110  
   111  cam.IndexPage.prototype.onNavClose = function() {
   112  	if (!this.blobItemContainer_.getElement()) {
   113  		return;
   114  	}
   115  	this.searchNavItem_.setText('');
   116  	this.searchNavItem_.blur();
   117  	goog.style.setStyle(this.blobItemContainer_.getElement(), {'transform': ''});
   118  };
   119  
   120  cam.IndexPage.SEARCH_PREFIX_ = {
   121  	RAW: 'raw'
   122  };
   123  
   124  cam.IndexPage.prototype.createDom = function() {
   125  	this.decorateInternal(this.dom_.createElement('div'));
   126  };
   127  
   128  cam.IndexPage.prototype.decorateInternal = function(element) {
   129  	cam.IndexPage.superClass_.decorateInternal.call(this, element);
   130  
   131  	var el = this.getElement();
   132  
   133  	document.title = this.config_.ownerName + '\'s Vault';
   134  
   135  	this.nav_.addChild(this.searchNavItem_, true);
   136  	this.nav_.addChild(this.newPermanodeNavItem_, true);
   137  	this.nav_.addChild(this.searchRootsNavItem_, true);
   138  	this.nav_.addChild(this.selectAsCurrentSetNavItem_, true);
   139  	this.nav_.addChild(this.addToSetNavItem_, true);
   140  	this.nav_.addChild(this.createSetWithSelectionNavItem_, true);
   141  	this.nav_.addChild(this.clearSelectionNavItem_, true);
   142  	this.nav_.addChild(this.embiggenNavItem_, true);
   143  	this.nav_.addChild(this.ensmallenNavItem_, true);
   144  	this.nav_.addChild(this.logoNavItem_, true);
   145  
   146  	this.detailViewHost_ = this.dom_.createElement('div');
   147  
   148  	this.addChild(this.nav_, true);
   149  	this.addChild(this.blobItemContainer_, true);
   150  	el.appendChild(this.detailViewHost_);
   151  };
   152  
   153  cam.IndexPage.prototype.updateNavButtonsForSelection_ = function() {
   154  	var blobItems = this.blobItemContainer_.getCheckedBlobItems();
   155  	var count = blobItems.length;
   156  
   157  	if (count) {
   158  		var txt = 'Create set with ' + count + ' item' + (count > 1 ? 's' : '');
   159  		this.createSetWithSelectionNavItem_.setContent(txt);
   160  		this.createSetWithSelectionNavItem_.setVisible(true);
   161  		this.clearSelectionNavItem_.setVisible(true);
   162  	} else {
   163  		this.createSetWithSelectionNavItem_.setContent('');
   164  		this.createSetWithSelectionNavItem_.setVisible(false);
   165  		this.clearSelectionNavItem_.setVisible(false);
   166  	}
   167  
   168  	if (this.blobItemContainer_.currentCollec_ && this.blobItemContainer_.currentCollec_ != "" && blobItems.length > 0) {
   169  		this.addToSetNavItem_.setVisible(true);
   170  	} else {
   171  		this.addToSetNavItem_.setVisible(false);
   172  	}
   173  
   174  	if (blobItems.length == 1 && blobItems[0].isCollection()) {
   175  		this.selectAsCurrentSetNavItem_.setVisible(true);
   176  	} else {
   177  		this.selectAsCurrentSetNavItem_.setVisible(false);
   178  	}
   179  };
   180  
   181  cam.IndexPage.prototype.disposeInternal = function() {
   182  	cam.IndexPage.superClass_.disposeInternal.call(this);
   183  	this.eh_.dispose();
   184  };
   185  
   186  cam.IndexPage.prototype.enterDocument = function() {
   187  	cam.IndexPage.superClass_.enterDocument.call(this);
   188  
   189  	this.connection_.serverStatus(goog.bind(function(resp) {
   190  		this.handleServerStatus_(resp);
   191  	}, this));
   192  
   193  	this.searchNavItem_.onSearch = this.setURLSearch_.bind(this);
   194  
   195  	this.embiggenNavItem_.onClick = function() {
   196  		if (this.blobItemContainer_.bigger()) {
   197  			var force = true;
   198  			this.blobItemContainer_.layout_(force);
   199  		}
   200  	}.bind(this);
   201  
   202  	this.ensmallenNavItem_.onClick = function() {
   203  		if (this.blobItemContainer_.smaller()) {
   204  			// Don't run a query. Let the browser do the image resizing on its own.
   205  			var force = true;
   206  			this.blobItemContainer_.layout_(force);
   207  			// Since things got smaller, we may need to fetch more content.
   208  			this.blobItemContainer_.handleScroll_();
   209  		}
   210  	}.bind(this);
   211  
   212  	this.createSetWithSelectionNavItem_.onClick = function() {
   213  		var blobItems = this.blobItemContainer_.getCheckedBlobItems();
   214  		this.createNewSetWithItems_(blobItems);
   215  	}.bind(this);
   216  
   217  	this.clearSelectionNavItem_.onClick = this.blobItemContainer_.unselectAll.bind(this.blobItemContainer_);
   218  
   219  	this.newPermanodeNavItem_.onClick = function() {
   220  		this.connection_.createPermanode(function(p) {
   221  			window.location = './?p=' + p;
   222  		}, function(failMsg) {
   223  			console.error('Failed to create permanode: ' + failMsg);
   224  		});
   225  	}.bind(this);
   226  
   227  	this.addToSetNavItem_.onClick = function() {
   228  		var blobItems = this.blobItemContainer_.getCheckedBlobItems();
   229  		this.addItemsToSet_(blobItems);
   230  	}.bind(this);
   231  
   232  	this.selectAsCurrentSetNavItem_.onClick = function() {
   233  		var blobItems = this.blobItemContainer_.getCheckedBlobItems();
   234  		// there should be only one item selected
   235  		if (blobItems.length != 1) {
   236  			alert("Cannet set multiple items as current collection");
   237  			return;
   238  		}
   239  		this.blobItemContainer_.currentCollec_ = blobItems[0].blobRef_;
   240  		this.blobItemContainer_.unselectAll();
   241  		this.updateNavButtonsForSelection_();
   242  	}.bind(this);
   243  
   244  	this.searchRootsNavItem_.onClick = this.setURLSearch_.bind(this, {
   245  		permanode: {
   246  			attr: 'camliRoot',
   247  			numValue: {
   248  				min: 1
   249  			}
   250  		}
   251  	});
   252  
   253  	this.eh_.listen(this.blobItemContainer_, cam.BlobItemContainer.EventType.SELECTION_CHANGED, this.updateNavButtonsForSelection_.bind(this));
   254  
   255  	this.eh_.listen(this.getElement(), 'keypress', function(e) {
   256  		if (String.fromCharCode(e.charCode) == '/') {
   257  			this.nav_.open();
   258  			this.searchNavItem_.focus();
   259  			e.preventDefault();
   260  		}
   261  	});
   262  
   263  	this.handleURL_(new goog.Uri(location.href));
   264  };
   265  
   266  cam.IndexPage.prototype.exitDocument = function() {
   267  	cam.IndexPage.superClass_.exitDocument.call(this);
   268  	// Clear event handlers here
   269  };
   270  
   271  cam.IndexPage.prototype.createNewSetWithItems_ = function(blobItems) {
   272  	this.connection_.createPermanode(goog.bind(this.addMembers_, this, true, blobItems));
   273  };
   274  
   275  cam.IndexPage.prototype.addItemsToSet_ = function(blobItems) {
   276  	if (!this.blobItemContainer_.currentCollec_ || this.blobItemContainer_.currentCollec_ == "") {
   277  		alert("no destination collection selected");
   278  	}
   279  	this.addMembers_(false, blobItems, this.blobItemContainer_.currentCollec_);
   280  };
   281  
   282  cam.IndexPage.prototype.addMembers_ = function(newSet, blobItems, permanode) {
   283  	var deferredList = [];
   284  	var complete = goog.bind(this.addItemsToSetDone_, this, permanode);
   285  	var callback = function() {
   286  		deferredList.push(1);
   287  		if (deferredList.length == blobItems.length) {
   288  			complete();
   289  		}
   290  	};
   291  
   292  	// TODO(mpl): newSet is a lame trick. Do better.
   293  	if (newSet) {
   294  		this.connection_.newSetAttributeClaim(permanode, 'title', 'My new set', function() {});
   295  	}
   296  	goog.array.forEach(blobItems, function(blobItem, index) {
   297  		this.connection_.newAddAttributeClaim(permanode, 'camliMember', blobItem.getBlobRef(), callback);
   298  	}, this);
   299  };
   300  
   301  cam.IndexPage.prototype.addItemsToSetDone_ = function(permanode) {
   302  	this.blobItemContainer_.unselectAll();
   303  	this.updateNavButtonsForSelection_();
   304  	this.setURLSearch_(' ');
   305  };
   306  
   307  cam.IndexPage.prototype.handleServerStatus_ = function(resp) {
   308  	if (resp && resp.version) {
   309  		// TODO(aa): Argh
   310  		//this.toolbar_.setStatus('v' + resp.version);
   311  	}
   312  };
   313  
   314  cam.IndexPage.prototype.setURLSearch_ = function(search) {
   315  	var searchText = goog.isString(search) ? goog.string.trim(search) :
   316  		goog.string.subs('%s:%s', this.constructor.SEARCH_PREFIX_.RAW, JSON.stringify(search));
   317  	var searchURL = this.baseURL_.clone();
   318  	searchURL.setParameterValue('q', searchText);
   319  	this.navigator_.navigate(searchURL);
   320  };
   321  
   322  // @param goog.Uri newURL The URL we have navigated to. At this point, location.href is already updated -- this is just the parsed representation.
   323  // @return boolean Whether the navigation was handled.
   324  cam.IndexPage.prototype.handleURL_ = function(newURL) {
   325  	if (this.currentURL_) {
   326  		if (newURL.getScheme() != this.currentURL_.getScheme() ||
   327  			newURL.getUserInfo() != this.currentURL_.getUserInfo() ||
   328  			newURL.getDomain() != this.currentURL_.getDomain() ||
   329  			newURL.getPort() != this.currentURL_.getPort() ||
   330  			newURL.getPath() != this.currentURL_.getPath()) {
   331  			return false;
   332  		}
   333  	}
   334  
   335  	// This is super finicky. We should improve the URL scheme and give things that are different different paths.
   336  	var query = newURL.clone().removeParameter('react').getQueryData();
   337  	this.inSearchMode_ = query.getCount() == 0 || (query.getCount() == 1 && query.containsKey('q'));
   338  	this.inDetailMode_ = query.containsKey('p') && query.get('newui') == '1';
   339  
   340  	if (!this.inSearchMode_ && !this.inDetailMode_) {
   341  		return false;
   342  	}
   343  
   344  	this.currentURL_ = newURL;
   345  	this.updateSearchSession_();
   346  	this.updateScrollbar_();
   347  	this.updateSearchView_();
   348  	this.updateDetailView_();
   349  	return true;
   350  };
   351  
   352  cam.IndexPage.prototype.updateSearchSession_ = function() {
   353  	var query = this.currentURL_.getParameterValue('q');
   354  	if (!query) {
   355  		query = ' ';
   356  	}
   357  
   358  	// TODO(aa): Remove this when the server can do something like the 'raw' operator.
   359  	if (goog.string.startsWith(query, this.constructor.SEARCH_PREFIX_.RAW + ':')) {
   360  		query = JSON.parse(query.substring(this.constructor.SEARCH_PREFIX_.RAW.length + 1));
   361  	}
   362  
   363  	if (this.searchSession_ && JSON.stringify(this.searchSession_.getQuery()) == JSON.stringify(query)) {
   364  		return;
   365  	}
   366  
   367  	if (this.searchSession_) {
   368  		this.searchSession_.close();
   369  	}
   370  
   371  	this.searchSession_ = new cam.SearchSession(this.connection_, new goog.Uri(location.href), query);
   372  };
   373  
   374  cam.IndexPage.prototype.updateScrollbar_ = function() {
   375  	// It makes it easier to compute the layout of the aligned tiles if the scrollbar is reliably on.
   376  	document.body.style.overflowY = this.inSearchMode_ ? 'scroll' : '';
   377  };
   378  
   379  cam.IndexPage.prototype.updateSearchView_ = function() {
   380  	if (this.inDetailMode_) {
   381  		this.savedScrollPosition_ = goog.dom.getDocumentScroll().y;
   382  		this.blobItemContainer_.setVisible(false);
   383  		return;
   384  	}
   385  
   386  	if (!this.blobItemContainer_.isVisible()) {
   387  		this.blobItemContainer_.setVisible(true);
   388  		goog.dom.getDocumentScrollElement().scrollTop = this.savedScrollPosition_;
   389  	}
   390  
   391  	if (this.nav_.isOpen()) {
   392  		this.setTransform_();
   393  	}
   394  
   395  	this.blobItemContainer_.showSearchSession(this.searchSession_);
   396  };
   397  
   398  cam.IndexPage.prototype.updateDetailView_ = function() {
   399  	if (!this.inDetailMode_) {
   400  		if (this.detail_) {
   401  			this.detailLoop_.stop();
   402  			React.unmountComponentAtNode(this.detailViewHost_);
   403  			this.detailLoop_ = null;
   404  			this.detail_ = null;
   405  		}
   406  		return;
   407  	}
   408  
   409  	var searchURL = this.baseURL_.clone();
   410  	if (this.currentURL_.getQueryData().containsKey('q')) {
   411  		searchURL.setParameterValue('q', this.currentURL_.getParameterValue('q'));
   412  	}
   413  
   414  	var oldURL = this.baseURL_.clone();
   415  	oldURL.setParameterValue('p', this.currentURL_.getParameterValue('p'));
   416  
   417  	var getDetailURL = function(blobRef) {
   418  		var result = this.currentURL_.clone();
   419  		result.setParameterValue('p', blobRef);
   420  		return result;
   421  	}.bind(this);
   422  
   423  	var props = {
   424  		blobref: this.currentURL_.getParameterValue('p'),
   425  		history: history,
   426  		searchSession: this.searchSession_,
   427  		searchURL: searchURL,
   428  		oldURL: oldURL,
   429  		getDetailURL: getDetailURL,
   430  		navigator: this.navigator_,
   431  		keyEventTarget: window,
   432  	}
   433  
   434  	if (this.detail_) {
   435  		this.detail_.setProps(props);
   436  		return;
   437  	}
   438  
   439  	var lastWidth = window.innerWidth;
   440  	var lastHeight = window.innerHeight;
   441  
   442  	this.detail_ = cam.DetailView(cam.object.extend(props, {
   443  		width: lastWidth,
   444  		height: lastHeight
   445  	}));
   446  	React.renderComponent(this.detail_, this.detailViewHost_);
   447  
   448  	this.detailLoop_ = new cam.AnimationLoop(window);
   449  	this.detailLoop_.addEventListener('frame', function() {
   450  		if (window.innerWidth != lastWidth || window.innerHeight != lastHeight) {
   451  			lastWidth = window.innerWidth;
   452  			lastHeight = window.innerHeight;
   453  			this.detail_.setProps({width:lastWidth, height:lastHeight});
   454  		}
   455  	}.bind(this));
   456  	this.detailLoop_.start();
   457  };