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