PageSourceSearch

https://sogn.dk/maps/js-info-bubble-gh-pages/src/infobubble.js?v=1790852318

js sogn.dk collected 2026-10-02 07:41:29 UTC 45,396 bytes, 1,799 lines download raw bytes

1// ==ClosureCompiler==
2// @compilation_level ADVANCED_OPTIMIZATIONS
3// @externs_url https://raw.githubusercontent.com/google/closure-compiler/master/contrib/externs/maps/google_maps_api_v3_16.js
4// ==/ClosureCompiler==
5
6/**
7 * @name CSS3 InfoBubble with tabs for Google Maps API V3
8 * @version 0.8
9 * @author Luke Mahe
10 * @fileoverview
11 * This library is a CSS Infobubble with tabs. It uses css3 rounded corners and
12 * drop shadows and animations. It also allows tabs
13 */
14
15/*
16 * Licensed under the Apache License, Version 2.0 (the "License");
17 * you may not use this file except in compliance with the License.
18 * You may obtain a copy of the License at
19 *
20 *     http://www.apache.org/licenses/LICENSE-2.0
21 *
22 * Unless required by applicable law or agreed to in writing, software
23 * distributed under the License is distributed on an "AS IS" BASIS,
24 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25 * See the License for the specific language governing permissions and
26 * limitations under the License.
27 */
28
29
30/**
31 * A CSS3 InfoBubble v0.8
32 * @param {Object.<string, *>=} opt_options Optional properties to set.
33 * @extends {google.maps.OverlayView}
34 * @constructor
35 */
36function InfoBubble(opt_options) {
37  this.extend(InfoBubble, google.maps.OverlayView);
38  this.tabs_ = [];
39  this.activeTab_ = null;
40  this.baseZIndex_ = 100;
41  this.isOpen_ = false;
42
43  var options = opt_options || {};
44
45  if (options['backgroundColor'] == undefined) {
46    options['backgroundColor'] = this.BACKGROUND_COLOR_;
47  }
48
49  if (options['borderColor'] == undefined) {
50    options['borderColor'] = this.BORDER_COLOR_;
51  }
52
53  if (options['borderRadius'] == undefined) {
54    options['borderRadius'] = this.BORDER_RADIUS_;
55  }
56
57  if (options['borderWidth'] == undefined) {
58    options['borderWidth'] = this.BORDER_WIDTH_;
59  }
60
61  if (options['padding'] == undefined) {
62    options['padding'] = this.PADDING_;
63  }
64
65  if (options['arrowPosition'] == undefined) {
66    options['arrowPosition'] = this.ARROW_POSITION_;
67  }
68
69  if (options['disableAutoPan'] == undefined) {
70    options['disableAutoPan'] = false;
71  }
72
73  if (options['disableAnimation'] == undefined) {
74    options['disableAnimation'] = false;
75  }
76
77  if (options['minWidth'] == undefined) {
78    options['minWidth'] = this.MIN_WIDTH_;
79  }
80
81  if (options['shadowStyle'] == undefined) {
82    options['shadowStyle'] = this.SHADOW_STYLE_;
83  }
84
85  if (options['arrowSize'] == undefined) {
86    options['arrowSize'] = this.ARROW_SIZE_;
87  }
88
89  if (options['arrowStyle'] == undefined) {
90    options['arrowStyle'] = this.ARROW_STYLE_;
91  }
92
93  if (options['closeSrc'] == undefined) {
94    options['closeSrc'] = this.CLOSE_SRC_;
95  }
96
97  this.buildDom_();
98  this.setValues(options);
99}
100window['InfoBubble'] = InfoBubble;
101
102
103/**
104 * Default arrow size
105 * @const
106 * @private
107 */
108InfoBubble.prototype.ARROW_SIZE_ = 15;
109
110
111/**
112 * Default arrow style
113 * @const
114 * @private
115 */
116InfoBubble.prototype.ARROW_STYLE_ = 0;
117
118
119/**
120 * Default shadow style
121 * @const
122 * @private
123 */
124InfoBubble.prototype.SHADOW_STYLE_ = 1;
125
126
127/**
128 * Default min width
129 * @const
130 * @private
131 */
132InfoBubble.prototype.MIN_WIDTH_ = 50;
133
134
135/**
136 * Default arrow position
137 * @const
138 * @private
139 */
140InfoBubble.prototype.ARROW_POSITION_ = 50;
141
142
143/**
144 * Default padding
145 * @const
146 * @private
147 */
148InfoBubble.prototype.PADDING_ = 10;
149
150
151/**
152 * Default border width
153 * @const
154 * @private
155 */
156InfoBubble.prototype.BORDER_WIDTH_ = 1;
157
158
159/**
160 * Default border color
161 * @const
162 * @private
163 */
164InfoBubble.prototype.BORDER_COLOR_ = '#ccc';
165
166
167/**
168 * Default border radius
169 * @const
170 * @private
171 */
172InfoBubble.prototype.BORDER_RADIUS_ = 10;
173
174
175/**
176 * Default background color
177 * @const
178 * @private
179 */
180InfoBubble.prototype.BACKGROUND_COLOR_ = '#fff';
181
182/**
183 * Default close image source
184 * @const
185 * @private
186 */
187InfoBubble.prototype.CLOSE_SRC_ = 'https://maps.gstatic.com/intl/en_us/mapfiles/iw_close.gif';
188
189/**
190 * sogn.dk: Erstatning for google.maps.event.addDomListener(), som er deprecated.
191 * Returnerer et objekt med remove(), så lytteren kan fjernes igen.
192 *
193 * @param {Element} element The element to listen on.
194 * @param {string} eventName The DOM event name.
195 * @param {Function} handler The handler.
196 * @return {{remove: Function}} Handle to remove the listener.
197 * @private
198 */
199InfoBubble.addDomListener_ = function(element, eventName, handler) {
200  element.addEventListener(eventName, handler);
201  return {
202    remove: function() {
203      element.removeEventListener(eventName, handler);
204    }
205  };
206};
207
208
209/**
210 * Extends a objects prototype by anothers.
211 *
212 * @param {Object} obj1 The object to be extended.
213 * @param {Object} obj2 The object to extend with.
214 * @return {Object} The new extended object.
215 * @ignore
216 */
217InfoBubble.prototype.extend = function(obj1, obj2) {
218  return (function(object) {
219    for (var property in object.prototype) {
220      this.prototype[property] = object.prototype[property];
221    }
222    return this;
223  }).apply(obj1, [obj2]);
224};
225
226
227/**
228 * Builds the InfoBubble dom
229 * @private
230 */
231InfoBubble.prototype.buildDom_ = function() {
232  var bubble = this.bubble_ = document.createElement('DIV');
233  bubble.style['position'] = 'absolute';
234  bubble.style['zIndex'] = this.baseZIndex_;
235
236  var tabsContainer = this.tabsContainer_ = document.createElement('DIV');
237  tabsContainer.style['position'] = 'relative';
238
239  // Close button
240  var close = this.close_ = document.createElement('IMG');
241  close.style['position'] = 'absolute';
242  close.style['border'] = 0;
243  close.style['zIndex'] = this.baseZIndex_ + 1;
244  close.style['cursor'] = 'pointer';
245  close.src = this.get('closeSrc');
246
247  var that = this;
248  InfoBubble.addDomListener_(close, 'click', function() {
249    that.close();
250    google.maps.event.trigger(that, 'closeclick');
251  });
252
253  // Content area
254  var contentContainer = this.contentContainer_ = document.createElement('DIV');
255  contentContainer.style['overflowX'] = 'auto';
256  contentContainer.style['overflowY'] = 'auto';
257  contentContainer.style['cursor'] = 'default';
258  contentContainer.style['clear'] = 'both';
259  contentContainer.style['position'] = 'relative';
260
261  var content = this.content_ = document.createElement('DIV');
262  contentContainer.appendChild(content);
263
264  // Arrow
265  var arrow = this.arrow_ = document.createElement('DIV');
266  arrow.style['position'] = 'relative';
267
268  var arrowOuter = this.arrowOuter_ = document.createElement('DIV');
269  var arrowInner = this.arrowInner_ = document.createElement('DIV');
270
271  var arrowSize = this.getArrowSize_();
272
273  arrowOuter.style['position'] = arrowInner.style['position'] = 'absolute';
274  arrowOuter.style['left'] = arrowInner.style['left'] = '50%';
275  arrowOuter.style['height'] = arrowInner.style['height'] = '0';
276  arrowOuter.style['width'] = arrowInner.style['width'] = '0';
277  arrowOuter.style['marginLeft'] = this.px(-arrowSize);
278  arrowOuter.style['borderWidth'] = this.px(arrowSize);
279  arrowOuter.style['borderBottomWidth'] = 0;
280
281  // Shadow
282  var bubbleShadow = this.bubbleShadow_ = document.createElement('DIV');
283  bubbleShadow.style['position'] = 'absolute';
284
285  // Hide the InfoBubble by default
286  bubble.style['display'] = bubbleShadow.style['display'] = 'none';
287
288  bubble.appendChild(this.tabsContainer_);
289  bubble.appendChild(close);
290  bubble.appendChild(contentContainer);
291  arrow.appendChild(arrowOuter);
292  arrow.appendChild(arrowInner);
293  bubble.appendChild(arrow);
294
295  var stylesheet = document.createElement('style');
296  stylesheet.setAttribute('type', 'text/css');
297
298  /**
299   * The animation for the infobubble
300   * @type {string}
301   */
302  this.animationName_ = '_ibani_' + Math.round(Math.random() * 10000);
303
304  var css = '.' + this.animationName_ + '{-webkit-animation-name:' +
305      this.animationName_ + ';-webkit-animation-duration:0.5s;' +
306      '-webkit-animation-iteration-count:1;}' +
307      '@-webkit-keyframes ' + this.animationName_ + ' {from {' +
308      '-webkit-transform: scale(0)}50% {-webkit-transform: scale(1.2)}90% ' +
309      '{-webkit-transform: scale(0.95)}to {-webkit-transform: scale(1)}}';
310
311  stylesheet.textContent = css;
312  document.getElementsByTagName('head')[0].appendChild(stylesheet);
313};
314
315
316/**
317 * Sets the background class name
318 *
319 * @param {string} className The class name to set.
320 */
321InfoBubble.prototype.setBackgroundClassName = function(className) {
322  this.set('backgroundClassName', className);
323};
324InfoBubble.prototype['setBackgroundClassName'] = InfoBubble.prototype.setBackgroundClassName;
325
326
327/**
328 * changed MVC callback
329 */
330InfoBubble.prototype.backgroundClassName_changed = function() {
331  this.content_.className = this.get('backgroundClassName');
332};
333InfoBubble.prototype['backgroundClassName_changed'] = InfoBubble.prototype.backgroundClassName_changed;
334
335
336/**
337 * Sets the class of the tab
338 *
339 * @param {string} className the class name to set.
340 */
341InfoBubble.prototype.setTabClassName = function(className) {
342  this.set('tabClassName', className);
343};
344InfoBubble.prototype['setTabClassName'] = InfoBubble.prototype.setTabClassName;
345
346
347/**
348 * tabClassName changed MVC callback
349 */
350InfoBubble.prototype.tabClassName_changed = function() {
351  this.updateTabStyles_();
352};
353InfoBubble.prototype['tabClassName_changed'] = InfoBubble.prototype.tabClassName_changed;
354
355
356/**
357 * Gets the style of the arrow
358 *
359 * @private
360 * @return {number} The style of the arrow.
361 */
362InfoBubble.prototype.getArrowStyle_ = function() {
363  return parseInt(this.get('arrowStyle'), 10) || 0;
364};
365
366
367/**
368 * Sets the style of the arrow
369 *
370 * @param {number} style The style of the arrow.
371 */
372InfoBubble.prototype.setArrowStyle = function(style) {
373  this.set('arrowStyle', style);
374};
375InfoBubble.prototype['setArrowStyle'] = InfoBubble.prototype.setArrowStyle;
376
377
378/**
379 * Arrow style changed MVC callback
380 */
381InfoBubble.prototype.arrowStyle_changed = function() {
382  this.arrowSize_changed();
383};
384InfoBubble.prototype['arrowStyle_changed'] = InfoBubble.prototype.arrowStyle_changed;
385
386
387/**
388 * Gets the size of the arrow
389 *
390 * @private
391 * @return {number} The size of the arrow.
392 */
393InfoBubble.prototype.getArrowSize_ = function() {
394  return parseInt(this.get('arrowSize'), 10) || 0;
395};
396
397
398/**
399 * Sets the size of the arrow
400 *
401 * @param {number} size The size of the arrow.
402 */
403InfoBubble.prototype.setArrowSize = function(size) {
404  this.set('arrowSize', size);
405};
406InfoBubble.prototype['setArrowSize'] = InfoBubble.prototype.setArrowSize;
407
408
409/**
410 * Arrow size changed MVC callback
411 */
412InfoBubble.prototype.arrowSize_changed = function() {
413  this.borderWidth_changed();
414};
415InfoBubble.prototype['arrowSize_changed'] = InfoBubble.prototype.arrowSize_changed;
416
417
418/**
419 * Set the position of the InfoBubble arrow
420 *
421 * @param {number} pos The position to set.
422 */
423InfoBubble.prototype.setArrowPosition = function(pos) {
424  this.set('arrowPosition', pos);
425};
426InfoBubble.prototype['setArrowPosition'] = InfoBubble.prototype.setArrowPosition;
427
428
429/**
430 * Get the position of the InfoBubble arrow
431 *
432 * @private
433 * @return {number} The position..
434 */
435InfoBubble.prototype.getArrowPosition_ = function() {
436  return parseInt(this.get('arrowPosition'), 10) || 0;
437};
438
439
440/**
441 * arrowPosition changed MVC callback
442 */
443InfoBubble.prototype.arrowPosition_changed = function() {
444  var pos = this.getArrowPosition_();
445  this.arrowOuter_.style['left'] = this.arrowInner_.style['left'] = pos + '%';
446
447  this.redraw_();
448};
449InfoBubble.prototype['arrowPosition_changed'] = InfoBubble.prototype.arrowPosition_changed;
450
451
452/**
453 * Set the zIndex of the InfoBubble
454 *
455 * @param {number} zIndex The zIndex to set.
456 */
457InfoBubble.prototype.setZIndex = function(zIndex) {
458  this.set('zIndex', zIndex);
459};
460InfoBubble.prototype['setZIndex'] = InfoBubble.prototype.setZIndex;
461
462
463/**
464 * Get the zIndex of the InfoBubble
465 *
466 * @return {number} The zIndex to set.
467 */
468InfoBubble.prototype.getZIndex = function() {
469  return parseInt(this.get('zIndex'), 10) || this.baseZIndex_;
470};
471
472
473/**
474 * zIndex changed MVC callback
475 */
476InfoBubble.prototype.zIndex_changed = function() {
477  var zIndex = this.getZIndex();
478
479  this.bubble_.style['zIndex'] = this.baseZIndex_ = zIndex;
480  this.close_.style['zIndex'] = zIndex + 1;
481};
482InfoBubble.prototype['zIndex_changed'] = InfoBubble.prototype.zIndex_changed;
483
484
485/**
486 * Set the style of the shadow
487 *
488 * @param {number} shadowStyle The style of the shadow.
489 */
490InfoBubble.prototype.setShadowStyle = function(shadowStyle) {
491  this.set('shadowStyle', shadowStyle);
492};
493InfoBubble.prototype['setShadowStyle'] = InfoBubble.prototype.setShadowStyle;
494
495
496/**
497 * Get the style of the shadow
498 *
499 * @private
500 * @return {number} The style of the shadow.
501 */
502InfoBubble.prototype.getShadowStyle_ = function() {
503  return parseInt(this.get('shadowStyle'), 10) || 0;
504};
505
506
507/**
508 * shadowStyle changed MVC callback
509 */
510InfoBubble.prototype.shadowStyle_changed = function() {
511  var shadowStyle = this.getShadowStyle_();
512
513  var display = '';
514  var shadow = '';
515  var backgroundColor = '';
516  switch (shadowStyle) {
517    case 0:
518      display = 'none';
519      break;
520    case 1:
521      shadow = '40px 15px 10px rgba(33,33,33,0.3)';
522      backgroundColor = 'transparent';
523      break;
524    case 2:
525      shadow = '0 0 2px rgba(33,33,33,0.3)';
526      backgroundColor = 'rgba(33,33,33,0.35)';
527      break;
528  }
529  this.bubbleShadow_.style['boxShadow'] =
530      this.bubbleShadow_.style['webkitBoxShadow'] =
531      this.bubbleShadow_.style['MozBoxShadow'] = shadow;
532  this.bubbleShadow_.style['backgroundColor'] = backgroundColor;
533  if (this.isOpen_) {
534    this.bubbleShadow_.style['display'] = display;
535    this.draw();
536  }
537};
538InfoBubble.prototype['shadowStyle_changed'] = InfoBubble.prototype.shadowStyle_changed;
539
540
541/**
542 * Show the close button
543 */
544InfoBubble.prototype.showCloseButton = function() {
545  this.set('hideCloseButton', false);
546};
547InfoBubble.prototype['showCloseButton'] = InfoBubble.prototype.showCloseButton;
548
549
550/**
551 * Hide the close button
552 */
553InfoBubble.prototype.hideCloseButton = function() {
554  this.set('hideCloseButton', true);
555};
556InfoBubble.prototype['hideCloseButton'] = InfoBubble.prototype.hideCloseButton;
557
558
559/**
560 * hideCloseButton changed MVC callback
561 */
562InfoBubble.prototype.hideCloseButton_changed = function() {
563  this.close_.style['display'] = this.get('hideCloseButton') ? 'none' : '';
564};
565InfoBubble.prototype['hideCloseButton_changed'] = InfoBubble.prototype.hideCloseButton_changed;
566
567
568/**
569 * Set the background color
570 *
571 * @param {string} color The color to set.
572 */
573InfoBubble.prototype.setBackgroundColor = function(color) {
574  if (color) {
575    this.set('backgroundColor', color);
576  }
577};
578InfoBubble.prototype['setBackgroundColor'] = InfoBubble.prototype.setBackgroundColor;
579
580
581/**
582 * backgroundColor changed MVC callback
583 */
584InfoBubble.prototype.backgroundColor_changed = function() {
585  var backgroundColor = this.get('backgroundColor');
586  this.contentContainer_.style['backgroundColor'] = backgroundColor;
587
588  this.arrowInner_.style['borderColor'] = backgroundColor +
589      ' transparent transparent';
590  this.updateTabStyles_();
591};
592InfoBubble.prototype['backgroundColor_changed'] = InfoBubble.prototype.backgroundColor_changed;
593
594
595/**
596 * Set the border color
597 *
598 * @param {string} color The border color.
599 */
600InfoBubble.prototype.setBorderColor = function(color) {
601  if (color) {
602    this.set('borderColor', color);
603  }
604};
605InfoBubble.prototype['setBorderColor'] = InfoBubble.prototype.setBorderColor;
606
607
608/**
609 * borderColor changed MVC callback
610 */
611InfoBubble.prototype.borderColor_changed = function() {
612  var borderColor = this.get('borderColor');
613
614  var contentContainer = this.contentContainer_;
615  var arrowOuter = this.arrowOuter_;
616  contentContainer.style['borderColor'] = borderColor;
617
618  arrowOuter.style['borderColor'] = borderColor +
619      ' transparent transparent';
620
621  contentContainer.style['borderStyle'] =
622      arrowOuter.style['borderStyle'] =
623      this.arrowInner_.style['borderStyle'] = 'solid';
624
625  this.updateTabStyles_();
626};
627InfoBubble.prototype['borderColor_changed'] = InfoBubble.prototype.borderColor_changed;
628
629
630/**
631 * Set the radius of the border
632 *
633 * @param {number} radius The radius of the border.
634 */
635InfoBubble.prototype.setBorderRadius = function(radius) {
636  this.set('borderRadius', radius);
637};
638InfoBubble.prototype['setBorderRadius'] = InfoBubble.prototype.setBorderRadius;
639
640
641/**
642 * Get the radius of the border
643 *
644 * @private
645 * @return {number} The radius of the border.
646 */
647InfoBubble.prototype.getBorderRadius_ = function() {
648  return parseInt(this.get('borderRadius'), 10) || 0;
649};
650
651
652/**
653 * borderRadius changed MVC callback
654 */
655InfoBubble.prototype.borderRadius_changed = function() {
656  var borderRadius = this.getBorderRadius_();
657  var borderWidth = this.getBorderWidth_();
658
659  this.contentContainer_.style['borderRadius'] =
660      this.contentContainer_.style['MozBorderRadius'] =
661      this.contentContainer_.style['webkitBorderRadius'] =
662      this.bubbleShadow_.style['borderRadius'] =
663      this.bubbleShadow_.style['MozBorderRadius'] =
664      this.bubbleShadow_.style['webkitBorderRadius'] = this.px(borderRadius);
665
666  this.tabsContainer_.style['paddingLeft'] =
667      this.tabsContainer_.style['paddingRight'] =
668      this.px(borderRadius + borderWidth);
669
670  this.redraw_();
671};
672InfoBubble.prototype['borderRadius_changed'] = InfoBubble.prototype.borderRadius_changed;
673
674
675/**
676 * Get the width of the border
677 *
678 * @private
679 * @return {number} width The width of the border.
680 */
681InfoBubble.prototype.getBorderWidth_ = function() {
682  return parseInt(this.get('borderWidth'), 10) || 0;
683};
684
685
686/**
687 * Set the width of the border
688 *
689 * @param {number} width The width of the border.
690 */
691InfoBubble.prototype.setBorderWidth = function(width) {
692  this.set('borderWidth', width);
693};
694InfoBubble.prototype['setBorderWidth'] = InfoBubble.prototype.setBorderWidth;
695
696
697/**
698 * borderWidth change MVC callback
699 */
700InfoBubble.prototype.borderWidth_changed = function() {
701  var borderWidth = this.getBorderWidth_();
702
703  this.contentContainer_.style['borderWidth'] = this.px(borderWidth);
704  this.tabsContainer_.style['top'] = this.px(borderWidth);
705
706  this.updateArrowStyle_();
707  this.updateTabStyles_();
708  this.borderRadius_changed();
709  this.redraw_();
710};
711InfoBubble.prototype['borderWidth_changed'] = InfoBubble.prototype.borderWidth_changed;
712
713
714/**
715 * Update the arrow style
716 * @private
717 */
718InfoBubble.prototype.updateArrowStyle_ = function() {
719  var borderWidth = this.getBorderWidth_();
720  var arrowSize = this.getArrowSize_();
721  var arrowStyle = this.getArrowStyle_();
722  var arrowOuterSizePx = this.px(arrowSize);
723  var arrowInnerSizePx = this.px(Math.max(0, arrowSize - borderWidth));
724
725  var outer = this.arrowOuter_;
726  var inner = this.arrowInner_;
727
728  this.arrow_.style['marginTop'] = this.px(-borderWidth);
729  outer.style['borderTopWidth'] = arrowOuterSizePx;
730  inner.style['borderTopWidth'] = arrowInnerSizePx;
731
732  // Full arrow or arrow pointing to the left
733  if (arrowStyle == 0 || arrowStyle == 1) {
734    outer.style['borderLeftWidth'] = arrowOuterSizePx;
735    inner.style['borderLeftWidth'] = arrowInnerSizePx;
736  } else {
737    outer.style['borderLeftWidth'] = inner.style['borderLeftWidth'] = 0;
738  }
739
740  // Full arrow or arrow pointing to the right
741  if (arrowStyle == 0 || arrowStyle == 2) {
742    outer.style['borderRightWidth'] = arrowOuterSizePx;
743    inner.style['borderRightWidth'] = arrowInnerSizePx;
744  } else {
745    outer.style['borderRightWidth'] = inner.style['borderRightWidth'] = 0;
746  }
747
748  if (arrowStyle < 2) {
749    outer.style['marginLeft'] = this.px(-(arrowSize));
750    inner.style['marginLeft'] = this.px(-(arrowSize - borderWidth));
751  } else {
752    outer.style['marginLeft'] = inner.style['marginLeft'] = 0;
753  }
754
755  // If there is no border then don't show thw outer arrow
756  if (borderWidth == 0) {
757    outer.style['display'] = 'none';
758  } else {
759    outer.style['display'] = '';
760  }
761};
762
763
764/**
765 * Set the padding of the InfoBubble
766 *
767 * @param {number} padding The padding to apply.
768 */
769InfoBubble.prototype.setPadding = function(padding) {
770  this.set('padding', padding);
771};
772InfoBubble.prototype['setPadding'] = InfoBubble.prototype.setPadding;
773
774
775/**
776 * Set the close image url
777 *
778 * @param {string} src The url of the image used as a close icon
779 */
780InfoBubble.prototype.setCloseSrc = function(src) {
781  if (src && this.close_) {
782    this.close_.src = src;
783  }
784};
785InfoBubble.prototype['setCloseSrc'] = InfoBubble.prototype.setCloseSrc;
786
787
788/**
789 * Set the padding of the InfoBubble
790 *
791 * @private
792 * @return {number} padding The padding to apply.
793 */
794InfoBubble.prototype.getPadding_ = function() {
795  return parseInt(this.get('padding'), 10) || 0;
796};
797
798
799/**
800 * padding changed MVC callback
801 */
802InfoBubble.prototype.padding_changed = function() {
803  var padding = this.getPadding_();
804  this.contentContainer_.style['padding'] = this.px(padding);
805  this.updateTabStyles_();
806
807  this.redraw_();
808};
809InfoBubble.prototype['padding_changed'] = InfoBubble.prototype.padding_changed;
810
811
812/**
813 * Add px extention to the number
814 *
815 * @param {number} num The number to wrap.
816 * @return {string|number} A wrapped number.
817 */
818InfoBubble.prototype.px = function(num) {
819  if (num) {
820    // 0 doesn't need to be wrapped
821    return num + 'px';
822  }
823  return num;
824};
825
826
827/**
828 * Add events to stop propagation
829 * @private
830 */
831InfoBubble.prototype.addEvents_ = function() {
832  // We want to cancel all the events so they do not go to the map
833  var events = ['mousedown', 'mousemove', 'mouseover', 'mouseout', 'mouseup',
834      'mousewheel', 'DOMMouseScroll', 'touchstart', 'touchend', 'touchmove',
835      'dblclick', 'contextmenu', 'click'];
836
837  var bubble = this.bubble_;
838  this.listeners_ = [];
839  for (var i = 0, event; event = events[i]; i++) {
840    this.listeners_.push(
841      InfoBubble.addDomListener_(bubble, event, function(e) {
842        e.cancelBubble = true;
843        if (e.stopPropagation) {
844          e.stopPropagation();
845        }
846      })
847    );
848  }
849};
850
851
852/**
853 * On Adding the InfoBubble to a map
854 * Implementing the OverlayView interface
855 */
856InfoBubble.prototype.onAdd = function() {
857  if (!this.bubble_) {
858    this.buildDom_();
859  }
860
861  this.addEvents_();
862
863  var panes = this.getPanes();
864  if (panes) {
865    panes.floatPane.appendChild(this.bubble_);
866    panes.floatShadow.appendChild(this.bubbleShadow_);
867  }
868
869  /* once the infoBubble has been added to the DOM, fire 'domready' event */
870  google.maps.event.trigger(this, 'domready');
871};
872InfoBubble.prototype['onAdd'] = InfoBubble.prototype.onAdd;
873
874
875/**
876 * Draw the InfoBubble
877 * Implementing the OverlayView interface
878 */
879InfoBubble.prototype.draw = function() {
880  var projection = this.getProjection();
881
882  if (!projection) {
883    // The map projection is not ready yet so do nothing
884    return;
885  }
886
887  var latLng = /** @type {google.maps.LatLng} */ (this.get('position'));
888
889  if (!latLng) {
890    this.close();
891    return;
892  }
893
894  var tabHeight = 0;
895
896  if (this.activeTab_) {
897    tabHeight = this.activeTab_.offsetHeight;
898  }
899
900  var anchorHeight = this.getAnchorHeight_();
901  var arrowSize = this.getArrowSize_();
902  var arrowPosition = this.getArrowPosition_();
903
904  arrowPosition = arrowPosition / 100;
905
906  var pos = projection.fromLatLngToDivPixel(latLng);
907  var width = this.contentContainer_.offsetWidth;
908  var height = this.bubble_.offsetHeight;
909
910  if (!width) {
911    return;
912  }
913
914  // Adjust for the height of the info bubble
915  var top = pos.y - (height + arrowSize);
916
917  if (anchorHeight) {
918    // If there is an anchor then include the height
919    top -= anchorHeight;
920  }
921
922  var left = pos.x - (width * arrowPosition);
923
924  this.bubble_.style['top'] = this.px(top);
925  this.bubble_.style['left'] = this.px(left);
926
927  var shadowStyle = parseInt(this.get('shadowStyle'), 10);
928
929  switch (shadowStyle) {
930    case 1:
931      // Shadow is behind
932      this.bubbleShadow_.style['top'] = this.px(top + tabHeight - 1);
933      this.bubbleShadow_.style['left'] = this.px(left);
934      this.bubbleShadow_.style['width'] = this.px(width);
935      this.bubbleShadow_.style['height'] =
936          this.px(this.contentContainer_.offsetHeight - arrowSize);
937      break;
938    case 2:
939      // Shadow is below
940      width = width * 0.8;
941      if (anchorHeight) {
942        this.bubbleShadow_.style['top'] = this.px(pos.y);
943      } else {
944        this.bubbleShadow_.style['top'] = this.px(pos.y + arrowSize);
945      }
946      this.bubbleShadow_.style['left'] = this.px(pos.x - width * arrowPosition);
947
948      this.bubbleShadow_.style['width'] = this.px(width);
949      this.bubbleShadow_.style['height'] = this.px(2);
950      break;
951  }
952};
953InfoBubble.prototype['draw'] = InfoBubble.prototype.draw;
954
955
956/**
957 * Removing the InfoBubble from a map
958 */
959InfoBubble.prototype.onRemove = function() {
960  if (this.bubble_ && this.bubble_.parentNode) {
961    this.bubble_.parentNode.removeChild(this.bubble_);
962  }
963  if (this.bubbleShadow_ && this.bubbleShadow_.parentNode) {
964    this.bubbleShadow_.parentNode.removeChild(this.bubbleShadow_);
965  }
966
967  for (var i = 0, listener; listener = this.listeners_[i]; i++) {
968    listener.remove();
969  }
970};
971InfoBubble.prototype['onRemove'] = InfoBubble.prototype.onRemove;
972
973
974/**
975 * Is the InfoBubble open
976 *
977 * @return {boolean} If the InfoBubble is open.
978 */
979InfoBubble.prototype.isOpen = function() {
980  return this.isOpen_;
981};
982InfoBubble.prototype['isOpen'] = InfoBubble.prototype.isOpen;
983
984
985/**
986 * Close the InfoBubble
987 */
988InfoBubble.prototype.close = function() {
989  if (this.bubble_) {
990    this.bubble_.style['display'] = 'none';
991    // Remove the animation so we next time it opens it will animate again
992    this.bubble_.className =
993        this.bubble_.className.replace(this.animationName_, '');
994  }
995
996  if (this.bubbleShadow_) {
997    this.bubbleShadow_.style['display'] = 'none';
998    this.bubbleShadow_.className =
999        this.bubbleShadow_.className.replace(this.animationName_, '');
1000  }
1001  this.isOpen_ = false;
1002};
1003InfoBubble.prototype['close'] = InfoBubble.prototype.close;
1004
1005
1006/**
1007 * Open the InfoBubble (asynchronous).
1008 *
1009 * @param {google.maps.Map=} opt_map Optional map to open on.
1010 * @param {google.maps.MVCObject=} opt_anchor Optional anchor to position at.
1011 */
1012InfoBubble.prototype.open = function(opt_map, opt_anchor) {
1013  var that = this;
1014  window.setTimeout(function() {
1015    that.open_(opt_map, opt_anchor);
1016  }, 0);
1017};
1018
1019
1020/**
1021 * Open the InfoBubble
1022 * @private
1023 * @param {google.maps.Map=} opt_map Optional map to open on.
1024 * @param {google.maps.MVCObject=} opt_anchor Optional anchor to position at.
1025 */
1026InfoBubble.prototype.open_ = function(opt_map, opt_anchor) {
1027  this.updateContent_();
1028
1029  if (opt_map) {
1030    this.setMap(opt_map);
1031  }
1032
1033  if (opt_anchor) {
1034    this.set('anchor', opt_anchor);
1035    this.bindTo('anchorPoint', opt_anchor);
1036    this.bindTo('position', opt_anchor);
1037  }
1038
1039  // Show the bubble and the show
1040  this.bubble_.style['display'] = this.bubbleShadow_.style['display'] = '';
1041  var animation = !this.get('disableAnimation');
1042
1043  if (animation) {
1044    // Add the animation
1045    this.bubble_.className += ' ' + this.animationName_;
1046    this.bubbleShadow_.className += ' ' + this.animationName_;
1047  }
1048
1049  this.redraw_();
1050  this.isOpen_ = true;
1051
1052  var pan = !this.get('disableAutoPan');
1053  if (pan) {
1054    var that = this;
1055    window.setTimeout(function() {
1056      // Pan into view, done in a time out to make it feel nicer :)
1057      that.panToView();
1058    }, 200);
1059  }
1060};
1061InfoBubble.prototype['open'] = InfoBubble.prototype.open;
1062
1063
1064/**
1065 * Set the position of the InfoBubble
1066 *
1067 * @param {google.maps.LatLng} position The position to set.
1068 */
1069InfoBubble.prototype.setPosition = function(position) {
1070  if (position) {
1071    this.set('position', position);
1072  }
1073};
1074InfoBubble.prototype['setPosition'] = InfoBubble.prototype.setPosition;
1075
1076
1077/**
1078 * Returns the position of the InfoBubble
1079 *
1080 * @return {google.maps.LatLng} the position.
1081 */
1082InfoBubble.prototype.getPosition = function() {
1083  return /** @type {google.maps.LatLng} */ (this.get('position'));
1084};
1085InfoBubble.prototype['getPosition'] = InfoBubble.prototype.getPosition;
1086
1087
1088/**
1089 * position changed MVC callback
1090 */
1091InfoBubble.prototype.position_changed = function() {
1092  this.draw();
1093};
1094InfoBubble.prototype['position_changed'] = InfoBubble.prototype.position_changed;
1095
1096
1097/**
1098 * Pan the InfoBubble into view
1099 */
1100InfoBubble.prototype.panToView = function() {
1101  var projection = this.getProjection();
1102
1103  if (!projection) {
1104    // The map projection is not ready yet so do nothing
1105    return;
1106  }
1107
1108  if (!this.bubble_) {
1109    // No Bubble yet so do nothing
1110    return;
1111  }
1112
1113  var anchorHeight = this.getAnchorHeight_();
1114  var height = this.bubble_.offsetHeight + anchorHeight;
1115  var map = this.get('map');
1116  var mapDiv = map.getDiv();
1117  var mapHeight = mapDiv.offsetHeight;
1118
1119  var latLng = this.getPosition();
1120  var centerPos = projection.fromLatLngToContainerPixel(map.getCenter());
1121  var pos = projection.fromLatLngToContainerPixel(latLng);
1122
1123  // Find out how much space at the top is free
1124  var spaceTop = centerPos.y - height;
1125
1126  // Fine out how much space at the bottom is free
1127  var spaceBottom = mapHeight - centerPos.y;
1128
1129  var needsTop = spaceTop < 0;
1130  var deltaY = 0;
1131
1132  if (needsTop) {
1133    spaceTop *= -1;
1134    deltaY = (spaceTop + spaceBottom) / 2;
1135  }
1136
1137  pos.y -= deltaY;
1138  latLng = projection.fromContainerPixelToLatLng(pos);
1139
1140  if (map.getCenter() != latLng) {
1141    map.panTo(latLng);
1142  }
1143};
1144InfoBubble.prototype['panToView'] = InfoBubble.prototype.panToView;
1145
1146
1147/**
1148 * Converts a HTML string to a document fragment.
1149 *
1150 * @param {string} htmlString The HTML string to convert.
1151 * @return {Node} A HTML document fragment.
1152 * @private
1153 */
1154InfoBubble.prototype.htmlToDocumentFragment_ = function(htmlString) {
1155  htmlString = htmlString.replace(/^\s*([\S\s]*)\b\s*$/, '$1');
1156  var tempDiv = document.createElement('DIV');
1157  tempDiv.innerHTML = htmlString;
1158  if (tempDiv.childNodes.length == 1) {
1159    return /** @type {!Node} */ (tempDiv.removeChild(tempDiv.firstChild));
1160  } else {
1161    var fragment = document.createDocumentFragment();
1162    while (tempDiv.firstChild) {
1163      fragment.appendChild(tempDiv.firstChild);
1164    }
1165    return fragment;
1166  }
1167};
1168
1169
1170/**
1171 * Removes all children from the node.
1172 *
1173 * @param {Node} node The node to remove all children from.
1174 * @private
1175 */
1176InfoBubble.prototype.removeChildren_ = function(node) {
1177  if (!node) {
1178    return;
1179  }
1180
1181  var child;
1182  while (child = node.firstChild) {
1183    node.removeChild(child);
1184  }
1185};
1186
1187
1188/**
1189 * Sets the content of the infobubble.
1190 *
1191 * @param {string|Node} content The content to set.
1192 */
1193InfoBubble.prototype.setContent = function(content) {
1194  this.set('content', content);
1195};
1196InfoBubble.prototype['setContent'] = InfoBubble.prototype.setContent;
1197
1198
1199/**
1200 * Get the content of the infobubble.
1201 *
1202 * @return {string|Node} The marker content.
1203 */
1204InfoBubble.prototype.getContent = function() {
1205  return /** @type {Node|string} */ (this.get('content'));
1206};
1207InfoBubble.prototype['getContent'] = InfoBubble.prototype.getContent;
1208
1209
1210/**
1211 * Sets the marker content and adds loading events to images
1212 */
1213InfoBubble.prototype.updateContent_ = function() {
1214  if (!this.content_) {
1215    // The Content area doesnt exist.
1216    return;
1217  }
1218
1219  this.removeChildren_(this.content_);
1220  var content = this.getContent();
1221  if (content) {
1222    if (typeof content == 'string') {
1223      content = this.htmlToDocumentFragment_(content);
1224    }
1225    this.content_.appendChild(content);
1226
1227    var that = this;
1228    var images = this.content_.getElementsByTagName('IMG');
1229    for (var i = 0, image; image = images[i]; i++) {
1230      // Because we don't know the size of an image till it loads, add a
1231      // listener to the image load so the marker can resize and reposition
1232      // itself to be the correct height.
1233      InfoBubble.addDomListener_(image, 'load', function() {
1234        that.imageLoaded_();
1235      });
1236    }
1237  }
1238  this.redraw_();
1239};
1240
1241
1242/**
1243 * Image loaded
1244 * @private
1245 */
1246InfoBubble.prototype.imageLoaded_ = function() {
1247  var pan = !this.get('disableAutoPan');
1248  this.redraw_();
1249  if (pan && (this.tabs_.length == 0 || this.activeTab_.index == 0)) {
1250    this.panToView();
1251  }
1252};
1253
1254
1255/**
1256 * Updates the styles of the tabs
1257 * @private
1258 */
1259InfoBubble.prototype.updateTabStyles_ = function() {
1260  if (this.tabs_ && this.tabs_.length) {
1261    for (var i = 0, tab; tab = this.tabs_[i]; i++) {
1262      this.setTabStyle_(tab.tab);
1263    }
1264    this.activeTab_.style['zIndex'] = this.baseZIndex_;
1265    var borderWidth = this.getBorderWidth_();
1266    var padding = this.getPadding_() / 2;
1267    this.activeTab_.style['borderBottomWidth'] = 0;
1268    this.activeTab_.style['paddingBottom'] = this.px(padding + borderWidth);
1269  }
1270};
1271
1272
1273/**
1274 * Sets the style of a tab
1275 * @private
1276 * @param {Element} tab The tab to style.
1277 */
1278InfoBubble.prototype.setTabStyle_ = function(tab) {
1279  var backgroundColor = this.get('backgroundColor');
1280  var borderColor = this.get('borderColor');
1281  var borderRadius = this.getBorderRadius_();
1282  var borderWidth = this.getBorderWidth_();
1283  var padding = this.getPadding_();
1284
1285  var marginRight = this.px(-(Math.max(padding, borderRadius)));
1286  var borderRadiusPx = this.px(borderRadius);
1287
1288  var index = this.baseZIndex_;
1289  if (tab.index) {
1290    index -= tab.index;
1291  }
1292
1293  // The styles for the tab
1294  var styles = {
1295    'cssFloat': 'left',
1296    'position': 'relative',
1297    'cursor': 'pointer',
1298    'backgroundColor': backgroundColor,
1299    'border': this.px(borderWidth) + ' solid ' + borderColor,
1300    'padding': this.px(padding / 2) + ' ' + this.px(padding),
1301    'marginRight': marginRight,
1302    'whiteSpace': 'nowrap',
1303    'borderRadiusTopLeft': borderRadiusPx,
1304    'MozBorderRadiusTopleft': borderRadiusPx,
1305    'webkitBorderTopLeftRadius': borderRadiusPx,
1306    'borderRadiusTopRight': borderRadiusPx,
1307    'MozBorderRadiusTopright': borderRadiusPx,
1308    'webkitBorderTopRightRadius': borderRadiusPx,
1309    'zIndex': index,
1310    'display': 'inline'
1311  };
1312
1313  for (var style in styles) {
1314    tab.style[style] = styles[style];
1315  }
1316
1317  var className = this.get('tabClassName');
1318  if (className != undefined) {
1319    tab.className += ' ' + className;
1320  }
1321};
1322
1323
1324/**
1325 * Add user actions to a tab
1326 * @private
1327 * @param {Object} tab The tab to add the actions to.
1328 */
1329InfoBubble.prototype.addTabActions_ = function(tab) {
1330  var that = this;
1331  tab.listener_ = InfoBubble.addDomListener_(tab, 'click', function() {
1332    that.setTabActive_(this);
1333  });
1334};
1335
1336
1337/**
1338 * Set a tab at a index to be active
1339 *
1340 * @param {number} index The index of the tab.
1341 */
1342InfoBubble.prototype.setTabActive = function(index) {
1343  var tab = this.tabs_[index - 1];
1344
1345  if (tab) {
1346    this.setTabActive_(tab.tab);
1347  }
1348};
1349InfoBubble.prototype['setTabActive'] = InfoBubble.prototype.setTabActive;
1350
1351
1352/**
1353 * Set a tab to be active
1354 * @private
1355 * @param {Object} tab The tab to set active.
1356 */
1357InfoBubble.prototype.setTabActive_ = function(tab) {
1358  if (!tab) {
1359    this.setContent('');
1360    this.updateContent_();
1361    return;
1362  }
1363
1364  var padding = this.getPadding_() / 2;
1365  var borderWidth = this.getBorderWidth_();
1366
1367  if (this.activeTab_) {
1368    var activeTab = this.activeTab_;
1369    activeTab.style['zIndex'] = this.baseZIndex_ - activeTab.index;
1370    activeTab.style['paddingBottom'] = this.px(padding);
1371    activeTab.style['borderBottomWidth'] = this.px(borderWidth);
1372  }
1373
1374  tab.style['zIndex'] = this.baseZIndex_;
1375  tab.style['borderBottomWidth'] = 0;
1376  tab.style['marginBottomWidth'] = '-10px';
1377  tab.style['paddingBottom'] = this.px(padding + borderWidth);
1378
1379  this.setContent(this.tabs_[tab.index].content);
1380  this.updateContent_();
1381
1382  this.activeTab_ = tab;
1383
1384  this.redraw_();
1385};
1386
1387
1388/**
1389 * Set the max width of the InfoBubble
1390 *
1391 * @param {number} width The max width.
1392 */
1393InfoBubble.prototype.setMaxWidth = function(width) {
1394  this.set('maxWidth', width);
1395};
1396InfoBubble.prototype['setMaxWidth'] = InfoBubble.prototype.setMaxWidth;
1397
1398
1399/**
1400 * maxWidth changed MVC callback
1401 */
1402InfoBubble.prototype.maxWidth_changed = function() {
1403  this.redraw_();
1404};
1405InfoBubble.prototype['maxWidth_changed'] = InfoBubble.prototype.maxWidth_changed;
1406
1407
1408/**
1409 * Set the max height of the InfoBubble
1410 *
1411 * @param {number} height The max height.
1412 */
1413InfoBubble.prototype.setMaxHeight = function(height) {
1414  this.set('maxHeight', height);
1415};
1416InfoBubble.prototype['setMaxHeight'] = InfoBubble.prototype.setMaxHeight;
1417
1418
1419/**
1420 * maxHeight changed MVC callback
1421 */
1422InfoBubble.prototype.maxHeight_changed = function() {
1423  this.redraw_();
1424};
1425InfoBubble.prototype['maxHeight_changed'] = InfoBubble.prototype.maxHeight_changed;
1426
1427
1428/**
1429 * Set the min width of the InfoBubble
1430 *
1431 * @param {number} width The min width.
1432 */
1433InfoBubble.prototype.setMinWidth = function(width) {
1434  this.set('minWidth', width);
1435};
1436InfoBubble.prototype['setMinWidth'] = InfoBubble.prototype.setMinWidth;
1437
1438
1439/**
1440 * minWidth changed MVC callback
1441 */
1442InfoBubble.prototype.minWidth_changed = function() {
1443  this.redraw_();
1444};
1445InfoBubble.prototype['minWidth_changed'] = InfoBubble.prototype.minWidth_changed;
1446
1447
1448/**
1449 * Set the min height of the InfoBubble
1450 *
1451 * @param {number} height The min height.
1452 */
1453InfoBubble.prototype.setMinHeight = function(height) {
1454  this.set('minHeight', height);
1455};
1456InfoBubble.prototype['setMinHeight'] = InfoBubble.prototype.setMinHeight;
1457
1458
1459/**
1460 * minHeight changed MVC callback
1461 */
1462InfoBubble.prototype.minHeight_changed = function() {
1463  this.redraw_();
1464};
1465InfoBubble.prototype['minHeight_changed'] = InfoBubble.prototype.minHeight_changed;
1466
1467
1468/**
1469 * Add a tab
1470 *
1471 * @param {string} label The label of the tab.
1472 * @param {string|Element} content The content of the tab.
1473 */
1474InfoBubble.prototype.addTab = function(label, content) {
1475  var tab = document.createElement('DIV');
1476  tab.innerHTML = label;
1477
1478  this.setTabStyle_(tab);
1479  this.addTabActions_(tab);
1480
1481  this.tabsContainer_.appendChild(tab);
1482
1483  this.tabs_.push({
1484    label: label,
1485    content: content,
1486    tab: tab
1487  });
1488
1489  tab.index = this.tabs_.length - 1;
1490  tab.style['zIndex'] = this.baseZIndex_ - tab.index;
1491
1492  if (!this.activeTab_) {
1493    this.setTabActive_(tab);
1494  }
1495
1496  tab.className = tab.className + ' ' + this.animationName_;
1497
1498  this.redraw_();
1499};
1500InfoBubble.prototype['addTab'] = InfoBubble.prototype.addTab;
1501
1502
1503/**
1504 * Update a tab at a speicifc index
1505 *
1506 * @param {number} index The index of the tab.
1507 * @param {?string} opt_label The label to change to.
1508 * @param {?string} opt_content The content to update to.
1509 */
1510InfoBubble.prototype.updateTab = function(index, opt_label, opt_content) {
1511  if (!this.tabs_.length || index < 0 || index >= this.tabs_.length) {
1512    return;
1513  }
1514
1515  var tab = this.tabs_[index];
1516  if (opt_label != undefined) {
1517    tab.tab.innerHTML = tab.label = opt_label;
1518  }
1519
1520  if (opt_content != undefined) {
1521    tab.content = opt_content;
1522  }
1523
1524  if (this.activeTab_ == tab.tab) {
1525    this.setContent(tab.content);
1526    this.updateContent_();
1527  }
1528  this.redraw_();
1529};
1530InfoBubble.prototype['updateTab'] = InfoBubble.prototype.updateTab;
1531
1532
1533/**
1534 * Remove a tab at a specific index
1535 *
1536 * @param {number} index The index of the tab to remove.
1537 */
1538InfoBubble.prototype.removeTab = function(index) {
1539  if (!this.tabs_.length || index < 0 || index >= this.tabs_.length) {
1540    return;
1541  }
1542
1543  var tab = this.tabs_[index];
1544  tab.tab.parentNode.removeChild(tab.tab);
1545
1546  tab.tab.listener_.remove();
1547
1548  this.tabs_.splice(index, 1);
1549
1550  delete tab;
1551
1552  for (var i = 0, t; t = this.tabs_[i]; i++) {
1553    t.tab.index = i;
1554  }
1555
1556  if (tab.tab == this.activeTab_) {
1557    // Removing the current active tab
1558    if (this.tabs_[index]) {
1559      // Show the tab to the right
1560      this.activeTab_ = this.tabs_[index].tab;
1561    } else if (this.tabs_[index - 1]) {
1562      // Show a tab to the left
1563      this.activeTab_ = this.tabs_[index - 1].tab;
1564    } else {
1565      // No tabs left to sho
1566      this.activeTab_ = undefined;
1567    }
1568
1569    this.setTabActive_(this.activeTab_);
1570  }
1571
1572  this.redraw_();
1573};
1574InfoBubble.prototype['removeTab'] = InfoBubble.prototype.removeTab;
1575
1576
1577/**
1578 * Get the size of an element
1579 * @private
1580 * @param {Node|string} element The element to size.
1581 * @param {number=} opt_maxWidth Optional max width of the element.
1582 * @param {number=} opt_maxHeight Optional max height of the element.
1583 * @return {google.maps.Size} The size of the element.
1584 */
1585InfoBubble.prototype.getElementSize_ = function(element, opt_maxWidth,
1586                                                opt_maxHeight) {
1587  var sizer = document.createElement('DIV');
1588  sizer.style['display'] = 'inline';
1589  sizer.style['position'] = 'absolute';
1590  sizer.style['visibility'] = 'hidden';
1591
1592  if (typeof element == 'string') {
1593    sizer.innerHTML = element;
1594  } else {
1595    sizer.appendChild(element.cloneNode(true));
1596  }
1597
1598  document.body.appendChild(sizer);
1599  var size = new google.maps.Size(sizer.offsetWidth, sizer.offsetHeight);
1600
1601  // If the width is bigger than the max width then set the width and size again
1602  if (opt_maxWidth && size.width > opt_maxWidth) {
1603    sizer.style['width'] = this.px(opt_maxWidth);
1604    size = new google.maps.Size(sizer.offsetWidth, sizer.offsetHeight);
1605  }
1606
1607  // If the height is bigger than the max height then set the height and size
1608  // again
1609  if (opt_maxHeight && size.height > opt_maxHeight) {
1610    sizer.style['height'] = this.px(opt_maxHeight);
1611    size = new google.maps.Size(sizer.offsetWidth, sizer.offsetHeight);
1612  }
1613
1614  document.body.removeChild(sizer);
1615  delete sizer;
1616  return size;
1617};
1618
1619
1620/**
1621 * Redraw the InfoBubble
1622 * @private
1623 */
1624InfoBubble.prototype.redraw_ = function() {
1625  this.figureOutSize_();
1626  this.positionCloseButton_();
1627  this.draw();
1628};
1629
1630
1631/**
1632 * Figure out the optimum size of the InfoBubble
1633 * @private
1634 */
1635InfoBubble.prototype.figureOutSize_ = function() {
1636  var map = this.get('map');
1637
1638  if (!map) {
1639    return;
1640  }
1641
1642  var padding = this.getPadding_();
1643  var borderWidth = this.getBorderWidth_();
1644  var borderRadius = this.getBorderRadius_();
1645  var arrowSize = this.getArrowSize_();
1646
1647  var mapDiv = map.getDiv();
1648  var gutter = arrowSize * 2;
1649  var mapWidth = mapDiv.offsetWidth - gutter;
1650  var mapHeight = mapDiv.offsetHeight - gutter - this.getAnchorHeight_();
1651  var tabHeight = 0;
1652  var width = /** @type {number} */ (this.get('minWidth') || 0);
1653  var height = /** @type {number} */ (this.get('minHeight') || 0);
1654  var maxWidth = /** @type {number} */ (this.get('maxWidth') || 0);
1655  var maxHeight = /** @type {number} */ (this.get('maxHeight') || 0);
1656
1657  maxWidth = Math.min(mapWidth, maxWidth);
1658  maxHeight = Math.min(mapHeight, maxHeight);
1659
1660  var tabWidth = 0;
1661  if (this.tabs_.length) {
1662    // If there are tabs then you need to check the size of each tab's content
1663    for (var i = 0, tab; tab = this.tabs_[i]; i++) {
1664      var tabSize = this.getElementSize_(tab.tab, maxWidth, maxHeight);
1665      var contentSize = this.getElementSize_(tab.content, maxWidth, maxHeight);
1666
1667      if (width < tabSize.width) {
1668        width = tabSize.width;
1669      }
1670
1671      // Add up all the tab widths because they might end up being wider than
1672      // the content
1673      tabWidth += tabSize.width;
1674
1675      if (height < tabSize.height) {
1676        height = tabSize.height;
1677      }
1678
1679      if (tabSize.height > tabHeight) {
1680        tabHeight = tabSize.height;
1681      }
1682
1683      if (width < contentSize.width) {
1684        width = contentSize.width;
1685      }
1686
1687      if (height < contentSize.height) {
1688        height = contentSize.height;
1689      }
1690    }
1691  } else {
1692    var content = /** @type {string|Node} */ (this.get('content'));
1693    if (typeof content == 'string') {
1694      content = this.htmlToDocumentFragment_(content);
1695    }
1696    if (content) {
1697      var contentSize = this.getElementSize_(content, maxWidth, maxHeight);
1698
1699      if (width < contentSize.width) {
1700        width = contentSize.width;
1701      }
1702
1703      if (height < contentSize.height) {
1704        height = contentSize.height;
1705      }
1706    }
1707  }
1708
1709  if (maxWidth) {
1710    width = Math.min(width, maxWidth);
1711  }
1712
1713  if (maxHeight) {
1714    height = Math.min(height, maxHeight);
1715  }
1716
1717  width = Math.max(width, tabWidth);
1718
1719  if (width == tabWidth) {
1720    width = width + 2 * padding;
1721  }
1722
1723  arrowSize = arrowSize * 2;
1724  width = Math.max(width, arrowSize);
1725
1726  // Maybe add this as a option so they can go bigger than the map if the user
1727  // wants
1728  if (width > mapWidth) {
1729    width = mapWidth;
1730  }
1731
1732  if (height > mapHeight) {
1733    height = mapHeight - tabHeight;
1734  }
1735
1736  if (this.tabsContainer_) {
1737    this.tabHeight_ = tabHeight;
1738    this.tabsContainer_.style['width'] = this.px(tabWidth);
1739  }
1740
1741  this.contentContainer_.style['width'] = this.px(width);
1742  this.contentContainer_.style['height'] = this.px(height);
1743};
1744
1745
1746/**
1747 *  Get the height of the anchor
1748 *
1749 *  This function is a hack for now and doesn't really work that good, need to
1750 *  wait for pixelBounds to be correctly exposed.
1751 *  @private
1752 *  @return {number} The height of the anchor.
1753 */
1754InfoBubble.prototype.getAnchorHeight_ = function() {
1755  var anchor = this.get('anchor');
1756  if (anchor) {
1757    var anchorPoint = /** @type google.maps.Point */(this.get('anchorPoint'));
1758
1759    if (anchorPoint) {
1760      return -1 * anchorPoint.y;
1761    }
1762  }
1763  return 0;
1764};
1765
1766InfoBubble.prototype.anchorPoint_changed = function() {
1767  this.draw();
1768};
1769InfoBubble.prototype['anchorPoint_changed'] = InfoBubble.prototype.anchorPoint_changed;
1770
1771
1772/**
1773 * Position the close button in the right spot.
1774 * @private
1775 */
1776InfoBubble.prototype.positionCloseButton_ = function() {
1777  var br = this.getBorderRadius_();
1778  var bw = this.getBorderWidth_();
1779
1780  var right = 2;
1781  var top = 2;
1782
1783  if (this.tabs_.length && this.tabHeight_) {
1784    top += this.tabHeight_;
1785  }
1786
1787  top += bw;
1788  right += bw;
1789
1790  var c = this.contentContainer_;
1791  if (c && c.clientHeight < c.scrollHeight) {
1792    // If there are scrollbars then move the cross in so it is not over
1793    // scrollbar
1794    right += 15;
1795  }
1796
1797  this.close_.style['right'] = this.px(right);
1798  this.close_.style['top'] = this.px(top);
1799};

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.