X-Git-Url: https://git.mdrn.pl/ReadingsJQM.git/blobdiff_plain/e5d84d972b58e6a9f3e071a80036ae07f1b2b068..389b7f33e7f11786f5d3dcc5ad9f3115bf96cc6b:/js/bookmark_bubble.js diff --git a/js/bookmark_bubble.js b/js/bookmark_bubble.js new file mode 100644 index 0000000..90c839e --- /dev/null +++ b/js/bookmark_bubble.js @@ -0,0 +1,559 @@ +/* + Copyright 2010 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/** + * @fileoverview Bookmark bubble library. This is meant to be included in the + * main JavaScript binary of a mobile web application. + * + * Supported browsers: iPhone / iPod / iPad Safari 3.0+ + */ + +var google = google || {}; +google.bookmarkbubble = google.bookmarkbubble || {}; + + +/** + * Binds a context object to the function. + * @param {Function} fn The function to bind to. + * @param {Object} context The "this" object to use when the function is run. + * @return {Function} A partially-applied form of fn. + */ +google.bind = function(fn, context) { + return function() { + return fn.apply(context, arguments); + }; +}; + + +/** + * Function used to define an abstract method in a base class. If a subclass + * fails to override the abstract method, then an error will be thrown whenever + * that method is invoked. + */ +google.abstractMethod = function() { + throw Error('Unimplemented abstract method.'); +}; + + + +/** + * The bubble constructor. Instantiating an object does not cause anything to + * be rendered yet, so if necessary you can set instance properties before + * showing the bubble. + * @constructor + */ +google.bookmarkbubble.Bubble = function() { + /** + * Handler for the scroll event. Keep a reference to it here, so it can be + * unregistered when the bubble is destroyed. + * @type {function()} + * @private + */ + this.boundScrollHandler_ = google.bind(this.setPosition, this); + + /** + * The bubble element. + * @type {Element} + * @private + */ + this.element_ = null; + + /** + * Whether the bubble has been destroyed. + * @type {boolean} + * @private + */ + this.hasBeenDestroyed_ = false; +}; + + +/** + * Shows the bubble if allowed. It is not allowed if: + * - The browser is not Mobile Safari, or + * - The user has dismissed it too often already, or + * - The hash parameter is present in the location hash, or + * - The application is in fullscreen mode, which means it was already loaded + * from a homescreen bookmark. + * @return {boolean} True if the bubble is being shown, false if it is not + * allowed to show for one of the aforementioned reasons. + */ +google.bookmarkbubble.Bubble.prototype.showIfAllowed = function() { + if (!this.isAllowedToShow_()) { + return false; + } + + this.show_(); + return true; +}; + + +/** + * Shows the bubble if allowed after loading the icon image. This method creates + * an image element to load the image into the browser's cache before showing + * the bubble to ensure that the image isn't blank. Use this instead of + * showIfAllowed if the image url is http and cacheable. + * This hack is necessary because Mobile Safari does not properly render + * image elements with border-radius CSS. + * @param {function()} opt_callback Closure to be called if and when the bubble + * actually shows. + * @return {boolean} True if the bubble is allowed to show. + */ +google.bookmarkbubble.Bubble.prototype.showIfAllowedWhenLoaded = + function(opt_callback) { + if (!this.isAllowedToShow_()) { + return false; + } + + var self = this; + // Attach to self to avoid garbage collection. + var img = self.loadImg_ = document.createElement('img'); + img.src = self.getIconUrl_(); + img.onload = function() { + if (img.complete) { + delete self.loadImg_; + img.onload = null; // Break the circular reference. + + self.show_(); + opt_callback && opt_callback(); + } + }; + img.onload(); + + return true; +}; + + +/** + * Sets the parameter in the location hash. As it is + * unpredictable what hash scheme is to be used, this method must be + * implemented by the host application. + * + * This gets called automatically when the bubble is shown. The idea is that if + * the user then creates a bookmark, we can later recognize on application + * startup whether it was from a bookmark suggested with this bubble. + */ +google.bookmarkbubble.Bubble.prototype.setHashParameter = google.abstractMethod; + + +/** + * Whether the parameter is present in the location hash. As it is + * unpredictable what hash scheme is to be used, this method must be + * implemented by the host application. + * + * Call this method during application startup if you want to log whether the + * application was loaded from a bookmark with the bookmark bubble promotion + * parameter in it. + * + * @return {boolean} Whether the bookmark bubble parameter is present in the + * location hash. + */ +google.bookmarkbubble.Bubble.prototype.hasHashParameter = google.abstractMethod; + + +/** + * The number of times the user must dismiss the bubble before we stop showing + * it. This is a public property and can be changed by the host application if + * necessary. + * @type {number} + */ +google.bookmarkbubble.Bubble.prototype.NUMBER_OF_TIMES_TO_DISMISS = 2; + + +/** + * Time in milliseconds. If the user does not dismiss the bubble, it will auto + * destruct after this amount of time. + * @type {number} + */ +google.bookmarkbubble.Bubble.prototype.TIME_UNTIL_AUTO_DESTRUCT = 15000; + + +/** + * The prefix for keys in local storage. This is a public property and can be + * changed by the host application if necessary. + * @type {string} + */ +google.bookmarkbubble.Bubble.prototype.LOCAL_STORAGE_PREFIX = 'BOOKMARK_'; + + +/** + * The key name for the dismissed state. + * @type {string} + * @private + */ +google.bookmarkbubble.Bubble.prototype.DISMISSED_ = 'DISMISSED_COUNT'; + + +/** + * The arrow image in base64 data url format. + * @type {string} + * @private + */ +google.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='; + + +/** + * The close image in base64 data url format. + * @type {string} + * @private + */ +google.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='; + + +/** + * The link used to locate the application's home screen icon to display inside + * the bubble. The default link used here is for an iPhone home screen icon + * without gloss. If your application uses a glossy icon, change this to + * 'apple-touch-icon'. + * @type {string} + * @private + */ +google.bookmarkbubble.Bubble.prototype.REL_ICON_ = + 'apple-touch-icon-precomposed'; + + +/** + * Regular expression for detecting an iPhone or iPod or iPad. + * @type {!RegExp} + * @private + */ +google.bookmarkbubble.Bubble.prototype.MOBILE_SAFARI_USERAGENT_REGEX_ = + /iPhone|iPod|iPad/; + + +/** + * Regular expression for detecting an iPad. + * @type {!RegExp} + * @private + */ +google.bookmarkbubble.Bubble.prototype.IPAD_USERAGENT_REGEX_ = /iPad/; + + +/** + * Determines whether the bubble should be shown or not. + * @return {boolean} Whether the bubble should be shown or not. + * @private + */ +google.bookmarkbubble.Bubble.prototype.isAllowedToShow_ = function() { + return this.isMobileSafari_() && + !this.hasBeenDismissedTooManyTimes_() && + !this.isFullscreen_() && + !this.hasHashParameter(); +}; + + +/** + * Builds and shows the bubble. + * @private + */ +google.bookmarkbubble.Bubble.prototype.show_ = function() { + this.element_ = this.build_(); + + document.body.appendChild(this.element_); + this.element_.style.WebkitTransform = + 'translateY(' + this.getHiddenYPosition_() + 'px)'; + + this.setHashParameter(); + + window.setTimeout(this.boundScrollHandler_, 1); + window.addEventListener('scroll', this.boundScrollHandler_, false); + + // If the user does not dismiss the bubble, slide out and destroy it after + // some time. + window.setTimeout(google.bind(this.autoDestruct_, this), + this.TIME_UNTIL_AUTO_DESTRUCT); +}; + + +/** + * Destroys the bubble by removing its DOM nodes from the document. + */ +google.bookmarkbubble.Bubble.prototype.destroy = function() { + if (this.hasBeenDestroyed_) { + return; + } + window.removeEventListener('scroll', this.boundScrollHandler_, false); + if (this.element_ && this.element_.parentNode == document.body) { + document.body.removeChild(this.element_); + this.element_ = null; + } + this.hasBeenDestroyed_ = true; +}; + + +/** + * Remember that the user has dismissed the bubble once more. + * @private + */ +google.bookmarkbubble.Bubble.prototype.rememberDismissal_ = function() { + if (window.localStorage) { + try { + var key = this.LOCAL_STORAGE_PREFIX + this.DISMISSED_; + var value = Number(window.localStorage[key]) || 0; + window.localStorage[key] = String(value + 1); + } catch (ex) { + // Looks like we've hit the storage size limit. Currently we have no + // fallback for this scenario, but we could use cookie storage instead. + // This would increase the code bloat though. + } + } +}; + + +/** + * Whether the user has dismissed the bubble often enough that we will not + * show it again. + * @return {boolean} Whether the user has dismissed the bubble often enough + * that we will not show it again. + * @private + */ +google.bookmarkbubble.Bubble.prototype.hasBeenDismissedTooManyTimes_ = + function() { + if (!window.localStorage) { + // If we can not use localStorage to remember how many times the user has + // dismissed the bubble, assume he has dismissed it. Otherwise we might end + // up showing it every time the host application loads, into eternity. + return true; + } + try { + var key = this.LOCAL_STORAGE_PREFIX + this.DISMISSED_; + + // If the key has never been set, localStorage yields undefined, which + // Number() turns into NaN. In that case we'll fall back to zero for + // clarity's sake. + var value = Number(window.localStorage[key]) || 0; + + return value >= this.NUMBER_OF_TIMES_TO_DISMISS; + } catch (ex) { + // If we got here, something is wrong with the localStorage. Make the same + // assumption as when it does not exist at all. Exceptions should only + // occur when setting a value (due to storage limitations) but let's be + // extra careful. + return true; + } +}; + + +/** + * Whether the application is running in fullscreen mode. + * @return {boolean} Whether the application is running in fullscreen mode. + * @private + */ +google.bookmarkbubble.Bubble.prototype.isFullscreen_ = function() { + return !!window.navigator.standalone; +}; + + +/** + * Whether the application is running inside Mobile Safari. + * @return {boolean} True if the current user agent looks like Mobile Safari. + * @private + */ +google.bookmarkbubble.Bubble.prototype.isMobileSafari_ = function() { + return this.MOBILE_SAFARI_USERAGENT_REGEX_.test(window.navigator.userAgent); +}; + + +/** + * Whether the application is running on an iPad. + * @return {boolean} True if the current user agent looks like an iPad. + * @private + */ +google.bookmarkbubble.Bubble.prototype.isIpad_ = function() { + return this.IPAD_USERAGENT_REGEX_.test(window.navigator.userAgent); +}; + + + +/** + * Positions the bubble at the bottom of the viewport using an animated + * transition. + */ +google.bookmarkbubble.Bubble.prototype.setPosition = function() { + this.element_.style.WebkitTransition = '-webkit-transform 0.7s ease-out'; + this.element_.style.WebkitTransform = + 'translateY(' + this.getVisibleYPosition_() + 'px)'; +}; + + +/** + * Destroys the bubble by removing its DOM nodes from the document, and + * remembers that it was dismissed. + * @private + */ +google.bookmarkbubble.Bubble.prototype.closeClickHandler_ = function() { + this.destroy(); + this.rememberDismissal_(); +}; + + +/** + * Gets called after a while if the user ignores the bubble. + * @private + */ +google.bookmarkbubble.Bubble.prototype.autoDestruct_ = function() { + if (this.hasBeenDestroyed_) { + return; + } + this.element_.style.WebkitTransition = '-webkit-transform 0.7s ease-in'; + this.element_.style.WebkitTransform = + 'translateY(' + this.getHiddenYPosition_() + 'px)'; + window.setTimeout(google.bind(this.destroy, this), 700); +}; + + +/** + * Gets the y offset used to show the bubble (i.e., position it on-screen). + * @return {number} The y offset. + * @private + */ +google.bookmarkbubble.Bubble.prototype.getVisibleYPosition_ = function() { + return this.isIpad_() ? window.pageYOffset + 17 : + window.pageYOffset - this.element_.offsetHeight + window.innerHeight - 17; +}; + + +/** + * Gets the y offset used to hide the bubble (i.e., position it off-screen). + * @return {number} The y offset. + * @private + */ +google.bookmarkbubble.Bubble.prototype.getHiddenYPosition_ = function() { + return this.isIpad_() ? window.pageYOffset - this.element_.offsetHeight : + window.pageYOffset + window.innerHeight; +}; + + +/** + * The url of the app's bookmark icon. + * @type {string|undefined} + * @private + */ +google.bookmarkbubble.Bubble.prototype.iconUrl_; + + +/** + * Scrapes the document for a link element that specifies an Apple favicon and + * returns the icon url. Returns an empty data url if nothing can be found. + * @return {string} A url string. + * @private + */ +google.bookmarkbubble.Bubble.prototype.getIconUrl_ = function() { + if (!this.iconUrl_) { + var link = this.getLink(this.REL_ICON_); + if (!link || !(this.iconUrl_ = link.href)) { + this.iconUrl_ = 'data:image/png;base64,'; + } + } + return this.iconUrl_; +}; + + +/** + * Gets the requested link tag if it exists. + * @param {string} rel The rel attribute of the link tag to get. + * @return {Element} The requested link tag or null. + */ +google.bookmarkbubble.Bubble.prototype.getLink = function(rel) { + rel = rel.toLowerCase(); + var links = document.getElementsByTagName('link'); + for (var i = 0; i < links.length; ++i) { + var currLink = /** @type {Element} */ (links[i]); + if (currLink.getAttribute('rel').toLowerCase() == rel) { + return currLink; + } + } + return null; +}; + + +/** + * Creates the bubble and appends it to the document. + * @return {Element} The bubble element. + * @private + */ +google.bookmarkbubble.Bubble.prototype.build_ = function() { + var bubble = document.createElement('div'); + var isIpad = this.isIpad_(); + + bubble.style.position = 'absolute'; + bubble.style.zIndex = 1000; + bubble.style.width = '100%'; + bubble.style.left = '0'; + bubble.style.top = '0'; + + var bubbleInner = document.createElement('div'); + bubbleInner.style.position = 'relative'; + bubbleInner.style.width = '214px'; + bubbleInner.style.margin = isIpad ? '0 0 0 82px' : '0 auto'; + bubbleInner.style.border = '2px solid #fff'; + bubbleInner.style.padding = '20px 20px 20px 10px'; + bubbleInner.style.WebkitBorderRadius = '8px'; + bubbleInner.style.WebkitBoxShadow = '0 0 8px rgba(0, 0, 0, 0.7)'; + bubbleInner.style.WebkitBackgroundSize = '100% 8px'; + bubbleInner.style.backgroundColor = '#b0c8ec'; + bubbleInner.style.background = '#cddcf3 -webkit-gradient(linear, ' + + 'left bottom, left top, ' + isIpad ? + 'from(#cddcf3), to(#b3caed)) no-repeat top' : + 'from(#b3caed), to(#cddcf3)) no-repeat bottom'; + bubbleInner.style.font = '13px/17px sans-serif'; + bubble.appendChild(bubbleInner); + + // The "Add to Home Screen" text is intended to be the exact same size text + // that is displayed in the menu of Mobile Safari on iPhone. + bubbleInner.innerHTML = 'Install this web app on your phone: tap ' + + '+ and then \'Add to Home Screen\''; + + var icon = document.createElement('div'); + icon.style['float'] = 'left'; + icon.style.width = '55px'; + icon.style.height = '55px'; + icon.style.margin = '-2px 7px 3px 5px'; + icon.style.background = + '#fff url(' + this.getIconUrl_() + ') no-repeat -1px -1px'; + icon.style.WebkitBackgroundSize = '57px'; + icon.style.WebkitBorderRadius = '10px'; + icon.style.WebkitBoxShadow = '0 2px 5px rgba(0, 0, 0, 0.4)'; + bubbleInner.insertBefore(icon, bubbleInner.firstChild); + + var arrow = document.createElement('div'); + arrow.style.backgroundImage = 'url(' + this.IMAGE_ARROW_DATA_URL_ + ')'; + arrow.style.width = '25px'; + arrow.style.height = '19px'; + arrow.style.position = 'absolute'; + arrow.style.left = '111px'; + if (isIpad) { + arrow.style.WebkitTransform = 'rotate(180deg)'; + arrow.style.top = '-19px'; + } else { + arrow.style.bottom = '-19px'; + } + bubbleInner.appendChild(arrow); + + var close = document.createElement('a'); + close.onclick = google.bind(this.closeClickHandler_, this); + close.style.position = 'absolute'; + close.style.display = 'block'; + close.style.top = '-3px'; + close.style.right = '-3px'; + close.style.width = '16px'; + close.style.height = '16px'; + close.style.border = '10px solid transparent'; + close.style.background = + 'url(' + this.IMAGE_CLOSE_DATA_URL_ + ') no-repeat'; + bubbleInner.appendChild(close); + + return bubble; +};