diff options
Diffstat (limited to 'infobubble.js')
| -rwxr-xr-x | infobubble.js | 1775 |
1 files changed, 1775 insertions, 0 deletions
diff --git a/infobubble.js b/infobubble.js new file mode 100755 index 0000000..06c3744 --- /dev/null +++ b/infobubble.js @@ -0,0 +1,1775 @@ +// ==ClosureCompiler== +// @compilation_level ADVANCED_OPTIMIZATIONS +// @externs_url http://closure-compiler.googlecode.com/svn/trunk/contrib/externs/maps/google_maps_api_v3.js +// ==/ClosureCompiler== + +/** + * @name CSS3 InfoBubble with tabs for Google Maps API V3 + * @version 0.8 + * @author Luke Mahe + * @fileoverview + * This library is a CSS Infobubble with tabs. It uses css3 rounded corners and + * drop shadows and animations. It also allows tabs + */ + +/* + * 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. + */ + + +/** + * A CSS3 InfoBubble v0.8 + * @param {Object.<string, *>=} opt_options Optional properties to set. + * @extends {google.maps.OverlayView} + * @constructor + */ +function InfoBubble(opt_options) { + this.extend(InfoBubble, google.maps.OverlayView); + this.tabs_ = []; + this.activeTab_ = null; + this.baseZIndex_ = 100; + this.isOpen_ = false; + + var options = opt_options || {}; + + if (options['backgroundColor'] == undefined) { + options['backgroundColor'] = this.BACKGROUND_COLOR_; + } + + if (options['borderColor'] == undefined) { + options['borderColor'] = this.BORDER_COLOR_; + } + + if (options['borderRadius'] == undefined) { + options['borderRadius'] = this.BORDER_RADIUS_; + } + + if (options['borderWidth'] == undefined) { + options['borderWidth'] = this.BORDER_WIDTH_; + } + + if (options['padding'] == undefined) { + options['padding'] = this.PADDING_; + } + + if (options['arrowPosition'] == undefined) { + options['arrowPosition'] = this.ARROW_POSITION_; + } + + if (options['disableAutoPan'] == undefined) { + options['disableAutoPan'] = false; + } + + if (options['disableAnimation'] == undefined) { + options['disableAnimation'] = false; + } + + if (options['minWidth'] == undefined) { + options['minWidth'] = this.MIN_WIDTH_; + } + + if (options['shadowStyle'] == undefined) { + options['shadowStyle'] = this.SHADOW_STYLE_; + } + + if (options['arrowSize'] == undefined) { + options['arrowSize'] = this.ARROW_SIZE_; + } + + if (options['arrowStyle'] == undefined) { + options['arrowStyle'] = this.ARROW_STYLE_; + } + + this.buildDom_(); + + this.setValues(options); +} +window['InfoBubble'] = InfoBubble; + + +/** + * Default arrow size + * @const + * @private + */ +InfoBubble.prototype.ARROW_SIZE_ = 15; + + +/** + * Default arrow style + * @const + * @private + */ +InfoBubble.prototype.ARROW_STYLE_ = 0; + + +/** + * Default shadow style + * @const + * @private + */ +InfoBubble.prototype.SHADOW_STYLE_ = 1; + + +/** + * Default min width + * @const + * @private + */ +InfoBubble.prototype.MIN_WIDTH_ = 50; + + +/** + * Default arrow position + * @const + * @private + */ +InfoBubble.prototype.ARROW_POSITION_ = 50; + + +/** + * Default padding + * @const + * @private + */ +InfoBubble.prototype.PADDING_ = 10; + + +/** + * Default border width + * @const + * @private + */ +InfoBubble.prototype.BORDER_WIDTH_ = 1; + + +/** + * Default border color + * @const + * @private + */ +InfoBubble.prototype.BORDER_COLOR_ = '#ccc'; + + +/** + * Default border radius + * @const + * @private + */ +InfoBubble.prototype.BORDER_RADIUS_ = 10; + + +/** + * Default background color + * @const + * @private + */ +InfoBubble.prototype.BACKGROUND_COLOR_ = '#fff'; + + +/** + * Extends a objects prototype by anothers. + * + * @param {Object} obj1 The object to be extended. + * @param {Object} obj2 The object to extend with. + * @return {Object} The new extended object. + * @ignore + */ +InfoBubble.prototype.extend = function(obj1, obj2) { + return (function(object) { + for (var property in object.prototype) { + this.prototype[property] = object.prototype[property]; + } + return this; + }).apply(obj1, [obj2]); +}; + + +/** + * Builds the InfoBubble dom + * @private + */ +InfoBubble.prototype.buildDom_ = function() { + var bubble = this.bubble_ = document.createElement('DIV'); + bubble.style['position'] = 'absolute'; + bubble.style['zIndex'] = this.baseZIndex_; + + var tabsContainer = this.tabsContainer_ = document.createElement('DIV'); + tabsContainer.style['position'] = 'relative'; + + // Close button + var close = this.close_ = document.createElement('IMG'); + close.style['position'] = 'absolute'; + close.style['width'] = this.px(12); + close.style['height'] = this.px(12); + close.style['border'] = 0; + close.style['zIndex'] = this.baseZIndex_ + 1; + close.style['cursor'] = 'pointer'; + close.src = 'http://maps.gstatic.com/intl/en_us/mapfiles/iw_close.gif'; + + var that = this; + google.maps.event.addDomListener(close, 'click', function() { + that.close(); + google.maps.event.trigger(that, 'closeclick'); + }); + + // Content area + var contentContainer = this.contentContainer_ = document.createElement('DIV'); + contentContainer.style['overflowX'] = 'auto'; + contentContainer.style['overflowY'] = 'auto'; + contentContainer.style['cursor'] = 'default'; + contentContainer.style['clear'] = 'both'; + contentContainer.style['position'] = 'relative'; + + var content = this.content_ = document.createElement('DIV'); + contentContainer.appendChild(content); + + // Arrow + var arrow = this.arrow_ = document.createElement('DIV'); + arrow.style['position'] = 'relative'; + + var arrowOuter = this.arrowOuter_ = document.createElement('DIV'); + var arrowInner = this.arrowInner_ = document.createElement('DIV'); + + var arrowSize = this.getArrowSize_(); + + arrowOuter.style['position'] = arrowInner.style['position'] = 'absolute'; + arrowOuter.style['left'] = arrowInner.style['left'] = '50%'; + arrowOuter.style['height'] = arrowInner.style['height'] = '0'; + arrowOuter.style['width'] = arrowInner.style['width'] = '0'; + arrowOuter.style['marginLeft'] = this.px(-arrowSize); + arrowOuter.style['borderWidth'] = this.px(arrowSize); + arrowOuter.style['borderBottomWidth'] = 0; + + // Shadow + var bubbleShadow = this.bubbleShadow_ = document.createElement('DIV'); + bubbleShadow.style['position'] = 'absolute'; + + // Hide the InfoBubble by default + bubble.style['display'] = bubbleShadow.style['display'] = 'none'; + + bubble.appendChild(this.tabsContainer_); + bubble.appendChild(close); + bubble.appendChild(contentContainer); + arrow.appendChild(arrowOuter); + arrow.appendChild(arrowInner); + bubble.appendChild(arrow); + + var stylesheet = document.createElement('style'); + stylesheet.setAttribute('type', 'text/css'); + + /** + * The animation for the infobubble + * @type {string} + */ + this.animationName_ = '_ibani_' + Math.round(Math.random() * 10000); + + var css = '.' + this.animationName_ + '{-webkit-animation-name:' + + this.animationName_ + ';-webkit-animation-duration:0.5s;' + + '-webkit-animation-iteration-count:1;}' + + '@-webkit-keyframes ' + this.animationName_ + ' {from {' + + '-webkit-transform: scale(0)}50% {-webkit-transform: scale(1.2)}90% ' + + '{-webkit-transform: scale(0.95)}to {-webkit-transform: scale(1)}}'; + + stylesheet.textContent = css; + document.getElementsByTagName('head')[0].appendChild(stylesheet); +}; + + +/** + * Sets the background class name + * + * @param {string} className The class name to set. + */ +InfoBubble.prototype.setBackgroundClassName = function(className) { + this.set('backgroundClassName', className); +}; +InfoBubble.prototype['setBackgroundClassName'] = + InfoBubble.prototype.setBackgroundClassName; + + +/** + * changed MVC callback + */ +InfoBubble.prototype.backgroundClassName_changed = function() { + this.content_.className = this.get('backgroundClassName'); +}; +InfoBubble.prototype['backgroundClassName_changed'] = + InfoBubble.prototype.backgroundClassName_changed; + + +/** + * Sets the class of the tab + * + * @param {string} className the class name to set. + */ +InfoBubble.prototype.setTabClassName = function(className) { + this.set('tabClassName', className); +}; +InfoBubble.prototype['setTabClassName'] = + InfoBubble.prototype.setTabClassName; + + +/** + * tabClassName changed MVC callback + */ +InfoBubble.prototype.tabClassName_changed = function() { + this.updateTabStyles_(); +}; +InfoBubble.prototype['tabClassName_changed'] = + InfoBubble.prototype.tabClassName_changed; + + +/** + * Gets the style of the arrow + * + * @private + * @return {number} The style of the arrow. + */ +InfoBubble.prototype.getArrowStyle_ = function() { + return parseInt(this.get('arrowStyle'), 10) || 0; +}; + + +/** + * Sets the style of the arrow + * + * @param {number} style The style of the arrow. + */ +InfoBubble.prototype.setArrowStyle = function(style) { + this.set('arrowStyle', style); +}; +InfoBubble.prototype['setArrowStyle'] = + InfoBubble.prototype.setArrowStyle; + + +/** + * Arrow style changed MVC callback + */ +InfoBubble.prototype.arrowStyle_changed = function() { + this.arrowSize_changed(); +}; +InfoBubble.prototype['arrowStyle_changed'] = + InfoBubble.prototype.arrowStyle_changed; + + +/** + * Gets the size of the arrow + * + * @private + * @return {number} The size of the arrow. + */ +InfoBubble.prototype.getArrowSize_ = function() { + return parseInt(this.get('arrowSize'), 10) || 0; +}; + + +/** + * Sets the size of the arrow + * + * @param {number} size The size of the arrow. + */ +InfoBubble.prototype.setArrowSize = function(size) { + this.set('arrowSize', size); +}; +InfoBubble.prototype['setArrowSize'] = + InfoBubble.prototype.setArrowSize; + + +/** + * Arrow size changed MVC callback + */ +InfoBubble.prototype.arrowSize_changed = function() { + this.borderWidth_changed(); +}; +InfoBubble.prototype['arrowSize_changed'] = + InfoBubble.prototype.arrowSize_changed; + + +/** + * Set the position of the InfoBubble arrow + * + * @param {number} pos The position to set. + */ +InfoBubble.prototype.setArrowPosition = function(pos) { + this.set('arrowPosition', pos); +}; +InfoBubble.prototype['setArrowPosition'] = + InfoBubble.prototype.setArrowPosition; + + +/** + * Get the position of the InfoBubble arrow + * + * @private + * @return {number} The position.. + */ +InfoBubble.prototype.getArrowPosition_ = function() { + return parseInt(this.get('arrowPosition'), 10) || 0; +}; + + +/** + * arrowPosition changed MVC callback + */ +InfoBubble.prototype.arrowPosition_changed = function() { + var pos = this.getArrowPosition_(); + this.arrowOuter_.style['left'] = this.arrowInner_.style['left'] = pos + '%'; + + this.redraw_(); +}; +InfoBubble.prototype['arrowPosition_changed'] = + InfoBubble.prototype.arrowPosition_changed; + + +/** + * Set the zIndex of the InfoBubble + * + * @param {number} zIndex The zIndex to set. + */ +InfoBubble.prototype.setZIndex = function(zIndex) { + this.set('zIndex', zIndex); +}; +InfoBubble.prototype['setZIndex'] = InfoBubble.prototype.setZIndex; + + +/** + * Get the zIndex of the InfoBubble + * + * @return {number} The zIndex to set. + */ +InfoBubble.prototype.getZIndex = function() { + return parseInt(this.get('zIndex'), 10) || this.baseZIndex_; +}; + + +/** + * zIndex changed MVC callback + */ +InfoBubble.prototype.zIndex_changed = function() { + var zIndex = this.getZIndex(); + + this.bubble_.style['zIndex'] = this.baseZIndex_ = zIndex; + this.close_.style['zIndex'] = zIndex + 1; +}; +InfoBubble.prototype['zIndex_changed'] = InfoBubble.prototype.zIndex_changed; + + +/** + * Set the style of the shadow + * + * @param {number} shadowStyle The style of the shadow. + */ +InfoBubble.prototype.setShadowStyle = function(shadowStyle) { + this.set('shadowStyle', shadowStyle); +}; +InfoBubble.prototype['setShadowStyle'] = InfoBubble.prototype.setShadowStyle; + + +/** + * Get the style of the shadow + * + * @private + * @return {number} The style of the shadow. + */ +InfoBubble.prototype.getShadowStyle_ = function() { + return parseInt(this.get('shadowStyle'), 10) || 0; +}; + + +/** + * shadowStyle changed MVC callback + */ +InfoBubble.prototype.shadowStyle_changed = function() { + var shadowStyle = this.getShadowStyle_(); + + var display = ''; + var shadow = ''; + var backgroundColor = ''; + switch (shadowStyle) { + case 0: + display = 'none'; + break; + case 1: + shadow = '40px 15px 10px rgba(33,33,33,0.3)'; + backgroundColor = 'transparent'; + break; + case 2: + shadow = '0 0 2px rgba(33,33,33,0.3)'; + backgroundColor = 'rgba(33,33,33,0.35)'; + break; + } + this.bubbleShadow_.style['boxShadow'] = + this.bubbleShadow_.style['webkitBoxShadow'] = + this.bubbleShadow_.style['MozBoxShadow'] = shadow; + this.bubbleShadow_.style['backgroundColor'] = backgroundColor; + if (this.isOpen_) { + this.bubbleShadow_.style['display'] = display; + this.draw(); + } +}; +InfoBubble.prototype['shadowStyle_changed'] = + InfoBubble.prototype.shadowStyle_changed; + + +/** + * Show the close button + */ +InfoBubble.prototype.showCloseButton = function() { + this.set('hideCloseButton', false); +}; +InfoBubble.prototype['showCloseButton'] = InfoBubble.prototype.showCloseButton; + + +/** + * Hide the close button + */ +InfoBubble.prototype.hideCloseButton = function() { + this.set('hideCloseButton', true); +}; +InfoBubble.prototype['hideCloseButton'] = InfoBubble.prototype.hideCloseButton; + + +/** + * hideCloseButton changed MVC callback + */ +InfoBubble.prototype.hideCloseButton_changed = function() { + this.close_.style['display'] = this.get('hideCloseButton') ? 'none' : ''; +}; +InfoBubble.prototype['hideCloseButton_changed'] = + InfoBubble.prototype.hideCloseButton_changed; + + +/** + * Set the background color + * + * @param {string} color The color to set. + */ +InfoBubble.prototype.setBackgroundColor = function(color) { + if (color) { + this.set('backgroundColor', color); + } +}; +InfoBubble.prototype['setBackgroundColor'] = + InfoBubble.prototype.setBackgroundColor; + + +/** + * backgroundColor changed MVC callback + */ +InfoBubble.prototype.backgroundColor_changed = function() { + var backgroundColor = this.get('backgroundColor'); + this.contentContainer_.style['backgroundColor'] = backgroundColor; + + this.arrowInner_.style['borderColor'] = backgroundColor + + ' transparent transparent'; + this.updateTabStyles_(); +}; +InfoBubble.prototype['backgroundColor_changed'] = + InfoBubble.prototype.backgroundColor_changed; + + +/** + * Set the border color + * + * @param {string} color The border color. + */ +InfoBubble.prototype.setBorderColor = function(color) { + if (color) { + this.set('borderColor', color); + } +}; +InfoBubble.prototype['setBorderColor'] = InfoBubble.prototype.setBorderColor; + + +/** + * borderColor changed MVC callback + */ +InfoBubble.prototype.borderColor_changed = function() { + var borderColor = this.get('borderColor'); + + var contentContainer = this.contentContainer_; + var arrowOuter = this.arrowOuter_; + contentContainer.style['borderColor'] = borderColor; + + arrowOuter.style['borderColor'] = borderColor + + ' transparent transparent'; + + contentContainer.style['borderStyle'] = + arrowOuter.style['borderStyle'] = + this.arrowInner_.style['borderStyle'] = 'solid'; + + this.updateTabStyles_(); +}; +InfoBubble.prototype['borderColor_changed'] = + InfoBubble.prototype.borderColor_changed; + + +/** + * Set the radius of the border + * + * @param {number} radius The radius of the border. + */ +InfoBubble.prototype.setBorderRadius = function(radius) { + this.set('borderRadius', radius); +}; +InfoBubble.prototype['setBorderRadius'] = InfoBubble.prototype.setBorderRadius; + + +/** + * Get the radius of the border + * + * @private + * @return {number} The radius of the border. + */ +InfoBubble.prototype.getBorderRadius_ = function() { + return parseInt(this.get('borderRadius'), 10) || 0; +}; + + +/** + * borderRadius changed MVC callback + */ +InfoBubble.prototype.borderRadius_changed = function() { + var borderRadius = this.getBorderRadius_(); + var borderWidth = this.getBorderWidth_(); + + this.contentContainer_.style['borderRadius'] = + this.contentContainer_.style['MozBorderRadius'] = + this.contentContainer_.style['webkitBorderRadius'] = + this.bubbleShadow_.style['borderRadius'] = + this.bubbleShadow_.style['MozBorderRadius'] = + this.bubbleShadow_.style['webkitBorderRadius'] = this.px(borderRadius); + + this.tabsContainer_.style['paddingLeft'] = + this.tabsContainer_.style['paddingRight'] = + this.px(borderRadius + borderWidth); + + this.redraw_(); +}; +InfoBubble.prototype['borderRadius_changed'] = + InfoBubble.prototype.borderRadius_changed; + + +/** + * Get the width of the border + * + * @private + * @return {number} width The width of the border. + */ +InfoBubble.prototype.getBorderWidth_ = function() { + return parseInt(this.get('borderWidth'), 10) || 0; +}; + + +/** + * Set the width of the border + * + * @param {number} width The width of the border. + */ +InfoBubble.prototype.setBorderWidth = function(width) { + this.set('borderWidth', width); +}; +InfoBubble.prototype['setBorderWidth'] = InfoBubble.prototype.setBorderWidth; + + +/** + * borderWidth change MVC callback + */ +InfoBubble.prototype.borderWidth_changed = function() { + var borderWidth = this.getBorderWidth_(); + + this.contentContainer_.style['borderWidth'] = this.px(borderWidth); + this.tabsContainer_.style['top'] = this.px(borderWidth); + + this.updateArrowStyle_(); + this.updateTabStyles_(); + this.borderRadius_changed(); + this.redraw_(); +}; +InfoBubble.prototype['borderWidth_changed'] = + InfoBubble.prototype.borderWidth_changed; + + +/** + * Update the arrow style + * @private + */ +InfoBubble.prototype.updateArrowStyle_ = function() { + var borderWidth = this.getBorderWidth_(); + var arrowSize = this.getArrowSize_(); + var arrowStyle = this.getArrowStyle_(); + var arrowOuterSizePx = this.px(arrowSize); + var arrowInnerSizePx = this.px(Math.max(0, arrowSize - borderWidth)); + + var outer = this.arrowOuter_; + var inner = this.arrowInner_; + + this.arrow_.style['marginTop'] = this.px(-borderWidth); + outer.style['borderTopWidth'] = arrowOuterSizePx; + inner.style['borderTopWidth'] = arrowInnerSizePx; + + // Full arrow or arrow pointing to the left + if (arrowStyle == 0 || arrowStyle == 1) { + outer.style['borderLeftWidth'] = arrowOuterSizePx; + inner.style['borderLeftWidth'] = arrowInnerSizePx; + } else { + outer.style['borderLeftWidth'] = inner.style['borderLeftWidth'] = 0; + } + + // Full arrow or arrow pointing to the right + if (arrowStyle == 0 || arrowStyle == 2) { + outer.style['borderRightWidth'] = arrowOuterSizePx; + inner.style['borderRightWidth'] = arrowInnerSizePx; + } else { + outer.style['borderRightWidth'] = inner.style['borderRightWidth'] = 0; + } + + if (arrowStyle < 2) { + outer.style['marginLeft'] = this.px(-(arrowSize)); + inner.style['marginLeft'] = this.px(-(arrowSize - borderWidth)); + } else { + outer.style['marginLeft'] = inner.style['marginLeft'] = 0; + } + + // If there is no border then don't show thw outer arrow + if (borderWidth == 0) { + outer.style['display'] = 'none'; + } else { + outer.style['display'] = ''; + } +}; + + +/** + * Set the padding of the InfoBubble + * + * @param {number} padding The padding to apply. + */ +InfoBubble.prototype.setPadding = function(padding) { + this.set('padding', padding); +}; +InfoBubble.prototype['setPadding'] = InfoBubble.prototype.setPadding; + + +/** + * Set the padding of the InfoBubble + * + * @private + * @return {number} padding The padding to apply. + */ +InfoBubble.prototype.getPadding_ = function() { + return parseInt(this.get('padding'), 10) || 0; +}; + + +/** + * padding changed MVC callback + */ +InfoBubble.prototype.padding_changed = function() { + var padding = this.getPadding_(); + this.contentContainer_.style['padding'] = this.px(padding); + this.updateTabStyles_(); + + this.redraw_(); +}; +InfoBubble.prototype['padding_changed'] = InfoBubble.prototype.padding_changed; + + +/** + * Add px extention to the number + * + * @param {number} num The number to wrap. + * @return {string|number} A wrapped number. + */ +InfoBubble.prototype.px = function(num) { + if (num) { + // 0 doesn't need to be wrapped + return num + 'px'; + } + return num; +}; + + +/** + * Add events to stop propagation + * @private + */ +InfoBubble.prototype.addEvents_ = function() { + // We want to cancel all the events so they do not go to the map + var events = ['mousedown', 'mousemove', 'mouseover', 'mouseout', 'mouseup', + 'mousewheel', 'DOMMouseScroll', 'touchstart', 'touchend', 'touchmove', + 'dblclick', 'contextmenu', 'click']; + + var bubble = this.bubble_; + this.listeners_ = []; + for (var i = 0, event; event = events[i]; i++) { + this.listeners_.push( + google.maps.event.addDomListener(bubble, event, function(e) { + e.cancelBubble = true; + if (e.stopPropagation) { + e.stopPropagation(); + } + }) + ); + } +}; + + +/** + * On Adding the InfoBubble to a map + * Implementing the OverlayView interface + */ +InfoBubble.prototype.onAdd = function() { + if (!this.bubble_) { + this.buildDom_(); + } + + this.addEvents_(); + + var panes = this.getPanes(); + if (panes) { + panes.floatPane.appendChild(this.bubble_); + panes.floatShadow.appendChild(this.bubbleShadow_); + } +}; +InfoBubble.prototype['onAdd'] = InfoBubble.prototype.onAdd; + + +/** + * Draw the InfoBubble + * Implementing the OverlayView interface + */ +InfoBubble.prototype.draw = function() { + var projection = this.getProjection(); + + if (!projection) { + // The map projection is not ready yet so do nothing + return; + } + + var latLng = /** @type {google.maps.LatLng} */ (this.get('position')); + + if (!latLng) { + this.close(); + return; + } + + var tabHeight = 0; + + if (this.activeTab_) { + tabHeight = this.activeTab_.offsetHeight; + } + + var anchorHeight = this.getAnchorHeight_(); + var arrowSize = this.getArrowSize_(); + var arrowPosition = this.getArrowPosition_(); + + arrowPosition = arrowPosition / 100; + + var pos = projection.fromLatLngToDivPixel(latLng); + var width = this.contentContainer_.offsetWidth; + var height = this.bubble_.offsetHeight; + + if (!width) { + return; + } + + // Adjust for the height of the info bubble + var top = pos.y - (height + arrowSize); + + if (anchorHeight) { + // If there is an anchor then include the height + top -= anchorHeight; + } + + var left = pos.x - (width * arrowPosition); + + this.bubble_.style['top'] = this.px(top); + this.bubble_.style['left'] = this.px(left); + + var shadowStyle = parseInt(this.get('shadowStyle'), 10); + + switch (shadowStyle) { + case 1: + // Shadow is behind + this.bubbleShadow_.style['top'] = this.px(top + tabHeight - 1); + this.bubbleShadow_.style['left'] = this.px(left); + this.bubbleShadow_.style['width'] = this.px(width); + this.bubbleShadow_.style['height'] = + this.px(this.contentContainer_.offsetHeight - arrowSize); + break; + case 2: + // Shadow is below + width = width * 0.8; + if (anchorHeight) { + this.bubbleShadow_.style['top'] = this.px(pos.y); + } else { + this.bubbleShadow_.style['top'] = this.px(pos.y + arrowSize); + } + this.bubbleShadow_.style['left'] = this.px(pos.x - width * arrowPosition); + + this.bubbleShadow_.style['width'] = this.px(width); + this.bubbleShadow_.style['height'] = this.px(2); + break; + } +}; +InfoBubble.prototype['draw'] = InfoBubble.prototype.draw; + + +/** + * Removing the InfoBubble from a map + */ +InfoBubble.prototype.onRemove = function() { + if (this.bubble_ && this.bubble_.parentNode) { + this.bubble_.parentNode.removeChild(this.bubble_); + } + if (this.bubbleShadow_ && this.bubbleShadow_.parentNode) { + this.bubbleShadow_.parentNode.removeChild(this.bubbleShadow_); + } + + for (var i = 0, listener; listener = this.listeners_[i]; i++) { + google.maps.event.removeListener(listener); + } +}; +InfoBubble.prototype['onRemove'] = InfoBubble.prototype.onRemove; + + +/** + * Is the InfoBubble open + * + * @return {boolean} If the InfoBubble is open. + */ +InfoBubble.prototype.isOpen = function() { + return this.isOpen_; +}; +InfoBubble.prototype['isOpen'] = InfoBubble.prototype.isOpen; + + +/** + * Close the InfoBubble + */ +InfoBubble.prototype.close = function() { + if (this.bubble_) { + this.bubble_.style['display'] = 'none'; + // Remove the animation so we next time it opens it will animate again + this.bubble_.className = + this.bubble_.className.replace(this.animationName_, ''); + } + + if (this.bubbleShadow_) { + this.bubbleShadow_.style['display'] = 'none'; + this.bubbleShadow_.className = + this.bubbleShadow_.className.replace(this.animationName_, ''); + } + this.isOpen_ = false; +}; +InfoBubble.prototype['close'] = InfoBubble.prototype.close; + + +/** + * Open the InfoBubble (asynchronous). + * + * @param {google.maps.Map=} opt_map Optional map to open on. + * @param {google.maps.MVCObject=} opt_anchor Optional anchor to position at. + */ +InfoBubble.prototype.open = function(opt_map, opt_anchor) { + var that = this; + window.setTimeout(function() { + that.open_(opt_map, opt_anchor); + }, 0); +}; + +/** + * Open the InfoBubble + * @private + * @param {google.maps.Map=} opt_map Optional map to open on. + * @param {google.maps.MVCObject=} opt_anchor Optional anchor to position at. + */ +InfoBubble.prototype.open_ = function(opt_map, opt_anchor) { + this.updateContent_(); + + if (opt_map) { + this.setMap(opt_map); + } + + if (opt_anchor) { + this.set('anchor', opt_anchor); + this.bindTo('anchorPoint', opt_anchor); + this.bindTo('position', opt_anchor); + } + + // Show the bubble and the show + this.bubble_.style['display'] = this.bubbleShadow_.style['display'] = ''; + var animation = !this.get('disableAnimation'); + + if (animation) { + // Add the animation + this.bubble_.className += ' ' + this.animationName_; + this.bubbleShadow_.className += ' ' + this.animationName_; + } + + this.redraw_(); + this.isOpen_ = true; + + var pan = !this.get('disableAutoPan'); + if (pan) { + var that = this; + window.setTimeout(function() { + // Pan into view, done in a time out to make it feel nicer :) + that.panToView(); + }, 200); + } +}; +InfoBubble.prototype['open'] = InfoBubble.prototype.open; + + +/** + * Set the position of the InfoBubble + * + * @param {google.maps.LatLng} position The position to set. + */ +InfoBubble.prototype.setPosition = function(position) { + if (position) { + this.set('position', position); + } +}; +InfoBubble.prototype['setPosition'] = InfoBubble.prototype.setPosition; + + +/** + * Returns the position of the InfoBubble + * + * @return {google.maps.LatLng} the position. + */ +InfoBubble.prototype.getPosition = function() { + return /** @type {google.maps.LatLng} */ (this.get('position')); +}; +InfoBubble.prototype['getPosition'] = InfoBubble.prototype.getPosition; + + +/** + * position changed MVC callback + */ +InfoBubble.prototype.position_changed = function() { + this.draw(); +}; +InfoBubble.prototype['position_changed'] = + InfoBubble.prototype.position_changed; + + +/** + * Pan the InfoBubble into view + */ +InfoBubble.prototype.panToView = function() { + var projection = this.getProjection(); + + if (!projection) { + // The map projection is not ready yet so do nothing + return; + } + + if (!this.bubble_) { + // No Bubble yet so do nothing + return; + } + + var anchorHeight = this.getAnchorHeight_(); + var height = this.bubble_.offsetHeight + anchorHeight; + var map = this.get('map'); + var mapDiv = map.getDiv(); + var mapHeight = mapDiv.offsetHeight; + + var latLng = this.getPosition(); + var centerPos = projection.fromLatLngToContainerPixel(map.getCenter()); + var pos = projection.fromLatLngToContainerPixel(latLng); + + // Find out how much space at the top is free + var spaceTop = centerPos.y - height; + + // Fine out how much space at the bottom is free + var spaceBottom = mapHeight - centerPos.y; + + var needsTop = spaceTop < 0; + var deltaY = 0; + + if (needsTop) { + spaceTop *= -1; + deltaY = (spaceTop + spaceBottom) / 2; + } + + pos.y -= deltaY; + latLng = projection.fromContainerPixelToLatLng(pos); + + if (map.getCenter() != latLng) { + map.panTo(latLng); + } +}; +InfoBubble.prototype['panToView'] = InfoBubble.prototype.panToView; + + +/** + * Converts a HTML string to a document fragment. + * + * @param {string} htmlString The HTML string to convert. + * @return {Node} A HTML document fragment. + * @private + */ +InfoBubble.prototype.htmlToDocumentFragment_ = function(htmlString) { + htmlString = htmlString.replace(/^\s*([\S\s]*)\b\s*$/, '$1'); + var tempDiv = document.createElement('DIV'); + tempDiv.innerHTML = htmlString; + if (tempDiv.childNodes.length == 1) { + return /** @type {!Node} */ (tempDiv.removeChild(tempDiv.firstChild)); + } else { + var fragment = document.createDocumentFragment(); + while (tempDiv.firstChild) { + fragment.appendChild(tempDiv.firstChild); + } + return fragment; + } +}; + + +/** + * Removes all children from the node. + * + * @param {Node} node The node to remove all children from. + * @private + */ +InfoBubble.prototype.removeChildren_ = function(node) { + if (!node) { + return; + } + + var child; + while (child = node.firstChild) { + node.removeChild(child); + } +}; + + +/** + * Sets the content of the infobubble. + * + * @param {string|Node} content The content to set. + */ +InfoBubble.prototype.setContent = function(content) { + this.set('content', content); +}; +InfoBubble.prototype['setContent'] = InfoBubble.prototype.setContent; + + +/** + * Get the content of the infobubble. + * + * @return {string|Node} The marker content. + */ +InfoBubble.prototype.getContent = function() { + return /** @type {Node|string} */ (this.get('content')); +}; +InfoBubble.prototype['getContent'] = InfoBubble.prototype.getContent; + + +/** + * Sets the marker content and adds loading events to images + */ +InfoBubble.prototype.updateContent_ = function() { + if (!this.content_) { + // The Content area doesnt exist. + return; + } + + this.removeChildren_(this.content_); + var content = this.getContent(); + if (content) { + if (typeof content == 'string') { + content = this.htmlToDocumentFragment_(content); + } + this.content_.appendChild(content); + + var that = this; + var images = this.content_.getElementsByTagName('IMG'); + for (var i = 0, image; image = images[i]; i++) { + // Because we don't know the size of an image till it loads, add a + // listener to the image load so the marker can resize and reposition + // itself to be the correct height. + google.maps.event.addDomListener(image, 'load', function() { + that.imageLoaded_(); + }); + } + google.maps.event.trigger(this, 'domready'); + } + this.redraw_(); +}; + +/** + * Image loaded + * @private + */ +InfoBubble.prototype.imageLoaded_ = function() { + var pan = !this.get('disableAutoPan'); + this.redraw_(); + if (pan && (this.tabs_.length == 0 || this.activeTab_.index == 0)) { + this.panToView(); + } +}; + +/** + * Updates the styles of the tabs + * @private + */ +InfoBubble.prototype.updateTabStyles_ = function() { + if (this.tabs_ && this.tabs_.length) { + for (var i = 0, tab; tab = this.tabs_[i]; i++) { + this.setTabStyle_(tab.tab); + } + this.activeTab_.style['zIndex'] = this.baseZIndex_; + var borderWidth = this.getBorderWidth_(); + var padding = this.getPadding_() / 2; + this.activeTab_.style['borderBottomWidth'] = 0; + this.activeTab_.style['paddingBottom'] = this.px(padding + borderWidth); + } +}; + + +/** + * Sets the style of a tab + * @private + * @param {Element} tab The tab to style. + */ +InfoBubble.prototype.setTabStyle_ = function(tab) { + var backgroundColor = this.get('backgroundColor'); + var borderColor = this.get('borderColor'); + var borderRadius = this.getBorderRadius_(); + var borderWidth = this.getBorderWidth_(); + var padding = this.getPadding_(); + + var marginRight = this.px(-(Math.max(padding, borderRadius))); + var borderRadiusPx = this.px(borderRadius); + + var index = this.baseZIndex_; + if (tab.index) { + index -= tab.index; + } + + // The styles for the tab + var styles = { + 'cssFloat': 'left', + 'position': 'relative', + 'cursor': 'pointer', + 'backgroundColor': backgroundColor, + 'border': this.px(borderWidth) + ' solid ' + borderColor, + 'padding': this.px(padding / 2) + ' ' + this.px(padding), + 'marginRight': marginRight, + 'whiteSpace': 'nowrap', + 'borderRadiusTopLeft': borderRadiusPx, + 'MozBorderRadiusTopleft': borderRadiusPx, + 'webkitBorderTopLeftRadius': borderRadiusPx, + 'borderRadiusTopRight': borderRadiusPx, + 'MozBorderRadiusTopright': borderRadiusPx, + 'webkitBorderTopRightRadius': borderRadiusPx, + 'zIndex': index, + 'display': 'inline' + }; + + for (var style in styles) { + tab.style[style] = styles[style]; + } + + var className = this.get('tabClassName'); + if (className != undefined) { + tab.className += ' ' + className; + } +}; + + +/** + * Add user actions to a tab + * @private + * @param {Object} tab The tab to add the actions to. + */ +InfoBubble.prototype.addTabActions_ = function(tab) { + var that = this; + tab.listener_ = google.maps.event.addDomListener(tab, 'click', function() { + that.setTabActive_(this); + }); +}; + + +/** + * Set a tab at a index to be active + * + * @param {number} index The index of the tab. + */ +InfoBubble.prototype.setTabActive = function(index) { + var tab = this.tabs_[index - 1]; + + if (tab) { + this.setTabActive_(tab.tab); + } +}; +InfoBubble.prototype['setTabActive'] = InfoBubble.prototype.setTabActive; + + +/** + * Set a tab to be active + * @private + * @param {Object} tab The tab to set active. + */ +InfoBubble.prototype.setTabActive_ = function(tab) { + if (!tab) { + this.setContent(''); + this.updateContent_(); + return; + } + + var padding = this.getPadding_() / 2; + var borderWidth = this.getBorderWidth_(); + + if (this.activeTab_) { + var activeTab = this.activeTab_; + activeTab.style['zIndex'] = this.baseZIndex_ - activeTab.index; + activeTab.style['paddingBottom'] = this.px(padding); + activeTab.style['borderBottomWidth'] = this.px(borderWidth); + } + + tab.style['zIndex'] = this.baseZIndex_; + tab.style['borderBottomWidth'] = 0; + tab.style['marginBottomWidth'] = '-10px'; + tab.style['paddingBottom'] = this.px(padding + borderWidth); + + this.setContent(this.tabs_[tab.index].content); + this.updateContent_(); + + this.activeTab_ = tab; + + this.redraw_(); +}; + + +/** + * Set the max width of the InfoBubble + * + * @param {number} width The max width. + */ +InfoBubble.prototype.setMaxWidth = function(width) { + this.set('maxWidth', width); +}; +InfoBubble.prototype['setMaxWidth'] = InfoBubble.prototype.setMaxWidth; + + +/** + * maxWidth changed MVC callback + */ +InfoBubble.prototype.maxWidth_changed = function() { + this.redraw_(); +}; +InfoBubble.prototype['maxWidth_changed'] = + InfoBubble.prototype.maxWidth_changed; + + +/** + * Set the max height of the InfoBubble + * + * @param {number} height The max height. + */ +InfoBubble.prototype.setMaxHeight = function(height) { + this.set('maxHeight', height); +}; +InfoBubble.prototype['setMaxHeight'] = InfoBubble.prototype.setMaxHeight; + + +/** + * maxHeight changed MVC callback + */ +InfoBubble.prototype.maxHeight_changed = function() { + this.redraw_(); +}; +InfoBubble.prototype['maxHeight_changed'] = + InfoBubble.prototype.maxHeight_changed; + + +/** + * Set the min width of the InfoBubble + * + * @param {number} width The min width. + */ +InfoBubble.prototype.setMinWidth = function(width) { + this.set('minWidth', width); +}; +InfoBubble.prototype['setMinWidth'] = InfoBubble.prototype.setMinWidth; + + +/** + * minWidth changed MVC callback + */ +InfoBubble.prototype.minWidth_changed = function() { + this.redraw_(); +}; +InfoBubble.prototype['minWidth_changed'] = + InfoBubble.prototype.minWidth_changed; + + +/** + * Set the min height of the InfoBubble + * + * @param {number} height The min height. + */ +InfoBubble.prototype.setMinHeight = function(height) { + this.set('minHeight', height); +}; +InfoBubble.prototype['setMinHeight'] = InfoBubble.prototype.setMinHeight; + + +/** + * minHeight changed MVC callback + */ +InfoBubble.prototype.minHeight_changed = function() { + this.redraw_(); +}; +InfoBubble.prototype['minHeight_changed'] = + InfoBubble.prototype.minHeight_changed; + + +/** + * Add a tab + * + * @param {string} label The label of the tab. + * @param {string|Element} content The content of the tab. + */ +InfoBubble.prototype.addTab = function(label, content) { + var tab = document.createElement('DIV'); + tab.innerHTML = label; + + this.setTabStyle_(tab); + this.addTabActions_(tab); + + this.tabsContainer_.appendChild(tab); + + this.tabs_.push({ + label: label, + content: content, + tab: tab + }); + + tab.index = this.tabs_.length - 1; + tab.style['zIndex'] = this.baseZIndex_ - tab.index; + + if (!this.activeTab_) { + this.setTabActive_(tab); + } + + tab.className = tab.className; // + ' ' + this.animationName_; get rid of annoying animation on tabs + + this.redraw_(); +}; +InfoBubble.prototype['addTab'] = InfoBubble.prototype.addTab; + +/** + * Update a tab at a speicifc index + * + * @param {number} index The index of the tab. + * @param {?string} opt_label The label to change to. + * @param {?string} opt_content The content to update to. + */ +InfoBubble.prototype.updateTab = function(index, opt_label, opt_content) { + if (!this.tabs_.length || index < 0 || index >= this.tabs_.length) { + return; + } + + var tab = this.tabs_[index]; + if (opt_label != undefined) { + tab.tab.innerHTML = tab.label = opt_label; + } + + if (opt_content != undefined) { + tab.content = opt_content; + } + + if (this.activeTab_ == tab.tab) { + this.setContent(tab.content); + this.updateContent_(); + } + this.redraw_(); +}; +InfoBubble.prototype['updateTab'] = InfoBubble.prototype.updateTab; + + +/** + * Remove a tab at a specific index + * + * @param {number} index The index of the tab to remove. + */ +InfoBubble.prototype.removeTab = function(index) { + if (!this.tabs_.length || index < 0 || index >= this.tabs_.length) { + return; + } + + var tab = this.tabs_[index]; + tab.tab.parentNode.removeChild(tab.tab); + + google.maps.event.removeListener(tab.tab.listener_); + + this.tabs_.splice(index, 1); + + delete tab; + + for (var i = 0, t; t = this.tabs_[i]; i++) { + t.tab.index = i; + } + + if (tab.tab == this.activeTab_) { + // Removing the current active tab + if (this.tabs_[index]) { + // Show the tab to the right + this.activeTab_ = this.tabs_[index].tab; + } else if (this.tabs_[index - 1]) { + // Show a tab to the left + this.activeTab_ = this.tabs_[index - 1].tab; + } else { + // No tabs left to sho + this.activeTab_ = undefined; + } + + this.setTabActive_(this.activeTab_); + } + + this.redraw_(); +}; +InfoBubble.prototype['removeTab'] = InfoBubble.prototype.removeTab; + + +/** + * Get the size of an element + * @private + * @param {Node|string} element The element to size. + * @param {number=} opt_maxWidth Optional max width of the element. + * @param {number=} opt_maxHeight Optional max height of the element. + * @return {google.maps.Size} The size of the element. + */ +InfoBubble.prototype.getElementSize_ = function(element, opt_maxWidth, + opt_maxHeight) { + var sizer = document.createElement('DIV'); + sizer.style['display'] = 'inline'; + sizer.style['position'] = 'absolute'; + sizer.style['visibility'] = 'hidden'; + + if (typeof element == 'string') { + sizer.innerHTML = element; + } else { + sizer.appendChild(element.cloneNode(true)); + } + + document.body.appendChild(sizer); + var size = new google.maps.Size(sizer.offsetWidth, sizer.offsetHeight); + + // If the width is bigger than the max width then set the width and size again + if (opt_maxWidth && size.width > opt_maxWidth) { + sizer.style['width'] = this.px(opt_maxWidth); + size = new google.maps.Size(sizer.offsetWidth, sizer.offsetHeight); + } + + // If the height is bigger than the max height then set the height and size + // again + if (opt_maxHeight && size.height > opt_maxHeight) { + sizer.style['height'] = this.px(opt_maxHeight); + size = new google.maps.Size(sizer.offsetWidth, sizer.offsetHeight); + } + + document.body.removeChild(sizer); + delete sizer; + return size; +}; + + +/** + * Redraw the InfoBubble + * @private + */ +InfoBubble.prototype.redraw_ = function() { + this.figureOutSize_(); + this.positionCloseButton_(); + this.draw(); +}; + + +/** + * Figure out the optimum size of the InfoBubble + * @private + */ +InfoBubble.prototype.figureOutSize_ = function() { + var map = this.get('map'); + + if (!map) { + return; + } + + var padding = this.getPadding_(); + var borderWidth = this.getBorderWidth_(); + var borderRadius = this.getBorderRadius_(); + var arrowSize = this.getArrowSize_(); + + var mapDiv = map.getDiv(); + var gutter = arrowSize * 2; + var mapWidth = mapDiv.offsetWidth - gutter; + var mapHeight = mapDiv.offsetHeight - gutter - this.getAnchorHeight_(); + var tabHeight = 0; + var width = /** @type {number} */ (this.get('minWidth') || 0); + var height = /** @type {number} */ (this.get('minHeight') || 0); + var maxWidth = /** @type {number} */ (this.get('maxWidth') || 0); + var maxHeight = /** @type {number} */ (this.get('maxHeight') || 0); + + maxWidth = Math.min(mapWidth, maxWidth); + maxHeight = Math.min(mapHeight, maxHeight); + + var tabWidth = 0; + if (this.tabs_.length) { + // If there are tabs then you need to check the size of each tab's content + for (var i = 0, tab; tab = this.tabs_[i]; i++) { + var tabSize = this.getElementSize_(tab.tab, maxWidth, maxHeight); + var contentSize = this.getElementSize_(tab.content, maxWidth, maxHeight); + + if (width < tabSize.width) { + width = tabSize.width; + } + + // Add up all the tab widths because they might end up being wider than + // the content + tabWidth += tabSize.width; + + if (height < tabSize.height) { + height = tabSize.height; + } + + if (tabSize.height > tabHeight) { + tabHeight = tabSize.height; + } + + if (width < contentSize.width) { + width = contentSize.width; + } + + if (height < contentSize.height) { + height = contentSize.height; + } + } + } else { + var content = /** @type {string|Node} */ (this.get('content')); + if (typeof content == 'string') { + content = this.htmlToDocumentFragment_(content); + } + if (content) { + var contentSize = this.getElementSize_(content, maxWidth, maxHeight); + + if (width < contentSize.width) { + width = contentSize.width; + } + + if (height < contentSize.height) { + height = contentSize.height; + } + } + } + + if (maxWidth) { + width = Math.min(width, maxWidth); + } + + if (maxHeight) { + height = Math.min(height, maxHeight); + } + + width = Math.max(width, tabWidth); + + if (width == tabWidth) { + width = width + 2 * padding; + } + + arrowSize = arrowSize * 2; + width = Math.max(width, arrowSize); + + // Maybe add this as a option so they can go bigger than the map if the user + // wants + if (width > mapWidth) { + width = mapWidth; + } + + if (height > mapHeight) { + height = mapHeight - tabHeight; + } + + if (this.tabsContainer_) { + this.tabHeight_ = tabHeight; + this.tabsContainer_.style['width'] = this.px(tabWidth); + } + + this.contentContainer_.style['width'] = this.px(width); + this.contentContainer_.style['height'] = this.px(height); +}; + + +/** + * Get the height of the anchor + * + * This function is a hack for now and doesn't really work that good, need to + * wait for pixelBounds to be correctly exposed. + * @private + * @return {number} The height of the anchor. + */ +InfoBubble.prototype.getAnchorHeight_ = function() { + var anchor = this.get('anchor'); + if (anchor) { + var anchorPoint = /** @type google.maps.Point */(this.get('anchorPoint')); + + if (anchorPoint) { + return -1 * anchorPoint.y; + } + } + return 0; +}; + +InfoBubble.prototype.anchorPoint_changed = function() { + this.draw(); +}; +InfoBubble.prototype['anchorPoint_changed'] = InfoBubble.prototype.anchorPoint_changed; + + +/** + * Position the close button in the right spot. + * @private + */ +InfoBubble.prototype.positionCloseButton_ = function() { + var br = this.getBorderRadius_(); + var bw = this.getBorderWidth_(); + + var right = 2; + var top = 2; + + if (this.tabs_.length && this.tabHeight_) { + top += this.tabHeight_; + } + + top += bw; + right += bw; + + var c = this.contentContainer_; + if (c && c.clientHeight < c.scrollHeight) { + // If there are scrollbars then move the cross in so it is not over + // scrollbar + right += 15; + } + + this.close_.style['right'] = this.px(right); + this.close_.style['top'] = this.px(top); +}; |
