diff --git a/.gitignore b/.gitignore index 3e1080a7c..f36b2c008 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ coverage/ index.html .mailmap bower.json -component.json \ No newline at end of file +component.json +debug/local/ diff --git a/build/deps.js b/build/deps.js index b493e7dd8..f266cd00e 100644 --- a/build/deps.js +++ b/build/deps.js @@ -69,6 +69,7 @@ var deps = { Popup: { src: [ + 'layer/PopupBase.js', 'layer/Popup.js', 'layer/Layer.Popup.js', 'layer/marker/Marker.Popup.js' @@ -77,6 +78,16 @@ var deps = { desc: 'Used to display the map popup (used mostly for binding HTML data to markers and paths on click).' }, + Label: { + src: [ + 'layer/Label.js', + 'layer/Layer.Label.js', + 'layer/marker/Marker.Label.js' + ], + deps: ['Popup', 'Marker'], + desc: 'Used to display the map popup (used mostly for binding HTML data to markers and paths on click).' + }, + LayerGroup: { src: ['layer/LayerGroup.js'], desc: 'Allows grouping several layers to handle them as one.' diff --git a/debug/map/label.html b/debug/map/label.html new file mode 100644 index 000000000..d3a770760 --- /dev/null +++ b/debug/map/label.html @@ -0,0 +1,52 @@ + + + + Leaflet debug page + + + + + + + + + + + +
+ + + + + diff --git a/dist/leaflet.css b/dist/leaflet.css index 7cb3a3227..1dd36d38e 100644 --- a/dist/leaflet.css +++ b/dist/leaflet.css @@ -74,6 +74,7 @@ .leaflet-overlay-pane { z-index: 400; } .leaflet-shadow-pane { z-index: 500; } .leaflet-marker-pane { z-index: 600; } +.leaflet-label-pane { z-index: 650; } .leaflet-popup-pane { z-index: 700; } .leaflet-map-pane canvas { z-index: 100; } @@ -507,3 +508,60 @@ background: #fff; border: 1px solid #666; } + + +/* Label */ +.leaflet-label { + background: rgb(235, 235, 235); + background: rgba(235, 235, 235, 0.81); + background-clip: padding-box; + border-color: #777; + border-color: rgba(0,0,0,0.25); + border-radius: 4px; + border-style: solid; + border-width: 4px; + color: #111; + display: block; + font: 12px/20px "Helvetica Neue", Arial, Helvetica, sans-serif; + font-weight: bold; + padding: 1px 6px; + position: absolute; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + white-space: nowrap; + z-index: 6; +} + +.leaflet-label.leaflet-clickable { + cursor: pointer; + pointer-events: auto; +} + +.leaflet-label:before, +.leaflet-label:after { + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + content: none; + position: absolute; + top: 5px; +} + +.leaflet-label:before { + border-right: 6px solid black; + border-right-color: inherit; + left: -10px; +} + +.leaflet-label:after { + border-left: 6px solid black; + border-left-color: inherit; + right: -10px; +} + +.leaflet-label-right:before, +.leaflet-label-left:after { + content: ""; +} diff --git a/src/layer/Label.js b/src/layer/Label.js new file mode 100644 index 000000000..572ef823b --- /dev/null +++ b/src/layer/Label.js @@ -0,0 +1,114 @@ +/* + * L.Label is used for displaying small texts on the map. + */ + +L.Label = L.PopupBase.extend({ + + options: { + pane: 'labelPane', + offset: [12, -15], + direction: 'right', + static: false, // Reserved word, use "permanent" instead? + followMouse: false, + clickable: false, + // className: '', + zoomAnimation: true + }, + + openOn: function (map) { + map.openLabel(this); + return this; + }, + + _close: function () { + if (this._map) { + this._map.closeLabel(this); + } + }, + + _initLayout: function () { + var prefix = 'leaflet-label', + className = prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide'); + + this._contentNode = this._container = L.DomUtil.create('div', className); + }, + + _updateLayout: function () {}, + + _adjustPan: function () {}, + + _updatePosition: function () { + var map = this._map, + pos = map.latLngToLayerPoint(this._latlng), + container = this._container, + centerPoint = map.latLngToContainerPoint(map.getCenter()), + labelPoint = map.layerPointToContainerPoint(pos), + direction = this.options.direction, + labelWidth = container.offsetWidth, + offset = L.point(this.options.offset); + + // position to the right (right or auto & needs to) + if (direction === 'right' || direction === 'auto' && labelPoint.x < centerPoint.x) { + L.DomUtil.addClass(container, 'leaflet-label-right'); + L.DomUtil.removeClass(container, 'leaflet-label-left'); + + // if (!this._zoomAnimated) { pos = pos.add(offset); } + pos = pos.add(offset); + } else { // position to the left + L.DomUtil.addClass(container, 'leaflet-label-left'); + L.DomUtil.removeClass(container, 'leaflet-label-right'); + + pos = pos.add(L.point(-offset.x - labelWidth, offset.y)); + } + + L.DomUtil.setPosition(container, pos); + }, + + setOpacity: function (opacity) { + this.options.opacity = opacity; + + if (this._container) { + L.DomUtil.setOpacity(this._container, opacity); + } + }, + + _animateZoom: function (e) { + var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center), offset; + if (this.options.offset) { + offset = L.point(this.options.offset); + pos = pos.add(offset); + } + L.DomUtil.setPosition(this._container, pos); + } + +}); + +L.label = function (options, source) { + return new L.Label(options, source); +}; + + +L.Map.include({ + openLabel: function (label, latlng, options) { + if (!(label instanceof L.Label)) { + label = new L.Label(options).setContent(label); + } + + if (latlng) { + label.setLatLng(latlng); + } + + if (this.hasLayer(label)) { + return this; + } + + return this.addLayer(label); + }, + + closeLabel: function (label) { + if (label) { + this.removeLayer(label); + } + return this; + } +}); diff --git a/src/layer/Layer.Label.js b/src/layer/Layer.Label.js new file mode 100644 index 000000000..89ba379fa --- /dev/null +++ b/src/layer/Layer.Label.js @@ -0,0 +1,164 @@ +/* + * Adds label-related methods to all layers. + */ + +L.Layer.include({ + + bindLabel: function (content, options) { + + if (content instanceof L.Label) { + L.setOptions(content, options); + this._label = content; + content._source = this; + } else { + if (!this._label || options) { + this._label = L.label(options, this); + } + this._label.setContent(content); + } + + this._initLabelInteractions(); + + if (this._label.options.static) { this.openLabel(); } + + // save the originally passed offset + this._originalLabelOffset = this._label.options.offset; + + return this; + }, + + unbindLabel: function () { + if (this._label) { + this._initLabelInteractions(true); + this._label = null; + } + return this; + }, + + _initLabelInteractions: function (remove) { + if (!remove && this._labelHandlersAdded) { return; } + var onOff = remove ? 'off' : 'on', + events = { + remove: this.closeLabel, + move: this._moveLabel + }; + if (!this._label.options.static) { + events.mouseover = this._openLabel; + events.mouseout = this.closeLabel; + if (this._label.options.followMouse) { + events.mousemove = this._moveLabel; + } + if (L.Browser.touch) { + events.click = this._openLabel; + } + } + this[onOff](events); + this._labelHandlersAdded = !remove; + }, + + openLabel: function (layer, latlng) { + if (!(layer instanceof L.Layer)) { + latlng = layer; + layer = this; + } + + if (layer instanceof L.FeatureGroup) { + for (var id in this._layers) { + layer = this._layers[id]; + break; + } + } + + if (!latlng) { + latlng = layer.getCenter ? layer.getCenter() : layer.getLatLng(); + } + + if (this._label && this._map) { + // set the label offset for this layer + this._label.options.offset = this._labelAnchor(layer); + + // set label source to this layer + this._label._source = layer; + + // update the label (content, layout, ect...) + this._label.update(); + + // open the label on the map + this._map.openLabel(this._label, latlng); + + if (this._label.options.clickable) { + L.DomUtil.addClass(this._label._container, 'leaflet-clickable'); + this.addInteractiveTarget(this._label._container); + } + } + + return this; + }, + + closeLabel: function () { + if (this._label) { + this._label._close(); + if (this._label.options.clickable) { + L.DomUtil.removeClass(this._label._container, 'leaflet-clickable'); + this.removeInteractiveTarget(this._label._container); + } + } + return this; + }, + + toggleLabel: function (target) { + if (this._label) { + if (this._label._map) { + this.closeLabel(); + } else { + this.openLabel(target); + } + } + return this; + }, + + isLabelOpen: function () { + return this._label.isOpen(); + }, + + setLabelContent: function (content) { + if (this._label) { + this._label.setContent(content); + } + return this; + }, + + getLabel: function () { + return this._label; + }, + + _openLabel: function (e) { + var layer = e.layer || e.target; + + if (!this._label || !this._map) { + return; + } + this.openLabel(layer, this._label.options.followMouse ? e.latlng : undefined); + }, + + _labelAnchor: function (layer) { + // where shold we anchor the label on this layer? + var anchor = layer._getLabelAnchor && !this._label.options.followMouse ? layer._getLabelAnchor() : [0, 0]; + + // add the users passed offset to that + var offsetToAdd = this._originalLabelOffset || L.Label.prototype.options.offset; + + // return the final point to anchor the label + return L.point(anchor).add(offsetToAdd); + }, + + _moveLabel: function (e) { + var latlng = e.latlng, containerPoint, layerPoint; + if (this._label.options.followMouse && e.originalEvent) { + containerPoint = this._map.mouseEventToContainerPoint(e.originalEvent); + layerPoint = this._map.containerPointToLayerPoint(containerPoint); + latlng = this._map.layerPointToLatLng(layerPoint); + } + this._label.setLatLng(latlng); + } +}); diff --git a/src/layer/Popup.js b/src/layer/Popup.js index bfcbfe2e3..3c31f00e7 100644 --- a/src/layer/Popup.js +++ b/src/layer/Popup.js @@ -6,7 +6,7 @@ L.Map.mergeOptions({ closePopupOnClick: true }); -L.Popup = L.Layer.extend({ +L.Popup = L.PopupBase.extend({ options: { pane: 'popupPane', @@ -28,36 +28,18 @@ L.Popup = L.Layer.extend({ zoomAnimation: true }, - initialize: function (options, source) { - L.setOptions(this, options); + getEvents: function () { + var events = L.PopupBase.prototype.getEvents.call(this); - this._source = source; - }, - - onAdd: function (map) { - this._zoomAnimated = this._zoomAnimated && this.options.zoomAnimation; - - if (!this._container) { - this._initLayout(); + if ('closeOnClick' in this.options ? this.options.closeOnClick : this._map.options.closePopupOnClick) { + events.preclick = this._close; } - if (map._fadeAnimated) { - L.DomUtil.setOpacity(this._container, 0); + if (this.options.keepInView) { + events.moveend = this._adjustPan; } - clearTimeout(this._removeTimeout); - this.getPane().appendChild(this._container); - this.update(); - - if (map._fadeAnimated) { - L.DomUtil.setOpacity(this._container, 1); - } - - map.fire('popupopen', {popup: this}); - - if (this._source) { - this._source.fire('popupopen', {popup: this}, true); - } + return events; }, openOn: function (map) { @@ -65,98 +47,6 @@ L.Popup = L.Layer.extend({ return this; }, - onRemove: function (map) { - if (map._fadeAnimated) { - L.DomUtil.setOpacity(this._container, 0); - this._removeTimeout = setTimeout(L.bind(L.DomUtil.remove, L.DomUtil, this._container), 200); - } else { - L.DomUtil.remove(this._container); - } - - map.fire('popupclose', {popup: this}); - - if (this._source) { - this._source.fire('popupclose', {popup: this}, true); - } - }, - - getLatLng: function () { - return this._latlng; - }, - - setLatLng: function (latlng) { - this._latlng = L.latLng(latlng); - if (this._map) { - this._updatePosition(); - this._adjustPan(); - } - return this; - }, - - getContent: function () { - return this._content; - }, - - setContent: function (content) { - this._content = content; - this.update(); - return this; - }, - - getElement: function () { - return this._container; - }, - - update: function () { - if (!this._map) { return; } - - this._container.style.visibility = 'hidden'; - - this._updateContent(); - this._updateLayout(); - this._updatePosition(); - - this._container.style.visibility = ''; - - this._adjustPan(); - }, - - getEvents: function () { - var events = { - zoom: this._updatePosition, - viewreset: this._updatePosition - }; - - if (this._zoomAnimated) { - events.zoomanim = this._animateZoom; - } - if ('closeOnClick' in this.options ? this.options.closeOnClick : this._map.options.closePopupOnClick) { - events.preclick = this._close; - } - if (this.options.keepInView) { - events.moveend = this._adjustPan; - } - return events; - }, - - isOpen: function () { - return !!this._map && this._map.hasLayer(this); - }, - - bringToFront: function () { - if (this._map) { - L.DomUtil.toFront(this._container); - } - return this; - }, - - bringToBack: function () { - if (this._map) { - L.DomUtil.toBack(this._container); - } - return this; - }, - _close: function () { if (this._map) { this._map.closePopup(this); @@ -189,21 +79,24 @@ L.Popup = L.Layer.extend({ this._tip = L.DomUtil.create('div', prefix + '-tip', this._tipContainer); }, - _updateContent: function () { - if (!this._content) { return; } + _updatePosition: function () { + if (!this._map) { return; } - var node = this._contentNode; - var content = (typeof this._content === 'function') ? this._content(this._source || this) : this._content; + var pos = this._map.latLngToLayerPoint(this._latlng), + offset = L.point(this.options.offset); - if (typeof content === 'string') { - node.innerHTML = content; + if (this._zoomAnimated) { + L.DomUtil.setPosition(this._container, pos); } else { - while (node.hasChildNodes()) { - node.removeChild(node.firstChild); - } - node.appendChild(content); + offset = offset.add(pos); } - this.fire('contentupdate'); + + var bottom = this._containerBottom = -offset.y, + left = this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x; + + // bottom position the popup in case the height of the popup changes (images loading etc) + this._container.style.bottom = bottom + 'px'; + this._container.style.left = left + 'px'; }, _updateLayout: function () { @@ -236,31 +129,6 @@ L.Popup = L.Layer.extend({ this._containerWidth = this._container.offsetWidth; }, - _updatePosition: function () { - if (!this._map) { return; } - - var pos = this._map.latLngToLayerPoint(this._latlng), - offset = L.point(this.options.offset); - - if (this._zoomAnimated) { - L.DomUtil.setPosition(this._container, pos); - } else { - offset = offset.add(pos); - } - - var bottom = this._containerBottom = -offset.y, - left = this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x; - - // bottom position the popup in case the height of the popup changes (images loading etc) - this._container.style.bottom = bottom + 'px'; - this._container.style.left = left + 'px'; - }, - - _animateZoom: function (e) { - var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center); - L.DomUtil.setPosition(this._container, pos); - }, - _adjustPan: function () { if (!this.options.autoPan || (this._map._panAnim && this._map._panAnim._inProgress)) { return; } @@ -304,7 +172,14 @@ L.Popup = L.Layer.extend({ _onCloseButtonClick: function (e) { this._close(); L.DomEvent.stop(e); + }, + + + _animateZoom: function (e) { + var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center); + L.DomUtil.setPosition(this._container, pos); } + }); L.popup = function (options, source) { diff --git a/src/layer/PopupBase.js b/src/layer/PopupBase.js new file mode 100644 index 000000000..26fa3eb30 --- /dev/null +++ b/src/layer/PopupBase.js @@ -0,0 +1,166 @@ +/* + * L.Popup is used for displaying popups on the map. + */ + +L.Map.mergeOptions({ + closePopupOnClick: true +}); + +L.PopupBase = L.Layer.extend({ + + options: { + minWidth: 50, + maxWidth: 300, + // maxHeight: , + offset: [0, 7], + + autoPan: true, + autoPanPadding: [5, 5], + // autoPanPaddingTopLeft: , + // autoPanPaddingBottomRight: , + + closeButton: true, + autoClose: true, + // keepInView: false, + // className: '', + zoomAnimation: true + }, + + initialize: function (options, source) { + L.setOptions(this, options); + + this._source = source; + }, + + onAdd: function (map) { + this._zoomAnimated = this._zoomAnimated && this.options.zoomAnimation; + + if (!this._container) { + this._initLayout(); + } + + if (map._fadeAnimated) { + L.DomUtil.setOpacity(this._container, 0); + } + + clearTimeout(this._removeTimeout); + this.getPane().appendChild(this._container); + this.update(); + + if (map._fadeAnimated) { + L.DomUtil.setOpacity(this._container, 1); + } + + this.bringToFront(); + + map.fire('popupopen', {popup: this}); + + if (this._source) { + this._source.fire('popupopen', {popup: this}, true); + } + }, + + onRemove: function (map) { + if (map._fadeAnimated) { + L.DomUtil.setOpacity(this._container, 0); + this._removeTimeout = setTimeout(L.bind(L.DomUtil.remove, L.DomUtil, this._container), 200); + } else { + L.DomUtil.remove(this._container); + } + + map.fire('popupclose', {popup: this}); + + if (this._source) { + this._source.fire('popupclose', {popup: this}, true); + } + }, + + getLatLng: function () { + return this._latlng; + }, + + setLatLng: function (latlng) { + this._latlng = L.latLng(latlng); + if (this._map) { + this._updatePosition(); + this._adjustPan(); + } + return this; + }, + + getContent: function () { + return this._content; + }, + + setContent: function (content) { + this._content = content; + this.update(); + return this; + }, + + getElement: function () { + return this._container; + }, + + update: function () { + if (!this._map) { return; } + + this._container.style.visibility = 'hidden'; + + this._updateContent(); + this._updateLayout(); + this._updatePosition(); + + this._container.style.visibility = ''; + + this._adjustPan(); + }, + + getEvents: function () { + var events = { + zoom: this._updatePosition, + viewreset: this._updatePosition + }; + + if (this._zoomAnimated) { + events.zoomanim = this._animateZoom; + } + return events; + }, + + isOpen: function () { + return !!this._map && this._map.hasLayer(this); + }, + + bringToFront: function () { + if (this._map) { + L.DomUtil.toFront(this._container); + } + return this; + }, + + bringToBack: function () { + if (this._map) { + L.DomUtil.toBack(this._container); + } + return this; + }, + + _updateContent: function () { + if (!this._content) { return; } + + var node = this._contentNode; + var content = (typeof this._content === 'function') ? this._content(this._source || this) : this._content; + + if (typeof content === 'string') { + node.innerHTML = content; + } else { + while (node.hasChildNodes()) { + node.removeChild(node.firstChild); + } + node.appendChild(content); + } + this.fire('contentupdate'); + } + +}); diff --git a/src/layer/marker/Icon.Default.js b/src/layer/marker/Icon.Default.js index 6eeca7bdc..90c401f45 100644 --- a/src/layer/marker/Icon.Default.js +++ b/src/layer/marker/Icon.Default.js @@ -8,6 +8,7 @@ L.Icon.Default = L.Icon.extend({ iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], + labelAnchor: [9, -20], shadowSize: [41, 41] }, diff --git a/src/layer/marker/Marker.Label.js b/src/layer/marker/Marker.Label.js new file mode 100644 index 000000000..368ed8297 --- /dev/null +++ b/src/layer/marker/Marker.Label.js @@ -0,0 +1,9 @@ +/* + * Label extension to L.Marker, adding label-related methods. + */ + +L.Marker.include({ + _getLabelAnchor: function () { + return this.options.icon.options.labelAnchor || [0, 0]; + } +}); diff --git a/src/map/Map.js b/src/map/Map.js index 6495ae1d8..b074ff993 100644 --- a/src/map/Map.js +++ b/src/map/Map.js @@ -519,6 +519,7 @@ L.Map = L.Evented.extend({ this.createPane('shadowPane'); this.createPane('overlayPane'); this.createPane('markerPane'); + this.createPane('labelPane'); this.createPane('popupPane'); if (!this.options.markerZoomAnimation) {