github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/server/camlistored/ui/detail.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.DetailView');
    18  
    19  goog.require('goog.array');
    20  goog.require('goog.events.EventHandler');
    21  goog.require('goog.math.Size');
    22  goog.require('goog.object');
    23  goog.require('goog.string');
    24  
    25  goog.require('cam.AnimationLoop');
    26  goog.require('cam.ImageDetail');
    27  goog.require('cam.Navigator');
    28  goog.require('cam.reactUtil');
    29  goog.require('cam.SearchSession');
    30  goog.require('cam.SpritedAnimation');
    31  
    32  // Top-level control for the detail view. Handles loading data specified in URL and left/right navigation.
    33  // The details of the actual rendering are left up to child controls which are chosen based on the type of data loaded. However, currently there is only one type of child control: cam.ImageDetail.
    34  cam.DetailView = React.createClass({
    35  	displayName: 'DetailView',
    36  
    37  	propTypes: {
    38  		aspects: cam.reactUtil.mapOf(React.PropTypes.shape({
    39  			getTitle: React.PropTypes.func.isRequired,
    40  			createContent: React.PropTypes.func.isRequired,
    41  		})).isRequired,
    42  		blobref: React.PropTypes.string.isRequired,
    43  		getDetailURL: React.PropTypes.func.isRequired,
    44  		history: React.PropTypes.shape({go:React.PropTypes.func.isRequired}).isRequired,
    45  		height: React.PropTypes.number.isRequired,
    46  		keyEventTarget: React.PropTypes.object.isRequired, // An event target we will addEventListener() on to receive key events.
    47  		navigator: React.PropTypes.instanceOf(cam.Navigator).isRequired,
    48  		searchSession: React.PropTypes.instanceOf(cam.SearchSession).isRequired,
    49  		searchURL: React.PropTypes.instanceOf(goog.Uri).isRequired,
    50  		width: React.PropTypes.number.isRequired,
    51  	},
    52  
    53  	getInitialState: function() {
    54  		return {
    55  			lastNavigateWasBackward: false,
    56  			selectedAspect: '',
    57  		};
    58  	},
    59  
    60  	componentWillMount: function() {
    61  		this.pendingNavigation_ = 0;
    62  		this.navCount_ = 1;
    63  		this.eh_ = new goog.events.EventHandler(this);
    64  	},
    65  
    66  	componentDidMount: function(root) {
    67  		this.eh_.listen(this.props.searchSession, cam.SearchSession.SEARCH_SESSION_CHANGED, this.searchUpdated_);
    68  		this.eh_.listen(this.props.keyEventTarget, 'keyup', this.handleKeyUp_);
    69  		this.searchUpdated_();
    70  	},
    71  
    72  	render: function() {
    73  		var activeAspects = null;
    74  		var selectedAspect = null;
    75  
    76  		if (this.dataIsLoaded_()) {
    77  			activeAspects = goog.object.filter(
    78  				goog.object.map(this.props.aspects, function(f) {
    79  					return f(this.props.blobref, this.props.searchSession);
    80  				}, this),
    81  				function(a) {
    82  					return a != null;
    83  				}
    84  			);
    85  
    86  			selectedAspect = activeAspects[this.state.selectedAspect] || goog.object.getAnyValue(activeAspects);
    87  		}
    88  
    89  		return React.DOM.div({className: 'cam-detail', style: {height: this.props.height}},
    90  			this.getAspectNav_(activeAspects),
    91  
    92  			// TODO(aa): Actually pick this based on the current URL
    93  			this.getAspectView_(selectedAspect)
    94  		);
    95  	},
    96  
    97  	getAspectNav_: function(aspects) {
    98  		if (!aspects) {
    99  			return null;
   100  		}
   101  		var items = goog.object.getValues(goog.object.map(aspects, function(aspect, name) {
   102  			// TODO(aa): URLs involving k I guess?
   103  			return React.DOM.a({href: '#', onClick: this.handleAspectClick_.bind(this, name)}, aspect.getTitle());
   104  		}, this));
   105  		items.push(React.DOM.a({href: this.props.searchURL.toString()}, 'Back to search'));
   106  		return React.DOM.div({className: 'cam-detail-aspect-nav'}, items);
   107  	},
   108  
   109  	getAspectView_: function(aspect) {
   110  		if (aspect) {
   111  			// TODO(aa): Why doesn't parent pass us |Size| instead of width/height?
   112  			return aspect.createContent(new goog.math.Size(this.props.width, this.props.height - 25), this.state.lastNavigateWasBackward);
   113  		} else {
   114  			return null;
   115  		}
   116  	},
   117  
   118  	componentWillUnmount: function() {
   119  		this.eh_.dispose();
   120  	},
   121  
   122  	handleAspectClick_: function(name) {
   123  		this.setState({
   124  			selectedAspect: name,
   125  		});
   126  	},
   127  
   128  	handleKeyUp_: function(e) {
   129  		if (e.keyCode == goog.events.KeyCodes.LEFT) {
   130  			this.navigate_(-1);
   131  		} else if (e.keyCode == goog.events.KeyCodes.RIGHT) {
   132  			this.navigate_(1);
   133  		} else if (e.keyCode == goog.events.KeyCodes.ESC) {
   134  			this.handleEscape_(e);
   135  		}
   136  	},
   137  
   138  	navigate_: function(offset) {
   139  		this.pendingNavigation_ = offset;
   140  		++this.navCount_;
   141  		this.setState({lastNavigateWasBackward: offset < 0});
   142  		this.handlePendingNavigation_();
   143  	},
   144  
   145  	handleEscape_: function(e) {
   146  		e.preventDefault();
   147  		e.stopPropagation();
   148  		history.go(-this.navCount_);
   149  	},
   150  
   151  	handlePendingNavigation_: function() {
   152  		if (!this.pendingNavigation_) {
   153  			return;
   154  		}
   155  
   156  		var results = this.props.searchSession.getCurrentResults();
   157  		var index = goog.array.findIndex(results.blobs, function(elm) {
   158  			return elm.blob == this.props.blobref;
   159  		}.bind(this));
   160  
   161  		if (index == -1) {
   162  			this.props.searchSession.loadMoreResults();
   163  			return;
   164  		}
   165  
   166  		index += this.pendingNavigation_;
   167  		if (index < 0) {
   168  			this.pendingNavigation_ = 0;
   169  			console.log('Cannot navigate past beginning of search result.');
   170  			return;
   171  		}
   172  
   173  		if (index >= results.blobs.length) {
   174  			if (this.props.searchSession.isComplete()) {
   175  				this.pendingNavigation_ = 0;
   176  				console.log('Cannot navigate past end of search result.');
   177  			} else {
   178  				this.props.searchSession.loadMoreResults();
   179  			}
   180  			return;
   181  		}
   182  
   183  		this.props.navigator.navigate(this.props.getDetailURL(results.blobs[index].blob));
   184  	},
   185  
   186  	searchUpdated_: function() {
   187  		this.handlePendingNavigation_();
   188  
   189  		if (this.dataIsLoaded_()) {
   190  			this.forceUpdate();
   191  			return;
   192  		}
   193  
   194  		if (this.props.searchSession.isComplete()) {
   195  			// TODO(aa): 404 UI.
   196  			var error = goog.string.subs('Could not find blobref %s in search session.', this.props.blobref);
   197  			alert(error);
   198  			throw new Error(error);
   199  		}
   200  
   201  		// TODO(aa): This can be inefficient in the case of a fresh page load if we have to load lots of pages to find the blobref.
   202  		// Our search protocol needs to be updated to handle the case of paging ahead to a particular item.
   203  		this.props.searchSession.loadMoreResults();
   204  	},
   205  
   206  	dataIsLoaded_: function() {
   207  		return Boolean(this.props.searchSession.getMeta(this.props.blobref));
   208  	},
   209  });