1/* 2 Copyright 2010 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/** 18 * @fileoverview Bookmark bubble library. This is meant to be included in the 19 * main JavaScript binary of a mobile web application. 20 * 21 * Supported browsers: iPhone / iPod / iPad Safari 3.0+ 22 */ 23 24var google = google || {}; 25google.bookmarkbubble = google.bookmarkbubble || {}; 26 27 28/** 29 * Binds a context object to the function. 30 * @param {Function} fn The function to bind to. 31 * @param {Object} context The "this" object to use when the function is run. 32 * @return {Function} A partially-applied form of fn. 33 */ 34google.bind = function(fn, context) { 35 return function() { 36 return fn.apply(context, arguments); 37 }; 38}; 39 40 41/** 42 * Function used to define an abstract method in a base class. If a subclass 43 * fails to override the abstract method, then an error will be thrown whenever 44 * that method is invoked. 45 */ 46google.abstractMethod = function() { 47 throw Error('Unimplemented abstract method.'); 48}; 49 50 51 52/** 53 * The bubble constructor. Instantiating an object does not cause anything to 54 * be rendered yet, so if necessary you can set instance properties before 55 * showing the bubble. 56 * @constructor 57 */ 58google.bookmarkbubble.Bubble = function() { 59 /** 60 * Handler for the scroll event. Keep a reference to it here, so it can be 61 * unregistered when the bubble is destroyed. 62 * @type {function()} 63 * @private 64 */ 65 this.boundScrollHandler_ = google.bind(this.setPosition, this); 66 67 /** 68 * The bubble element. 69 * @type {Element} 70 * @private 71 */ 72 this.element_ = null; 73 74 /** 75 * Whether the bubble has been destroyed. 76 * @type {boolean} 77 * @private 78 */ 79 this.hasBeenDestroyed_ = false; 80}; 81 82 83/** 84 * Shows the bubble if allowed. It is not allowed if: 85 * - The browser is not Mobile Safari, or 86 * - The user has dismissed it too often already, or 87 * - The hash parameter is present in the location hash, or 88 * - The application is in fullscreen mode, which means it was already loaded 89 * from a homescreen bookmark. 90 * @return {boolean} True if the bubble is being shown, false if it is not 91 * allowed to show for one of the aforementioned reasons. 92 */ 93google.bookmarkbubble.Bubble.prototype.showIfAllowed = function() { 94 if (!this.isAllowedToShow_()) { 95 return false; 96 } 97 98 this.show_(); 99 return true; 100}; 101 102 103/** 104 * Shows the bubble if allowed after loading the icon image. This method creates 105 * an image element to load the image into the browser's cache before showing 106 * the bubble to ensure that the image isn't blank. Use this instead of 107 * showIfAllowed if the image url is http and cacheable. 108 * This hack is necessary because Mobile Safari does not properly render 109 * image elements with border-radius CSS. 110 * @param {function()} opt_callback Closure to be called if and when the bubble 111 * actually shows. 112 * @return {boolean} True if the bubble is allowed to show. 113 */ 114google.bookmarkbubble.Bubble.prototype.showIfAllowedWhenLoaded = 115 function(opt_callback) { 116 if (!this.isAllowedToShow_()) { 117 return false; 118 } 119 120 var self = this; 121 // Attach to self to avoid garbage collection. 122 var img = self.loadImg_ = document.createElement('img'); 123 img.src = self.getIconUrl_(); 124 img.onload = function() { 125 if (img.complete) { 126 delete self.loadImg_; 127 img.onload = null; // Break the circular reference. 128 129 self.show_(); 130 opt_callback && opt_callback(); 131 } 132 }; 133 img.onload(); 134 135 return true; 136}; 137 138 139/** 140 * Sets the parameter in the location hash. As it is 141 * unpredictable what hash scheme is to be used, this method must be 142 * implemented by the host application. 143 * 144 * This gets called automatically when the bubble is shown. The idea is that if 145 * the user then creates a bookmark, we can later recognize on application 146 * startup whether it was from a bookmark suggested with this bubble. 147 * 148 * NOTE: Using a hash parameter to track whether the bubble has been shown 149 * conflicts with the navigation system in jQuery Mobile. If you are using that 150 * library, you should implement this function to track the bubble's status in 151 * a different way, e.g. using window.localStorage in HTML5. 152 */ 153google.bookmarkbubble.Bubble.prototype.setHashParameter = google.abstractMethod; 154 155 156/** 157 * Whether the parameter is present in the location hash. As it is 158 * unpredictable what hash scheme is to be used, this method must be 159 * implemented by the host application. 160 * 161 * Call this method during application startup if you want to log whether the 162 * application was loaded from a bookmark with the bookmark bubble promotion 163 * parameter in it. 164 * 165 * @return {boolean} Whether the bookmark bubble parameter is present in the 166 * location hash. 167 */ 168google.bookmarkbubble.Bubble.prototype.hasHashParameter = google.abstractMethod; 169 170 171/** 172 * The number of times the user must dismiss the bubble before we stop showing 173 * it. This is a public property and can be changed by the host application if 174 * necessary. 175 * @type {number} 176 */ 177google.bookmarkbubble.Bubble.prototype.NUMBER_OF_TIMES_TO_DISMISS = 2; 178 179 180/** 181 * Time in milliseconds. If the user does not dismiss the bubble, it will auto 182 * destruct after this amount of time. 183 * @type {number} 184 */ 185google.bookmarkbubble.Bubble.prototype.TIME_UNTIL_AUTO_DESTRUCT = 10000; 186 187 188/** 189 * The prefix for keys in local storage. This is a public property and can be 190 * changed by the host application if necessary. 191 * @type {string} 192 */ 193google.bookmarkbubble.Bubble.prototype.LOCAL_STORAGE_PREFIX = 'BOOKMARK_'; 194 195 196/** 197 * The key name for the dismissed state. 198 * @type {string} 199 * @private 200 */ 201google.bookmarkbubble.Bubble.prototype.DISMISSED_ = 'DISMISSED_COUNT'; 202 203 204/** 205 * The arrow image in base64 data url format. 206 * @type {string} 207 * @private 208 */ 209google.bookmarkbubble.Bubble.prototype.IMAGE_ARROW_DATA_URL_ = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAATCAMAAABSrFY3AAABKVBMVEUAAAD///8AAAAAAAAAAAAAAAAAAADf398AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD09PQAAAAAAAAAAAC9vb0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD19fUAAAAAAAAAAAAAAADq6uoAAAAAAAAAAAC8vLzU1NTT09MAAADg4OAAAADs7OwAAAAAAAAAAAD///+cueenwerA0vC1y+3a5fb5+/3t8vr4+v3w9PuwyOy3zO3h6vfh6vjq8Pqkv+mat+fE1fHB0/Cduuifu+iuxuuivemrxOvC1PDz9vzJ2fKpwuqmwOrb5vapw+q/0vDf6ffK2vLN3PPprJISAAAAQHRSTlMAAAEGExES7FM+JhUoQSxIRwMbNfkJUgXXBE4kDQIMHSA0Tw4xIToeTSc4Chz4OyIjPfI3QD/X5OZR6zzwLSUPrm1y3gAAAQZJREFUeF5lzsVyw0AURNE3IMsgmZmZgszQZoeZOf//EYlG5Yrhbs+im4Dj7slM5wBJ4OJ+undAUr68gK/Hyb6Bcp5yBR/w8jreNeAr5Eg2XE7g6e2/0z6cGw1JQhpmHP3u5aiPPnTTkIK48Hj9Op7bD3btAXTfgUdwYjwSDCVXMbizO0O4uDY/x4kYC5SWFnfC6N1a9RCO7i2XEmQJj2mHK1Hgp9Vq3QBRl9shuBLGhcNtHexcdQCnDUoUGetxDD+H2DQNG2xh6uAWgG2/17o1EmLqYH0Xej0UjHAaFxZIV6rJ/WK1kg7QZH8HU02zmdJinKZJaDV3TVMjM5Q9yiqYpUwiMwa/1apDXTNESjsAAAAASUVORK5CYII='; 210 211 212/** 213 * The close image in base64 data url format. 214 * @type {string} 215 * @private 216 */ 217google.bookmarkbubble.Bubble.prototype.IMAGE_CLOSE_DATA_URL_ = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAALVBMVEXM3fm+1Pfb5/rF2fjw9f23z/aavPOhwfTp8PyTt/L3+v7T4vqMs/K7zP////+qRWzhAAAAXElEQVQIW2O4CwUM996BwVskxtOqd++2rwMyPI+ve31GD8h4Madqz2mwms5jZ/aBGS/mHIDoen3m+DowY8/hOVUgxusz+zqPg7SvPA1UxQfSvu/du0YUK2AMmDMA5H1qhVX33T8AAAAASUVORK5CYII='; 218 219 220/** 221 * The link used to locate the application's home screen icon to display inside 222 * the bubble. The default link used here is for an iPhone home screen icon 223 * without gloss. If your application uses a glossy icon, change this to 224 * 'apple-touch-icon'. 225 * @type {string} 226 * @private 227 */ 228google.bookmarkbubble.Bubble.prototype.REL_ICON_ = 229 'apple-touch-icon-precomposed'; 230 231 232/** 233 * Regular expression for detecting an iPhone or iPod or iPad.
234 * @type {!RegExp} 235 * @private 236 */ 237google.bookmarkbubble.Bubble.prototype.MOBILE_SAFARI_USERAGENT_REGEX_ = 238 /iPhone|iPod|iPad/; 239 240 241/** 242 * Regular expression for detecting an iPad. 243 * @type {!RegExp} 244 * @private 245 */ 246google.bookmarkbubble.Bubble.prototype.IPAD_USERAGENT_REGEX_ = /iPad/; 247 248 249/** 250 * Regular expression for extracting the iOS version. Only matches 2.0 and up. 251 * @type {!RegExp} 252 * @private 253 */ 254google.bookmarkbubble.Bubble.prototype.IOS_VERSION_USERAGENT_REGEX_ = 255 /OS (\d)_(\d)(?:_(\d))?/; 256 257 258/** 259 * Determines whether the bubble should be shown or not. 260 * @return {boolean} Whether the bubble should be shown or not. 261 * @private 262 */ 263google.bookmarkbubble.Bubble.prototype.isAllowedToShow_ = function() { 264 return this.isMobileSafari_() && 265 !this.hasBeenDismissedTooManyTimes_() && 266 !this.isFullscreen_() && 267 !this.hasHashParameter(); 268}; 269 270 271/** 272 * Builds and shows the bubble. 273 * @private 274 */ 275google.bookmarkbubble.Bubble.prototype.show_ = function() { 276 this.element_ = this.build_(); 277 278 document.body.appendChild(this.element_); 279 this.element_.style.WebkitTransform = 280 'translate3d(0,' + this.getHiddenYPosition_() + 'px,0)'; 281 282 this.setHashParameter(); 283 284 window.setTimeout(this.boundScrollHandler_, 1); 285 window.addEventListener('scroll', this.boundScrollHandler_, false); 286 287 // If the user does not dismiss the bubble, slide out and destroy it after 288 // some time. 289 window.setTimeout(google.bind(this.autoDestruct_, this), 290 this.TIME_UNTIL_AUTO_DESTRUCT); 291}; 292 293 294/** 295 * Destroys the bubble by removing its DOM nodes from the document. 296 */ 297google.bookmarkbubble.Bubble.prototype.destroy = function() { 298 if (this.hasBeenDestroyed_) { 299 return; 300 } 301 window.removeEventListener('scroll', this.boundScrollHandler_, false); 302 if (this.element_ && this.element_.parentNode == document.body) { 303 document.body.removeChild(this.element_); 304 this.element_ = null; 305 } 306 this.hasBeenDestroyed_ = true; 307}; 308 309 310/** 311 * Remember that the user has dismissed the bubble once more. 312 * @private 313 */ 314google.bookmarkbubble.Bubble.prototype.rememberDismissal_ = function() { 315 if (window.localStorage) { 316 try { 317 var key = this.LOCAL_STORAGE_PREFIX + this.DISMISSED_; 318 var value = Number(window.localStorage[key]) || 0; 319 window.localStorage[key] = String(value + 1); 320 } catch (ex) { 321 // Looks like we've hit the storage size limit. Currently we have no 322 // fallback for this scenario, but we could use cookie storage instead. 323 // This would increase the code bloat though. 324 } 325 } 326}; 327 328 329/** 330 * Whether the user has dismissed the bubble often enough that we will not 331 * show it again. 332 * @return {boolean} Whether the user has dismissed the bubble often enough 333 * that we will not show it again. 334 * @private 335 */ 336google.bookmarkbubble.Bubble.prototype.hasBeenDismissedTooManyTimes_ = 337 function() { 338 if (!window.localStorage) { 339 // If we can not use localStorage to remember how many times the user has 340 // dismissed the bubble, assume he has dismissed it. Otherwise we might end 341 // up showing it every time the host application loads, into eternity. 342 return true; 343 } 344 try { 345 var key = this.LOCAL_STORAGE_PREFIX + this.DISMISSED_; 346 347 // If the key has never been set, localStorage yields undefined, which 348 // Number() turns into NaN. In that case we'll fall back to zero for 349 // clarity's sake. 350 var value = Number(window.localStorage[key]) || 0; 351 352 return value >= this.NUMBER_OF_TIMES_TO_DISMISS; 353 } catch (ex) { 354 // If we got here, something is wrong with the localStorage. Make the same 355 // assumption as when it does not exist at all. Exceptions should only 356 // occur when setting a value (due to storage limitations) but let's be 357 // extra careful. 358 return true; 359 } 360}; 361 362 363/** 364 * Whether the application is running in fullscreen mode. 365 * @return {boolean} Whether the application is running in fullscreen mode. 366 * @private 367 */ 368google.bookmarkbubble.Bubble.prototype.isFullscreen_ = function() { 369 return !!window.navigator.standalone; 370}; 371 372 373/** 374 * Whether the application is running inside Mobile Safari. 375 * @return {boolean} True if the current user agent looks like Mobile Safari. 376 * @private 377 */ 378google.bookmarkbubble.Bubble.prototype.isMobileSafari_ = function() { 379 return this.MOBILE_SAFARI_USERAGENT_REGEX_.test(window.navigator.userAgent); 380}; 381 382 383/** 384 * Whether the application is running on an iPad. 385 * @return {boolean} True if the current user agent looks like an iPad. 386 * @private 387 */ 388google.bookmarkbubble.Bubble.prototype.isIpad_ = function() { 389 return this.IPAD_USERAGENT_REGEX_.test(window.navigator.userAgent); 390}; 391 392 393/** 394 * Creates a version number from 4 integer pieces between 0 and 127 (inclusive). 395 * @param {*=} opt_a The major version. 396 * @param {*=} opt_b The minor version. 397 * @param {*=} opt_c The revision number. 398 * @param {*=} opt_d The build number. 399 * @return {number} A representation of the version. 400 * @private 401 */ 402google.bookmarkbubble.Bubble.prototype.getVersion_ = function(opt_a, opt_b, 403 opt_c, opt_d) { 404 // We want to allow implicit conversion of any type to number while avoiding 405 // compiler warnings about the type. 406 return /** @type {number} */ (opt_a) << 21 | 407 /** @type {number} */ (opt_b) << 14 | 408 /** @type {number} */ (opt_c) << 7 | 409 /** @type {number} */ (opt_d); 410}; 411 412 413/** 414 * Gets the iOS version of the device. Only works for 2.0+. 415 * @return {number} The iOS version. 416 * @private 417 */ 418google.bookmarkbubble.Bubble.prototype.getIosVersion_ = function() { 419 var groups = this.IOS_VERSION_USERAGENT_REGEX_.exec( 420 window.navigator.userAgent) || []; 421 groups.shift(); 422 return this.getVersion_.apply(this, groups); 423}; 424 425 426/** 427 * Positions the bubble at the bottom of the viewport using an animated 428 * transition. 429 */ 430google.bookmarkbubble.Bubble.prototype.setPosition = function() { 431 this.element_.style.WebkitTransition = '-webkit-transform 0.7s ease-out'; 432 this.element_.style.WebkitTransform = 433 'translate3d(0,' + this.getVisibleYPosition_() + 'px,0)'; 434}; 435 436 437/** 438 * Destroys the bubble by removing its DOM nodes from the document, and 439 * remembers that it was dismissed. 440 * @private 441 */ 442google.bookmarkbubble.Bubble.prototype.closeClickHandler_ = function() { 443 this.destroy(); 444 this.rememberDismissal_(); 445}; 446 447 448/** 449 * Gets called after a while if the user ignores the bubble. 450 * @private 451 */ 452google.bookmarkbubble.Bubble.prototype.autoDestruct_ = function() { 453 if (this.hasBeenDestroyed_) { 454 return; 455 } 456 this.element_.style.WebkitTransition = '-webkit-transform 0.7s ease-in'; 457 this.element_.style.WebkitTransform = 458 'translate3d(0,' + this.getHiddenYPosition_() + 'px,0)'; 459 window.setTimeout(google.bind(this.destroy, this), 700); 460}; 461 462 463/** 464 * Gets the y offset used to show the bubble (i.e., position it on-screen). 465 * @return {number} The y offset. 466 * @private 467 */ 468google.bookmarkbubble.Bubble.prototype.getVisibleYPosition_ = function() { 469 return this.isIpad_() ? window.pageYOffset + 17 : 470 window.pageYOffset - this.element_.offsetHeight + window.innerHeight - 17; 471}; 472 473 474/** 475 * Gets the y offset used to hide the bubble (i.e., position it off-screen). 476 * @return {number} The y offset. 477 * @private 478 */ 479google.bookmarkbubble.Bubble.prototype.getHiddenYPosition_ = function() { 480 return this.isIpad_() ? window.pageYOffset - this.element_.offsetHeight : 481 window.pageYOffset + window.innerHeight; 482}; 483 484 485/** 486 * The url of the app's bookmark icon. 487 * @type {string|undefined} 488 * @private 489 */ 490google.bookmarkbubble.Bubble.prototype.iconUrl_; 491 492 493/** 494 * Scrapes the document for a link element that specifies an Apple favicon and 495 * returns the icon url. Returns an empty data url if nothing can be found. 496 * @return {string} A url string. 497 * @private 498 */ 499google.bookmarkbubble.Bubble.prototype.getIconUrl_ = function() { 500 if (!this.iconUrl_) { 501 var link = this.getLink(this.REL_ICON_); 502 if (!link || !(this.iconUrl_ = link.href)) { 503 this.iconUrl_ = 'data:image/png;base64,'; 504 } 505 } 506 return this.iconUrl_; 507}; 508 509 510/** 511 * Gets the requested link tag if it exists. 512 * @param {string} rel The rel attribute of the link tag to get. 513 * @return {Element} The requested link tag or null. 514 */ 515google.bookmarkbubble.Bubble.prototype.getLink = function(rel) { 516 rel = rel.toLowerCase(); 517 var links = document.getElementsByTagName('link'); 518 for (var i = 0; i < links.length; ++i) { 519 var currLink = /** @type {Element} */ (links[i]); 520 if (currLink.getAttribute('rel').toLowerCase() == rel) { 521 return currLink; 522 } 523 } 524 return null; 525}; 526 527 528/** 529 * Creates the bubble and appends it to the document. 530 * @return {Element} The bubble element. 531 * @private 532 */ 533google.bookmarkbubble.Bubble.prototype.build_ = function() { 534 var bubble = document.createElement('div'); 535 var isIpad = this.isIpad_(); 536 537 bubble.style.position = 'absolute'; 538 bubble.style.zIndex = 1000; 539 bubble.style.width = '100%'; 540 bubble.style.left = '0'; 541 bubble.style.top = '0'; 542 543 var bubbleInner = document.createElement('div'); 544 bubbleInner.style.position = 'relative'; 545 bubbleInner.style.width = '250px'; 546 bubbleInner.style.margin = isIpad ? '0 0 0 82px' : '0 auto'; 547 bubbleInner.style.border = '2px solid #fff'; 548 bubbleInner.style.padding = '20px 20px 20px 10px'; 549 bubbleInner.style.WebkitBorderRadius = '8px'; 550 bubbleInner.style.WebkitBoxShadow = '0 0 8px rgba(0, 0, 0, 0.7)';
551 bubbleInner.style.WebkitBackgroundSize = '100% 8px'; 552 bubbleInner.style.backgroundColor = '#b0c8ec'; 553 bubbleInner.style.background = '#cddcf3 -webkit-gradient(linear, ' + 554 'left bottom, left top, ' + isIpad ? 555 'from(#cddcf3), to(#b3caed)) no-repeat top' : 556 'from(#b3caed), to(#cddcf3)) no-repeat bottom'; 557 bubbleInner.style.font = '13px/17px sans-serif'; 558 bubble.appendChild(bubbleInner); 559 560 // The "Add to Home Screen" text is intended to be the exact same text 561 // that is displayed in the menu of Mobile Safari. 562 if (this.getIosVersion_() >= this.getVersion_(4, 2)) { 563 bubbleInner.innerHTML = 'ã²ã¼ã ãã©ã¤ããç»é²ãããï¼<br>ç¢å°ãã¿ãããã¦ããã¼ã ç»é¢ã«è¿½å ãã§å®äºï¼'; 564 } else { 565 bubbleInner.innerHTML = 'ã²ã¼ã ãã©ã¤ããç»é²ãããï¼<br>ç¢å°ãã¿ãããã¦ããã¼ã ç»é¢ã«è¿½å ãã§å®äºï¼'; 566 } 567 568 var icon = document.createElement('div'); 569 icon.style['float'] = 'left'; 570 icon.style.width = '55px'; 571 icon.style.height = '55px'; 572 icon.style.margin = '-2px 7px 3px 5px'; 573 icon.style.background = 574 '#fff url(' + this.getIconUrl_() + ') no-repeat -1px -1px'; 575 icon.style.WebkitBackgroundSize = '57px'; 576 icon.style.WebkitBorderRadius = '10px'; 577 icon.style.WebkitBoxShadow = '0 2px 5px rgba(0, 0, 0, 0.4)'; 578 bubbleInner.insertBefore(icon, bubbleInner.firstChild); 579 580 var arrow = document.createElement('div'); 581 arrow.style.backgroundImage = 'url(' + this.IMAGE_ARROW_DATA_URL_ + ')'; 582 arrow.style.width = '25px'; 583 arrow.style.height = '19px'; 584 arrow.style.position = 'absolute'; 585 arrow.style.left = '111px'; 586 if (isIpad) { 587 arrow.style.WebkitTransform = 'rotate(180deg)'; 588 arrow.style.top = '-19px'; 589 } else { 590 arrow.style.bottom = '-19px'; 591 } 592 bubbleInner.appendChild(arrow); 593 594 var close = document.createElement('a'); 595 close.onclick = google.bind(this.closeClickHandler_, this); 596 close.style.position = 'absolute'; 597 close.style.display = 'block'; 598 close.style.top = '-5px'; 599 close.style.right = '-2px'; 600 close.style.width = '32px'; 601 close.style.height = '32px'; 602 close.style.border = '10px solid transparent'; 603 close.style.background = 604 'url(' + this.IMAGE_CLOSE_DATA_URL_ + ') no-repeat'; 605 bubbleInner.appendChild(close); 606 607 return bubble; 608};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.