diff --git a/css/app.css b/css/app.css
index 6e2772cdd..8f17d668a 100644
--- a/css/app.css
+++ b/css/app.css
@@ -349,3 +349,45 @@ div.typeahead a.active {
-webkit-tap-highlight-color:rgba(0,0,0,0);
-webkit-touch-callout:none;
}
+
+.tooltip {
+ position: absolute;
+ z-index: 1030;
+ display: block;
+ padding: 5px;
+ font-size: 11px;
+ opacity: 0;
+ filter: alpha(opacity=0);
+ visibility: visible;
+}
+
+.tooltip.in {
+ opacity: 0.8;
+ filter: alpha(opacity=80);
+}
+
+.tooltip-inner {
+ max-width: 200px;
+ padding: 3px 8px;
+ color: #ffffff;
+ text-align: center;
+ text-decoration: none;
+ background-color: #000000;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+.tooltip-arrow {
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+}
+.tooltip.bottom .tooltip-arrow {
+ top: 0;
+ left: 50%;
+ margin-left: -5px;
+ border-bottom-color: #000000;
+ border-width: 0 5px 5px;
+}
diff --git a/index.html b/index.html
index 77fce4dff..ab9e86956 100644
--- a/index.html
+++ b/index.html
@@ -21,6 +21,8 @@
+
+
diff --git a/js/id/id.js b/js/id/id.js
index a58dfafb2..87a6b40f1 100644
--- a/js/id/id.js
+++ b/js/id/id.js
@@ -46,13 +46,17 @@ window.iD = function(container) {
.attr({ id: 'undo', 'class': 'mini' })
.property('disabled', true)
.html('←')
- .on('click', history.undo);
+ .on('click', history.undo)
+ .call(bootstrap.tooltip()
+ .placement('bottom'));
undo_buttons.append('button')
.attr({ id: 'redo', 'class': 'mini' })
.property('disabled', true)
.html('→')
- .on('click', history.redo);
+ .on('click', history.redo)
+ .call(bootstrap.tooltip()
+ .placement('bottom'));
bar.append('input')
.attr({ type: 'text', placeholder: 'find a place', id: 'geocode-location' })
@@ -137,13 +141,11 @@ window.iD = function(container) {
bar.select('#undo')
.property('disabled', !undo)
- .select('small')
- .text(undo);
+ .attr('data-original-title', undo);
bar.select('#redo')
.property('disabled', !redo)
- .select('small')
- .text(redo);
+ .attr('data-original-title', redo);
});
window.onresize = function() {
diff --git a/js/lib/bootstrap-tooltip.js b/js/lib/bootstrap-tooltip.js
new file mode 100644
index 000000000..8cdf669d5
--- /dev/null
+++ b/js/lib/bootstrap-tooltip.js
@@ -0,0 +1,166 @@
+(function(exports) {
+
+ var bootstrap = (typeof exports.bootstrap === "object") ?
+ exports.bootstrap :
+ (exports.bootstrap = {});
+
+ bootstrap.tooltip = function() {
+
+ var tooltip = function(selection) {
+ selection.each(setup);
+ },
+ animation = d3.functor(false),
+ html = d3.functor(false),
+ title = function() {
+ var title = this.getAttribute("data-original-title");
+ if (title) {
+ return title;
+ } else {
+ title = this.getAttribute("title");
+ this.removeAttribute("title");
+ this.setAttribute("data-original-title", title);
+ }
+ return title;
+ },
+ over = "mouseenter.tooltip",
+ out = "mouseleave.tooltip",
+ placements = "top left bottom right".split(" "),
+ placement = d3.functor("top");
+
+ tooltip.title = function(_) {
+ if (arguments.length) {
+ title = d3.functor(_);
+ return tooltip;
+ } else {
+ return title;
+ }
+ };
+
+ tooltip.html = function(_) {
+ if (arguments.length) {
+ html = d3.functor(_);
+ return tooltip;
+ } else {
+ return html;
+ }
+ };
+
+ tooltip.placement = function(_) {
+ if (arguments.length) {
+ placement = d3.functor(_);
+ return tooltip;
+ } else {
+ return placement;
+ }
+ };
+
+ tooltip.show = function(selection) {
+ selection.each(show);
+ };
+
+ tooltip.hide = function(selection) {
+ selection.each(hide);
+ };
+
+ tooltip.toggle = function(selection) {
+ selection.each(toggle);
+ };
+
+ tooltip.destroy = function(selection) {
+ selection
+ .on(over, null)
+ .on(out, null)
+ .attr("title", function() {
+ return this.getAttribute("data-original-title") || this.getAttribute("title");
+ })
+ .attr("data-original-title", null)
+ .select(".tooltip")
+ .remove();
+ };
+
+ function setup() {
+ var root = d3.select(this),
+ animate = animation.apply(this, arguments),
+ tip = root.append("div")
+ .attr("class", "tooltip");
+
+ if (animate) {
+ tip.classed("fade", true);
+ }
+
+ // TODO "inside" checks?
+
+ tip.append("div")
+ .attr("class", "tooltip-arrow");
+ tip.append("div")
+ .attr("class", "tooltip-inner");
+
+ var place = placement.apply(this, arguments);
+ tip.classed(place, true);
+
+ root.on(over, show);
+ root.on(out, hide);
+ }
+
+ function show() {
+ var root = d3.select(this),
+ content = title.apply(this, arguments),
+ tip = root.select(".tooltip")
+ .classed("in", true),
+ markup = html.apply(this, arguments),
+ innercontent = tip.select(".tooltip-inner")[markup ? "html" : "text"](content),
+ place = placement.apply(this, arguments),
+ outer = getPosition(root.node()),
+ inner = getPosition(tip.node()),
+ pos;
+
+ switch (place) {
+ case "top":
+ pos = {x: outer.x + (outer.w - inner.w) / 2, y: outer.y - inner.h};
+ break;
+ case "right":
+ pos = {x: outer.x + outer.w, y: outer.y + (outer.h - inner.h) / 2};
+ break;
+ case "left":
+ pos = {x: outer.x - inner.w, y: outer.y + (outer.h - inner.h) / 2};
+ break;
+ case "bottom":
+ pos = {x: outer.x + (outer.w - inner.w) / 2, y: outer.y + outer.h};
+ break;
+ }
+
+ tip.style(pos ?
+ {left: ~~pos.x + "px", top: ~~pos.y + "px"} :
+ {left: null, top: null});
+
+ this.tooltipVisible = true;
+ }
+
+ function hide() {
+ d3.select(this).select(".tooltip")
+ .classed("in", false);
+
+ this.tooltipVisible = false;
+ }
+
+ function toggle() {
+ if (this.tooltipVisible) {
+ hide.apply(this, arguments);
+ } else {
+ show.apply(this, arguments);
+ }
+ }
+
+ return tooltip;
+ };
+
+ function getPosition(node) {
+ return {
+ x: node.offsetLeft,
+ y: node.offsetTop,
+ w: node.offsetWidth,
+ h: node.offsetHeight
+ };
+ }
+
+})(this);
diff --git a/js/lib/d3-compat.js b/js/lib/d3-compat.js
new file mode 100644
index 000000000..88a8fafe7
--- /dev/null
+++ b/js/lib/d3-compat.js
@@ -0,0 +1,48 @@
+(function() {
+
+ // get a reference to the d3.selection prototype,
+ // and keep a reference to the old d3.selection.on
+ var d3_selectionPrototype = d3.selection.prototype,
+ d3_on = d3_selectionPrototype.on;
+
+ // our shims are organized by event:
+ // "desired-event": ["shimmed-event", wrapperFunction]
+ var shims = {
+ "mouseenter": ["mouseover", relatedTarget],
+ "mouseleave": ["mouseout", relatedTarget]
+ };
+
+ // rewrite the d3.selection.on function to shim the events with wrapped
+ // callbacks
+ d3_selectionPrototype.on = function(evt, callback, useCapture) {
+ var bits = evt.split("."),
+ type = bits.shift(),
+ shim = shims[type];
+ if (shim) {
+ evt = bits.length ? [shim[0], bits].join(".") : shim[0];
+ if (typeof callback === "function") {
+ callback = shim[1](callback);
+ }
+ return d3_on.call(this, evt, callback, useCapture);
+ } else {
+ return d3_on.apply(this, arguments);
+ }
+ };
+
+ function relatedTarget(callback) {
+ return function() {
+ var related = d3.event.relatedTarget;
+ if (this === related || childOf(this, related)) {
+ return undefined;
+ }
+ return callback.apply(this, arguments);
+ };
+ }
+
+ function childOf(p, c) {
+ if (p === c) return false;
+ while (c && c !== p) c = c.parentNode;
+ return c === p;
+ }
+
+})();