github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/server/camlistored/ui/blob_item.js (about) 1 /* 2 Copyright 2013 The Camlistore Authors 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.BlobItem'); 18 19 goog.require('goog.dom'); 20 goog.require('goog.dom.classes'); 21 goog.require('goog.events.EventHandler'); 22 goog.require('goog.events.EventType'); 23 goog.require('goog.ui.Control'); 24 25 goog.require('cam.imageUtil'); 26 goog.require('cam.ServerType'); 27 28 29 // @fileoverview An item showing in a blob item container; represents a blob that has already been uploaded in the system, or acts as a placeholder for a new blob. 30 // @param {string} blobRef BlobRef for the item. 31 // @param {cam.ServerType.IndexerMetaBag} metaBag Maps blobRefs to metadata for this blob and related blobs. 32 // @param {string} opt_contentLink if "true", use the contained file blob as link when decorating 33 // @param {goog.dom.DomHelper=} opt_domHelper DOM helper to use. 34 // @extends {goog.ui.Control} 35 // @constructor 36 cam.BlobItem = function(blobRef, metaBag, opt_contentLink, opt_domHelper) { 37 goog.base(this, null, null, opt_domHelper); 38 39 this.update(blobRef, metaBag, opt_contentLink); 40 41 this.setSupportedState(goog.ui.Component.State.CHECKED, true); 42 this.setSupportedState(goog.ui.Component.State.DISABLED, true); 43 this.setAutoStates(goog.ui.Component.State.CHECKED, false); 44 45 // Blob items dispatch state when checked. 46 this.setDispatchTransitionEvents(goog.ui.Component.State.CHECKED, true); 47 }; 48 goog.inherits(cam.BlobItem, goog.ui.Control); 49 50 cam.BlobItem.prototype.update = function(blobRef, metaBag, opt_contentLink) { 51 // TODO(mpl): Hack so we know when to decorate with the blobref of the contained file, instead of with the permanode, as the link. Idiomatic alternative suggestion very welcome. 52 53 this.useContentAsLink_ = "false"; 54 if (typeof opt_contentLink !== "undefined" && opt_contentLink == "true") { 55 this.useContentAsLink_ = opt_contentLink; 56 } 57 58 this.blobRef_ = blobRef; 59 this.metaBag_ = metaBag; 60 this.metaData_ = this.metaBag_[this.blobRef_]; 61 this.resolvedMetaData_ = cam.BlobItem.resolve(this.blobRef_, this.metaBag_); 62 this.currentIntrinsicImageHeight_ = 0; 63 }; 64 65 cam.BlobItem.TITLE_HEIGHT = 21; 66 67 // TODO(bslatkin): Handle more permanode types. 68 // @param {string} blobRef string BlobRef to resolve. 69 // @param {cam.ServerType.IndexerMetaBag} metaBag Metadata bag to use for resolving the blobref. 70 // @return {cam.ServerType.IndexerMeta?} 71 cam.BlobItem.resolve = function(blobRef, metaBag) { 72 var metaData = metaBag[blobRef]; 73 if (metaData.camliType == 'permanode' && metaData.permanode && metaData.permanode.attr) { 74 if (metaData.permanode.attr.camliContent) { 75 // Permanode is pointing at another blob. 76 var content = metaData.permanode.attr.camliContent; 77 if (content.length == 1) { 78 return metaBag[content[0]]; 79 } 80 } else { 81 // Permanode is its own content. 82 return metaData; 83 } 84 } 85 return null; 86 }; 87 88 cam.BlobItem.prototype.isCollection = function() { 89 // TODO(mpl): for now disallow being a collection if it 90 // has members. What else to check? 91 if (!this.resolvedMetaData_ || this.resolvedMetaData_.camliType != 'permanode' || !this.resolvedMetaData_.permanode || !this.resolvedMetaData_.permanode.attr || this.resolvedMetaData_.permanode.attr.camliContent) { 92 return false; 93 } 94 return true; 95 }; 96 97 cam.BlobItem.prototype.getBlobRef = function() { 98 return this.blobRef_; 99 }; 100 101 cam.BlobItem.prototype.getThumbAspect = function() { 102 if (!this.metaData_.thumbnailWidth || !this.metaData_.thumbnailHeight) { 103 return 0; 104 } 105 return this.metaData_.thumbnailWidth / this.metaData_.thumbnailHeight; 106 }; 107 108 cam.BlobItem.prototype.getWidth = function() { 109 return parseInt(this.getElement().style.width); 110 }; 111 112 cam.BlobItem.prototype.getHeight = function() { 113 return parseInt(this.getElement().style.height); 114 }; 115 116 cam.BlobItem.prototype.setWidth = function(w) { 117 this.setSize(w, this.getHeight()); 118 }; 119 120 cam.BlobItem.prototype.setHeight = function(h) { 121 this.setSize(this.getWidth(), h); 122 }; 123 124 // Sets the display size of the item. The thumbnail will be scaled, centered, 125 // and clipped within this size as appropriate. 126 // @param {number} w 127 // @param {number} h 128 cam.BlobItem.prototype.setSize = function(w, h) { 129 this.getElement().style.width = w + 'px'; 130 this.getElement().style.height = h + 'px'; 131 132 var thumbHeight = h; 133 if (!this.isImage()) { 134 thumbHeight -= this.constructor.TITLE_HEIGHT; 135 } 136 this.setThumbSize(w, thumbHeight); 137 }; 138 139 // Sets the display size of just the thumbnail. It will be scaled, centered, and 140 // clipped within this size as appropriate. 141 // @param {number} w 142 // @param {number} h 143 cam.BlobItem.prototype.setThumbSize = function(w, h) { 144 // In the case of images, we want a full bleed to both w and h, so we clip the bigger dimension as necessary. It's not easy to notice that a few pixels have been shaved off the edge of a photo. 145 // In the case of non-images, we have an icon with text underneath, so we cannot clip. Instead, just constrain the icon to fit the available space. 146 var adjustedHeight; 147 if (this.isImage()) { 148 adjustedHeight = this.getThumbAspect() < w / h ? w / this.getThumbAspect() : h; 149 } else { 150 adjustedHeight = this.getThumbAspect() < w / h ? h : w / this.getThumbAspect(); 151 } 152 var adjustedWidth = adjustedHeight * this.getThumbAspect(); 153 154 this.thumb_.width = Math.round(adjustedWidth); 155 this.thumb_.height = Math.round(adjustedHeight); 156 157 this.thumbClip_.style.width = w + 'px'; 158 this.thumbClip_.style.height = h + 'px'; 159 160 this.thumb_.style.top = Math.round((h - adjustedHeight) / 2) + 'px'; 161 this.thumb_.style.left = Math.round((w - adjustedWidth) / 2) + 'px'; 162 163 this.loading_.style.top = Math.round((h - 85) / 2) + 'px'; 164 this.loading_.style.left = Math.round((w - 70) / 2) + 'px'; 165 166 // Load a differently sized image from server if necessary. 167 if (!this.thumb_.src || adjustedWidth > parseInt(this.thumbClip_.style.width) || adjustedHeight > parseInt(this.thumbClip_.style.height)) { 168 this.currentIntrinsicImageHeight_ = cam.imageUtil.getSizeToRequest(adjustedHeight, this.currentIntrinsicImageHeight_); 169 170 var tv = ''; 171 if (window.CAMLISTORE_CONFIG) { 172 tv = CAMLISTORE_CONFIG.thumbVersion || ''; 173 } 174 175 // TODO(aa): The mh param is kind of a hack, it would be better if the server just returned the base URL and the aspect ratio, rather than specific dimensions. 176 var newThumb = this.getThumbSrc_().split('?')[0] + '?mh=' + 177 this.currentIntrinsicImageHeight_ + '&tv=' + tv; 178 179 // It's important to only assign the new src if it has changed. Assigning a src causes layout and style recalc. 180 if (newThumb != this.thumb_.getAttribute('src')) { 181 this.thumb_.src = newThumb; 182 } 183 } 184 }; 185 186 cam.BlobItem.prototype.isImage = function() { 187 return Boolean(this.resolvedMetaData_.image); 188 }; 189 190 cam.BlobItem.prototype.getThumbSrc_ = function() { 191 return './' + this.metaData_.thumbnailSrc; 192 }; 193 194 cam.BlobItem.prototype.getLink_ = function() { 195 if (this.useContentAsLink_ == "true") { 196 var b = this.getFileBlobref_(); 197 if (b == "") { 198 b = this.getDirBlobref_(); 199 } 200 return './?b=' + b; 201 } 202 203 // The new detail page looks ridiculous for non-images, so don't go to it for those yet. 204 var uri = new goog.Uri(location.href); 205 uri.setParameterValue('p', this.blobRef_); 206 if (this.isImage()) { 207 uri.setParameterValue('newui', '1'); 208 } 209 return uri.toString(); 210 }; 211 212 cam.BlobItem.prototype.getFileBlobref_ = function() { 213 if (this.resolvedMetaData_ && this.resolvedMetaData_.camliType == 'file') { 214 return this.resolvedMetaData_.blobRef; 215 } 216 return ""; 217 } 218 219 cam.BlobItem.prototype.getDirBlobref_ = function() { 220 if (this.resolvedMetaData_ && this.resolvedMetaData_.camliType == 'directory') { 221 return this.resolvedMetaData_.blobRef; 222 } 223 return ""; 224 } 225 226 cam.BlobItem.prototype.getTitle_ = function() { 227 if (this.metaData_) { 228 if (this.metaData_.camliType == 'permanode' && 229 !!this.metaData_.permanode && 230 !!this.metaData_.permanode.attr && 231 !!this.metaData_.permanode.attr.title) { 232 return this.metaData_.permanode.attr.title; 233 } 234 } 235 if (this.resolvedMetaData_) { 236 if (this.resolvedMetaData_.camliType == 'file' && 237 !!this.resolvedMetaData_.file) { 238 return this.resolvedMetaData_.file.fileName; 239 } 240 if (this.resolvedMetaData_.camliType == 'directory' && 241 !!this.resolvedMetaData_.dir) { 242 return this.resolvedMetaData_.dir.fileName; 243 } 244 if (this.resolvedMetaData_.camliType == 'permanode' && 245 !!this.resolvedMetaData_.permanode && 246 !!this.resolvedMetaData_.permanode.attr && 247 !!this.resolvedMetaData_.permanode.attr.title) { 248 return this.resolvedMetaData_.permanode.attr.title; 249 } 250 } 251 return 'Unknown title'; 252 }; 253 254 cam.BlobItem.prototype.createDom = function() { 255 this.decorateInternal(this.dom_.createElement('div')); 256 }; 257 258 cam.BlobItem.prototype.decorateInternal = function(element) { 259 cam.BlobItem.superClass_.decorateInternal.call(this, element); 260 261 var el = this.getElement(); 262 goog.dom.classes.add(el, 'cam-blobitem'); 263 264 this.link_ = this.dom_.createDom('a'); 265 266 this.thumbClip_ = this.dom_.createDom('div', 'cam-blobitem-thumbclip cam-blobitem-loading'); 267 this.link_.appendChild(this.thumbClip_); 268 269 this.loading_ = this.dom_.createDom('div', 'cam-blobitem-progress', 270 this.dom_.createDom('div', 'lefttop'), 271 this.dom_.createDom('div', 'leftbottom'), 272 this.dom_.createDom('div', 'righttop'), 273 this.dom_.createDom('div', 'rightbottom')); 274 this.thumbClip_.appendChild(this.loading_); 275 276 this.thumb_ = this.dom_.createDom('img', 'cam-blobitem-thumb'); 277 this.thumb_.onload = function(e){ 278 goog.dom.removeNode(this.loading_); 279 goog.dom.classes.remove(this.thumbClip_, 'cam-blobitem-loading'); 280 }.bind(this); 281 this.thumbClip_.appendChild(this.thumb_); 282 283 el.appendChild(this.link_); 284 285 this.checkmark_ = this.dom_.createDom('div', 'checkmark'); 286 this.getElement().appendChild(this.checkmark_); 287 288 this.label_ = this.dom_.createDom('span', 'cam-blobitem-thumbtitle'); 289 this.link_.appendChild(this.label_); 290 291 this.updateDom(); 292 293 this.getElement().addEventListener('click', this.handleClick_.bind(this)); 294 this.setEnabled(false); 295 }; 296 297 // The image src is not set here because that depends on layout. Instead, it 298 // gets set as a side-effect of BlobItemContainer.prototype.layout(). 299 cam.BlobItem.prototype.updateDom = function() { 300 this.link_.href = this.getLink_(); 301 302 if (this.isImage()) { 303 this.addClassName('cam-blobitem-image'); 304 this.thumb_.title = this.getTitle_(); 305 this.label_.textContent = ''; 306 } else { 307 this.removeClassName('cam-blobitem-image'); 308 this.label_.textContent = this.getTitle_(); 309 } 310 }; 311 312 cam.BlobItem.prototype.handleClick_ = function(e) { 313 if (!this.checkmark_) { 314 return; 315 } 316 317 if (e.target == this.checkmark_ || this.checkmark_.contains(e.target)) { 318 this.setChecked(!this.isChecked()); 319 e.preventDefault(); 320 } 321 };