--- a/labs/openlayers/lib/OpenLayers/Popup.js +++ b/labs/openlayers/lib/OpenLayers/Popup.js @@ -1,1 +1,1053 @@ - +/* Copyright (c) 2006-2010 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the Clear BSD license. + * See http://svn.openlayers.org/trunk/openlayers/license.txt for the + * full text of the license. */ + + +/** + * Class: OpenLayers.Popup + * A popup is a small div that can opened and closed on the map. + * Typically opened in response to clicking on a marker. + * See <OpenLayers.Marker>. Popup's don't require their own + * layer and are added the the map using the <OpenLayers.Map.addPopup> + * method. + * + * Example: + * (code) + * popup = new OpenLayers.Popup("chicken", + * new OpenLayers.LonLat(5,40), + * new OpenLayers.Size(200,200), + * "example popup", + * true); + * + * map.addPopup(popup); + * (end) + */ +OpenLayers.Popup = OpenLayers.Class({ + + /** + * Property: events + * {<OpenLayers.Events>} custom event manager + */ + events: null, + + /** Property: id + * {String} the unique identifier assigned to this popup. + */ + id: "", + + /** + * Property: lonlat + * {<OpenLayers.LonLat>} the position of this popup on the map + */ + lonlat: null, + + /** + * Property: div + * {DOMElement} the div that contains this popup. + */ + div: null, + + /** + * Property: contentSize + * {<OpenLayers.Size>} the width and height of the content. + */ + contentSize: null, + + /** + * Property: size + * {<OpenLayers.Size>} the width and height of the popup. + */ + size: null, + + /** + * Property: contentHTML + * {String} An HTML string for this popup to display. + */ + contentHTML: null, + + /** + * Property: backgroundColor + * {String} the background color used by the popup. + */ + backgroundColor: "", + + /** + * Property: opacity + * {float} the opacity of this popup (between 0.0 and 1.0) + */ + opacity: "", + + /** + * Property: border + * {String} the border size of the popup. (eg 2px) + */ + border: "", + + /** + * Property: contentDiv + * {DOMElement} a reference to the element that holds the content of + * the div. + */ + contentDiv: null, + + /** + * Property: groupDiv + * {DOMElement} First and only child of 'div'. The group Div contains the + * 'contentDiv' and the 'closeDiv'. + */ + groupDiv: null, + + /** + * Property: closeDiv + * {DOMElement} the optional closer image + */ + closeDiv: null, + + /** + * APIProperty: autoSize + * {Boolean} Resize the popup to auto-fit the contents. + * Default is false. + */ + autoSize: false, + + /** + * APIProperty: minSize + * {<OpenLayers.Size>} Minimum size allowed for the popup's contents. + */ + minSize: null, + + /** + * APIProperty: maxSize + * {<OpenLayers.Size>} Maximum size allowed for the popup's contents. + */ + maxSize: null, + + /** + * Property: displayClass + * {String} The CSS class of the popup. + */ + displayClass: "olPopup", + + /** + * Property: contentDisplayClass + * {String} The CSS class of the popup content div. + */ + contentDisplayClass: "olPopupContent", + + /** + * Property: padding + * {int or <OpenLayers.Bounds>} An extra opportunity to specify internal + * padding of the content div inside the popup. This was originally + * confused with the css padding as specified in style.css's + * 'olPopupContent' class. We would like to get rid of this altogether, + * except that it does come in handy for the framed and anchoredbubble + * popups, who need to maintain yet another barrier between their + * content and the outer border of the popup itself. + * + * Note that in order to not break API, we must continue to support + * this property being set as an integer. Really, though, we'd like to + * have this specified as a Bounds object so that user can specify + * distinct left, top, right, bottom paddings. With the 3.0 release + * we can make this only a bounds. + */ + padding: 0, + + /** + * Property: disableFirefoxOverflowHack + * {Boolean} The hack for overflow in Firefox causes all elements + * to be re-drawn, which causes Flash elements to be + * re-initialized, which is troublesome. + * With this property the hack can be disabled. + */ + disableFirefoxOverflowHack: false, + + /** + * Method: fixPadding + * To be removed in 3.0, this function merely helps us to deal with the + * case where the user may have set an integer value for padding, + * instead of an <OpenLayers.Bounds> object. + */ + fixPadding: function() { + if (typeof this.padding == "number") { + this.padding = new OpenLayers.Bounds( + this.padding, this.padding, this.padding, this.padding + ); + } + }, + + /** + * APIProperty: panMapIfOutOfView + * {Boolean} When drawn, pan map such that the entire popup is visible in + * the current viewport (if necessary). + * Default is false. + */ + panMapIfOutOfView: false, + + /** + * APIProperty: keepInMap + * {Boolean} If panMapIfOutOfView is false, and this property is true, + * contrain the popup such that it always fits in the available map + * space. By default, this is not set on the base class. If you are + * creating popups that are near map edges and not allowing pannning, + * and especially if you have a popup which has a + * fixedRelativePosition, setting this to false may be a smart thing to + * do. Subclasses may want to override this setting. + * + * Default is false. + */ + keepInMap: false, + + /** + * APIProperty: closeOnMove + * {Boolean} When map pans, close the popup. + * Default is false. + */ + closeOnMove: false, + + /** + * Property: map + * {<OpenLayers.Map>} this gets set in Map.js when the popup is added to the map + */ + map: null, + + /** + * Constructor: OpenLayers.Popup + * Create a popup. + * + * Parameters: + * id - {String} a unqiue identifier for this popup. If null is passed + * an identifier will be automatically generated. + * lonlat - {<OpenLayers.LonLat>} The position on the map the popup will + * be shown. + * contentSize - {<OpenLayers.Size>} The size of the content. + * contentHTML - {String} An HTML string to display inside the + * popup. + * closeBox - {Boolean} Whether to display a close box inside + * the popup. + * closeBoxCallback - {Function} Function to be called on closeBox click. + */ + initialize:function(id, lonlat, contentSize, contentHTML, closeBox, closeBoxCallback) { + if (id == null) { + id = OpenLayers.Util.createUniqueID(this.CLASS_NAME + "_"); + } + + this.id = id; + this.lonlat = lonlat; + + this.contentSize = (contentSize != null) ? contentSize + : new OpenLayers.Size( + OpenLayers.Popup.WIDTH, + OpenLayers.Popup.HEIGHT); + if (contentHTML != null) { + this.contentHTML = contentHTML; + } + this.backgroundColor = OpenLayers.Popup.COLOR; + this.opacity = OpenLayers.Popup.OPACITY; + this.border = OpenLayers.Popup.BORDER; + + this.div = OpenLayers.Util.createDiv(this.id, null, null, + null, null, null, "hidden"); + this.div.className = this.displayClass; + + var groupDivId = this.id + "_GroupDiv"; + this.groupDiv = OpenLayers.Util.createDiv(groupDivId, null, null, + null, "relative", null, + "hidden"); + + var id = this.div.id + "_contentDiv"; + this.contentDiv = OpenLayers.Util.createDiv(id, null, this.contentSize.clone(), + null, "relative"); + this.contentDiv.className = this.contentDisplayClass; + this.groupDiv.appendChild(this.contentDiv); + this.div.appendChild(this.groupDiv); + + if (closeBox) { + this.addCloseBox(closeBoxCallback); + } + + this.registerEvents(); + }, + + /** + * Method: destroy + * nullify references to prevent circular references and memory leaks + */ + destroy: function() { + + this.id = null; + this.lonlat = null; + this.size = null; + this.contentHTML = null; + + this.backgroundColor = null; + this.opacity = null; + this.border = null; + + if (this.closeOnMove && this.map) { + this.map.events.unregister("movestart", this, this.hide); + } + + this.events.destroy(); + this.events = null; + + if (this.closeDiv) { + OpenLayers.Event.stopObservingElement(this.closeDiv); + this.groupDiv.removeChild(this.closeDiv); + } + this.closeDiv = null; + + this.div.removeChild(this.groupDiv); + this.groupDiv = null; + + if (this.map != null) { + this.map.removePopup(this); + } + this.map = null; + this.div = null; + + this.autoSize = null; + this.minSize = null; + this.maxSize = null; + this.padding = null; + this.panMapIfOutOfView = null; + }, + + /** + * Method: draw + * Constructs the elements that make up the popup. + * + * Parameters: + * px - {<OpenLayers.Pixel>} the position the popup in pixels. + * + * Returns: + * {DOMElement} Reference to a div that contains the drawn popup + */ + draw: function(px) { + if (px == null) { + if ((this.lonlat != null) && (this.map != null)) { + px = this.map.getLayerPxFromLonLat(this.lonlat); + } + } + + // this assumes that this.map already exists, which is okay because + // this.draw is only called once the popup has been added to the map. + if (this.closeOnMove) { + this.map.events.register("movestart", this, this.hide); + } + + //listen to movestart, moveend to disable overflow (FF bug) + if (!this.disableFirefoxOverflowHack && OpenLayers.Util.getBrowserName() == 'firefox') { + this.map.events.register("movestart", this, function() { + var style = document.defaultView.getComputedStyle( + this.contentDiv, null + ); + var currentOverflow = style.getPropertyValue("overflow"); + if (currentOverflow != "hidden") { + this.contentDiv._oldOverflow = currentOverflow; + this.contentDiv.style.overflow = "hidden"; + } + }); + this.map.events.register("moveend", this, function() { + var oldOverflow = this.contentDiv._oldOverflow; + if (oldOverflow) { + this.contentDiv.style.overflow = oldOverflow; + this.contentDiv._oldOverflow = null; + } + }); + } + + this.moveTo(px); + if (!this.autoSize && !this.size) { + this.setSize(this.contentSize); + } + this.setBackgroundColor(); + this.setOpacity(); + this.setBorder(); + this.setContentHTML(); + + if (this.panMapIfOutOfView) { + this.panIntoView(); + } + + return this.div; + }, + + /** + * Method: updatePosition + * if the popup has a lonlat and its map members set, + * then have it move itself to its proper position + */ + updatePosition: function() { + if ((this.lonlat) && (this.map)) { + var px = this.map.getLayerPxFromLonLat(this.lonlat); + if (px) { + this.moveTo(px); + } + } + }, + + /** + * Method: moveTo + * + * Parameters: + * px - {<OpenLayers.Pixel>} the top and left position of the popup div. + */ + moveTo: function(px) { + if ((px != null) && (this.div != null)) { + this.div.style.left = px.x + "px"; + this.div.style.top = px.y + "px"; + } + }, + + /** + * Method: visible + * + * Returns: + * {Boolean} Boolean indicating whether or not the popup is visible + */ + visible: function() { + return OpenLayers.Element.visible(this.div); + }, + + /** + * Method: toggle + * Toggles visibility of the popup. + */ + toggle: function() { + if (this.visible()) { + this.hide(); + } else { + this.show(); + } + }, + + /** + * Method: show + * Makes the popup visible. + */ + show: function() { + OpenLayers.Element.show(this.div); + + if (this.panMapIfOutOfView) { + this.panIntoView(); + } + }, + + /** + * Method: hide + * Makes the popup invisible. + */ + hide: function() { + OpenLayers.Element.hide(this.div); + }, + + /** + * Method: setSize + * Used to adjust the size of the popup. + * + * Parameters: + * contentSize - {<OpenLayers.Size>} the new size for the popup's + * contents div (in pixels). + */ + setSize:function(contentSize) { + this.size = contentSize.clone(); + + // if our contentDiv has a css 'padding' set on it by a stylesheet, we + // must add that to the desired "size". + var contentDivPadding = this.getContentDivPadding(); + var wPadding = contentDivPadding.left + contentDivPadding.right; + var hPadding = contentDivPadding.top + contentDivPadding.bottom; + + // take into account the popup's 'padding' property + this.fixPadding(); + wPadding += this.padding.left + this.padding.right; + hPadding += this.padding.top + this.padding.bottom; + + // make extra space for the close div + if (this.closeDiv) { + var closeDivWidth = parseInt(this.closeDiv.style.width); + wPadding += closeDivWidth + contentDivPadding.right; + } + + //increase size of the main popup div to take into account the + // users's desired padding and close div. + this.size.w += wPadding; + this.size.h += hPadding; + + //now if our browser is IE, we need to actually make the contents + // div itself bigger to take its own padding into effect. this makes + // me want to shoot someone, but so it goes. + if (OpenLayers.Util.getBrowserName() == "msie") { + this.contentSize.w += + contentDivPadding.left + contentDivPadding.right; + this.contentSize.h += + contentDivPadding.bottom + contentDivPadding.top; + } + + if (this.div != null) { + this.div.style.width = this.size.w + "px"; + this.div.style.height = this.size.h + "px"; + } + if (this.contentDiv != null){ + this.contentDiv.style.width = contentSize.w + "px"; + this.contentDiv.style.height = contentSize.h + "px"; + } + }, + + /** + * APIMethod: updateSize + * Auto size the popup so that it precisely fits its contents (as + * determined by this.contentDiv.innerHTML). Popup size will, of + * course, be limited by the available space on the current map + */ + updateSize: function() { + + // determine actual render dimensions of the contents by putting its + // contents into a fake contentDiv (for the CSS) and then measuring it + var preparedHTML = "<div class='" + this.contentDisplayClass+ "'>" + + this.contentDiv.innerHTML + + "</div>"; + + var containerElement = (this.map) ? this.map.layerContainerDiv + : document.body; + var realSize = OpenLayers.Util.getRenderedDimensions( + preparedHTML, null, { + displayClass: this.displayClass, + containerElement: containerElement + } + ); + + // is the "real" size of the div is safe to display in our map? + var safeSize = this.getSafeContentSize(realSize); + + var newSize = null; + if (safeSize.equals(realSize)) { + //real size of content is small enough to fit on the map, + // so we use real size. + newSize = realSize; + + } else { + + //make a new OL.Size object with the clipped dimensions + // set or null if not clipped. + var fixedSize = new OpenLayers.Size(); + fixedSize.w = (safeSize.w < realSize.w) ? safeSize.w : null; + fixedSize.h = (safeSize.h < realSize.h) ? safeSize.h : null; + + if (fixedSize.w && fixedSize.h) { + //content is too big in both directions, so we will use + // max popup size (safeSize), knowing well that it will + // overflow both ways. + newSize = safeSize; + } else { + //content is clipped in only one direction, so we need to + // run getRenderedDimensions() again with a fixed dimension + var clippedSize = OpenLayers.Util.getRenderedDimensions( + preparedHTML, fixedSize, { + displayClass: this.contentDisplayClass, + containerElement: containerElement + } + ); + + //if the clipped size is still the same as the safeSize, + // that means that our content must be fixed in the + // offending direction. If overflow is 'auto', this means + // we are going to have a scrollbar for sure, so we must + // adjust for that. + // + var currentOverflow = OpenLayers.Element.getStyle( + this.contentDiv, "overflow" + ); + if ( (currentOverflow != "hidden") && + (clippedSize.equals(safeSize)) ) { + var scrollBar = OpenLayers.Util.getScrollbarWidth(); + if (fixedSize.w) { + clippedSize.h += scrollBar; + } else { + clippedSize.w += scrollBar; + } + } + + newSize = this.getSafeContentSize(clippedSize); + } + } + this.setSize(newSize); + }, + + /** + * Method: setBackgroundColor + * Sets the background color of the popup. + * + * Parameters: + * color - {String} the background color. eg "#FFBBBB" + */ + setBackgroundColor:function(color) { + if (color != undefined) { + this.backgroundColor = color; + } + + if (this.div != null) { + this.div.style.backgroundColor = this.backgroundColor; + } + }, + + /** + * Method: setOpacity + * Sets the opacity of the popup. + * + * Parameters: + * opacity - {float} A value between 0.0 (transparent) and 1.0 (solid). + */ + setOpacity:function(opacity) { + if (opacity != undefined) { + this.opacity = opacity; + } + + if (this.div != null) { + // for Mozilla and Safari + this.div.style.opacity = this.opacity; + + // for IE + this.div.style.filter = 'alpha(opacity=' + this.opacity*100 + ')'; + } + }, + + /** + * Method: setBorder + * Sets the border style of the popup. + * + * Parameters: + * border - {String} The border style value. eg 2px + */ + setBorder:function(border) { + if (border != undefined) { + this.border = border; + } + + if (this.div != null) { + this.div.style.border = this.border; + } + }, + + /** + * Method: setContentHTML + * Allows the user to set the HTML content of the popup. + * + * Parameters: + * contentHTML - {String} HTML for the div. + */ + setContentHTML:function(contentHTML) { + + if (contentHTML != null) { + this.contentHTML = contentHTML; + } + + if ((this.contentDiv != null) && + (this.contentHTML != null) && + (this.contentHTML != this.contentDiv.innerHTML)) { + + this.contentDiv.innerHTML = this.contentHTML; + + if (this.autoSize) { + + //if popup has images, listen for when they finish + // loading and resize accordingly + this.registerImageListeners(); + + //auto size the popup to its current contents + this.updateSize(); + } + } + + }, + + /** + * Method: registerImageListeners + * Called when an image contained by the popup loaded. this function + * updates the popup size, then unregisters the image load listener. + */ + registerImageListeners: function() { + + // As the images load, this function will call updateSize() to + // resize the popup to fit the content div (which presumably is now + // bigger than when the image was not loaded). + // + // If the 'panMapIfOutOfView' property is set, we will pan the newly + // resized popup back into view. + // + // Note that this function, when called, will have 'popup' and + // 'img' properties in the context. + // + var onImgLoad = function() { + + this.popup.updateSize(); + + if ( this.popup.visible() && this.popup.panMapIfOutOfView ) { + this.popup.panIntoView(); + } + + OpenLayers.Event.stopObserving( + this.img, "load", this.img._onImageLoad + ); + + }; + + //cycle through the images and if their size is 0x0, that means that + // they haven't been loaded yet, so we attach the listener, which + // will fire when the images finish loading and will resize the + // popup accordingly to its new size. + var images = this.contentDiv.getElementsByTagName("img"); + for (var i = 0, len = images.length; i < len; i++) { + var img = images[i]; + if (img.width == 0 || img.height == 0) { + + var context = { + 'popup': this, + 'img': img + }; + + //expando this function to the image itself before registering + // it. This way we can easily and properly unregister it. + img._onImgLoad = OpenLayers.Function.bind(onImgLoad, context); + + OpenLayers.Event.observe(img, 'load', img._onImgLoad); + } + } + }, + + /** + * APIMethod: getSafeContentSize + * + * Parameters: + * size - {<OpenLayers.Size>} Desired size to make the popup. + * + * Returns: + * {<OpenLayers.Size>} A size to make the popup which is neither smaller + * than the specified minimum size, nor bigger than the maximum + * size (which is calculated relative to the size of the viewport). + */ + getSafeContentSize: function(size) { + + var safeContentSize = size.clone(); + + // if our contentDiv has a css 'padding' set on it by a stylesheet, we + // must add that to the desired "size". + var contentDivPadding = this.getContentDivPadding(); + var wPadding = contentDivPadding.left + contentDivPadding.right; + var hPadding = contentDivPadding.top + contentDivPadding.bottom; + + // take into account the popup's 'padding' property + this.fixPadding(); + wPadding += this.padding.left + this.padding.right; + hPadding += this.padding.top + this.padding.bottom; + + if (this.closeDiv) { + var closeDivWidth = parseInt(this.closeDiv.style.width); + wPadding += closeDivWidth + contentDivPadding.right; + } + + // prevent the popup from being smaller than a specified minimal size + if (this.minSize) { + safeContentSize.w = Math.max(safeContentSize.w, + (this.minSize.w - wPadding)); + safeContentSize.h = Math.max(safeContentSize.h, + (this.minSize.h - hPadding)); + } + + // prevent the popup from being bigger than a specified maximum size + if (this.maxSize) { + safeContentSize.w = Math.min(safeContentSize.w, + (this.maxSize.w - wPadding)); + safeContentSize.h = Math.min(safeContentSize.h, + (this.maxSize.h - hPadding)); + } + + //make sure the desired size to set doesn't result in a popup that + // is bigger than the map's viewport. + // + if (this.map && this.map.size) { + + var extraX = 0, extraY = 0; + if (this.keepInMap && !this.panMapIfOutOfView) { + var px = this.map.getPixelFromLonLat(this.lonlat); + switch (this.relativePosition) { + case "tr": + extraX = px.x; + extraY = this.map.size.h - px.y; + break; + case "tl": + extraX = this.map.size.w - px.x; + extraY = this.map.size.h - px.y; + break; + case "bl": + extraX = this.map.size.w - px.x; + extraY = px.y; + break; + case "br": + extraX = px.x; + extraY = px.y; + break; + default: + extraX = px.x; + extraY = this.map.size.h - px.y; + break; + } + } + + var maxY = this.map.size.h - + this.map.paddingForPopups.top - + this.map.paddingForPopups.bottom - + hPadding - extraY; + + var maxX = this.map.size.w - + this.map.paddingForPopups.left - + this.map.paddingForPopups.right - + wPadding - extraX; + + safeContentSize.w = Math.min(safeContentSize.w, maxX); + safeContentSize.h = Math.min(safeContentSize.h, maxY); + } + + return safeContentSize; + }, + + /** + * Method: getContentDivPadding + * Glorious, oh glorious hack in order to determine the css 'padding' of + * the contentDiv. IE/Opera return null here unless we actually add the + * popup's main 'div' element (which contains contentDiv) to the DOM. + * So we make it invisible and then add it to the document temporarily. + * + * Once we've taken the padding readings we need, we then remove it + * from the DOM (it will actually get added to the DOM in + * Map.js's addPopup) + * + * Returns: + * {<OpenLayers.Bounds>} + */ + getContentDivPadding: function() { + + //use cached value if we have it + var contentDivPadding = this._contentDivPadding; + if (!contentDivPadding) { + + if (this.div.parentNode == null) { + //make the div invisible and add it to the page + this.div.style.display = "none"; + document.body.appendChild(this.div); + } + + //read the padding settings from css, put them in an OL.Bounds + contentDivPadding = new OpenLayers.Bounds( + OpenLayers.Element.getStyle(this.contentDiv, "padding-left"), + OpenLayers.Element.getStyle(this.contentDiv, "padding-bottom"), + OpenLayers.Element.getStyle(this.contentDiv, "padding-right"), + OpenLayers.Element.getStyle(this.contentDiv, "padding-top") + ); + + //cache the value + this._contentDivPadding = contentDivPadding; + + if (this.div.parentNode == document.body) { + //remove the div from the page and make it visible again + document.body.removeChild(this.div); + this.div.style.display = ""; + } + } + return contentDivPadding; + }, + + /** + * Method: addCloseBox + * + * Parameters: + * callback - {Function} The callback to be called when the close button + * is clicked. + */ + addCloseBox: function(callback) { + + this.closeDiv = OpenLayers.Util.createDiv( + this.id + "_close", null, new OpenLayers.Size(17, 17) + ); + this.closeDiv.className = "olPopupCloseBox"; + + // use the content div's css padding to determine if we should + // padd the close div + var contentDivPadding = this.getContentDivPadding(); + + this.closeDiv.style.right = contentDivPadding.right + "px"; + this.closeDiv.style.top = contentDivPadding.top + "px"; + this.groupDiv.appendChild(this.closeDiv); + <