/** * @name InfoBox * @version 1.1.9 [October 2, 2011] * @author Gary Little (inspired by proof-of-concept code from Pamela Fox of Google) * @copyright Copyright 2010 Gary Little [gary at luxcentral.com] * @fileoverview InfoBox extends the Google Maps JavaScript API V3 OverlayView class. *

* An InfoBox behaves like a google.maps.InfoWindow, but it supports several * additional properties for advanced styling. An InfoBox can also be used as a map label. *

* An InfoBox also fires the same events as a google.maps.InfoWindow. *

* Browsers tested: *

* Mac -- Safari (4.0.4), Firefox (3.6), Opera (10.10), Chrome (4.0.249.43), OmniWeb (5.10.1) *
* Win -- Safari, Firefox, Opera, Chrome (3.0.195.38), Internet Explorer (8.0.6001.18702) *
* iPod Touch/iPhone -- Safari (3.1.2) */ /*! * * 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. */ /*jslint browser:true */ /*global google */ /** * @name InfoBoxOptions * @class This class represents the optional parameter passed to the {@link InfoBox} constructor. * @property {string|Node} content The content of the InfoBox (plain text or an HTML DOM node). * @property {boolean} disableAutoPan Disable auto-pan on open (default is false). * @property {number} maxWidth The maximum width (in pixels) of the InfoBox. Set to 0 if no maximum. * @property {Size} pixelOffset The offset (in pixels) from the top left corner of the InfoBox * (or the bottom left corner if the alignBottom property is true) * to the map pixel corresponding to position. * @property {LatLng} position The geographic location at which to display the InfoBox. * @property {number} zIndex The CSS z-index style value for the InfoBox. * Note: This value overrides a zIndex setting specified in the boxStyle property. * @property {string} boxClass The name of the CSS class defining the styles for the InfoBox container. * The default name is infoBox. * @property {Object} [boxStyle] An object literal whose properties define specific CSS * style values to be applied to the InfoBox. Style values defined here override those that may * be defined in the boxClass style sheet. If this property is changed after the * InfoBox has been created, all previously set styles (except those defined in the style sheet) * are removed from the InfoBox before the new style values are applied. * @property {string} closeBoxMargin The CSS margin style value for the close box. * The default is "2px" (a 2-pixel margin on all sides). * @property {string} closeBoxURL The URL of the image representing the close box. * Note: The default is the URL for Google's standard close box. * Set this property to "" if no close box is required. * @property {Size} infoBoxClearance Minimum offset (in pixels) from the InfoBox to the * map edge after an auto-pan. * @property {boolean} isHidden Hide the InfoBox on open (default is false). * @property {boolean} alignBottom Align the bottom left corner of the InfoBox to the position * location (default is false which means that the top left corner of the InfoBox is aligned). * @property {string} pane The pane where the InfoBox is to appear (default is "floatPane"). * Set the pane to "mapPane" if the InfoBox is being used as a map label. * Valid pane names are the property names for the google.maps.MapPanes object. * @property {boolean} enableEventPropagation Propagate mousedown, click, dblclick, * and contextmenu events in the InfoBox (default is false to mimic the behavior * of a google.maps.InfoWindow). Set this property to true if the InfoBox * is being used as a map label. iPhone note: This property setting has no effect; events are * always propagated. */ /** * Creates an InfoBox with the options specified in {@link InfoBoxOptions}. * Call InfoBox.open to add the box to the map. * @constructor * @param {InfoBoxOptions} [opt_opts] */ function InfoBox(opt_opts) { opt_opts = opt_opts || {}; google.maps.OverlayView.apply(this, arguments); // Standard options (in common with google.maps.InfoWindow): // this.content_ = opt_opts.content || ""; this.disableAutoPan_ = opt_opts.disableAutoPan || false; this.maxWidth_ = opt_opts.maxWidth || 0; this.pixelOffset_ = opt_opts.pixelOffset || new google.maps.Size(0, 0); this.position_ = opt_opts.position || new google.maps.LatLng(0, 0); this.zIndex_ = opt_opts.zIndex || null; // Additional options (unique to InfoBox): // this.boxClass_ = opt_opts.boxClass || "infoBox"; this.boxStyle_ = opt_opts.boxStyle || {}; this.closeBoxMargin_ = opt_opts.closeBoxMargin || "2px"; this.closeBoxURL_ = opt_opts.closeBoxURL || "http://www.google.com/intl/en_us/mapfiles/close.gif"; if (opt_opts.closeBoxURL === "") { this.closeBoxURL_ = ""; } this.infoBoxClearance_ = opt_opts.infoBoxClearance || new google.maps.Size(1, 1); this.isHidden_ = opt_opts.isHidden || false; this.alignBottom_ = opt_opts.alignBottom || false; this.pane_ = opt_opts.pane || "floatPane"; this.enableEventPropagation_ = opt_opts.enableEventPropagation || false; this.div_ = null; this.closeListener_ = null; this.eventListener1_ = null; this.eventListener2_ = null; this.eventListener3_ = null; this.moveListener_ = null; this.contextListener_ = null; this.fixedWidthSet_ = null; } /* InfoBox extends OverlayView in the Google Maps API v3. */ InfoBox.prototype = new google.maps.OverlayView(); /** * Creates the DIV representing the InfoBox. * @private */ InfoBox.prototype.createInfoBoxDiv_ = function () { var bw; var me = this; // This handler prevents an event in the InfoBox from being passed on to the map. // var cancelHandler = function (e) { e.cancelBubble = true; if (e.stopPropagation) { e.stopPropagation(); } }; // This handler ignores the current event in the InfoBox and conditionally prevents // the event from being passed on to the map. It is used for the contextmenu event. // var ignoreHandler = function (e) { e.returnValue = false; if (e.preventDefault) { e.preventDefault(); } if (!me.enableEventPropagation_) { cancelHandler(e); } }; if (!this.div_) { this.div_ = document.createElement("div"); this.setBoxStyle_(); if (typeof this.content_.nodeType === "undefined") { this.div_.innerHTML = this.getCloseBoxImg_() + this.content_; } else { this.div_.innerHTML = this.getCloseBoxImg_(); this.div_.appendChild(this.content_); } // Add the InfoBox DIV to the DOM this.getPanes()[this.pane_].appendChild(this.div_); this.addClickHandler_(); if (this.div_.style.width) { this.fixedWidthSet_ = true; } else { if (this.maxWidth_ !== 0 && this.div_.offsetWidth > this.maxWidth_) { this.div_.style.width = this.maxWidth_; this.div_.style.overflow = "auto"; this.fixedWidthSet_ = true; } else { // The following code is needed to overcome problems with MSIE bw = this.getBoxWidths_(); this.div_.style.width = (this.div_.offsetWidth - bw.left - bw.right) + "px"; this.fixedWidthSet_ = false; } } this.panBox_(this.disableAutoPan_); if (!this.enableEventPropagation_) { // Cancel event propagation. // this.eventListener1_ = google.maps.event.addDomListener(this.div_, "mousedown", cancelHandler); this.eventListener2_ = google.maps.event.addDomListener(this.div_, "click", cancelHandler); this.eventListener3_ = google.maps.event.addDomListener(this.div_, "dblclick", cancelHandler); this.eventListener4_ = google.maps.event.addDomListener(this.div_, "mouseover", function (e) { this.style.cursor = "default"; }); } this.contextListener_ = google.maps.event.addDomListener(this.div_, "contextmenu", ignoreHandler); /** * This event is fired when the DIV containing the InfoBox's content is attached to the DOM. * @name InfoBox#domready * @event */ google.maps.event.trigger(this, "domready"); } }; /** * Returns the HTML tag for the close box. * @private */ InfoBox.prototype.getCloseBoxImg_ = function () { var img = ""; if (this.closeBoxURL_ !== "") { img = " mapWidth) { xOffset = pixPosition.x + iwWidth + iwOffsetX + padX - mapWidth; } if (this.alignBottom_) { if (pixPosition.y < (-iwOffsetY + padY + iwHeight)) { yOffset = pixPosition.y + iwOffsetY - padY - iwHeight; } else if ((pixPosition.y + iwOffsetY + padY) > mapHeight) { yOffset = pixPosition.y + iwOffsetY + padY - mapHeight; } } else { if (pixPosition.y < (-iwOffsetY + padY)) { yOffset = pixPosition.y + iwOffsetY - padY; } else if ((pixPosition.y + iwHeight + iwOffsetY + padY) > mapHeight) { yOffset = pixPosition.y + iwHeight + iwOffsetY + padY - mapHeight; } } if (!(xOffset === 0 && yOffset === 0)) { // Move the map to the shifted center. // var c = map.getCenter(); map.panBy(xOffset, yOffset); } } } }; /** * Sets the style of the InfoBox by setting the style sheet and applying * other specific styles requested. * @private */ InfoBox.prototype.setBoxStyle_ = function () { var i, boxStyle; if (this.div_) { // Apply style values from the style sheet defined in the boxClass parameter: this.div_.className = this.boxClass_; // Clear existing inline style values: this.div_.style.cssText = ""; // Apply style values defined in the boxStyle parameter: boxStyle = this.boxStyle_; for (i in boxStyle) { if (boxStyle.hasOwnProperty(i)) { this.div_.style[i] = boxStyle[i]; } } // Fix up opacity style for benefit of MSIE: // if (typeof this.div_.style.opacity !== "undefined" && this.div_.style.opacity !== "") { this.div_.style.filter = "alpha(opacity=" + (this.div_.style.opacity * 100) + ")"; } // Apply required styles: // this.div_.style.position = "absolute"; this.div_.style.visibility = 'hidden'; if (this.zIndex_ !== null) { this.div_.style.zIndex = this.zIndex_; } } }; /** * Get the widths of the borders of the InfoBox. * @private * @return {Object} widths object (top, bottom left, right) */ InfoBox.prototype.getBoxWidths_ = function () { var computedStyle; var bw = {top: 0, bottom: 0, left: 0, right: 0}; var box = this.div_; if (document.defaultView && document.defaultView.getComputedStyle) { computedStyle = box.ownerDocument.defaultView.getComputedStyle(box, ""); if (computedStyle) { // The computed styles are always in pixel units (good!) bw.top = parseInt(computedStyle.borderTopWidth, 10) || 0; bw.bottom = parseInt(computedStyle.borderBottomWidth, 10) || 0; bw.left = parseInt(computedStyle.borderLeftWidth, 10) || 0; bw.right = parseInt(computedStyle.borderRightWidth, 10) || 0; } } else if (document.documentElement.currentStyle) { // MSIE if (box.currentStyle) { // The current styles may not be in pixel units, but assume they are (bad!) bw.top = parseInt(box.currentStyle.borderTopWidth, 10) || 0; bw.bottom = parseInt(box.currentStyle.borderBottomWidth, 10) || 0; bw.left = parseInt(box.currentStyle.borderLeftWidth, 10) || 0; bw.right = parseInt(box.currentStyle.borderRightWidth, 10) || 0; } } return bw; }; /** * Invoked when close is called. Do not call it directly. */ InfoBox.prototype.onRemove = function () { if (this.div_) { this.div_.parentNode.removeChild(this.div_); this.div_ = null; } }; /** * Draws the InfoBox based on the current map projection and zoom level. */ InfoBox.prototype.draw = function () { this.createInfoBoxDiv_(); var pixPosition = this.getProjection().fromLatLngToDivPixel(this.position_); this.div_.style.left = (pixPosition.x + this.pixelOffset_.width) + "px"; if (this.alignBottom_) { this.div_.style.bottom = -(pixPosition.y + this.pixelOffset_.height) + "px"; } else { this.div_.style.top = (pixPosition.y + this.pixelOffset_.height) + "px"; } if (this.isHidden_) { this.div_.style.visibility = 'hidden'; } else { this.div_.style.visibility = "visible"; } }; /** * Sets the options for the InfoBox. Note that changes to the maxWidth, * closeBoxMargin, closeBoxURL, and enableEventPropagation * properties have no affect until the current InfoBox is closed and a new one * is opened. * @param {InfoBoxOptions} opt_opts */ InfoBox.prototype.setOptions = function (opt_opts) { if (typeof opt_opts.boxClass !== "undefined") { // Must be first this.boxClass_ = opt_opts.boxClass; this.setBoxStyle_(); } if (typeof opt_opts.boxStyle !== "undefined") { // Must be second this.boxStyle_ = opt_opts.boxStyle; this.setBoxStyle_(); } if (typeof opt_opts.content !== "undefined") { this.setContent(opt_opts.content); } if (typeof opt_opts.disableAutoPan !== "undefined") { this.disableAutoPan_ = opt_opts.disableAutoPan; } if (typeof opt_opts.maxWidth !== "undefined") { this.maxWidth_ = opt_opts.maxWidth; } if (typeof opt_opts.pixelOffset !== "undefined") { this.pixelOffset_ = opt_opts.pixelOffset; } if (typeof opt_opts.alignBottom !== "undefined") { this.alignBottom_ = opt_opts.alignBottom; } if (typeof opt_opts.position !== "undefined") { this.setPosition(opt_opts.position); } if (typeof opt_opts.zIndex !== "undefined") { this.setZIndex(opt_opts.zIndex); } if (typeof opt_opts.closeBoxMargin !== "undefined") { this.closeBoxMargin_ = opt_opts.closeBoxMargin; } if (typeof opt_opts.closeBoxURL !== "undefined") { this.closeBoxURL_ = opt_opts.closeBoxURL; } if (typeof opt_opts.infoBoxClearance !== "undefined") { this.infoBoxClearance_ = opt_opts.infoBoxClearance; } if (typeof opt_opts.isHidden !== "undefined") { this.isHidden_ = opt_opts.isHidden; } if (typeof opt_opts.enableEventPropagation !== "undefined") { this.enableEventPropagation_ = opt_opts.enableEventPropagation; } if (this.div_) { this.draw(); } }; /** * Sets the content of the InfoBox. * The content can be plain text or an HTML DOM node. * @param {string|Node} content */ InfoBox.prototype.setContent = function (content) { this.content_ = content; if (this.div_) { if (this.closeListener_) { google.maps.event.removeListener(this.closeListener_); this.closeListener_ = null; } // Odd code required to make things work with MSIE. // if (!this.fixedWidthSet_) { this.div_.style.width = ""; } if (typeof content.nodeType === "undefined") { this.div_.innerHTML = this.getCloseBoxImg_() + content; } else { this.div_.innerHTML = this.getCloseBoxImg_(); this.div_.appendChild(content); } // Perverse code required to make things work with MSIE. // (Ensures the close box does, in fact, float to the right.) // if (!this.fixedWidthSet_) { this.div_.style.width = this.div_.offsetWidth + "px"; if (typeof content.nodeType === "undefined") { this.div_.innerHTML = this.getCloseBoxImg_() + content; } else { this.div_.innerHTML = this.getCloseBoxImg_(); this.div_.appendChild(content); } } this.addClickHandler_(); } /** * This event is fired when the content of the InfoBox changes. * @name InfoBox#content_changed * @event */ google.maps.event.trigger(this, "content_changed"); }; /** * Sets the geographic location of the InfoBox. * @param {LatLng} latlng */ InfoBox.prototype.setPosition = function (latlng) { this.position_ = latlng; if (this.div_) { this.draw(); } /** * This event is fired when the position of the InfoBox changes. * @name InfoBox#position_changed * @event */ google.maps.event.trigger(this, "position_changed"); }; /** * Sets the zIndex style for the InfoBox. * @param {number} index */ InfoBox.prototype.setZIndex = function (index) { this.zIndex_ = index; if (this.div_) { this.div_.style.zIndex = index; } /** * This event is fired when the zIndex of the InfoBox changes. * @name InfoBox#zindex_changed * @event */ google.maps.event.trigger(this, "zindex_changed"); }; /** * Returns the content of the InfoBox. * @returns {string} */ InfoBox.prototype.getContent = function () { return this.content_; }; /** * Returns the geographic location of the InfoBox. * @returns {LatLng} */ InfoBox.prototype.getPosition = function () { return this.position_; }; /** * Returns the zIndex for the InfoBox. * @returns {number} */ InfoBox.prototype.getZIndex = function () { return this.zIndex_; }; /** * Shows the InfoBox. */ InfoBox.prototype.show = function () { this.isHidden_ = false; if (this.div_) { this.div_.style.visibility = "visible"; } }; /** * Hides the InfoBox. */ InfoBox.prototype.hide = function () { this.isHidden_ = true; if (this.div_) { this.div_.style.visibility = "hidden"; } }; /** * Adds the InfoBox to the specified map or Street View panorama. If anchor * (usually a google.maps.Marker) is specified, the position * of the InfoBox is set to the position of the anchor. If the * anchor is dragged to a new location, the InfoBox moves as well. * @param {Map|StreetViewPanorama} map * @param {MVCObject} [anchor] */ InfoBox.prototype.open = function (map, anchor) { var me = this; if (anchor) { this.position_ = anchor.getPosition(); this.moveListener_ = google.maps.event.addListener(anchor, "position_changed", function () { me.setPosition(this.getPosition()); }); } this.setMap(map); if (this.div_) { this.panBox_(); } }; /** * Removes the InfoBox from the map. */ InfoBox.prototype.close = function () { if (this.closeListener_) { google.maps.event.removeListener(this.closeListener_); this.closeListener_ = null; } if (this.eventListener1_) { google.maps.event.removeListener(this.eventListener1_); google.maps.event.removeListener(this.eventListener2_); google.maps.event.removeListener(this.eventListener3_); google.maps.event.removeListener(this.eventListener4_); this.eventListener1_ = null; this.eventListener2_ = null; this.eventListener3_ = null; this.eventListener4_ = null; } if (this.moveListener_) { google.maps.event.removeListener(this.moveListener_); this.moveListener_ = null; } if (this.contextListener_) { google.maps.event.removeListener(this.contextListener_); this.contextListener_ = null; } this.setMap(null); }; ; /** * @name MarkerClustererPlus for Google Maps V3 * @version 2.1.1 [November 4, 2013] * @author Gary Little * @fileoverview * The library creates and manages per-zoom-level clusters for large amounts of markers. *

* This is an enhanced V3 implementation of the * V2 MarkerClusterer by Xiaoxi Wu. It is based on the * V3 MarkerClusterer port by Luke Mahe. MarkerClustererPlus was created by Gary Little. *

* v2.0 release: MarkerClustererPlus v2.0 is backward compatible with MarkerClusterer v1.0. It * adds support for the ignoreHidden, title, batchSizeIE, * and calculator properties as well as support for four more events. It also allows * greater control over the styling of the text that appears on the cluster marker. The * documentation has been significantly improved and the overall code has been simplified and * polished. Very large numbers of markers can now be managed without causing Javascript timeout * errors on Internet Explorer. Note that the name of the clusterclick event has been * deprecated. The new name is click, so please change your application code now. */ /** * 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. */ /** * @name ClusterIconStyle * @class This class represents the object for values in the styles array passed * to the {@link MarkerClusterer} constructor. The element in this array that is used to * style the cluster icon is determined by calling the calculator function. * * @property {string} url The URL of the cluster icon image file. Required. * @property {number} height The display height (in pixels) of the cluster icon. Required. * @property {number} width The display width (in pixels) of the cluster icon. Required. * @property {Array} [anchorText] The position (in pixels) from the center of the cluster icon to * where the text label is to be centered and drawn. The format is [yoffset, xoffset] * where yoffset increases as you go down from center and xoffset * increases to the right of center. The default is [0, 0]. * @property {Array} [anchorIcon] The anchor position (in pixels) of the cluster icon. This is the * spot on the cluster icon that is to be aligned with the cluster position. The format is * [yoffset, xoffset] where yoffset increases as you go down and * xoffset increases to the right of the top-left corner of the icon. The default * anchor position is the center of the cluster icon. * @property {string} [textColor="black"] The color of the label text shown on the * cluster icon. * @property {number} [textSize=11] The size (in pixels) of the label text shown on the * cluster icon. * @property {string} [textDecoration="none"] The value of the CSS text-decoration * property for the label text shown on the cluster icon. * @property {string} [fontWeight="bold"] The value of the CSS font-weight * property for the label text shown on the cluster icon. * @property {string} [fontStyle="normal"] The value of the CSS font-style * property for the label text shown on the cluster icon. * @property {string} [fontFamily="Arial,sans-serif"] The value of the CSS font-family * property for the label text shown on the cluster icon. * @property {string} [backgroundPosition="0 0"] The position of the cluster icon image * within the image defined by url. The format is "xpos ypos" * (the same format as for the CSS background-position property). You must set * this property appropriately when the image defined by url represents a sprite * containing multiple images. Note that the position must be specified in px units. */ /** * @name ClusterIconInfo * @class This class is an object containing general information about a cluster icon. This is * the object that a calculator function returns. * * @property {string} text The text of the label to be shown on the cluster icon. * @property {number} index The index plus 1 of the element in the styles * array to be used to style the cluster icon. * @property {string} title The tooltip to display when the mouse moves over the cluster icon. * If this value is undefined or "", title is set to the * value of the title property passed to the MarkerClusterer. */ /** * A cluster icon. * * @constructor * @extends google.maps.OverlayView * @param {Cluster} cluster The cluster with which the icon is to be associated. * @param {Array} [styles] An array of {@link ClusterIconStyle} defining the cluster icons * to use for various cluster sizes. * @private */ function ClusterIcon(cluster, styles) { cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView); this.cluster_ = cluster; this.className_ = cluster.getMarkerClusterer().getClusterClass(); this.styles_ = styles; this.center_ = null; this.div_ = null; this.sums_ = null; this.visible_ = false; this.setMap(cluster.getMap()); // Note: this causes onAdd to be called } /** * Adds the icon to the DOM. */ ClusterIcon.prototype.onAdd = function () { var cClusterIcon = this; var cMouseDownInCluster; var cDraggingMapByCluster; this.div_ = document.createElement("div"); this.div_.className = this.className_; if (this.visible_) { this.show(); } this.getPanes().overlayMouseTarget.appendChild(this.div_); // Fix for Issue 157 this.boundsChangedListener_ = google.maps.event.addListener(this.getMap(), "bounds_changed", function () { cDraggingMapByCluster = cMouseDownInCluster; }); google.maps.event.addDomListener(this.div_, "mousedown", function () { cMouseDownInCluster = true; cDraggingMapByCluster = false; }); google.maps.event.addDomListener(this.div_, "click", function (e) { cMouseDownInCluster = false; if (!cDraggingMapByCluster) { var theBounds; var mz; var mc = cClusterIcon.cluster_.getMarkerClusterer(); /** * This event is fired when a cluster marker is clicked. * @name MarkerClusterer#click * @param {Cluster} c The cluster that was clicked. * @event */ google.maps.event.trigger(mc, "click", cClusterIcon.cluster_); google.maps.event.trigger(mc, "clusterclick", cClusterIcon.cluster_); // deprecated name // The default click handler follows. Disable it by setting // the zoomOnClick property to false. if (mc.getZoomOnClick()) { // Zoom into the cluster. mz = mc.getMaxZoom(); theBounds = cClusterIcon.cluster_.getBounds(); mc.getMap().fitBounds(theBounds); // There is a fix for Issue 170 here: setTimeout(function () { mc.getMap().fitBounds(theBounds); // Don't zoom beyond the max zoom level if (mz !== null && (mc.getMap().getZoom() > mz)) { mc.getMap().setZoom(mz + 1); } }, 100); } // Prevent event propagation to the map: e.cancelBubble = true; if (e.stopPropagation) { e.stopPropagation(); } } }); google.maps.event.addDomListener(this.div_, "mouseover", function () { var mc = cClusterIcon.cluster_.getMarkerClusterer(); /** * This event is fired when the mouse moves over a cluster marker. * @name MarkerClusterer#mouseover * @param {Cluster} c The cluster that the mouse moved over. * @event */ google.maps.event.trigger(mc, "mouseover", cClusterIcon.cluster_); }); google.maps.event.addDomListener(this.div_, "mouseout", function () { var mc = cClusterIcon.cluster_.getMarkerClusterer(); /** * This event is fired when the mouse moves out of a cluster marker. * @name MarkerClusterer#mouseout * @param {Cluster} c The cluster that the mouse moved out of. * @event */ google.maps.event.trigger(mc, "mouseout", cClusterIcon.cluster_); }); }; /** * Removes the icon from the DOM. */ ClusterIcon.prototype.onRemove = function () { if (this.div_ && this.div_.parentNode) { this.hide(); google.maps.event.removeListener(this.boundsChangedListener_); google.maps.event.clearInstanceListeners(this.div_); this.div_.parentNode.removeChild(this.div_); this.div_ = null; } }; /** * Draws the icon. */ ClusterIcon.prototype.draw = function () { if (this.visible_) { var pos = this.getPosFromLatLng_(this.center_); this.div_.style.top = pos.y + "px"; this.div_.style.left = pos.x + "px"; } }; /** * Hides the icon. */ ClusterIcon.prototype.hide = function () { if (this.div_) { this.div_.style.display = "none"; } this.visible_ = false; }; /** * Positions and shows the icon. */ ClusterIcon.prototype.show = function () { if (this.div_) { var img = ""; // NOTE: values must be specified in px units var bp = this.backgroundPosition_.split(" "); var spriteH = parseInt(bp[0].trim(), 10); var spriteV = parseInt(bp[1].trim(), 10); var pos = this.getPosFromLatLng_(this.center_); this.div_.style.cssText = this.createCss(pos); img = ""; this.div_.innerHTML = img + "

" + this.sums_.text + "
"; if (typeof this.sums_.title === "undefined" || this.sums_.title === "") { this.div_.title = this.cluster_.getMarkerClusterer().getTitle(); } else { this.div_.title = this.sums_.title; } this.div_.style.display = ""; } this.visible_ = true; }; /** * Sets the icon styles to the appropriate element in the styles array. * * @param {ClusterIconInfo} sums The icon label text and styles index. */ ClusterIcon.prototype.useStyle = function (sums) { this.sums_ = sums; var index = Math.max(0, sums.index - 1); index = Math.min(this.styles_.length - 1, index); var style = this.styles_[index]; this.url_ = style.url; this.height_ = style.height; this.width_ = style.width; this.anchorText_ = style.anchorText || [0, 0]; this.anchorIcon_ = style.anchorIcon || [parseInt(this.height_ / 2, 10), parseInt(this.width_ / 2, 10)]; this.textColor_ = style.textColor || "black"; this.textSize_ = style.textSize || 11; this.textDecoration_ = style.textDecoration || "none"; this.fontWeight_ = style.fontWeight || "bold"; this.fontStyle_ = style.fontStyle || "normal"; this.fontFamily_ = style.fontFamily || "Arial,sans-serif"; this.backgroundPosition_ = style.backgroundPosition || "0 0"; }; /** * Sets the position at which to center the icon. * * @param {google.maps.LatLng} center The latlng to set as the center. */ ClusterIcon.prototype.setCenter = function (center) { this.center_ = center; }; /** * Creates the cssText style parameter based on the position of the icon. * * @param {google.maps.Point} pos The position of the icon. * @return {string} The CSS style text. */ ClusterIcon.prototype.createCss = function (pos) { var style = []; style.push("cursor: pointer;"); style.push("position: absolute; top: " + pos.y + "px; left: " + pos.x + "px;"); style.push("width: " + this.width_ + "px; height: " + this.height_ + "px;"); return style.join(""); }; /** * Returns the position at which to place the DIV depending on the latlng. * * @param {google.maps.LatLng} latlng The position in latlng. * @return {google.maps.Point} The position in pixels. */ ClusterIcon.prototype.getPosFromLatLng_ = function (latlng) { var pos = this.getProjection().fromLatLngToDivPixel(latlng); pos.x -= this.anchorIcon_[1]; pos.y -= this.anchorIcon_[0]; pos.x = parseInt(pos.x, 10); pos.y = parseInt(pos.y, 10); return pos; }; /** * Creates a single cluster that manages a group of proximate markers. * Used internally, do not call this constructor directly. * @constructor * @param {MarkerClusterer} mc The MarkerClusterer object with which this * cluster is associated. */ function Cluster(mc) { this.markerClusterer_ = mc; this.map_ = mc.getMap(); this.gridSize_ = mc.getGridSize(); this.minClusterSize_ = mc.getMinimumClusterSize(); this.averageCenter_ = mc.getAverageCenter(); this.markers_ = []; this.center_ = null; this.bounds_ = null; this.clusterIcon_ = new ClusterIcon(this, mc.getStyles()); } /** * Returns the number of markers managed by the cluster. You can call this from * a click, mouseover, or mouseout event handler * for the MarkerClusterer object. * * @return {number} The number of markers in the cluster. */ Cluster.prototype.getSize = function () { return this.markers_.length; }; /** * Returns the array of markers managed by the cluster. You can call this from * a click, mouseover, or mouseout event handler * for the MarkerClusterer object. * * @return {Array} The array of markers in the cluster. */ Cluster.prototype.getMarkers = function () { return this.markers_; }; /** * Returns the center of the cluster. You can call this from * a click, mouseover, or mouseout event handler * for the MarkerClusterer object. * * @return {google.maps.LatLng} The center of the cluster. */ Cluster.prototype.getCenter = function () { return this.center_; }; /** * Returns the map with which the cluster is associated. * * @return {google.maps.Map} The map. * @ignore */ Cluster.prototype.getMap = function () { return this.map_; }; /** * Returns the MarkerClusterer object with which the cluster is associated. * * @return {MarkerClusterer} The associated marker clusterer. * @ignore */ Cluster.prototype.getMarkerClusterer = function () { return this.markerClusterer_; }; /** * Returns the bounds of the cluster. * * @return {google.maps.LatLngBounds} the cluster bounds. * @ignore */ Cluster.prototype.getBounds = function () { var i; var bounds = new google.maps.LatLngBounds(this.center_, this.center_); var markers = this.getMarkers(); for (i = 0; i < markers.length; i++) { bounds.extend(markers[i].getPosition()); } return bounds; }; /** * Removes the cluster from the map. * * @ignore */ Cluster.prototype.remove = function () { this.clusterIcon_.setMap(null); this.markers_ = []; delete this.markers_; }; /** * Adds a marker to the cluster. * * @param {google.maps.Marker} marker The marker to be added. * @return {boolean} True if the marker was added. * @ignore */ Cluster.prototype.addMarker = function (marker) { var i; var mCount; var mz; if (this.isMarkerAlreadyAdded_(marker)) { return false; } if (!this.center_) { this.center_ = marker.getPosition(); this.calculateBounds_(); } else { if (this.averageCenter_) { var l = this.markers_.length + 1; var lat = (this.center_.lat() * (l - 1) + marker.getPosition().lat()) / l; var lng = (this.center_.lng() * (l - 1) + marker.getPosition().lng()) / l; this.center_ = new google.maps.LatLng(lat, lng); this.calculateBounds_(); } } marker.isAdded = true; this.markers_.push(marker); mCount = this.markers_.length; mz = this.markerClusterer_.getMaxZoom(); if (mz !== null && this.map_.getZoom() > mz) { // Zoomed in past max zoom, so show the marker. if (marker.getMap() !== this.map_) { marker.setMap(this.map_); } } else if (mCount < this.minClusterSize_) { // Min cluster size not reached so show the marker. if (marker.getMap() !== this.map_) { marker.setMap(this.map_); } } else if (mCount === this.minClusterSize_) { // Hide the markers that were showing. for (i = 0; i < mCount; i++) { this.markers_[i].setMap(null); } } else { marker.setMap(null); } this.updateIcon_(); return true; }; /** * Determines if a marker lies within the cluster's bounds. * * @param {google.maps.Marker} marker The marker to check. * @return {boolean} True if the marker lies in the bounds. * @ignore */ Cluster.prototype.isMarkerInClusterBounds = function (marker) { return this.bounds_.contains(marker.getPosition()); }; /** * Calculates the extended bounds of the cluster with the grid. */ Cluster.prototype.calculateBounds_ = function () { var bounds = new google.maps.LatLngBounds(this.center_, this.center_); this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds); }; /** * Updates the cluster icon. */ Cluster.prototype.updateIcon_ = function () { var mCount = this.markers_.length; var mz = this.markerClusterer_.getMaxZoom(); if (mz !== null && this.map_.getZoom() > mz) { this.clusterIcon_.hide(); return; } if (mCount < this.minClusterSize_) { // Min cluster size not yet reached. this.clusterIcon_.hide(); return; } var numStyles = this.markerClusterer_.getStyles().length; var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles); this.clusterIcon_.setCenter(this.center_); this.clusterIcon_.useStyle(sums); this.clusterIcon_.show(); }; /** * Determines if a marker has already been added to the cluster. * * @param {google.maps.Marker} marker The marker to check. * @return {boolean} True if the marker has already been added. */ Cluster.prototype.isMarkerAlreadyAdded_ = function (marker) { var i; if (this.markers_.indexOf) { return this.markers_.indexOf(marker) !== -1; } else { for (i = 0; i < this.markers_.length; i++) { if (marker === this.markers_[i]) { return true; } } } return false; }; /** * @name MarkerClustererOptions * @class This class represents the optional parameter passed to * the {@link MarkerClusterer} constructor. * @property {number} [gridSize=60] The grid size of a cluster in pixels. The grid is a square. * @property {number} [maxZoom=null] The maximum zoom level at which clustering is enabled or * null if clustering is to be enabled at all zoom levels. * @property {boolean} [zoomOnClick=true] Whether to zoom the map when a cluster marker is * clicked. You may want to set this to false if you have installed a handler * for the click event and it deals with zooming on its own. * @property {boolean} [averageCenter=false] Whether the position of a cluster marker should be * the average position of all markers in the cluster. If set to false, the * cluster marker is positioned at the location of the first marker added to the cluster. * @property {number} [minimumClusterSize=2] The minimum number of markers needed in a cluster * before the markers are hidden and a cluster marker appears. * @property {boolean} [ignoreHidden=false] Whether to ignore hidden markers in clusters. You * may want to set this to true to ensure that hidden markers are not included * in the marker count that appears on a cluster marker (this count is the value of the * text property of the result returned by the default calculator). * If set to true and you change the visibility of a marker being clustered, be * sure to also call MarkerClusterer.repaint(). * @property {string} [title=""] The tooltip to display when the mouse moves over a cluster * marker. (Alternatively, you can use a custom calculator function to specify a * different tooltip for each cluster marker.) * @property {function} [calculator=MarkerClusterer.CALCULATOR] The function used to determine * the text to be displayed on a cluster marker and the index indicating which style to use * for the cluster marker. The input parameters for the function are (1) the array of markers * represented by a cluster marker and (2) the number of cluster icon styles. It returns a * {@link ClusterIconInfo} object. The default calculator returns a * text property which is the number of markers in the cluster and an * index property which is one higher than the lowest integer such that * 10^i exceeds the number of markers in the cluster, or the size of the styles * array, whichever is less. The styles array element used has an index of * index minus 1. For example, the default calculator returns a * text value of "125" and an index of 3 * for a cluster icon representing 125 markers so the element used in the styles * array is 2. A calculator may also return a title * property that contains the text of the tooltip to be used for the cluster marker. If * title is not defined, the tooltip is set to the value of the title * property for the MarkerClusterer. * @property {string} [clusterClass="cluster"] The name of the CSS class defining general styles * for the cluster markers. Use this class to define CSS styles that are not set up by the code * that processes the styles array. * @property {Array} [styles] An array of {@link ClusterIconStyle} elements defining the styles * of the cluster markers to be used. The element to be used to style a given cluster marker * is determined by the function defined by the calculator property. * The default is an array of {@link ClusterIconStyle} elements whose properties are derived * from the values for imagePath, imageExtension, and * imageSizes. * @property {boolean} [enableRetinaIcons=false] Whether to allow the use of cluster icons that * have sizes that are some multiple (typically double) of their actual display size. Icons such * as these look better when viewed on high-resolution monitors such as Apple's Retina displays. * Note: if this property is true, sprites cannot be used as cluster icons. * @property {number} [batchSize=MarkerClusterer.BATCH_SIZE] Set this property to the * number of markers to be processed in a single batch when using a browser other than * Internet Explorer (for Internet Explorer, use the batchSizeIE property instead). * @property {number} [batchSizeIE=MarkerClusterer.BATCH_SIZE_IE] When Internet Explorer is * being used, markers are processed in several batches with a small delay inserted between * each batch in an attempt to avoid Javascript timeout errors. Set this property to the * number of markers to be processed in a single batch; select as high a number as you can * without causing a timeout error in the browser. This number might need to be as low as 100 * if 15,000 markers are being managed, for example. * @property {string} [imagePath=MarkerClusterer.IMAGE_PATH] * The full URL of the root name of the group of image files to use for cluster icons. * The complete file name is of the form imagePathn.imageExtension * where n is the image file number (1, 2, etc.). * @property {string} [imageExtension=MarkerClusterer.IMAGE_EXTENSION] * The extension name for the cluster icon image files (e.g., "png" or * "jpg"). * @property {Array} [imageSizes=MarkerClusterer.IMAGE_SIZES] * An array of numbers containing the widths of the group of * imagePathn.imageExtension image files. * (The images are assumed to be square.) */ /** * Creates a MarkerClusterer object with the options specified in {@link MarkerClustererOptions}. * @constructor * @extends google.maps.OverlayView * @param {google.maps.Map} map The Google map to attach to. * @param {Array.} [opt_markers] The markers to be added to the cluster. * @param {MarkerClustererOptions} [opt_options] The optional parameters. */ function MarkerClusterer(map, opt_markers, opt_options) { // MarkerClusterer implements google.maps.OverlayView interface. We use the // extend function to extend MarkerClusterer with google.maps.OverlayView // because it might not always be available when the code is defined so we // look for it at the last possible moment. If it doesn't exist now then // there is no point going ahead :) this.extend(MarkerClusterer, google.maps.OverlayView); opt_markers = opt_markers || []; opt_options = opt_options || {}; this.markers_ = []; this.clusters_ = []; this.listeners_ = []; this.activeMap_ = null; this.ready_ = false; this.gridSize_ = opt_options.gridSize || 60; this.minClusterSize_ = opt_options.minimumClusterSize || 2; this.maxZoom_ = opt_options.maxZoom || null; this.styles_ = opt_options.styles || []; this.title_ = opt_options.title || ""; this.zoomOnClick_ = true; if (opt_options.zoomOnClick !== undefined) { this.zoomOnClick_ = opt_options.zoomOnClick; } this.averageCenter_ = false; if (opt_options.averageCenter !== undefined) { this.averageCenter_ = opt_options.averageCenter; } this.ignoreHidden_ = false; if (opt_options.ignoreHidden !== undefined) { this.ignoreHidden_ = opt_options.ignoreHidden; } this.enableRetinaIcons_ = false; if (opt_options.enableRetinaIcons !== undefined) { this.enableRetinaIcons_ = opt_options.enableRetinaIcons; } this.imagePath_ = opt_options.imagePath || MarkerClusterer.IMAGE_PATH; this.imageExtension_ = opt_options.imageExtension || MarkerClusterer.IMAGE_EXTENSION; this.imageSizes_ = opt_options.imageSizes || MarkerClusterer.IMAGE_SIZES; this.calculator_ = opt_options.calculator || MarkerClusterer.CALCULATOR; this.batchSize_ = opt_options.batchSize || MarkerClusterer.BATCH_SIZE; this.batchSizeIE_ = opt_options.batchSizeIE || MarkerClusterer.BATCH_SIZE_IE; this.clusterClass_ = opt_options.clusterClass || "cluster"; if (navigator.userAgent.toLowerCase().indexOf("msie") !== -1) { // Try to avoid IE timeout when processing a huge number of markers: this.batchSize_ = this.batchSizeIE_; } this.setupStyles_(); this.addMarkers(opt_markers, true); this.setMap(map); // Note: this causes onAdd to be called } /** * Implementation of the onAdd interface method. * @ignore */ MarkerClusterer.prototype.onAdd = function () { var cMarkerClusterer = this; this.activeMap_ = this.getMap(); this.ready_ = true; this.repaint(); // Add the map event listeners this.listeners_ = [ google.maps.event.addListener(this.getMap(), "zoom_changed", function () { cMarkerClusterer.resetViewport_(false); // Workaround for this Google bug: when map is at level 0 and "-" of // zoom slider is clicked, a "zoom_changed" event is fired even though // the map doesn't zoom out any further. In this situation, no "idle" // event is triggered so the cluster markers that have been removed // do not get redrawn. Same goes for a zoom in at maxZoom. if (this.getZoom() === (this.get("minZoom") || 0) || this.getZoom() === this.get("maxZoom")) { google.maps.event.trigger(this, "idle"); } }), google.maps.event.addListener(this.getMap(), "idle", function () { cMarkerClusterer.redraw_(); }) ]; }; /** * Implementation of the onRemove interface method. * Removes map event listeners and all cluster icons from the DOM. * All managed markers are also put back on the map. * @ignore */ MarkerClusterer.prototype.onRemove = function () { var i; // Put all the managed markers back on the map: for (i = 0; i < this.markers_.length; i++) { if (this.markers_[i].getMap() !== this.activeMap_) { this.markers_[i].setMap(this.activeMap_); } } // Remove all clusters: for (i = 0; i < this.clusters_.length; i++) { this.clusters_[i].remove(); } this.clusters_ = []; // Remove map event listeners: for (i = 0; i < this.listeners_.length; i++) { google.maps.event.removeListener(this.listeners_[i]); } this.listeners_ = []; this.activeMap_ = null; this.ready_ = false; }; /** * Implementation of the draw interface method. * @ignore */ MarkerClusterer.prototype.draw = function () {}; /** * Sets up the styles object. */ MarkerClusterer.prototype.setupStyles_ = function () { var i, size; if (this.styles_.length > 0) { return; } for (i = 0; i < this.imageSizes_.length; i++) { size = this.imageSizes_[i]; this.styles_.push({ url: this.imagePath_ + (i + 1) + "." + this.imageExtension_, height: size, width: size }); } }; /** * Fits the map to the bounds of the markers managed by the clusterer. */ MarkerClusterer.prototype.fitMapToMarkers = function () { var i; var markers = this.getMarkers(); var bounds = new google.maps.LatLngBounds(); for (i = 0; i < markers.length; i++) { bounds.extend(markers[i].getPosition()); } this.getMap().fitBounds(bounds); }; /** * Returns the value of the gridSize property. * * @return {number} The grid size. */ MarkerClusterer.prototype.getGridSize = function () { return this.gridSize_; }; /** * Sets the value of the gridSize property. * * @param {number} gridSize The grid size. */ MarkerClusterer.prototype.setGridSize = function (gridSize) { this.gridSize_ = gridSize; }; /** * Returns the value of the minimumClusterSize property. * * @return {number} The minimum cluster size. */ MarkerClusterer.prototype.getMinimumClusterSize = function () { return this.minClusterSize_; }; /** * Sets the value of the minimumClusterSize property. * * @param {number} minimumClusterSize The minimum cluster size. */ MarkerClusterer.prototype.setMinimumClusterSize = function (minimumClusterSize) { this.minClusterSize_ = minimumClusterSize; }; /** * Returns the value of the maxZoom property. * * @return {number} The maximum zoom level. */ MarkerClusterer.prototype.getMaxZoom = function () { return this.maxZoom_; }; /** * Sets the value of the maxZoom property. * * @param {number} maxZoom The maximum zoom level. */ MarkerClusterer.prototype.setMaxZoom = function (maxZoom) { this.maxZoom_ = maxZoom; }; /** * Returns the value of the styles property. * * @return {Array} The array of styles defining the cluster markers to be used. */ MarkerClusterer.prototype.getStyles = function () { return this.styles_; }; /** * Sets the value of the styles property. * * @param {Array.} styles The array of styles to use. */ MarkerClusterer.prototype.setStyles = function (styles) { this.styles_ = styles; }; /** * Returns the value of the title property. * * @return {string} The content of the title text. */ MarkerClusterer.prototype.getTitle = function () { return this.title_; }; /** * Sets the value of the title property. * * @param {string} title The value of the title property. */ MarkerClusterer.prototype.setTitle = function (title) { this.title_ = title; }; /** * Returns the value of the zoomOnClick property. * * @return {boolean} True if zoomOnClick property is set. */ MarkerClusterer.prototype.getZoomOnClick = function () { return this.zoomOnClick_; }; /** * Sets the value of the zoomOnClick property. * * @param {boolean} zoomOnClick The value of the zoomOnClick property. */ MarkerClusterer.prototype.setZoomOnClick = function (zoomOnClick) { this.zoomOnClick_ = zoomOnClick; }; /** * Returns the value of the averageCenter property. * * @return {boolean} True if averageCenter property is set. */ MarkerClusterer.prototype.getAverageCenter = function () { return this.averageCenter_; }; /** * Sets the value of the averageCenter property. * * @param {boolean} averageCenter The value of the averageCenter property. */ MarkerClusterer.prototype.setAverageCenter = function (averageCenter) { this.averageCenter_ = averageCenter; }; /** * Returns the value of the ignoreHidden property. * * @return {boolean} True if ignoreHidden property is set. */ MarkerClusterer.prototype.getIgnoreHidden = function () { return this.ignoreHidden_; }; /** * Sets the value of the ignoreHidden property. * * @param {boolean} ignoreHidden The value of the ignoreHidden property. */ MarkerClusterer.prototype.setIgnoreHidden = function (ignoreHidden) { this.ignoreHidden_ = ignoreHidden; }; /** * Returns the value of the enableRetinaIcons property. * * @return {boolean} True if enableRetinaIcons property is set. */ MarkerClusterer.prototype.getEnableRetinaIcons = function () { return this.enableRetinaIcons_; }; /** * Sets the value of the enableRetinaIcons property. * * @param {boolean} enableRetinaIcons The value of the enableRetinaIcons property. */ MarkerClusterer.prototype.setEnableRetinaIcons = function (enableRetinaIcons) { this.enableRetinaIcons_ = enableRetinaIcons; }; /** * Returns the value of the imageExtension property. * * @return {string} The value of the imageExtension property. */ MarkerClusterer.prototype.getImageExtension = function () { return this.imageExtension_; }; /** * Sets the value of the imageExtension property. * * @param {string} imageExtension The value of the imageExtension property. */ MarkerClusterer.prototype.setImageExtension = function (imageExtension) { this.imageExtension_ = imageExtension; }; /** * Returns the value of the imagePath property. * * @return {string} The value of the imagePath property. */ MarkerClusterer.prototype.getImagePath = function () { return this.imagePath_; }; /** * Sets the value of the imagePath property. * * @param {string} imagePath The value of the imagePath property. */ MarkerClusterer.prototype.setImagePath = function (imagePath) { this.imagePath_ = imagePath; }; /** * Returns the value of the imageSizes property. * * @return {Array} The value of the imageSizes property. */ MarkerClusterer.prototype.getImageSizes = function () { return this.imageSizes_; }; /** * Sets the value of the imageSizes property. * * @param {Array} imageSizes The value of the imageSizes property. */ MarkerClusterer.prototype.setImageSizes = function (imageSizes) { this.imageSizes_ = imageSizes; }; /** * Returns the value of the calculator property. * * @return {function} the value of the calculator property. */ MarkerClusterer.prototype.getCalculator = function () { return this.calculator_; }; /** * Sets the value of the calculator property. * * @param {function(Array., number)} calculator The value * of the calculator property. */ MarkerClusterer.prototype.setCalculator = function (calculator) { this.calculator_ = calculator; }; /** * Returns the value of the batchSizeIE property. * * @return {number} the value of the batchSizeIE property. */ MarkerClusterer.prototype.getBatchSizeIE = function () { return this.batchSizeIE_; }; /** * Sets the value of the batchSizeIE property. * * @param {number} batchSizeIE The value of the batchSizeIE property. */ MarkerClusterer.prototype.setBatchSizeIE = function (batchSizeIE) { this.batchSizeIE_ = batchSizeIE; }; /** * Returns the value of the clusterClass property. * * @return {string} the value of the clusterClass property. */ MarkerClusterer.prototype.getClusterClass = function () { return this.clusterClass_; }; /** * Sets the value of the clusterClass property. * * @param {string} clusterClass The value of the clusterClass property. */ MarkerClusterer.prototype.setClusterClass = function (clusterClass) { this.clusterClass_ = clusterClass; }; /** * Returns the array of markers managed by the clusterer. * * @return {Array} The array of markers managed by the clusterer. */ MarkerClusterer.prototype.getMarkers = function () { return this.markers_; }; /** * Returns the number of markers managed by the clusterer. * * @return {number} The number of markers. */ MarkerClusterer.prototype.getTotalMarkers = function () { return this.markers_.length; }; /** * Returns the current array of clusters formed by the clusterer. * * @return {Array} The array of clusters formed by the clusterer. */ MarkerClusterer.prototype.getClusters = function () { return this.clusters_; }; /** * Returns the number of clusters formed by the clusterer. * * @return {number} The number of clusters formed by the clusterer. */ MarkerClusterer.prototype.getTotalClusters = function () { return this.clusters_.length; }; /** * Adds a marker to the clusterer. The clusters are redrawn unless * opt_nodraw is set to true. * * @param {google.maps.Marker} marker The marker to add. * @param {boolean} [opt_nodraw] Set to true to prevent redrawing. */ MarkerClusterer.prototype.addMarker = function (marker, opt_nodraw) { this.pushMarkerTo_(marker); if (!opt_nodraw) { this.redraw_(); } }; /** * Adds an array of markers to the clusterer. The clusters are redrawn unless * opt_nodraw is set to true. * * @param {Array.} markers The markers to add. * @param {boolean} [opt_nodraw] Set to true to prevent redrawing. */ MarkerClusterer.prototype.addMarkers = function (markers, opt_nodraw) { var key; for (key in markers) { if (markers.hasOwnProperty(key)) { this.pushMarkerTo_(markers[key]); } } if (!opt_nodraw) { this.redraw_(); } }; /** * Pushes a marker to the clusterer. * * @param {google.maps.Marker} marker The marker to add. */ MarkerClusterer.prototype.pushMarkerTo_ = function (marker) { // If the marker is draggable add a listener so we can update the clusters on the dragend: if (marker.getDraggable()) { var cMarkerClusterer = this; google.maps.event.addListener(marker, "dragend", function () { if (cMarkerClusterer.ready_) { this.isAdded = false; cMarkerClusterer.repaint(); } }); } marker.isAdded = false; this.markers_.push(marker); }; /** * Removes a marker from the cluster. The clusters are redrawn unless * opt_nodraw is set to true. Returns true if the * marker was removed from the clusterer. * * @param {google.maps.Marker} marker The marker to remove. * @param {boolean} [opt_nodraw] Set to true to prevent redrawing. * @return {boolean} True if the marker was removed from the clusterer. */ MarkerClusterer.prototype.removeMarker = function (marker, opt_nodraw) { var removed = this.removeMarker_(marker); if (!opt_nodraw && removed) { this.repaint(); } return removed; }; /** * Removes an array of markers from the cluster. The clusters are redrawn unless * opt_nodraw is set to true. Returns true if markers * were removed from the clusterer. * * @param {Array.} markers The markers to remove. * @param {boolean} [opt_nodraw] Set to true to prevent redrawing. * @return {boolean} True if markers were removed from the clusterer. */ MarkerClusterer.prototype.removeMarkers = function (markers, opt_nodraw) { var i, r; var removed = false; for (i = 0; i < markers.length; i++) { r = this.removeMarker_(markers[i]); removed = removed || r; } if (!opt_nodraw && removed) { this.repaint(); } return removed; }; /** * Removes a marker and returns true if removed, false if not. * * @param {google.maps.Marker} marker The marker to remove * @return {boolean} Whether the marker was removed or not */ MarkerClusterer.prototype.removeMarker_ = function (marker) { var i; var index = -1; if (this.markers_.indexOf) { index = this.markers_.indexOf(marker); } else { for (i = 0; i < this.markers_.length; i++) { if (marker === this.markers_[i]) { index = i; break; } } } if (index === -1) { // Marker is not in our list of markers, so do nothing: return false; } marker.setMap(null); this.markers_.splice(index, 1); // Remove the marker from the list of managed markers return true; }; /** * Removes all clusters and markers from the map and also removes all markers * managed by the clusterer. */ MarkerClusterer.prototype.clearMarkers = function () { this.resetViewport_(true); this.markers_ = []; }; /** * Recalculates and redraws all the marker clusters from scratch. * Call this after changing any properties. */ MarkerClusterer.prototype.repaint = function () { var oldClusters = this.clusters_.slice(); this.clusters_ = []; this.resetViewport_(false); this.redraw_(); // Remove the old clusters. // Do it in a timeout to prevent blinking effect. setTimeout(function () { var i; for (i = 0; i < oldClusters.length; i++) { oldClusters[i].remove(); } }, 0); }; /** * Returns the current bounds extended by the grid size. * * @param {google.maps.LatLngBounds} bounds The bounds to extend. * @return {google.maps.LatLngBounds} The extended bounds. * @ignore */ MarkerClusterer.prototype.getExtendedBounds = function (bounds) { var projection = this.getProjection(); // Turn the bounds into latlng. var tr = new google.maps.LatLng(bounds.getNorthEast().lat(), bounds.getNorthEast().lng()); var bl = new google.maps.LatLng(bounds.getSouthWest().lat(), bounds.getSouthWest().lng()); // Convert the points to pixels and the extend out by the grid size. var trPix = projection.fromLatLngToDivPixel(tr); trPix.x += this.gridSize_; trPix.y -= this.gridSize_; var blPix = projection.fromLatLngToDivPixel(bl); blPix.x -= this.gridSize_; blPix.y += this.gridSize_; // Convert the pixel points back to LatLng var ne = projection.fromDivPixelToLatLng(trPix); var sw = projection.fromDivPixelToLatLng(blPix); // Extend the bounds to contain the new bounds. bounds.extend(ne); bounds.extend(sw); return bounds; }; /** * Redraws all the clusters. */ MarkerClusterer.prototype.redraw_ = function () { this.createClusters_(0); }; /** * Removes all clusters from the map. The markers are also removed from the map * if opt_hide is set to true. * * @param {boolean} [opt_hide] Set to true to also remove the markers * from the map. */ MarkerClusterer.prototype.resetViewport_ = function (opt_hide) { var i, marker; // Remove all the clusters for (i = 0; i < this.clusters_.length; i++) { this.clusters_[i].remove(); } this.clusters_ = []; // Reset the markers to not be added and to be removed from the map. for (i = 0; i < this.markers_.length; i++) { marker = this.markers_[i]; marker.isAdded = false; if (opt_hide) { marker.setMap(null); } } }; /** * Calculates the distance between two latlng locations in km. * * @param {google.maps.LatLng} p1 The first lat lng point. * @param {google.maps.LatLng} p2 The second lat lng point. * @return {number} The distance between the two points in km. * @see http://www.movable-type.co.uk/scripts/latlong.html */ MarkerClusterer.prototype.distanceBetweenPoints_ = function (p1, p2) { var R = 6371; // Radius of the Earth in km var dLat = (p2.lat() - p1.lat()) * Math.PI / 180; var dLon = (p2.lng() - p1.lng()) * Math.PI / 180; var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2); var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); var d = R * c; return d; }; /** * Determines if a marker is contained in a bounds. * * @param {google.maps.Marker} marker The marker to check. * @param {google.maps.LatLngBounds} bounds The bounds to check against. * @return {boolean} True if the marker is in the bounds. */ MarkerClusterer.prototype.isMarkerInBounds_ = function (marker, bounds) { return bounds.contains(marker.getPosition()); }; /** * Adds a marker to a cluster, or creates a new cluster. * * @param {google.maps.Marker} marker The marker to add. */ MarkerClusterer.prototype.addToClosestCluster_ = function (marker) { var i, d, cluster, center; var distance = 40000; // Some large number var clusterToAddTo = null; for (i = 0; i < this.clusters_.length; i++) { cluster = this.clusters_[i]; center = cluster.getCenter(); if (center) { d = this.distanceBetweenPoints_(center, marker.getPosition()); if (d < distance) { distance = d; clusterToAddTo = cluster; } } } if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) { clusterToAddTo.addMarker(marker); } else { cluster = new Cluster(this); cluster.addMarker(marker); this.clusters_.push(cluster); } }; /** * Creates the clusters. This is done in batches to avoid timeout errors * in some browsers when there is a huge number of markers. * * @param {number} iFirst The index of the first marker in the batch of * markers to be added to clusters. */ MarkerClusterer.prototype.createClusters_ = function (iFirst) { var i, marker; var mapBounds; var cMarkerClusterer = this; if (!this.ready_) { return; } // Cancel previous batch processing if we're working on the first batch: if (iFirst === 0) { /** * This event is fired when the MarkerClusterer begins * clustering markers. * @name MarkerClusterer#clusteringbegin * @param {MarkerClusterer} mc The MarkerClusterer whose markers are being clustered. * @event */ google.maps.event.trigger(this, "clusteringbegin", this); if (typeof this.timerRefStatic !== "undefined") { clearTimeout(this.timerRefStatic); delete this.timerRefStatic; } } // Get our current map view bounds. // Create a new bounds object so we don't affect the map. // // See Comments 9 & 11 on Issue 3651 relating to this workaround for a Google Maps bug: if (this.getMap().getZoom() > 3) { mapBounds = new google.maps.LatLngBounds(this.getMap().getBounds().getSouthWest(), this.getMap().getBounds().getNorthEast()); } else { mapBounds = new google.maps.LatLngBounds(new google.maps.LatLng(85.02070771743472, -178.48388434375), new google.maps.LatLng(-85.08136444384544, 178.00048865625)); } var bounds = this.getExtendedBounds(mapBounds); var iLast = Math.min(iFirst + this.batchSize_, this.markers_.length); for (i = iFirst; i < iLast; i++) { marker = this.markers_[i]; if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) { if (!this.ignoreHidden_ || (this.ignoreHidden_ && marker.getVisible())) { this.addToClosestCluster_(marker); } } } if (iLast < this.markers_.length) { this.timerRefStatic = setTimeout(function () { cMarkerClusterer.createClusters_(iLast); }, 0); } else { delete this.timerRefStatic; /** * This event is fired when the MarkerClusterer stops * clustering markers. * @name MarkerClusterer#clusteringend * @param {MarkerClusterer} mc The MarkerClusterer whose markers are being clustered. * @event */ google.maps.event.trigger(this, "clusteringend", this); } }; /** * Extends an object's prototype by another's. * * @param {Object} obj1 The object to be extended. * @param {Object} obj2 The object to extend with. * @return {Object} The new extended object. * @ignore */ MarkerClusterer.prototype.extend = function (obj1, obj2) { return (function (object) { var property; for (property in object.prototype) { this.prototype[property] = object.prototype[property]; } return this; }).apply(obj1, [obj2]); }; /** * The default function for determining the label text and style * for a cluster icon. * * @param {Array.} markers The array of markers represented by the cluster. * @param {number} numStyles The number of marker styles available. * @return {ClusterIconInfo} The information resource for the cluster. * @constant * @ignore */ MarkerClusterer.CALCULATOR = function (markers, numStyles) { var index = 0; var title = ""; var count = markers.length.toString(); var dv = count; while (dv !== 0) { dv = parseInt(dv / 10, 10); index++; } index = Math.min(index, numStyles); return { text: count, index: index, title: title }; }; /** * The number of markers to process in one batch. * * @type {number} * @constant */ MarkerClusterer.BATCH_SIZE = 2000; /** * The number of markers to process in one batch (IE only). * * @type {number} * @constant */ MarkerClusterer.BATCH_SIZE_IE = 500; /** * The default root name for the marker cluster images. * * @type {string} * @constant */ MarkerClusterer.IMAGE_PATH = "http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclustererplus/images/m"; /** * The default extension name for the marker cluster images. * * @type {string} * @constant */ MarkerClusterer.IMAGE_EXTENSION = "png"; /** * The default array of sizes for the marker cluster images. * * @type {Array.} * @constant */ MarkerClusterer.IMAGE_SIZES = [53, 56, 66, 78, 90]; if (typeof String.prototype.trim !== 'function') { /** * IE hack since trim() doesn't exist in all browsers * @return {string} The string with removed whitespace */ String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ''); } } ; (function(){/* OverlappingMarkerSpiderfier https://github.com/jawj/OverlappingMarkerSpiderfier Copyright (c) 2011 - 2012 George MacKerron Released under the MIT licence: http://opensource.org/licenses/mit-license Note: The Google Maps API v3 must be included *before* this code */ var h=!0,u=null,v=!1; (function(){var A,B={}.hasOwnProperty,C=[].slice;if(((A=this.google)!=u?A.maps:void 0)!=u)this.OverlappingMarkerSpiderfier=function(){function w(b,d){var a,g,f,e,c=this;this.map=b;d==u&&(d={});for(a in d)B.call(d,a)&&(g=d[a],this[a]=g);this.e=new this.constructor.g(this.map);this.n();this.b={};e=["click","zoom_changed","maptypeid_changed"];g=0;for(f=e.length;gd)return this;g=this.j.splice(d,1)[0];f=0;for(e=g.length;fa||this.b[b].splice(a,1);return this};c.clearListeners=function(b){this.b[b]=[];return this};c.trigger=function(){var b,d,a,g,f,e;d=arguments[0];b=2<=arguments.length?C.call(arguments,1):[];d=(a=this.b[d])!=u?a:[];e=[];g=0;for(f=d.length;gb;a=0<=b?++e:--e)a=this.circleStartAngle+a*g,c.push(new s.Point(d.x+f*Math.cos(a),d.y+f*Math.sin(a)));return c};c.v=function(b,d){var a,g,f,e,c;f=this.spiralLengthStart;a=0;c=[];for(g=e=0;0<=b?eb;g=0<=b?++e:--e)a+=this.spiralFootSeparation/f+5E-4*g,g=new s.Point(d.x+f*Math.cos(a),d.y+f*Math.sin(a)),f+=y*this.spiralLengthFactor/a,c.push(g);return c};c.F=function(b,d){var a,g,f,e,c, m,l,x,n;e=b._omsData!=u;(!e||!this.keepSpiderfied)&&this.unspiderfy();if(e||this.map.getStreetView().getVisible()||"GoogleEarthAPI"===this.map.getMapTypeId())return this.trigger("click",b,d);e=[];c=[];a=this.nearbyDistance;m=a*a;f=this.c(b.position);n=this.a;l=0;for(x=n.length;l=this.circleSpiralSwitchover?this.v(q,a).reverse():this.u(q,a);a=function(){var a,d,k,q=this;k=[];a=0;for(d=e.length;a', '"' : '"', ''' : "'", '’' : "’", '‘' : "‘", '–' : "–", '—' : "—", '…' : "…", '”' : '”' }; for ( var i = 0; i < propertiesMapData.length; i++ ) { if ( propertiesMapData[i].lat && propertiesMapData[i].lng ) { var iconURL = propertiesMapData[i].icon; var size = new google.maps.Size( 42, 57 ); if ( window.devicePixelRatio > 1.5 ) { if ( propertiesMapData[i].retinaIcon ) { iconURL = propertiesMapData[i].retinaIcon; size = new google.maps.Size( 83, 113 ); } } if ( propertiesMapOptions.marker_type === 'circle' ) { var markerIcon = { path : google.maps.SymbolPath.CIRCLE, scale : 25, fillColor : propertiesMapOptions.marker_color, strokeColor : propertiesMapOptions.marker_color, fillOpacity : 0.5, strokeWeight : 0.6 } } else { var markerIcon = { url : iconURL, size : size, scaledSize : new google.maps.Size( 42, 57 ), origin : new google.maps.Point( 0, 0 ), anchor : new google.maps.Point( 21, 56 ) }; } markers[i] = new google.maps.Marker( { position : new google.maps.LatLng( propertiesMapData[i].lat, propertiesMapData[i].lng ), map : propertiesMap, id : propertiesMapData[i].id, icon : markerIcon, title : propertiesMapData[i].title.replace( /\&[\w\d\#]{2,5}\;/g, function ( m ) { return map[m]; } ), // Decode PHP's html special characters encoding with Javascript animation : google.maps.Animation.DROP, visible : true } ); // Extend bounds mapBounds.extend( markers[i].getPosition() ); // Prepare info window contents var boxText = document.createElement( "div" ); boxText.className = 'map-info-window'; var innerHTML = ""; // Info window image place holder URL to improve performance var infoBoxPlaceholderURL = ""; if ( ( typeof mapStuff !== "undefined" ) && mapStuff.infoBoxPlaceholder ) { infoBoxPlaceholderURL = mapStuff.infoBoxPlaceholder; } if ( propertiesMapData[i].thumb ) { innerHTML += '' + '' + propertiesMapData[i].title + '' + ''; } else { innerHTML += '' + '' + propertiesMapData[i].title + '' + ''; } if ( rhMapsData.isUltra ) { innerHTML += '
'; innerHTML += '
' + propertiesMapData[i].title + '
'; innerHTML += '
'; if ( propertiesMapData[i].propertyType ) { innerHTML += '' + propertiesMapData[i].propertyType + ''; } if ( propertiesMapData[i].price ) { innerHTML += '

' + propertiesMapData[i].price + '

'; } innerHTML += '
'; innerHTML += '
'; boxText.innerHTML = '
' + innerHTML + '
'; } else { innerHTML += '
' + propertiesMapData[i].title + '
'; if ( propertiesMapData[i].price ) { innerHTML += '

' + propertiesMapData[i].price + '

'; } innerHTML += '
'; boxText.innerHTML = innerHTML; } // Info window close icon URL var closeIconURL = ""; if ( ( typeof mapStuff !== "undefined" ) && mapStuff.closeIcon ) { closeIconURL = mapStuff.closeIcon; } let iwPixelOffsetY = -48; if ( rhMapsData.isUltra ) { iwPixelOffsetY = -70; } else if ( rhMapsData.isClassic ) { iwPixelOffsetY = -52; } if ( propertiesMapOptions.marker_type === 'circle' ) { iwPixelOffsetY = -22; if ( rhMapsData.isClassic ) { iwPixelOffsetY = -24; } } let iwMaxWidth = 0, iwPixelOffsetX = -122, iwCloseBoxMargin = '0 0 -16px -16px'; if ( rhMapsData.isUltra ) { iwMaxWidth = 450; iwPixelOffsetX = -225; iwCloseBoxMargin = '8px 8px -24px -16px'; } // Finalize info window var infoWindowOptions = { content : boxText, disableAutoPan : true, maxWidth : iwMaxWidth, alignBottom : true, pixelOffset : new google.maps.Size( iwPixelOffsetX, iwPixelOffsetY ), zIndex : null, closeBoxMargin : iwCloseBoxMargin, closeBoxURL : closeIconURL, infoBoxClearance : new google.maps.Size( 1, 1 ), isHidden : false, pane : "floatPane", enableEventPropagation : false }; var currentInfoBox = new InfoBox( infoWindowOptions ); // Attach info window to marker attachInfoBoxToMarker( propertiesMap, markers[i], currentInfoBox ); } } // Apply overlapping marker spiderfier to marker propertiesMap.addListener( "idle", () => { if ( markers.length ) { markers.forEach( function ( marker ) { overlappingMarkerSpiderfier.addMarker( marker ); } ); } } ); // Fit map to bound as per markers or radius circle. if ( circleOnMap ) { // Draw searched radius range circle. drawCircleOnMap( propertiesMap ); } else { propertiesMap.fitBounds( mapBounds ); } // Cluster icon URL var clusterIconURL = ""; if ( ( typeof mapStuff !== "undefined" ) && mapStuff.clusterIcon ) { clusterIconURL = mapStuff.clusterIcon; } // Markers clusters var markerClustererOptions = { ignoreHidden : true, // gridSize: 60, maxZoom : 14, styles : [{ textColor : '#ffffff', url : clusterIconURL, height : 48, width : 48 }] }; var markerClusterer = new MarkerClusterer( propertiesMap, markers, markerClustererOptions ); } else { // Fallback Map in Case of No Properties var fallback_lat, fallback_lng; if ( undefined !== propertiesMapOptions.fallback_location && propertiesMapOptions.fallback_location.lat && propertiesMapOptions.fallback_location.lng ) { fallback_lat = propertiesMapOptions.fallback_location.lat; fallback_lng = propertiesMapOptions.fallback_location.lng; } else { // Default location of Florida in fallback map. fallback_lat = '27.664827'; fallback_lng = '-81.515755'; } var fallBackLocation = new google.maps.LatLng( fallback_lat, fallback_lng ); var fallBackOptions = { center : fallBackLocation, zoom : 14, maxZoom : 16, scrollwheel : false }; // Map Styles if ( undefined !== propertiesMapOptions.styles ) { fallBackOptions.styles = JSON.parse( propertiesMapOptions.styles ); } var fallBackMap = new google.maps.Map( document.getElementById( "listing-map" ), fallBackOptions ); if ( circleOnMap ) { // Draw searched radius range circle if Geo location is enabled. drawCircleOnMap( fallBackMap ); } } } var updateZoomRepaint = function ( zoom ) { markerClusterer.setMaxZoom( zoom ); markerClusterer.repaint(); } window.realhomesInfoboxPopupTrigger = function () { if ( ! markers.length ) { return false; } let popupInfoSelectors = '.rh_popup_info_map'; if ( rhMapsData.isClassic ) { popupInfoSelectors = '.half-map-layout-properties .rh_popup_info_map'; } $( popupInfoSelectors ).each( function ( i ) { $( this ).on( 'mouseenter', function () { var property_ID = $( this ).attr( 'data-rh-id' ).replace( /[^\d.]/g, '' ); markers.forEach( function ( marker ) { if ( propertiesMap && marker && marker.id == property_ID ) { google.maps.event.trigger( marker, 'click' ); updateZoomRepaint( 1 ); } } ); } ); } ); $( popupInfoSelectors ).on( 'mouseleave', function () { updateZoomRepaint( 14 ); closeOpenedWindows(); } ); return false; }; var RHisMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test( navigator.userAgent ) ? true : false; if ( ! RHisMobile ) { realhomesInfoboxPopupTrigger(); } } realhomes_update_google_map( propertiesMapData ); // Clicking anywhere on the map will close opened infoboxes for better UX var OpenedInfoBox = document.getElementsByClassName( 'infoBox' ); var ListingMap = document.getElementById( 'listing-map' ); document.addEventListener( 'click', function ( event ) { if ( typeof OpenedInfoBox[0] !== "undefined" ) { // Detect the clicked target var map_container = ListingMap.contains( event.target ); var infobox_container = OpenedInfoBox[0].contains( event.target ); // Close infoboxes only if clicked outside the infobox's container if ( ! infobox_container && map_container ) { closeOpenedWindows(); } } } ); } )( jQuery );; /** * JavaScript Related to Search Form */ (function ($) { "use strict"; /** * Change Min and Max Price fields based on selected status for rent */ if ( localizedSearchParams.rent_slug !== undefined) { var property_status_changed = function (new_status) { var price_for_others = $('.advance-search-form .price-for-others'); var price_for_rent = $('.advance-search-form .price-for-rent'); if (price_for_others.length > 0 && price_for_rent.length > 0) { if (new_status == localizedSearchParams.rent_slug) { price_for_others.addClass('hide-fields').find('select').prop('disabled', true); price_for_rent.removeClass('hide-fields').find('select').prop('disabled', false); } else { price_for_rent.addClass('hide-fields').find('select').prop('disabled', true); price_for_others.removeClass('hide-fields').find('select').prop('disabled', false); } } } $('.advance-search-form #select-status').change(function (e) { var selected_status = $(this).val(); property_status_changed(selected_status); }); /* On page load ( as on search page ) */ var selected_status = $('.advance-search-form #select-status').val(); if (selected_status == localizedSearchParams.rent_slug) { property_status_changed(selected_status); } } /** * Max and Min Price * Shows red outline if min price is bigger than max price */ /* for normal prices */ $('#select-min-price,#select-max-price').change(function (obj, e) { var min_text_val = $('#select-min-price').val(); var min_int_val = (isNaN(min_text_val)) ? 0 : parseInt(min_text_val); var max_text_val = $('#select-max-price').val(); var max_int_val = (isNaN(max_text_val)) ? 0 : parseInt(max_text_val); if ((min_int_val >= max_int_val) && (min_int_val != 0) && (max_int_val != 0)) { $('#select-min-price,#select-max-price').siblings('button.dropdown-toggle').css('outline', '1px solid red'); } else { $('#select-min-price,#select-max-price').siblings('button.dropdown-toggle').css('outline', 'none'); } }); /* for rent prices */ $('#select-min-price-for-rent, #select-max-price-for-rent').change(function (obj, e) { var min_text_val = $('#select-min-price-for-rent').val(); var min_int_val = (isNaN(min_text_val)) ? 0 : parseInt(min_text_val); var max_text_val = $('#select-max-price-for-rent').val(); var max_int_val = (isNaN(max_text_val)) ? 0 : parseInt(max_text_val); if ((min_int_val >= max_int_val) && (min_int_val != 0) && (max_int_val != 0)) { $('#select-min-price-for-rent, #select-max-price-for-rent').siblings('button.dropdown-toggle').css('outline', '1px solid red'); } else { $('#select-min-price-for-rent, #select-max-price-for-rent').siblings('button.dropdown-toggle').css('outline', 'none'); } }); /** * Max and Min Area * To show red outline if min is bigger than max */ $('#min-area,#max-area').change(function (obj, e) { var min_text_val = $('#min-area').val(); var min_int_val = (isNaN(min_text_val)) ? 0 : parseInt(min_text_val); var max_text_val = $('#max-area').val(); var max_int_val = (isNaN(max_text_val)) ? 0 : parseInt(max_text_val); if ((min_int_val >= max_int_val) && (min_int_val != 0) && (max_int_val != 0)) { $('#min-area,#max-area').css('outline', '1px solid red'); } else { $('#min-area,#max-area').css('outline', 'none'); } }); /** * Disable empty values on submission to reduce query string size */ $('.advance-search-form').submit(function (event) { var searchFormElements = $(this).find(':input'); $.each(searchFormElements, function (index, element) { if (element.value == '' || element.value == 'any') { if (!element.disabled) { element.disabled = true; } } }); }); /** * Add to compare -- Search Page */ function removeBorder () { let screenWidth = $(window).width(); let isRtl = $('body').hasClass('rtl'); if ((979 < screenWidth && 1200 > screenWidth) || (767 >= screenWidth && 500 <= screenWidth)) { if (!isRtl) { let addToCompareSpan = $('.page-template-template-search .property-item .compare-meta span.add-to-compare-span').length; if (addToCompareSpan) { $('.page-template-template-search .property-item .compare-meta span').css({ 'border': 'none' }); $('.page-template-template-search .property-item .compare-meta span.add-to-compare-span').css({ 'margin-right': '-20px', 'border-left': 'none' }); } } else { let addToCompareSpan = $('.page-template-template-search .property-item .compare-meta span.add-to-compare-span').length; if (addToCompareSpan) { $('.page-template-template-search .property-item .compare-meta span').css({ 'border-left': 'none' }); $('.page-template-template-search .property-item .compare-meta span.add-to-compare-span').css({ 'margin-left': '-20px', 'border-right': 'none', 'float': 'left' }); } } } else if (500 <= screenWidth) { if (!isRtl) { let addToCompareSpan = $('.page-template-template-search .property-item .compare-meta span.add-to-compare-span').length; if (addToCompareSpan) { $('.page-template-template-search .property-item .compare-meta span:nth-last-child(2)').css({ 'border': 'none' }); $('.page-template-template-search .property-item .compare-meta span.add-to-compare-span').css({ 'margin-right': '-20px', 'border-left': '1px solid #dedede' }); } } else { let addToCompareSpan = $('.page-template-template-search .property-item .compare-meta span.add-to-compare-span').length; if (addToCompareSpan) { $('.page-template-template-search .property-item .compare-meta span:nth-last-child(2)').css({ 'border': 'none' }); $('.page-template-template-search .property-item .compare-meta span.add-to-compare-span').css({ 'margin-left': '-20px', 'border-right': '1px solid #dedede', 'float': 'left' }); } } } else { if (!isRtl) { let addToCompareSpan = $('.page-template-template-search .property-item .compare-meta span.add-to-compare-span').length; if (addToCompareSpan) { $('.page-template-template-search .property-item .compare-meta span.add-to-compare-span').css({ 'margin-right': '0', 'border-left': 'none' }); } } else { let addToCompareSpan = $('.page-template-template-search .property-item .compare-meta span.add-to-compare-span').length; if (addToCompareSpan) { $('.page-template-template-search .property-item .compare-meta span.add-to-compare-span').css({ 'margin-right': '0', 'border-left': 'none', 'float': 'right' }); } } } } removeBorder(); /* Execute again on window resize. */ $(window).on('resize', function () { removeBorder(); }); /** * Ajax Search for keyword in Search Form */ $(document).ready(function () { $('#keyword-txt').keyup(function () { var words = $(this).val().split(' '); var wordcounts = words.length; if (wordcounts > 1) { $('.rh_sfoi_data_fetch_list').slideDown(); $('.rh_sfoi_ajax_loader').show(); $.ajax({ url: frontEndAjaxUrl.sfoiajaxurl, type: 'POST', data: { action: 'rh_sfoi_data_fetch', keyword: $(this).val() }, success: function (data) { $('.rh_sfoi_data_fetch_list').html(data); $('.rh_sfoi_ajax_loader').hide(); } }); } else { $('.rh_sfoi_data_fetch_list').slideUp(); } }); }); })(jQuery); ; ( function ( $ ) { "use strict"; const $window = $( window ), $body = $( 'body' ), isRtl = $body.hasClass( 'rtl' ); /** * Sticky footer related code. */ function footerStick() { $( '.rh_wrap_stick_footer' ).css( 'padding-bottom', $( '.rh_sticky_wrapper_footer' ).outerHeight() + 'px' ) } /** * Form AJAX validation and submission * Validation Plugin : https://jqueryvalidation.org/ * Form Ajax Plugin : http://www.malsup.com/jquery/form/ * * @since 4.0.0 */ function realhomesValidateForm( form_id ) { const form = $( form_id ); if ( ! form.length ) { return false; } const submitButton = form.find( '.submit-button' ), ajaxLoader = form.find( '.ajax-loader' ), messageContainer = form.find( '.message-container' ), errorContainer = form.find( ".error-container" ), formOptions = { beforeSubmit : function () { submitButton.attr( 'disabled', 'disabled' ); ajaxLoader.fadeIn( 'fast' ); messageContainer.fadeOut( 'fast' ); errorContainer.fadeOut( 'fast' ); }, success : function ( ajax_response, statusText, xhr, $form ) { const response = $.parseJSON( ajax_response ); ajaxLoader.fadeOut( 'fast' ); submitButton.removeAttr( 'disabled' ); if ( response.success ) { form.resetForm(); messageContainer.html( response.message ).fadeIn( 'fast' ); // call reset function if it exists if ( typeof inspiryResetReCAPTCHA == 'function' ) { inspiryResetReCAPTCHA(); } if ( typeof contactFromData !== 'undefined' ) { setTimeout( function () { window.location.replace( contactFromData.redirectPageUrl ); }, 1000 ); } else { setTimeout( function () { messageContainer.fadeOut( 'slow' ) }, 3000 ); } } else { errorContainer.html( response.message ).fadeIn( 'fast' ); } } }; if ( jQuery().validate && jQuery().ajaxSubmit ) { form.validate( { errorLabelContainer : errorContainer, submitHandler : function ( form ) { $( form ).ajaxSubmit( formOptions ); } } ); } } // Contact form realhomesValidateForm( '#contact-form' ); // Agent contact form realhomesValidateForm( '#agent-single-form' ); // Agency contact form realhomesValidateForm( '#agency-single-form' ); // Schedule A Tour form realhomesValidateForm( '#schedule-a-tour' ); /** * Sticky Header * * @since 4.0.0 */ $( function () { const stickyHeader = $( '.rh-sticky-header' ); if ( stickyHeader.length ) { const headerHeight = stickyHeader.height(); $window.on( 'scroll', function () { let $this = $( this ); if ( $this.width() > 1024 ) { if ( $this.scrollTop() > ( headerHeight + 100 ) ) { stickyHeader.addClass( 'sticked' ); } else { stickyHeader.removeClass( 'sticked' ); } } } ); } } ); $( document ).ready( function () { /** * Datepicker for Single Property */ let singleProperty = document.querySelector( '.single-property' ); if ( singleProperty ) { let satForm = document.getElementById( 'schedule-a-tour' ); if ( satForm ) { $( "#sat-date" ).datepicker( { minDate : 0, showAnim : 'slideDown', beforeShow : function ( input, inst ) { inst.dpDiv[0].classList.add( 'schedule-section-wrapper' ); } } ); } } /** * Sliders */ if ( jQuery().flexslider ) { // Gallery Post Slider const galleryPostSlider = $( '.gallery-post-slider' ); if ( galleryPostSlider.length ) { galleryPostSlider.flexslider( { animation : "slide", slideshow : false, controlNav : false, prevText : "", nextText : "", start : function ( slider ) { galleryPostSlider.find( '.clone' ).children().removeAttr( "data-fancybox" ); } } ); } } /** * Single Property Slider */ var singlePropertySlider = $( '.rh-ultra-property-slider' ); var singlePropertyCarousel = $( '.rh-ultra-horizontal-carousel-trigger' ); var singlePropertyVerticalCarousel = $( '.rh-ultra-vertical-carousel-trigger' ); // Responsive Nav $( '.rh-ultra-responsive-nav' ).hcOffcanvasNav( { disableAt : 1139, insertClose : true, insertBack : true, labelClose : mobileNavLabels.labelClose, labelBack : mobileNavLabels.labelBack, levelSpacing : 40, // levelTitleAsBack: true, // pushContent: '.rh_section', customToggle : '.rh-responsive-toggle' } ); // TODO: Dynamic Slides counter in future // if ( singlePropertySlider.length ) { // var currentSlide; // var slidesCount; // var sliderCounter = $( '.rh-slider-item-total' ); // // var rhUpdateSliderCounter = function ( slick, currentIndex ) { // // currentSlide = slick.slickCurrentSlide() + 1; // slidesCount = slick.slideCount; // // jQuery(sliderCounter).text(currentSlide + '/' + slidesCount) // // // if ( currentSlide - slidesCount !== 0 ) { // $( sliderCounter ).text( slidesCount - currentSlide ); // } else { // $( sliderCounter ).text( slidesCount ); // } // }; // // singlePropertySlider.on( 'init', function ( event, slick ) { // // $slider.append(sliderCounter); // rhUpdateSliderCounter( slick ); // } ); // // singlePropertySlider.on( 'afterChange', function ( event, slick, currentSlide ) { // rhUpdateSliderCounter( slick, currentSlide ); // } ); // // } let syncEnable = ''; let centerMode = false; singlePropertyCarousel.on( 'init', function ( event, slick ) { if ( singlePropertyCarousel.data( 'count' ) > singlePropertyCarousel.find( '.slick-active' ).length ) { syncEnable = singlePropertyCarousel; centerMode = true; } } ); singlePropertyVerticalCarousel.on( 'init', function ( event, slick ) { if ( singlePropertyVerticalCarousel.data( 'count' ) > singlePropertyVerticalCarousel.find( '.slick-active' ).length ) { syncEnable = singlePropertyCarousel; centerMode = true; } } ); singlePropertySlider.on( 'init afterChange', function ( event, slick ) { if ( syncEnable === '' ) { singlePropertyCarousel.find( '.slick-slide' ).removeClass( 'slick-current' ); singlePropertyCarousel.find( '.slick-slide' ).eq( slick.currentSlide ).addClass( "slick-current" ); } } ); singlePropertyCarousel.slick( { slidesToShow : 6, slidesToScroll : 1, asNavFor : singlePropertySlider, // dots: false, infinite : true, centerMode : centerMode, focusOnSelect : true, touchThreshold : 200, arrows : false, centerPadding : '0', rtl : isRtl, responsive : [ { breakpoint : 1200, settings : { slidesToShow : 4 } }, { breakpoint : 767, settings : { slidesToShow : 3 } } ] } ); singlePropertyVerticalCarousel.slick( { slidesToShow : 3, vertical : true, verticalSwiping : true, slidesToScroll : 1, asNavFor : singlePropertySlider, // dots: false, infinite : true, centerMode : centerMode, focusOnSelect : true, touchThreshold : 200, arrows : false, centerPadding : '0', responsive : [ { breakpoint : 1200, settings : { slidesToShow : 3, vertical : false, verticalSwiping : false, rtl : isRtl } } ] } ); singlePropertySlider.slick( { slidesToShow : 1, slidesToScroll : 1, arrows : true, fade : true, adaptiveHeight : true, infinite : true, rtl : isRtl, asNavFor : syncEnable // asNavFor: singlePropertyCarousel, } ); /** * Property Ratings */ if ( jQuery().barrating ) { $( '#rate-it' ).barrating( { theme : 'fontawesome-stars', initialRating : 5 } ); } /** * AJAX Pagination for Listing & Archive Pages */ const propertiesSection = $( '#properties-listing' ); if ( propertiesSection.hasClass( 'ajax-pagination' ) ) { const propertiesContainer = propertiesSection.find( '.rh-ultra-list-box, .rh-ultra-grid-box' ), svgLoader = propertiesSection.find( '.svg-loader' ), paginationContainer = $( '.rh_pagination' ), statsContainer = $( '.rh_pagination__stats' ), mapServiceType = localized.mapService.toString(), page_id = statsContainer.data( 'page-id' ); let cards_container = 'rh-ultra-list-box'; if ( propertiesSection.find( '.rh-ultra-grid-box' ).length ) { cards_container = 'rh-ultra-grid-box'; } $( 'body' ).on( 'click', '.rh-ultra-pagination .rh_pagination > a', function ( e ) { e.preventDefault(); let currentButton = $( this ); svgLoader.slideDown( 'fast' ); propertiesContainer.fadeTo( 'slow', 0.5 ); $( '.rh_pagination > a' ).removeClass( 'current' ); currentButton.addClass( 'current' ); let current_page = parseInt( currentButton.attr( 'data-page-number' ) ); statsContainer.attr( 'data-page', current_page ); paginationContainer.load( currentButton.attr( 'href' ) + ' ' + '.rh_pagination' ); statsContainer.load( currentButton.attr( 'href' ) + ' ' + '.rh_pagination__stats' ); let url = currentButton.attr( 'href' ) + ' ' + '.' + cards_container + ' > *'; propertiesContainer.load( url, function ( response, status, xhr ) { if ( status == 'success' ) { propertiesContainer.fadeTo( 100, 1, function () { } ); svgLoader.slideUp( 'fast' ); $( 'html, body' ).animate( { scrollTop : propertiesSection.find( '.' + cards_container ).offset().top - 120 }, 1000 ); inspirySelectPicker( 'body .inspiry_select_picker_trigger' ); rhUltraTooltip( '.rh-ui-tooltip' ); } else { propertiesContainer.fadeTo( 'slow', 1 ); } if ( typeof realhomesInfoboxPopupTrigger === 'function' ) { realhomesInfoboxPopupTrigger(); } // Binding Favorites & Compare Properties Features if ( typeof realhomes_update_favorites === 'function' ) { realhomes_update_favorites(); } if ( typeof realhomes_update_compare_properties === 'function' ) { realhomes_update_compare_properties(); } } ); // If this pagination is for ajax search results if ( propertiesSection.hasClass( 'realhomes_ajax_search' ) ) { realhomes_update_ajax_map_results( current_page ); let currentQueryStrings = statsContainer.data( 'query-strings' ); let searchURL = $( '.rh_page' ).data( 'search-url' ); if ( current_page === 1 ) { window.history.pushState( {}, '', searchURL + currentQueryStrings ); } else { window.history.pushState( {}, '', searchURL + 'page/' + current_page + '/' + currentQueryStrings ); } } else { $.ajax( { url : ajaxurl, type : 'post', data : { action : 'realhomes_map_ajax_search_results', page_id : page_id, page : current_page }, success : ( response ) => { let propertiesMapData = response.data.propertiesData; if ( typeof realhomes_update_open_street_map !== 'undefined' && typeof mapServiceType !== "undefined" && mapServiceType === 'openstreetmaps' ) { realhomes_update_open_street_map( propertiesMapData ); } else if ( typeof realhomes_update_mapbox !== 'undefined' && typeof mapServiceType !== "undefined" && mapServiceType === 'mapbox' ) { $( '#map-head' ).empty().append( '
' ); realhomes_update_mapbox( propertiesMapData ); } else if ( typeof realhomes_update_google_map !== 'undefined' ) { realhomes_update_google_map( propertiesMapData ); } } } ); window.history.pushState( {}, '', currentButton.attr( 'href' ) ); } window.history.pushState( {}, '', currentButton.attr( 'href' ) ); } ); } /** * Property Floor Plans */ $( '.rh-floor-plan-tab' ).click( function ( e ) { e.preventDefault(); if ( ! $( this ).hasClass( 'rh-current-tab' ) ) { $( '.rh-floor-plan-tab' ).removeClass( 'rh-current-tab' ); $( this ).addClass( 'rh-current-tab' ); $( ".rh-floor-plan" ).removeClass( 'rh-active-tab' ); $( ".rh-floor-plan[data-id='" + $( this ).attr( 'data-id' ) + "']" ).addClass( "rh-active-tab" ); } } ); $( '.rh_wrapper_property_videos_slider' ).slick( { slidesToShow : 1, slidesToScroll : 1, arrows : false, dots : true, fade : true } ); /** * Post Nav Support */ $( function () { var post_nav = $( '.inspiry-post-nav' ); $( window ).on( 'scroll', function () { if ( $( window ).width() > 980 ) { if ( $( this ).scrollTop() > 650 ) { post_nav.fadeIn( 'fast' ).css( "display", "flex" ); return; } } post_nav.fadeOut( 'fast' ); } ); } ); /** * Scroll to Top */ $( function () { const scroll_anchor = $( '#scroll-top' ); $( window ).on( 'scroll', function () { if ( $( this ).scrollTop() > 250 ) { scroll_anchor.addClass( 'show' ); return; } scroll_anchor.removeClass( 'show' ); } ); scroll_anchor.on( 'click', function ( event ) { event.preventDefault(); $( 'html, body' ).animate( { scrollTop : 0 }, 'slow' ); } ); } ); /** * Sidebar Properties Sliders */ var ereSideBarSlider = $( '.ere-ultra-properties-slider' ); ereSideBarSlider.each( function () { var thisContainer = $( this ).next( '.rh-ultra-widget-dots' ); $( this ).owlCarousel( { stagePadding : 0, singleItem : true, loop : false, dots : true, nav : true, items : 1, margin : 10, navText : ['', ''], dotsContainer : thisContainer } ); } ); /** * Properties Sorting */ function insertParam( key, value ) { key = encodeURI( key ); value = encodeURI( value ); var kvp = document.location.search.substr( 1 ).split( '&' ); var i = kvp.length; var x; while ( i-- ) { x = kvp[i].split( '=' ); if ( x[0] == key ) { x[1] = value; kvp[i] = x.join( '=' ); break; } } if ( i < 0 ) { kvp[kvp.length] = [key, value].join( '=' ); } //this will reload the page, it's likely better to store this until finished document.location.search = kvp.join( '&' ); } $( '#sort-properties' ).on( 'change', function () { var key = 'sortby'; var value = $( this ).val(); insertParam( key, value ); } ); /** * Properties Gallery Isotope */ if ( $.isFunction( $.fn.isotope ) ) { const container = $( "#properties-gallery-container" ), filterLinks = $( "#filter-by a" ), isotopeOptions = { filter : "*", layoutMode : 'fitRows', itemSelector : '.isotope-item', animationEngine : 'best-available' }; // RTL support if ( isRtl ) { isotopeOptions.originLeft = false; } // To fix floating bugs due to variation in height setTimeout( function () { container.isotope( isotopeOptions ); }, 1000 ); filterLinks.on( 'click', function ( event ) { let self = $( this ), selector = self.data( 'filter' ); container.isotope( { filter : '.' + selector } ); filterLinks.removeClass( 'active' ); self.addClass( 'active' ); event.preventDefault(); } ); } // footerStick - Run on document ready. footerStick(); function rhDecorateWhatsAppLink() { //set up the url var url = 'https://api.whatsapp.com/send?text='; var thisShareData = $( '.share-this' ); //get property title var name = thisShareData.data( 'property-name' ); //get property permalink var permalink = thisShareData.data( 'property-permalink' ); //encode the text var encodedText = encodeURIComponent( name + ' ' + permalink ); //find the link var whatsApp = $( ".inspiry_whats_app_share_link" ); //set the href attribute on the link whatsApp.attr( 'href', url + encodedText ); } rhDecorateWhatsAppLink(); // Scripts to run on window load and resize events. $window.on( 'load resize', function () { footerStick(); } ); } ); } )( jQuery );; /*! jQuery UI - v1.13.3 - 2024-04-26 * https://jqueryui.com * Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-patch.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js * Copyright jQuery Foundation and other contributors; Licensed MIT */ !function(t){"use strict";"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)}(function(x){"use strict";var t,e,i,n,W,C,o,s,r,l,a,h,u;function E(t,e,i){return[parseFloat(t[0])*(a.test(t[0])?e/100:1),parseFloat(t[1])*(a.test(t[1])?i/100:1)]}function L(t,e){return parseInt(x.css(t,e),10)||0}function N(t){return null!=t&&t===t.window}x.ui=x.ui||{},x.ui.version="1.13.3", /*! * jQuery UI :data 1.13.3 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors * Released under the MIT license. * https://jquery.org/license */ x.extend(x.expr.pseudos,{data:x.expr.createPseudo?x.expr.createPseudo(function(e){return function(t){return!!x.data(t,e)}}):function(t,e,i){return!!x.data(t,i[3])}}), /*! * jQuery UI Disable Selection 1.13.3 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors * Released under the MIT license. * https://jquery.org/license */ x.fn.extend({disableSelection:(t="onselectstart"in document.createElement("div")?"selectstart":"mousedown",function(){return this.on(t+".ui-disableSelection",function(t){t.preventDefault()})}),enableSelection:function(){return this.off(".ui-disableSelection")}}), /*! * jQuery UI Focusable 1.13.3 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors * Released under the MIT license. * https://jquery.org/license */ x.ui.focusable=function(t,e){var i,n,o,s=t.nodeName.toLowerCase();return"area"===s?(o=(i=t.parentNode).name,!(!t.href||!o||"map"!==i.nodeName.toLowerCase())&&0<(i=x("img[usemap='#"+o+"']")).length&&i.is(":visible")):(/^(input|select|textarea|button|object)$/.test(s)?(n=!t.disabled)&&(o=x(t).closest("fieldset")[0])&&(n=!o.disabled):n="a"===s&&t.href||e,n&&x(t).is(":visible")&&function(t){var e=t.css("visibility");for(;"inherit"===e;)t=t.parent(),e=t.css("visibility");return"visible"===e}(x(t)))},x.extend(x.expr.pseudos,{focusable:function(t){return x.ui.focusable(t,null!=x.attr(t,"tabindex"))}}),x.fn._form=function(){return"string"==typeof this[0].form?this.closest("form"):x(this[0].form)}, /*! * jQuery UI Form Reset Mixin 1.13.3 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors * Released under the MIT license. * https://jquery.org/license */ x.ui.formResetMixin={_formResetHandler:function(){var e=x(this);setTimeout(function(){var t=e.data("ui-form-reset-instances");x.each(t,function(){this.refresh()})})},_bindFormResetHandler:function(){var t;this.form=this.element._form(),this.form.length&&((t=this.form.data("ui-form-reset-instances")||[]).length||this.form.on("reset.ui-form-reset",this._formResetHandler),t.push(this),this.form.data("ui-form-reset-instances",t))},_unbindFormResetHandler:function(){var t;this.form.length&&((t=this.form.data("ui-form-reset-instances")).splice(x.inArray(this,t),1),t.length?this.form.data("ui-form-reset-instances",t):this.form.removeData("ui-form-reset-instances").off("reset.ui-form-reset"))}},x.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()), /*! * jQuery UI Support for jQuery core 1.8.x and newer 1.13.3 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors * Released under the MIT license. * https://jquery.org/license * */ x.expr.pseudos||(x.expr.pseudos=x.expr[":"]),x.uniqueSort||(x.uniqueSort=x.unique),x.escapeSelector||(e=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,i=function(t,e){return e?"\0"===t?"�":t.slice(0,-1)+"\\"+t.charCodeAt(t.length-1).toString(16)+" ":"\\"+t},x.escapeSelector=function(t){return(t+"").replace(e,i)}),x.fn.even&&x.fn.odd||x.fn.extend({even:function(){return this.filter(function(t){return t%2==0})},odd:function(){return this.filter(function(t){return t%2==1})}}), /*! * jQuery UI Keycode 1.13.3 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors * Released under the MIT license. * https://jquery.org/license */ x.ui.keyCode={BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}, /*! * jQuery UI Labels 1.13.3 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors * Released under the MIT license. * https://jquery.org/license */ x.fn.labels=function(){var t,e,i;return this.length?this[0].labels&&this[0].labels.length?this.pushStack(this[0].labels):(e=this.eq(0).parents("label"),(t=this.attr("id"))&&(i=(i=this.eq(0).parents().last()).add((i.length?i:this).siblings()),t="label[for='"+x.escapeSelector(t)+"']",e=e.add(i.find(t).addBack(t))),this.pushStack(e)):this.pushStack([])},x.ui.plugin={add:function(t,e,i){var n,o=x.ui[t].prototype;for(n in i)o.plugins[n]=o.plugins[n]||[],o.plugins[n].push([e,i[n]])},call:function(t,e,i,n){var o,s=t.plugins[e];if(s&&(n||t.element[0].parentNode&&11!==t.element[0].parentNode.nodeType))for(o=0;o
")).children()[0],x("body").append(e),t=i.offsetWidth,e.css("overflow","scroll"),t===(i=i.offsetWidth)&&(i=e[0].clientWidth),e.remove(),n=t-i)},getScrollInfo:function(t){var e=t.isWindow||t.isDocument?"":t.element.css("overflow-x"),i=t.isWindow||t.isDocument?"":t.element.css("overflow-y"),e="scroll"===e||"auto"===e&&t.widthW(C(n),C(o))?s.important="horizontal":s.important="vertical",f.using.call(this,t,s)}),r.offset(x.extend(h,{using:t}))})):h.apply(this,arguments)},x.ui.position={fit:{left:function(t,e){var i,n=e.within,o=n.isWindow?n.scrollLeft:n.offset.left,n=n.width,s=t.left-e.collisionPosition.marginLeft,r=o-s,l=s+e.collisionWidth-n-o;n",options:{classes:{},disabled:!1,create:null},_createWidget:function(t,e){e=x(e||this.defaultElement||this)[0],this.element=x(e),this.uuid=c++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=x(),this.hoverable=x(),this.focusable=x(),this.classesElementLookup={},e!==this&&(x.data(e,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===e&&this.destroy()}}),this.document=x(e.style?e.ownerDocument:e.document||e),this.window=x(this.document[0].defaultView||this.document[0].parentWindow)),this.options=x.widget.extend({},this.options,this._getCreateOptions(),t),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:x.noop,_create:x.noop,_init:x.noop,destroy:function(){var i=this;this._destroy(),x.each(this.classesElementLookup,function(t,e){i._removeClass(e,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:x.noop,widget:function(){return this.element},option:function(t,e){var i,n,o,s=t;if(0===arguments.length)return x.widget.extend({},this.options);if("string"==typeof t)if(s={},t=(i=t.split(".")).shift(),i.length){for(n=s[t]=x.widget.extend({},this.options[t]),o=0;o").text(t).html()},hide:!0,items:"[title]:not([disabled])",position:{my:"left top+15",at:"left bottom",collision:"flipfit flip"},show:!0,track:!1,close:null,open:null},_addDescribedBy:function(t,i){var e=(t.attr("aria-describedby")||"").split(/\s+/);e.push(i),t.data("ui-tooltip-id",i).attr("aria-describedby",String.prototype.trim.call(e.join(" ")))},_removeDescribedBy:function(t){var i=t.data("ui-tooltip-id"),e=(t.attr("aria-describedby")||"").split(/\s+/),i=r.inArray(i,e);-1!==i&&e.splice(i,1),t.removeData("ui-tooltip-id"),(e=String.prototype.trim.call(e.join(" ")))?t.attr("aria-describedby",e):t.removeAttr("aria-describedby")},_create:function(){this._on({mouseover:"open",focusin:"open"}),this.tooltips={},this.parents={},this.liveRegion=r("
").attr({role:"log","aria-live":"assertive","aria-relevant":"additions"}).appendTo(this.document[0].body),this._addClass(this.liveRegion,null,"ui-helper-hidden-accessible"),this.disabledTitles=r([])},_setOption:function(t,i){var e=this;this._super(t,i),"content"===t&&r.each(this.tooltips,function(t,i){e._updateContent(i.element)})},_setOptionDisabled:function(t){this[t?"_disable":"_enable"]()},_disable:function(){var o=this;r.each(this.tooltips,function(t,i){var e=r.Event("blur");e.target=e.currentTarget=i.element[0],o.close(e,!0)}),this.disabledTitles=this.disabledTitles.add(this.element.find(this.options.items).addBack().filter(function(){var t=r(this);if(t.is("[title]"))return t.data("ui-tooltip-title",t.attr("title")).removeAttr("title")}))},_enable:function(){this.disabledTitles.each(function(){var t=r(this);t.data("ui-tooltip-title")&&t.attr("title",t.data("ui-tooltip-title"))}),this.disabledTitles=r([])},open:function(t){var e=this,i=r(t?t.target:this.element).closest(this.options.items);i.length&&!i.data("ui-tooltip-id")&&(i.attr("title")&&i.data("ui-tooltip-title",i.attr("title")),i.data("ui-tooltip-open",!0),t&&"mouseover"===t.type&&i.parents().each(function(){var t,i=r(this);i.data("ui-tooltip-open")&&((t=r.Event("blur")).target=t.currentTarget=this,e.close(t,!0)),i.attr("title")&&(i.uniqueId(),e.parents[this.id]={element:this,title:i.attr("title")},i.attr("title",""))}),this._registerCloseHandlers(t,i),this._updateContent(i,t))},_updateContent:function(i,e){var t=this.options.content,o=this,n=e?e.type:null;if("string"==typeof t||t.nodeType||t.jquery)return this._open(e,i,t);(t=t.call(i[0],function(t){o._delay(function(){i.data("ui-tooltip-open")&&(e&&(e.type=n),this._open(e,i,t))})}))&&this._open(e,i,t)},_open:function(t,i,e){var o,n,s,l=r.extend({},this.options.position);function a(t){l.of=t,o.is(":hidden")||o.position(l)}e&&((s=this._find(i))?s.tooltip.find(".ui-tooltip-content").html(e):(i.is("[title]")&&(t&&"mouseover"===t.type?i.attr("title",""):i.removeAttr("title")),s=this._tooltip(i),o=s.tooltip,this._addDescribedBy(i,o.attr("id")),o.find(".ui-tooltip-content").html(e),this.liveRegion.children().hide(),(s=r("
").html(o.find(".ui-tooltip-content").html())).removeAttr("name").find("[name]").removeAttr("name"),s.removeAttr("id").find("[id]").removeAttr("id"),s.appendTo(this.liveRegion),this.options.track&&t&&/^mouse/.test(t.type)?(this._on(this.document,{mousemove:a}),a(t)):o.position(r.extend({of:i},this.options.position)),o.hide(),this._show(o,this.options.show),this.options.track&&this.options.show&&this.options.show.delay&&(n=this.delayedShow=setInterval(function(){o.is(":visible")&&(a(l.of),clearInterval(n))},13)),this._trigger("open",t,{tooltip:o})))},_registerCloseHandlers:function(t,i){var e={keyup:function(t){t.keyCode===r.ui.keyCode.ESCAPE&&((t=r.Event(t)).currentTarget=i[0],this.close(t,!0))}};i[0]!==this.element[0]&&(e.remove=function(){var t=this._find(i);t&&this._removeTooltip(t.tooltip)}),t&&"mouseover"!==t.type||(e.mouseleave="close"),t&&"focusin"!==t.type||(e.focusout="close"),this._on(!0,i,e)},close:function(t){var i,e=this,o=r(t?t.currentTarget:this.element),n=this._find(o);n?(i=n.tooltip,n.closing||(clearInterval(this.delayedShow),o.data("ui-tooltip-title")&&!o.attr("title")&&o.attr("title",o.data("ui-tooltip-title")),this._removeDescribedBy(o),n.hiding=!0,i.stop(!0),this._hide(i,this.options.hide,function(){e._removeTooltip(r(this))}),o.removeData("ui-tooltip-open"),this._off(o,"mouseleave focusout keyup"),o[0]!==this.element[0]&&this._off(o,"remove"),this._off(this.document,"mousemove"),t&&"mouseleave"===t.type&&r.each(this.parents,function(t,i){r(i.element).attr("title",i.title),delete e.parents[t]}),n.closing=!0,this._trigger("close",t,{tooltip:i}),n.hiding)||(n.closing=!1)):o.removeData("ui-tooltip-open")},_tooltip:function(t){var i=r("
").attr("role","tooltip"),e=r("
").appendTo(i),o=i.uniqueId().attr("id");return this._addClass(e,"ui-tooltip-content"),this._addClass(i,"ui-tooltip","ui-widget ui-widget-content"),i.appendTo(this._appendTo(t)),this.tooltips[o]={element:t,tooltip:i}},_find:function(t){t=t.data("ui-tooltip-id");return t?this.tooltips[t]:null},_removeTooltip:function(t){clearInterval(this.delayedShow),t.remove(),delete this.tooltips[t.attr("id")]},_appendTo:function(t){t=t.closest(".ui-front, dialog");return t=t.length?t:this.document[0].body},_destroy:function(){var o=this;r.each(this.tooltips,function(t,i){var e=r.Event("blur"),i=i.element;e.target=e.currentTarget=i[0],o.close(e,!0),r("#"+t).remove(),i.data("ui-tooltip-title")&&(i.attr("title")||i.attr("title",i.data("ui-tooltip-title")),i.removeData("ui-tooltip-title"))}),this.liveRegion.remove()}}),!1!==r.uiBackCompat&&r.widget("ui.tooltip",r.ui.tooltip,{options:{tooltipClass:null},_tooltip:function(){var t=this._superApply(arguments);return this.options.tooltipClass&&t.tooltip.addClass(this.options.tooltipClass),t}}),r.ui.tooltip});; ( function( $ ) { "use strict"; $( document ).ready( function() { var progress_bar = false; var rhLoginMessageBox = $('.rh_login_modal_messages'); var rhLoginMessagesCommon = $('.rh_login_modal_messages .rh_modal__msg'); var rhLoginModalLoader = $('.rh_modal_login_loader'); if ( $('#rh_progress').length ) { progress_bar = new ProgressBar.Line('#rh_progress', { easing: 'easeInOut', color: '#1ea69a', strokeWidth: 0.3, }); } // var modal_switch = $( 'div.switch' ); // var modal_switch_link = modal_switch.find( 'a' ); // modal_switch_link.click( function(e) { // // e.preventDefault(); // Prevent default. // // var switch_to = $( this ).attr( 'data-switch' ); // Switch to modal. // var target = ''; // // if ( 'forgot' === switch_to ) { // // var switch_parent = $( this ).parents( 'div.modal' ); // target = $( 'div.rh_modal__forgot_wrap' ); // switch_parent.slideToggle( 'slow' ); // target.slideToggle( 'slow' ); // // } else if ( 'register' === switch_to ) { // // var switch_parent = $( this ).parents( 'div.modal' ); // // target = $( 'div.rh_modal__register_wrap' ); // switch_parent.slideToggle( 'slow' ); // target.slideToggle( 'slow' ); // // } else if ( 'login' === switch_to ) { // // var switch_parent = $( this ).parents( 'div.modal' ); // // target = $( 'div.rh_modal__login_wrap' ); // switch_parent.slideToggle( 'slow' ); // target.slideToggle( 'slow' ); // // } // } ); if ( jQuery().validate && jQuery().ajaxSubmit ) { /** * AJAX Login Form */ var loginButton = $('#login-button'), // loginAjaxLoader = $('#login-loader'), loginError = $("#login-error" ), loginMessage = $('#login-message'); var loginOptions = { beforeSubmit: function () { if ( progress_bar ) { progress_bar.set(0); progress_bar.animate(1); } loginButton.attr('disabled', 'disabled'); rhLoginModalLoader.removeClass('rh_modal_login_loader_hide'); // loginAjaxLoader.fadeIn( 200 ); }, success: function (ajax_response, statusText, xhr, $form) { var response = $.parseJSON( ajax_response ); // loginAjaxLoader.fadeOut( 100 ); loginButton.removeAttr('disabled'); rhLoginModalLoader.addClass('rh_modal_login_loader_hide'); rhLoginMessagesCommon.fadeOut( 50,'swing',function () { rhLoginMessageBox.slideDown('fast'); } ); if ( response.success ) { loginMessage.html( response.message ).fadeIn( 200 ); if ( window.location.href == response.redirect ) { window.location.reload( true ); } else { window.location.replace( response.redirect ); } } else { loginError.html( response.message ).fadeIn( 200 ); // call reset function if it exists if ( typeof inspiryResetReCAPTCHA == 'function' ) { inspiryResetReCAPTCHA(); } } } }; $('#rh_modal__login_form, #login-form').validate({ submitHandler: function (form) { $(form).ajaxSubmit(loginOptions); } }); /** * AJAX Register Form */ var registerButton = $('#register-button'), registerError = $("#register-error" ), registerMessage = $('#register-message'); var registerOptions = { beforeSubmit: function () { if ( progress_bar ) { progress_bar.set(0); progress_bar.animate(1); } registerButton.attr('disabled', 'disabled'); rhLoginModalLoader.removeClass('rh_modal_login_loader_hide'); }, success: function (ajax_response, statusText, xhr, $form) { var response = $.parseJSON( ajax_response ); // registerAjaxLoader.fadeOut('fast'); registerButton.removeAttr('disabled'); rhLoginMessagesCommon.fadeOut( 50,'swing',function () { rhLoginMessageBox.slideDown('fast'); } ); rhLoginModalLoader.addClass('rh_modal_login_loader_hide'); if ( response.success ) { registerMessage.html( response.message ).fadeIn('fast'); $form.resetForm(); } else { registerError.html( response.message ).fadeIn('fast'); // call reset function if it exists if ( typeof inspiryResetReCAPTCHA == 'function' ) { inspiryResetReCAPTCHA(); } } } }; $('#rh_modal__register_form, #register-form').validate({ rules: { register_username: { required: true }, register_email: { required: true, email: true } }, submitHandler: function (form) { $(form).ajaxSubmit(registerOptions); } }); /** * Forgot Password Form */ var forgotButton = $('#forgot-button'), // forgotAjaxLoader = $('#forgot-loader'), forgotError = $("#forgot-error" ), forgotMessage = $('#forgot-message'); var forgotOptions = { beforeSubmit: function () { if ( progress_bar ) { progress_bar.set(0); progress_bar.animate(1); } forgotButton.attr('disabled', 'disabled'); // forgotAjaxLoader.fadeIn('fast'); // forgotMessage.fadeOut('fast'); // forgotError.fadeOut('fast'); rhLoginModalLoader.removeClass('rh_modal_login_loader_hide'); }, success: function (ajax_response, statusText, xhr, $form) { var response = $.parseJSON( ajax_response ); // forgotAjaxLoader.fadeOut('fast'); forgotButton.removeAttr('disabled'); rhLoginMessagesCommon.fadeOut( 50,'swing',function () { rhLoginMessageBox.slideDown('fast'); } ); rhLoginModalLoader.addClass('rh_modal_login_loader_hide'); if ( response.success ) { forgotMessage.html( response.message ).fadeIn('fast'); $form.resetForm(); } else { forgotError.html( response.message ).fadeIn('fast'); // call reset function if it exists if ( typeof inspiryResetReCAPTCHA == 'function' ) { inspiryResetReCAPTCHA(); } } } }; $('#rh_modal__forgot_form, #forgot-form').validate({ submitHandler: function (form) { $(form).ajaxSubmit(forgotOptions); } }); } /** * Forgot Form */ $('.rh_form #rh_modal__forgot_form').slideUp('fast'); $('.rh_form .toggle-forgot-form').on('click', function(event){ event.preventDefault(); $('.rh_form #rh_modal__forgot_form').slideToggle('fast'); }); /*-----------------------------------------------------------------------------------*/ /* Login Modal /*-----------------------------------------------------------------------------------*/ function rhSetLoginFormHeight(){ var heights = $("div.rh_form_modal").map(function () { return $(this).outerHeight(); }).get(); var maxHeight = Math.max.apply(null, heights); $('.rh_wrapper_login_forms').css('height',maxHeight); } $(window).resize(rhSetLoginFormHeight); $('.rh_login_target').on('click',function () { if(!$(this).hasClass('rh_active')){ $('.rh_login_tab').removeClass('rh_active'); $(this).addClass('rh_active'); $('.rh_form_modal').slideUp(500); $('.rh_login_form').slideDown(500); } }); $('.rh_register_target').on('click',function () { if(!$(this).hasClass('rh_active')){ $('.rh_login_tab').removeClass('rh_active'); $(this).addClass('rh_active'); $('.rh_form_modal').slideUp(500); $('.rh_register_form').slideDown(500); } }); $('.rh_forget_password_trigger').on('click',function () { $('.rh_login_tab').removeClass('rh_active'); $('.rh_form_modal').slideUp(500); $('.rh_password_reset_form').slideDown(500); }); var rhLoginViz = false; $('.rh-ultra-menu-user-profile, .rh_menu__user_profile, .rhea_menu__user_profile svg, .rhea_menu__user_profile > span, .rh-user-account-profile-image .user-icon').on('click',function (e) { // e.preventDefault(); $('.rh_login_modal_wrapper').css("display", "flex").hide().fadeIn(500); rhSetLoginFormHeight(); rhLoginViz = true; }); // A common class to ask for login where needed in the theme. $('.ask-for-login').on('click',function (event) { event.preventDefault(); $('.rh_login_modal_wrapper').css("display", "flex").hide().fadeIn(500); rhSetLoginFormHeight(); rhLoginViz = true; }); $('.rh_login_close').on('click',function () { $('.rh_login_modal_wrapper').fadeOut(500); rhLoginViz = false; $('.rh_modal_field').val(''); }); $('body').on('click','.rh_login_modal_wrapper',function(e) { if (e.target === this){ $(this).fadeOut(500); rhLoginMessageBox.slideUp('fast'); } }); $('body').on('click','.rh_login_modal_box',function(e) { if (e.target !== rhLoginMessageBox){ rhLoginMessageBox.slideUp('fast'); // rhLoginMessageBox.find('p').fadeOut('fast'); } }); $('.rh_login_close_message').on('click',function () { rhLoginMessageBox.slideUp('fast'); }); } ); } )( jQuery ); ; ( function ( $ ) { "use strict"; $( document ).ready( function () { // defining variables for multiple use const $body = $( 'body' ); let comparePropStorageKey = 'inspiry_compare'; if ( typeof comparePropVars !== "undefined" ) { comparePropStorageKey += comparePropVars.id } // Render the added properties to compare data after page load. render_compare_properties_data(); // Required for Ajax Pagination. window.realhomes_update_compare_properties = function () { // Get compare properties data from localStorage. let properties_string = window.localStorage.getItem( comparePropStorageKey ); if ( null != properties_string ) { let properties_array_string = properties_string.split( '||' ); if ( Array.isArray( properties_array_string ) && properties_array_string.length && properties_array_string[0] !== '' ) { // Preparing an array for add to compare button placeholder let properties_array = []; properties_array_string.forEach( function ( property ) { properties_array.push( JSON.parse( property ) ); } ); properties_array.forEach( function ( property ) { // Highlight the add to compare button. add_to_compare_btn_placeholder( property.property_id, true ); } ); } } } // Toggle the compare properties tray and its button. $( 'html' ).on( 'click', '.rh_floating_compare_button', function ( e ) { $( '.rh_wrapper_properties_compare' ).toggleClass( 'rh_compare_open' ); $( '.rh_fixed_side_bar_compare' ).fadeToggle( 200 ); e.stopPropagation(); } ); // Add property to compare. $body.on( 'click', 'a.rh_trigger_compare', function ( event ) { event.preventDefault(); // Prepare property data that has to be added. let compare_link = $( this ); // Add to compare button. let property_id = compare_link.parent().data( 'property-id' ); // Do nothing if property ID is not defined. if ( undefined === property_id ) { return; } let property_img = compare_link.parent().data( 'property-image' ), property_url = compare_link.parent().data( 'property-url' ), property_title = compare_link.parent().data( 'property-title' ); // Highlight the add to compare button. add_to_compare_btn_placeholder( property_id, true ); // Apply properties compare limit. apply_compare_properties_limit(); // Add property card to the compare tray. add_property_to_compare_tray( property_id, property_title, property_img, property_url ); // Add property to localStorage. add_property_to_localStorage( property_id, property_title, property_img, property_url ); // Update compare properties button url. update_compare_button_url(); // Update compare properties tray counter with number of properties available to compare. update_compare_tray_counter(); // Control the properties compare tray display. control_compare_tray_display(); } ); /** * Remove property from compare. * */ $body.on( 'click', 'a.rh_compare__remove, span.compare-placeholder', function ( event ) { event.preventDefault(); // Prepare property data that has to be removed. let compare_link = $( this ), property_id = parseInt( compare_link.data( 'property-id' ) ), property_card = compare_link.parents( '.rh_compare__slide' ); const property_toggle_id = parseInt( compare_link.parent().data( 'property-id' ) ), property_toggle_card = $( '.rh_compare__carousel' ) .find( `[data-property-id = ' ${property_toggle_id} ']` ), property_toggle_remove = property_toggle_card.parents( '.rh_compare__slide' ); // Remove highlight of add to compare button. add_to_compare_btn_placeholder( property_id, false ); // Remove highlight of add to compare button. add_to_compare_btn_placeholder( property_toggle_id, false ); // Remove property card from compare tray. property_card.remove(); // Remove property card from compare tray. property_toggle_remove.remove(); // Update compare properties tray counter with number of properties available to compare. update_compare_tray_counter(); // Control the properties compare tray display. control_compare_tray_display(); if ( compare_link.hasClass( 'highlight' ) ) { // Remove property from localStorage. remove_property_from_localStorage( property_toggle_id ); } else { // Remove property from localStorage. remove_property_from_localStorage( property_id ); } // Update compare properties button url. update_compare_button_url(); } ); // Render compare properties data on page load. function render_compare_properties_data() { // Get compare properties data from localStorage. let properties_string = window.localStorage.getItem( comparePropStorageKey ); if ( null != properties_string ) { let properties_array_string = properties_string.split( '||' ); if ( Array.isArray( properties_array_string ) && properties_array_string.length && properties_array_string[0] !== '' ) { // Build array of array from array of strings. let properties_array = []; properties_array_string.forEach( function ( property ) { properties_array.push( JSON.parse( property ) ); } ); properties_array.forEach( function ( property ) { // Highlight the add to compare button. add_to_compare_btn_placeholder( property.property_id, true ); // Add property card to the compare tray. add_property_to_compare_tray( property.property_id, property.property_title, property.property_img, property.property_url ); } ); // Update compare properties tray counter with number of properties available to compare. update_compare_tray_counter(); // Control the properties compare tray display. control_compare_tray_display(); // Update compare properties button url. update_compare_button_url(); } } } // Control compare tray display. function control_compare_tray_display() { let wrapperPropertiesCompare = $( '.rh_wrapper_properties_compare' ), compare_properties_number = $( '.rh_compare .rh_compare__carousel > div' ).length; if ( compare_properties_number !== 0 ) { // Show the compare tray button. wrapperPropertiesCompare.addClass( 'rh_has_compare_children' ); $body.addClass( 'rh-has-compare-properties' ); } else { // Remove active color of compare tray button. wrapperPropertiesCompare.removeClass( 'rh_compare_open' ); // Hide compare tray button. wrapperPropertiesCompare.removeClass( 'rh_has_compare_children' ); $body.removeClass( 'rh-has-compare-properties' ); $( '.rh_fixed_side_bar_compare' ).fadeOut( 0 ); // Hide compare tray. } } // Update compare properties count in compare tray. function update_compare_tray_counter() { let compareCountWrap = $( '.rh_compare_count' ); compareCountWrap.fadeOut( 200, function () { let getDivCount = $( 'body .rh_compare .rh_compare__carousel > div' ).length; $( '.rh_wrapper_properties_compare .rh_compare_count' ).html( ' ( ' + getDivCount + '/4 ) ' ); } ); compareCountWrap.fadeIn( 200 ); } function add_to_compare_btn_placeholder( property_id, placeholder ) { let compareButton = $( '.compare-btn-' + property_id ); // Highlight the add to compare button. if ( placeholder ) { compareButton.find( '.compare-placeholder' ).removeClass( 'hide' ); compareButton.find( 'a.rh_trigger_compare' ).addClass( 'hide' ); } else { compareButton.find( '.compare-placeholder' ).addClass( 'hide' ); compareButton.find( 'a.rh_trigger_compare' ).removeClass( 'hide' ); } } // Compare limit for exceeding more than four properties in compare. function apply_compare_properties_limit() { const notificationBar = $( '#rh_compare_action_notification' ); // Remove the oldest property from the list if number of properties goes above four. let slides_number = $( '.rh_compare__carousel .rh_compare__slide' ).length; if ( slides_number >= 4 ) { $( '.rh_compare__carousel .rh_compare__slide:nth-child(1) a.rh_compare__remove' ).trigger( "click" ); notificationBar.addClass( 'show' ); setTimeout( function () { notificationBar.removeClass( 'show' ); }, 6000 ); } } // Add property card to the properties compare tray. function add_property_to_compare_tray( property_id, property_title, property_img, property_url ) { $( '.rh_compare__carousel' ).append( '
' + '
' + '
' + '' + '' + '' + '' + '' + '
' + '' + property_title + '' + '
' + '
' ); } // Add property to the localStorage. function add_property_to_localStorage( property_id, property_title, property_img, property_url ) { // Prepare property data object. let property_obj = { property_id, property_title, property_url, property_img }; let new_property = JSON.stringify( property_obj ); /** * Add property to the localStorage. */ // Get compare properties data from localStorage. let old_properties = window.localStorage.getItem( comparePropStorageKey ); if ( '' !== old_properties && null !== old_properties ) { window.localStorage.setItem( comparePropStorageKey, old_properties + '||' + new_property ); } else { window.localStorage.setItem( comparePropStorageKey, new_property ); } } // Remove property from localStorage. function remove_property_from_localStorage( property_id ) { // Get compare properties data from localStorage. let properties_array_string = window.localStorage.getItem( comparePropStorageKey ).split( '||' ); // Build an array of array from array of strings. let properties_array = []; properties_array_string.forEach( function ( property ) { properties_array.push( JSON.parse( property ) ); } ); // Prepare properties array except property to remove. let properties_array_filtered = $.grep( properties_array, function ( property ) { return property.property_id != property_id && property.property_id != undefined; } ); let properties_string = ''; properties_array_filtered.forEach( function ( property ) { if ( properties_string !== '' ) { properties_string += '||'; } properties_string += JSON.stringify( property ); } ); window.localStorage.setItem( comparePropStorageKey, properties_string ); } // Update compare properties button url with properties ids. function update_compare_button_url() { const compare_link = $( '.rh_compare__submit' ); if ( ! compare_link.length ) { return false; } let compareLink = compare_link.attr( 'href' ); if ( ! compareLink ) { return false; } let properties_array_string = window.localStorage.getItem( comparePropStorageKey ).split( '||' ); if ( Array.isArray( properties_array_string ) && properties_array_string.length && properties_array_string[0] !== '' ) { let compare_url = new URL( compareLink ); let search_params = compare_url.searchParams; let properties_array = []; properties_array_string.forEach( function ( property ) { properties_array.push( JSON.parse( property ) ); } ); let property_ids = ''; properties_array.forEach( function ( property ) { if ( '' === property_ids ) { property_ids = property.property_id; } else { property_ids += ',' + property.property_id; } } ); search_params.append( 'id', property_ids ); compare_url.search = search_params.toString(); compareLink = compare_url.toString(); } compare_link.attr( 'href', compareLink ); } } ); /** * Adding compare properties list share by email * */ let shareButtonsWrap = $( '.compare-share-buttons' ), shareMailBtn = $( '.compare-share-buttons ul li' ), shareMailWrap = $( '.share-by-mail-wrap' ), compareShareURL = $( '#compare-share-url' ).val(), compareShareNonce = $( '#compare-share-nonce' ).val(), compareShareProgress = $( '.compare-share-progress' ), compareEmailField = $( '#compare-share-email' ), compareLoader = $( '.compare-share-progress .loader' ), compareMessage = $( '.compare-share-progress .message' ); // Triggering email button to open/close email form shareMailBtn.on( 'click', '.email', function ( e ) { e.preventDefault(); if ( $( this ).hasClass( 'active' ) ) { $( this ).removeClass( 'active' ).parent( 'li' ).parent( 'ul' ).css( 'left', '' ).css( 'opacity', '' ); shareMailWrap.fadeOut( 100 ); } else { let shareButtonsWrapLeft = $('body').hasClass('design_ultra') ? '92px' : '86.6px'; $( this ).addClass( 'active' ).parent( 'li' ).parent( 'ul' ).css( 'left', shareButtonsWrapLeft ).css( 'opacity', '1' ); shareMailWrap.fadeIn( 100 ); } } ); // Triggering close button for email form wrap shareMailWrap.on( 'click', '.mail-wrap-close', function () { shareMailBtn.find( '.email' ).removeClass( 'active' ); shareButtonsWrap.find( 'ul' ).css( 'left', '' ).css( 'opacity', '' ); shareMailWrap.hide( 100 ); } ); // Removing error message from email field upon keypress compareEmailField.on( 'keypress', null, function () { $( '.fields .email-error' ).remove(); } ); // Triggering email submit request shareMailWrap.on( 'click', '#compare-mail-submit', function ( e ) { e.preventDefault(); let targetEmail = compareEmailField.val(), emailErrorStatement = compareEmailField.data( 'error-statement' ); // Checking if email is good to go if ( realhomes_is_email( targetEmail ) ) { shareMailWrap.find( '.fields, label' ).fadeOut( 200, function () { compareShareProgress.fadeIn( 200 ); } ); // Email ajax request $.ajax( { type : 'post', dataType : 'html', url : ajaxurl, data : { action : 'realhomes_share_compare_list_by_email', compare_url : compareShareURL, target_email : targetEmail, compare_nonce : compareShareNonce }, success : function ( response ) { // Check if response is already parsed or needs parsing if ( typeof response === 'string' ) { response = JSON.parse( response ); } if ( response.success ) { compareLoader.fadeOut( 200, function () { compareMessage.html( '
' + response.message ) .fadeIn( 200 ); setTimeout( function () { shareMailWrap.fadeOut( 200, function () { // All the form reset process compareMessage.hide(); compareLoader.show(); compareShareProgress.hide(); shareMailBtn.find( '.email' ).removeClass( 'active' ); shareButtonsWrap.find( 'ul' ).css( 'left', '' ).css( 'opacity', '' ); shareButtonsWrap.find( '.share-by-mail-wrap' ).hide(); shareMailWrap.find( '.fields, label' ).show(); compareEmailField.val( '' ); $( '.fields .email-error' ).remove(); } ); }, 1200 ); } ); } } } ); } else { // Error message if wrong email is provided compareEmailField.after( '' ); } } ); } )( jQuery ); /** * To check if field or any variable has valid email ID * In case of multiple emails, it will still verify each one * */ if ( typeof realhomes_is_email !== 'function' ) { function realhomes_is_email( emails ) { // Check if the input is empty or contains only whitespace if ( ! emails.trim() ) { return false; } // Regular expression to match a valid email format let emailReg = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; // Split the string by commas, trim each email, and filter out any empty strings let emailList = emails.split( ',' ).map( email => email.trim() ).filter( email => email !== '' ); // Check if every email in the list matches the regex pattern return emailList.every( email => emailReg.test( email ) ); } };