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 });