diff --git a/css/app.css b/css/app.css
index 87730f4f9..ded14c4c6 100644
--- a/css/app.css
+++ b/css/app.css
@@ -192,7 +192,7 @@ button {
font-size:14px;
display: inline-block;
height:40px;
- cursor:url(../img/cursor-pointer.png) 9 9, auto;
+ cursor:url(../img/cursor-pointer.png) 6 1, auto;
}
button:hover {
@@ -201,7 +201,7 @@ button:hover {
button.active {
box-shadow: inset 0 0 0px 1px #fff, inset 0 0 6px 1px rgba(0,0,0,.35);
- cursor:url(../img/cursor-pointing.png) 9 9, auto;
+ cursor:url(../img/cursor-pointing.png) 6 1, auto;
}
button.active:not([disabled]) {
diff --git a/index.html b/index.html
index b0153fd9a..585b32281 100644
--- a/index.html
+++ b/index.html
@@ -68,6 +68,7 @@
+
diff --git a/js/id/behavior/hover.js b/js/id/behavior/hover.js
new file mode 100644
index 000000000..9d50cbbd0
--- /dev/null
+++ b/js/id/behavior/hover.js
@@ -0,0 +1,33 @@
+/*
+ The hover behavior adds the `.hover` class on mouseover to all elements to which
+ the identical datum is bound, and removes it on mouseout.
+
+ The :hover pseudo-class is insufficient for iD's purposes because a datum's visual
+ representation may consist of several elements scattered throughout the DOM hierarchy.
+ Only one of these elements can have the :hover pseudo-class, but all of them will
+ have the .hover class.
+ */
+iD.behavior.Hover = function () {
+ var hover = function(selection) {
+ selection.on('mouseover.hover', function () {
+ var datum = d3.event.target.__data__;
+ if (datum) {
+ selection.selectAll('*')
+ .filter(function (d) { return d === datum; })
+ .classed('hover', true);
+ }
+ });
+
+ selection.on('mouseout.hover', function () {
+ selection.selectAll('.hover')
+ .classed('hover', false);
+ });
+ };
+
+ hover.off = function(selection) {
+ selection.on('mouseover.hover', null)
+ .on('mouseout.hover', null);
+ };
+
+ return hover;
+};
diff --git a/js/id/modes/add_area.js b/js/id/modes/add_area.js
index 8051da962..33facb041 100644
--- a/js/id/modes/add_area.js
+++ b/js/id/modes/add_area.js
@@ -12,7 +12,6 @@ iD.modes.AddArea = function() {
controller = mode.controller;
map.dblclickEnable(false)
- .hoverEnable(false)
.hint('Click on the map to start drawing an area, like a park, lake, or building.');
map.surface.on('click.addarea', function() {
@@ -48,8 +47,7 @@ iD.modes.AddArea = function() {
mode.exit = function() {
window.setTimeout(function() {
- mode.map.dblclickEnable(true)
- .hoverEnable(true);
+ mode.map.dblclickEnable(true);
}, 1000);
mode.map.hint(false);
mode.map.surface.on('click.addarea', null);
diff --git a/js/id/modes/add_line.js b/js/id/modes/add_line.js
index 24a53a115..baa4364bc 100644
--- a/js/id/modes/add_line.js
+++ b/js/id/modes/add_line.js
@@ -13,7 +13,6 @@ iD.modes.AddLine = function() {
controller = mode.controller;
map.dblclickEnable(false)
- .hoverEnable(false)
.hint('Click on the map to start drawing an road, path, or route.');
map.surface.on('click.addline', function() {
@@ -69,7 +68,7 @@ iD.modes.AddLine = function() {
};
mode.exit = function() {
- mode.map.dblclickEnable(true).hoverEnable(true);
+ mode.map.dblclickEnable(true);
mode.map.hint(false);
mode.map.surface.on('click.addline', null);
mode.map.keybinding().on('⎋.addline', null);
diff --git a/js/id/modes/add_point.js b/js/id/modes/add_point.js
index 06eefd00b..37767f3de 100644
--- a/js/id/modes/add_point.js
+++ b/js/id/modes/add_point.js
@@ -10,8 +10,6 @@ iD.modes.AddPoint = function() {
history = mode.history,
controller = mode.controller;
- map.hoverEnable(false);
-
map.hint('Click on the map to add a point.');
map.surface.on('click.addpoint', function() {
@@ -30,7 +28,6 @@ iD.modes.AddPoint = function() {
};
mode.exit = function() {
- mode.map.hoverEnable(true);
mode.map.hint(false);
mode.map.surface.on('click.addpoint', null);
mode.map.keybinding().on('⎋.addpoint', null);
diff --git a/js/id/modes/browse.js b/js/id/modes/browse.js
index 3d5654a87..60b08889f 100644
--- a/js/id/modes/browse.js
+++ b/js/id/modes/browse.js
@@ -12,6 +12,7 @@ iD.modes.Browse = function() {
var surface = mode.map.surface;
behaviors = [
+ iD.behavior.Hover(),
iD.behavior.DragNode(mode),
iD.behavior.DragAccuracyHandle(mode)];
diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js
index e54163dda..00ef6590f 100644
--- a/js/id/modes/draw_area.js
+++ b/js/id/modes/draw_area.js
@@ -16,7 +16,6 @@ iD.modes.DrawArea = function(wayId) {
node = iD.Node({loc: map.mouseCoordinates()});
map.dblclickEnable(false)
- .hoverEnable(false)
.fastEnable(false);
map.hint('Click on the map to add points to your area. Finish the ' +
'area by clicking on your first point');
@@ -126,8 +125,7 @@ iD.modes.DrawArea = function(wayId) {
mode.exit = function() {
mode.map.hint(false);
- mode.map.fastEnable(true)
- .hoverEnable(true);
+ mode.map.fastEnable(true);
mode.map.surface
.on('mousemove.drawarea', null)
diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js
index 56c5cfe6b..b7e614012 100644
--- a/js/id/modes/draw_line.js
+++ b/js/id/modes/draw_line.js
@@ -16,7 +16,6 @@ iD.modes.DrawLine = function(wayId, direction) {
map.dblclickEnable(false)
.fastEnable(false)
- .hoverEnable(false)
.hint('Click to add more points to the line. ' +
'Click on other lines to connect to them, and double-click to ' +
'end the line.');
@@ -122,8 +121,7 @@ iD.modes.DrawLine = function(wayId, direction) {
mode.exit = function() {
mode.map.hint(false);
- mode.map.fastEnable(true)
- .hoverEnable(true);
+ mode.map.fastEnable(true);
mode.map.surface
.on('mousemove.drawline', null)
diff --git a/js/id/modes/select.js b/js/id/modes/select.js
index 8defb17ba..fe79d0f88 100644
--- a/js/id/modes/select.js
+++ b/js/id/modes/select.js
@@ -30,6 +30,7 @@ iD.modes.Select = function (entity) {
var surface = mode.map.surface;
behaviors = [
+ iD.behavior.Hover(),
iD.behavior.DragNode(mode),
iD.behavior.DragWay(mode),
iD.behavior.DragAccuracyHandle(mode)];
@@ -44,6 +45,21 @@ iD.modes.Select = function (entity) {
.datum(entity)
.call(inspector);
+ // Pan the map if the clicked feature intersects with the position
+ // of the inspector
+ var inspector_size = d3.select('.inspector-wrap').size(),
+ map_size = mode.map.size(),
+ entity_extent = entity.extent(mode.history.graph()),
+ left_edge = map_size[0] - inspector_size[0],
+ left = mode.map.projection(entity_extent[1])[0],
+ right = mode.map.projection(entity_extent[0])[0];
+
+ if (left > left_edge &&
+ right > left_edge) mode.map.centerEase(
+ iD.util.geo.interp(
+ entity_extent[0],
+ entity_extent[1], 0.5));
+
inspector.on('changeTags', function(d, tags) {
mode.history.perform(
iD.actions.ChangeEntityTags(d.id, tags),
diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js
index 4cc7de184..431282fa9 100644
--- a/js/id/renderer/map.js
+++ b/js/id/renderer/map.js
@@ -2,7 +2,6 @@ iD.Map = function() {
var connection, history,
dimensions = [],
dispatch = d3.dispatch('move'),
- hover = null,
translateStart,
keybinding = d3.keybinding(),
projection = d3.geo.mercator().scale(1024),
@@ -12,7 +11,6 @@ iD.Map = function() {
.scaleExtent([1024, 256 * Math.pow(2, 24)])
.on('zoom', zoomPan),
dblclickEnabled = true,
- hoverEnabled = true,
fastEnabled = true,
notice,
background = iD.Background()
@@ -43,8 +41,6 @@ iD.Map = function() {
.attr({ x: 0, y: 0 });
r = surface.append('g')
- .on('mouseover', hoverIn)
- .on('mouseout', hoverOut)
.attr('clip-path', 'url(#clip)');
g = ['fill', 'casing', 'stroke', 'text', 'hit', 'temp'].reduce(function(mem, i) {
@@ -66,7 +62,6 @@ iD.Map = function() {
}
function pxCenter() { return [dimensions[0] / 2, dimensions[1] / 2]; }
- function classHover(d) { return d.id === hover; }
function getline(d) { return d._line; }
function key(d) { return d.id; }
function nodeline(d) {
@@ -98,23 +93,36 @@ iD.Map = function() {
else editOn();
for (var i = 0; i < all.length; i++) {
- var a = all[i];
- if (a.type === 'way') {
- a._line = nodeline(a);
- ways.push(a);
- if (a.isArea()) areas.push(a);
- else lines.push(a);
- } else if (a._poi) {
- points.push(a);
- } else if (!a._poi && a.type === 'node' && a.intersects(extent)) {
- vertices.push(a);
+ var entity = all[i];
+ switch (entity.geometry()) {
+ case 'line':
+ entity._line = nodeline(entity);
+ ways.push(entity);
+ lines.push(entity);
+ break;
+
+ case 'area':
+ entity._line = nodeline(entity);
+ ways.push(entity);
+ areas.push(entity);
+ break;
+
+ case 'point':
+ points.push(entity);
+ break;
+
+ case 'vertex':
+ vertices.push(entity);
+ break;
}
}
+
var parentStructure = graph.parentStructure(ways);
var wayAccuracyHandles = [];
for (i = 0; i < ways.length; i++) {
accuracyHandles(ways[i], wayAccuracyHandles);
}
+
drawVertices(vertices, parentStructure, filter);
drawAccuracyHandles(wayAccuracyHandles, filter);
drawCasings(lines, filter);
@@ -163,8 +171,7 @@ iD.Map = function() {
.attr('r', 4);
circles.attr('transform', pointTransform)
- .classed('shared', shared)
- .classed('hover', classHover);
+ .classed('shared', shared);
}
function drawAccuracyHandles(waynodes, filter) {
@@ -191,13 +198,11 @@ iD.Map = function() {
.filter(filter)
.data(data, key);
lines.exit().remove();
- lines.enter().append('path')
- .classed('hover', classHover);
+ lines.enter().append('path');
lines
.order()
.attr('d', getline)
- .attr('class', class_gen)
- .classed('hover', classHover);
+ .attr('class', class_gen);
return lines;
}
@@ -210,7 +215,6 @@ iD.Map = function() {
}
function drawPoints(points, filter) {
-
var groups = g.hit.selectAll('g.point')
.filter(filter)
.data(points, key);
@@ -234,7 +238,6 @@ iD.Map = function() {
groups.attr('transform', pointTransform);
- groups.classed('hover', classHover);
groups.select('image').attr('xlink:href', iD.Style.pointImage);
}
@@ -276,25 +279,6 @@ iD.Map = function() {
redraw(Object.keys(result.entities));
}
- function hoverIn() {
- if (!hoverEnabled) return;
- var datum = d3.select(d3.event.target).datum();
- if (datum instanceof iD.Entity) {
- hover = datum.id;
- redraw([hover]);
- d3.select('.messages').text(datum.tags.name || '#' + datum.id);
- }
- }
-
- function hoverOut() {
- if (hoverEnabled && hover) {
- var oldHover = hover;
- hover = null;
- redraw([oldHover]);
- d3.select('.messages').text('');
- }
- }
-
function zoomPan() {
if (d3.event && d3.event.sourceEvent.type === 'dblclick') {
if (!dblclickEnabled) {
@@ -363,12 +347,6 @@ iD.Map = function() {
return map;
};
- map.hoverEnable = function(_) {
- if (!arguments.length) return hoverEnabled;
- hoverEnabled = _;
- return map;
- };
-
map.fastEnable = function(_) {
if (!arguments.length) return fastEnabled;
fastEnabled = _;
@@ -423,6 +401,14 @@ iD.Map = function() {
}
};
+ map.centerEase = function(loc) {
+ var from = map.center().slice(), t = 0;
+ d3.timer(function() {
+ map.center(iD.util.geo.interp(from, loc, (t += 1) / 10));
+ return t == 10;
+ }, 20);
+ };
+
map.extent = function() {
return [projection.invert([0, 0]), projection.invert(dimensions)];
};
diff --git a/js/id/ui/inspector.js b/js/id/ui/inspector.js
index db9377d94..40bea7889 100644
--- a/js/id/ui/inspector.js
+++ b/js/id/ui/inspector.js
@@ -34,35 +34,14 @@ iD.Inspector = function() {
.text('+ Add New Tag')
.on('click', function() {
addTag();
- tagList.selectAll('li:last-child input.key').node().focus();
+ focusNewKey();
});
- var formsel = drawTags(entity.tags);
+ drawTags(entity.tags);
inspectorbody.append('div')
.attr('class', 'inspector-buttons')
.call(drawButtons);
-
- var inHeight = inspectorbody.node().offsetHeight;
-
- inspectorbody.style('display', 'none')
- .style('margin-top', (-inHeight) + 'px');
-
- var inspectortoggle = selection.append('button')
- .attr('class', 'inspector-toggle action')
- .on('click', function() {
- inspectortoggle.style('display', 'none');
- inspectorbody
- .style('display', 'block')
- .transition()
- .style('margin-top', '0px');
- });
-
- formsel.selectAll('input').node().focus();
-
- inspectortoggle.append('span')
- .text('Details')
- .attr('class','label');
}
function drawHead(selection) {
@@ -180,11 +159,16 @@ iD.Inspector = function() {
helpBtn.append('span')
.attr('class', 'icon inspect');
+ if (tags.length === 1 && tags[0].key === '' && tags[0].value === '') {
+ focusNewKey();
+ }
+
return li;
}
function pushMore() {
- if (d3.event.keyCode === 9 && tagList.selectAll('li:last-child input.value').node() === this) {
+ if (d3.event.keyCode === 9 &&
+ tagList.selectAll('li:last-child input.value').node() === this) {
addTag();
}
}
@@ -205,7 +189,10 @@ iD.Inspector = function() {
value.call(d3.typeahead()
.data(function(_, callback) {
- taginfo.values({key: key.property('value'), query: value.property('value')}, function(err, data) {
+ taginfo.values({
+ key: key.property('value'),
+ query: value.property('value')
+ }, function(err, data) {
callback(data.data.map(function (d) {
return {value: d.value, title: d.description};
}));
@@ -213,6 +200,10 @@ iD.Inspector = function() {
}));
}
+ function focusNewKey() {
+ tagList.selectAll('li:last-child input.key').node().focus();
+ }
+
function addTag() {
var tags = inspector.tags();
tags[''] = '';
diff --git a/test/index.html b/test/index.html
index f45262f73..e58eb7f39 100644
--- a/test/index.html
+++ b/test/index.html
@@ -66,6 +66,7 @@
+
@@ -111,6 +112,8 @@
+
+
diff --git a/test/index_packaged.html b/test/index_packaged.html
index 4866e8e30..eb211a0b7 100644
--- a/test/index_packaged.html
+++ b/test/index_packaged.html
@@ -38,6 +38,8 @@
+
+
diff --git a/test/spec/behavior/hover.js b/test/spec/behavior/hover.js
new file mode 100644
index 000000000..817654d59
--- /dev/null
+++ b/test/spec/behavior/hover.js
@@ -0,0 +1,36 @@
+describe("iD.behavior.Hover", function() {
+ var container;
+
+ beforeEach(function() {
+ container = d3.select('body').append('div');
+ });
+
+ afterEach(function() {
+ container.remove();
+ });
+
+ describe("mouseover", function () {
+ it("adds the 'hover' class to all elements to which the same datum is bound", function () {
+ container.selectAll('span')
+ .data(['a', 'b', 'a', 'b'])
+ .enter().append('span').attr('class', Object);
+
+ container.call(iD.behavior.Hover());
+ container.selectAll('.a').trigger('mouseover');
+
+ expect(container.selectAll('.a.hover')[0]).to.have.length(2);
+ expect(container.selectAll('.b.hover')[0]).to.have.length(0);
+ });
+ });
+
+ describe("mouseout", function () {
+ it("removes the 'hover' class from all elements", function () {
+ container.append('span').attr('class', 'hover');
+
+ container.call(iD.behavior.Hover());
+ container.selectAll('.hover').trigger('mouseout');
+
+ expect(container.selectAll('.hover')[0]).to.have.length(0);
+ });
+ });
+});