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; + } + +})();