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) {