From 748b597c84ce43da1ecdb2b6abe6f356fdd0e065 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 22 Jan 2013 12:23:51 -0500 Subject: [PATCH 1/8] Fast zooming based on transforms. * Replaces the mouseup-tranformStart method with a redraw debouced by 1/5s --- css/app.css | 8 +++++++- js/id/renderer/map.js | 45 ++++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/css/app.css b/css/app.css index f57611633..e69d7e521 100644 --- a/css/app.css +++ b/css/app.css @@ -783,9 +783,14 @@ img.tile { -o-transform-origin:0 0; } -#tile-g { +#surface, #tile-g { position:absolute; top:0; + transform-origin:0 0; + -ms-transform-origin:0 0; + -webkit-transform-origin:0 0; + -moz-transform-origin:0 0; + -o-transform-origin:0 0; } /* About Section @@ -993,6 +998,7 @@ div.typeahead a:first-child { height:38px; padding:10px 20px; background:#fff; + color:#000; font-weight: normal; line-height: 21px; border-radius:5px; diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index c2de92855..0a77aedc6 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', 'drawn'), - translateStart, keybinding = d3.keybinding(), projection = d3.geo.mercator().scale(1024), roundedProjection = iD.svg.RoundProjection(projection), @@ -32,19 +31,15 @@ iD.Map = function() { var supersurface = selection.append('div') .style('position', 'absolute') - .on('mousedown.drag', function() { - translateStart = projection.translate(); - }) .call(zoom); surface = supersurface.append('svg') - .on('mouseup.reset-transform', resetTransform) - .on('touchend.reset-transform', resetTransform) .on('mousedown.zoom', function() { if (d3.event.button == 2) { d3.event.stopPropagation(); } }, true) + .attr('id', 'surface') .call(iD.svg.Surface()); @@ -136,32 +131,35 @@ iD.Map = function() { .text('Cannot zoom out further in current mode.'); return map.zoom(16); } - var fast = (d3.event.scale === projection.scale() && fastEnabled); + projection .translate(d3.event.translate) .scale(d3.event.scale); - if (fast && translateStart) { - var a = d3.event.translate, - b = translateStart, - translate = 'translate(' + ~~(a[0] - b[0]) + 'px,' + - ~~(a[1] - b[1]) + 'px)'; - tilegroup.style(transformProp, translate); - surface.style(transformProp, translate); - } else { - redraw(); - translateStart = null; - } + + var ascale = d3.event.scale; + var bscale = transformStart[0]; + var scale = (ascale / bscale); + + var tX = Math.round((d3.event.translate[0] / scale) - (transformStart[1][0])); + var tY = Math.round((d3.event.translate[1] / scale) - (transformStart[1][1])); + + var transform = + 'scale(' + scale + ')' + + 'translate(' + tX + 'px,' + tY + 'px) '; + + tilegroup.style(transformProp, transform); + surface.style(transformProp, transform); + redraw(); } function resetTransform() { if (!surface.style(transformProp)) return; - translateStart = null; surface.style(transformProp, ''); tilegroup.style(transformProp, ''); - redraw(); } - var redraw = _.throttle(function(difference) { + var redraw = _.debounce(function(difference) { + resetTransform(); dispatch.move(map); surface.attr('data-zoom', ~~map.zoom()); tilegroup.call(background); @@ -171,8 +169,11 @@ iD.Map = function() { } else { editOff(); } + transformStart = [ + projection.scale(), + projection.translate().slice()]; return map; - }, 10); + }, 200); function pointLocation(p) { var translate = projection.translate(), From 64445e50ac82ef4ce44da30f98058cb1bfb88de7 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 22 Jan 2013 14:17:27 -0500 Subject: [PATCH 2/8] Slow and fast redraw --- js/id/renderer/map.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 0a77aedc6..b228ea765 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -149,7 +149,7 @@ iD.Map = function() { tilegroup.style(transformProp, transform); surface.style(transformProp, transform); - redraw(); + queueRedraw(); } function resetTransform() { @@ -158,7 +158,7 @@ iD.Map = function() { tilegroup.style(transformProp, ''); } - var redraw = _.debounce(function(difference) { + function redraw(difference) { resetTransform(); dispatch.move(map); surface.attr('data-zoom', ~~map.zoom()); @@ -173,7 +173,9 @@ iD.Map = function() { projection.scale(), projection.translate().slice()]; return map; - }, 200); + } + + var queueRedraw = _.debounce(redraw, 200); function pointLocation(p) { var translate = projection.translate(), From 9a3d545f17119b706c6f3d794384dd94b3c99da1 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 22 Jan 2013 14:42:21 -0500 Subject: [PATCH 3/8] Do not focus text fields after initial placement --- js/id/modes/add_point.js | 2 +- js/id/modes/draw_area.js | 4 ++-- js/id/modes/draw_line.js | 9 +++++---- js/id/modes/select.js | 4 ++-- js/id/ui/inspector.js | 11 +++++++++-- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/js/id/modes/add_point.js b/js/id/modes/add_point.js index 21d6343e0..82ad283c2 100644 --- a/js/id/modes/add_point.js +++ b/js/id/modes/add_point.js @@ -19,7 +19,7 @@ iD.modes.AddPoint = function() { iD.actions.AddNode(node), 'added a point'); - controller.enter(iD.modes.Select(node)); + controller.enter(iD.modes.Select(node, true)); }); map.keybinding().on('⎋.addpoint', function() { diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js index 196a4de53..2f1fa6304 100644 --- a/js/id/modes/draw_area.js +++ b/js/id/modes/draw_area.js @@ -43,7 +43,7 @@ iD.modes.DrawArea = function(wayId) { if (datum.id === tailId || datum.id === headId) { if (way.nodes.length > 3) { history.undo(); - controller.enter(iD.modes.Select(way)); + controller.enter(iD.modes.Select(way, true)); } else { // Areas with less than 3 nodes gets deleted history.replace(iD.actions.DeleteWay(way.id)); @@ -94,7 +94,7 @@ iD.modes.DrawArea = function(wayId) { function ret() { d3.event.preventDefault(); history.replace(iD.actions.DeleteNode(node.id)); - controller.enter(iD.modes.Select(way)); + controller.enter(iD.modes.Select(way, true)); } surface diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js index 4be2e5527..8442fbf80 100644 --- a/js/id/modes/draw_line.js +++ b/js/id/modes/draw_line.js @@ -48,7 +48,7 @@ iD.modes.DrawLine = function(wayId, direction) { iD.actions.AddWayNode(wayId, tailId, index), 'added to a line'); - controller.enter(iD.modes.Select(way)); + controller.enter(iD.modes.Select(way, true)); } else { history.replace(iD.actions.DeleteWay(way.id)); @@ -59,7 +59,7 @@ iD.modes.DrawLine = function(wayId, direction) { // finish the way history.undo(); - controller.enter(iD.modes.Select(way)); + controller.enter(iD.modes.Select(way, true)); } else if (datum.type === 'node' && datum.id !== node.id) { // connect the way to an existing node @@ -71,13 +71,14 @@ iD.modes.DrawLine = function(wayId, direction) { controller.enter(iD.modes.DrawLine(wayId, direction)); } else if (datum.type === 'way' || datum.midpoint) { + var choice; // connect the way to an existing way if (datum.midpoint) { // if clicked on midpoint datum.id = datum.way; choice = datum; } else { - var choice = iD.util.geo.chooseIndex(datum, d3.mouse(surface.node()), map); + choice = iD.util.geo.chooseIndex(datum, d3.mouse(surface.node()), map); } history.replace( @@ -120,7 +121,7 @@ iD.modes.DrawLine = function(wayId, direction) { function ret() { d3.event.preventDefault(); history.replace(iD.actions.DeleteNode(node.id)); - controller.enter(iD.modes.Select(way)); + controller.enter(iD.modes.Select(way, true)); } function undo() { diff --git a/js/id/modes/select.js b/js/id/modes/select.js index add2e3750..883acacb1 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -1,11 +1,11 @@ -iD.modes.Select = function(entity) { +iD.modes.Select = function(entity, initial) { var mode = { id: 'select', button: 'browse', entity: entity }; - var inspector = iD.ui.inspector(), + var inspector = iD.ui.inspector().initial(!!initial), behaviors; function remove() { diff --git a/js/id/ui/inspector.js b/js/id/ui/inspector.js index aa3ad3add..7837ba21f 100644 --- a/js/id/ui/inspector.js +++ b/js/id/ui/inspector.js @@ -2,6 +2,7 @@ iD.ui.inspector = function() { var event = d3.dispatch('changeTags', 'changeWayDirection', 'update', 'remove', 'close', 'splitWay'), taginfo = iD.taginfo(), + initial = false, tagList; function inspector(selection) { @@ -194,7 +195,8 @@ iD.ui.inspector = function() { helpBtn.append('span') .attr('class', 'icon inspect'); - if (tags.length === 1 && tags[0].key === '' && tags[0].value === '') { + if (initial && tags.length === 1 && + tags[0].key === '' && tags[0].value === '') { focusNewKey(); } @@ -271,7 +273,7 @@ iD.ui.inspector = function() { event.close(entity); } - inspector.tags = function (tags) { + inspector.tags = function(tags) { if (!arguments.length) { tags = {}; tagList.selectAll('li').each(function() { @@ -286,5 +288,10 @@ iD.ui.inspector = function() { } }; + inspector.initial = function(_) { + initial = _; + return inspector; + }; + return d3.rebind(inspector, event, 'on'); }; From 6f73ae48d7d665e0a4131887eab54590404144d8 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 22 Jan 2013 14:49:28 -0500 Subject: [PATCH 4/8] Fix global leak --- js/id/renderer/map.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index b228ea765..ee145103f 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -12,6 +12,7 @@ iD.Map = function() { .on('zoom', zoomPan), dblclickEnabled = true, fastEnabled = true, + transformStart, minzoom = 0, background = iD.Background() .projection(projection), From 851eae68b86f44f465a83bb8b8e0fb90256c2eb2 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 22 Jan 2013 09:52:38 -0500 Subject: [PATCH 5/8] Match terminology with ReverseWay action --- js/id/modes/select.js | 2 +- js/id/ui/inspector.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 883acacb1..0cfd725f7 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -74,7 +74,7 @@ iD.modes.Select = function(entity, initial) { inspector .on('changeTags', changeTags) - .on('changeWayDirection', function(d) { + .on('reverseWay', function(d) { mode.history.perform( iD.actions.ReverseWay(d.id), 'reversed a way'); diff --git a/js/id/ui/inspector.js b/js/id/ui/inspector.js index 7837ba21f..be9db53f0 100644 --- a/js/id/ui/inspector.js +++ b/js/id/ui/inspector.js @@ -1,5 +1,5 @@ iD.ui.inspector = function() { - var event = d3.dispatch('changeTags', 'changeWayDirection', + var event = d3.dispatch('changeTags', 'reverseWay', 'update', 'remove', 'close', 'splitWay'), taginfo = iD.taginfo(), initial = false, @@ -84,7 +84,7 @@ iD.ui.inspector = function() { minorButtons.append('a') .attr('href', '#') .text('Reverse Direction') - .on('click', function() { event.changeWayDirection(entity); }); + .on('click', function() { event.reverseWay(entity); }); } if (entity.geometry() === 'vertex') { minorButtons.append('a') From ff15aa8e7b077b86ba05fa1415231f0730011eb9 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 22 Jan 2013 14:02:02 -0500 Subject: [PATCH 6/8] Rewrite d3.keybinding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A keybinding now represents a set of key commands that can be unbound as a set. Multiple keybindings are possible, and, providing a namespace is provided to the constructor, will not conflict with each other. Also, key combination strings such as ⌘+A are now supported. --- js/id/id.js | 32 ++-- js/id/modes/add_area.js | 9 +- js/id/modes/add_line.js | 9 +- js/id/modes/add_point.js | 9 +- js/id/modes/draw_area.js | 21 +-- js/id/modes/draw_line.js | 30 ++-- js/id/modes/select.js | 12 +- js/id/renderer/map.js | 9 - js/lib/d3.keybinding.js | 307 +++++++++++++++++++++------------ test/index.html | 2 + test/lib/happen.js | 27 +-- test/spec/lib/d3.keybinding.js | 55 ++++++ 12 files changed, 329 insertions(+), 193 deletions(-) create mode 100644 test/spec/lib/d3.keybinding.js diff --git a/js/id/id.js b/js/id/id.js index 31ff26836..9d7fb38bf 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -184,26 +184,18 @@ window.iD = function(container) { map.size(m.size()); }); - map.keybinding() - .on('a', function(evt, mods) { - if (mods) return; - controller.enter(iD.modes.AddArea()); - }) - .on('⌫.prevent_navigation', function(evt, mods) { - evt.preventDefault(); - }) - .on('p', function(evt, mods) { - if (mods) return; - controller.enter(iD.modes.AddPoint()); - }) - .on('l', function(evt, mods) { - if (mods) return; - controller.enter(iD.modes.AddLine()); - }) - .on('z', function(evt, mods) { - if (mods === '⇧⌘' || mods === '⌃⇧') history.redo(); - if (mods === '⌘' || mods === '⌃') history.undo(); - }); + var keybinding = d3.keybinding('main') + .on('P', function() { controller.enter(iD.modes.AddPoint()); }) + .on('L', function() { controller.enter(iD.modes.AddLine()); }) + .on('A', function() { controller.enter(iD.modes.AddArea()); }) + .on('⌘+Z', function() { history.undo(); }) + .on('⌃+Z', function() { history.undo(); }) + .on('⌘+⇧+Z', function() { history.redo(); }) + .on('⌃+⇧+Z', function() { history.redo(); }) + .on('⌫', function(e) { e.preventDefault(); }); + + d3.select(document) + .call(keybinding); var hash = iD.Hash().controller(controller).map(map); diff --git a/js/id/modes/add_area.js b/js/id/modes/add_area.js index a9fcb830e..d3936229e 100644 --- a/js/id/modes/add_area.js +++ b/js/id/modes/add_area.js @@ -6,6 +6,8 @@ iD.modes.AddArea = function() { description: 'Add parks, buildings, lakes, or other areas to the map.' }; + var keybinding = d3.keybinding('add-area'); + mode.enter = function() { var map = mode.map, history = mode.history, @@ -38,9 +40,12 @@ iD.modes.AddArea = function() { controller.enter(iD.modes.DrawArea(way.id)); }); - map.keybinding().on('⎋.addarea', function() { + keybinding.on('⎋', function() { controller.exit(); }); + + d3.select(document) + .call(keybinding); }; mode.exit = function() { @@ -49,7 +54,7 @@ iD.modes.AddArea = function() { }, 1000); mode.map.tail(false); mode.map.surface.on('click.addarea', null); - mode.map.keybinding().on('⎋.addarea', null); + keybinding.off(); }; return mode; diff --git a/js/id/modes/add_line.js b/js/id/modes/add_line.js index b83202097..db4e88e18 100644 --- a/js/id/modes/add_line.js +++ b/js/id/modes/add_line.js @@ -6,6 +6,8 @@ iD.modes.AddLine = function() { description: 'Lines can be highways, streets, pedestrian paths, or even canals.' }; + var keybinding = d3.keybinding('add-line'); + mode.enter = function() { var map = mode.map, node, @@ -60,16 +62,19 @@ iD.modes.AddLine = function() { controller.enter(iD.modes.DrawLine(way.id, direction)); }); - map.keybinding().on('⎋.addline', function() { + keybinding.on('⎋', function() { controller.exit(); }); + + d3.select(document) + .call(keybinding); }; mode.exit = function() { mode.map.dblclickEnable(true); mode.map.tail(false); mode.map.surface.on('click.addline', null); - mode.map.keybinding().on('⎋.addline', null); + keybinding.off(); }; return mode; diff --git a/js/id/modes/add_point.js b/js/id/modes/add_point.js index 82ad283c2..70275d050 100644 --- a/js/id/modes/add_point.js +++ b/js/id/modes/add_point.js @@ -5,6 +5,8 @@ iD.modes.AddPoint = function() { description: 'Restaurants, monuments, and postal boxes are points.' }; + var keybinding = d3.keybinding('add-point'); + mode.enter = function() { var map = mode.map, history = mode.history, @@ -22,15 +24,18 @@ iD.modes.AddPoint = function() { controller.enter(iD.modes.Select(node, true)); }); - map.keybinding().on('⎋.addpoint', function() { + keybinding.on('⎋', function() { controller.exit(); }); + + d3.select(document) + .call(keybinding); }; mode.exit = function() { mode.map.tail(false); mode.map.surface.on('click.addpoint', null); - mode.map.keybinding().on('⎋.addpoint', null); + keybinding.off(); }; return mode; diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js index 2f1fa6304..2fe02f37c 100644 --- a/js/id/modes/draw_area.js +++ b/js/id/modes/draw_area.js @@ -4,6 +4,8 @@ iD.modes.DrawArea = function(wayId) { id: 'draw-area' }; + var keybinding = d3.keybinding('draw-area'); + mode.enter = function() { var map = mode.map, surface = map.surface, @@ -102,11 +104,14 @@ iD.modes.DrawArea = function(wayId) { .on('mouseover.drawarea', mouseover) .on('click.drawarea', click); - map.keybinding() - .on('⌫.drawarea', backspace) - .on('⌦.drawarea', del) - .on('⎋.drawarea', ret) - .on('↩.drawarea', ret); + keybinding + .on('⌫', backspace) + .on('⌦', del) + .on('⎋', ret) + .on('↩', ret); + + d3.select(document) + .call(keybinding); }; mode.exit = function() { @@ -122,11 +127,7 @@ iD.modes.DrawArea = function(wayId) { .on('mousemove.drawarea', null) .on('click.drawarea', null); - mode.map.keybinding() - .on('⎋.drawarea', null) - .on('⌫.drawarea', null) - .on('⌦.drawarea', null) - .on('↩.drawarea', null); + keybinding.off(); window.setTimeout(function() { mode.map.dblclickEnable(true); diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js index 8442fbf80..e857bafe4 100644 --- a/js/id/modes/draw_line.js +++ b/js/id/modes/draw_line.js @@ -4,6 +4,8 @@ iD.modes.DrawLine = function(wayId, direction) { id: 'draw-line' }; + var keybinding = d3.keybinding('draw-line'); + mode.enter = function() { var map = mode.map, surface = map.surface, @@ -133,16 +135,19 @@ iD.modes.DrawLine = function(wayId, direction) { .on('mousemove.drawline', mousemove) .on('click.drawline', click); - map.keybinding() - .on('⌫.drawline', backspace) - .on('⌦.drawline', del) - .on('⎋.drawline', ret) - .on('↩.drawline', ret) - .on('z.drawline', function(evt, mods) { - if (mods === '⌘' || mods === '⌃') undo(); - }); + keybinding + .on('⌫', backspace) + .on('⌦', del) + .on('⎋', ret) + .on('↩', ret) + .on('⌘-Z', undo) + .on('⌃-Z', undo); - d3.select('#undo').on('click.drawline', undo); + d3.select(document) + .call(keybinding); + + d3.select('#undo') + .on('click.drawline', undo); }; mode.exit = function() { @@ -159,12 +164,7 @@ iD.modes.DrawLine = function(wayId, direction) { .on('mousemove.drawline', null) .on('click.drawline', null); - mode.map.keybinding() - .on('⌫.drawline', null) - .on('⌦.drawline', null) - .on('⎋.drawline', null) - .on('↩.drawline', null) - .on('z.drawline', null); + keybinding.off(); d3.select('#undo').on('click.drawline', null); diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 0cfd725f7..b735352aa 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -6,6 +6,7 @@ iD.modes.Select = function(entity, initial) { }; var inspector = iD.ui.inspector().initial(!!initial), + keybinding = d3.keybinding('select'), behaviors; function remove() { @@ -132,10 +133,10 @@ iD.modes.Select = function(entity, initial) { surface.on('click.select', click) .on('dblclick.browse', dblclick); - mode.map.keybinding().on('⌫.select', function(e) { - remove(); - e.preventDefault(); - }); + keybinding.on('⌫', remove); + + d3.select(document) + .call(keybinding); surface.selectAll("*") .filter(function (d) { @@ -166,8 +167,9 @@ iD.modes.Select = function(entity, initial) { var q = iD.util.stringQs(location.hash.substring(1)); location.hash = '#' + iD.util.qsString(_.omit(q, 'id'), true); + keybinding.off(); + surface.on("click.select", null); - mode.map.keybinding().on('⌫.select', null); mode.history.on('change.entity-undone', null); surface.selectAll(".selected") diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index ee145103f..7e74d7b66 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', 'drawn'), - keybinding = d3.keybinding(), projection = d3.geo.mercator().scale(1024), roundedProjection = iD.svg.RoundProjection(projection), zoom = d3.behavior.zoom() @@ -49,8 +48,6 @@ iD.Map = function() { supersurface .call(tail); - - d3.select(document).call(keybinding); } function pxCenter() { return [dimensions[0] / 2, dimensions[1] / 2]; } @@ -345,12 +342,6 @@ iD.Map = function() { return map; }; - map.keybinding = function (_) { - if (!arguments.length) return keybinding; - keybinding = _; - return map; - }; - map.background = background; map.projection = projection; map.redraw = redraw; diff --git a/js/lib/d3.keybinding.js b/js/lib/d3.keybinding.js index 01256aa20..e16ade725 100644 --- a/js/lib/d3.keybinding.js +++ b/js/lib/d3.keybinding.js @@ -1,120 +1,197 @@ -d3.keybinding = function() { - // via https://github.com/keithamus/jwerty/ - // and https://github.com/madrobby/keymaster - var _keys = { - // MOD aka toggleable keys - mods: { - // Shift key, ⇧ - '⇧': 16, - // CTRL key, on Mac: ⌃ - '⌃': 17, - // ALT key, on Mac: ⌥ (Alt) - '⌥': 18, - // META, on Mac: ⌘ (CMD), on Windows (Win), on Linux (Super) - '⌘': 91 - }, - // Normal keys - keys: { - // Backspace key, on Mac: ⌫ (Backspace) - '⌫': 8, backspace: 8, - // Tab Key, on Mac: ⇥ (Tab), on Windows ⇥⇥ - '⇥': 9, '⇆': 9, tab: 9, - // Return key, ↩ - '↩': 13, 'return': 13, enter: 13, '⌅': 13, - // Pause/Break key - 'pause': 19, 'pause-break': 19, - // Caps Lock key, ⇪ - '⇪': 20, caps: 20, 'caps-lock': 20, - // Escape key, on Mac: ⎋, on Windows: Esc - '⎋': 27, escape: 27, esc: 27, - // Space key - space: 32, - // Page-Up key, or pgup, on Mac: ↖ - '↖': 33, pgup: 33, 'page-up': 33, - // Page-Down key, or pgdown, on Mac: ↘ - '↘': 34, pgdown: 34, 'page-down': 34, - // END key, on Mac: ⇟ - '⇟': 35, end: 35, - // HOME key, on Mac: ⇞ - '⇞': 36, home: 36, - // Insert key, or ins - ins: 45, insert: 45, - // Delete key, on Mac: ⌦ (Delete) - '⌦': 46, del: 46, 'delete': 46, - // Left Arrow Key, or ← - '←': 37, left: 37, 'arrow-left': 37, - // Up Arrow Key, or ↑ - '↑': 38, up: 38, 'arrow-up': 38, - // Right Arrow Key, or → - '→': 39, right: 39, 'arrow-right': 39, - // Up Arrow Key, or ↓ - '↓': 40, down: 40, 'arrow-down': 40, - // odities, printing characters that come out wrong: - // Num-Multiply, or * - '*': 106, star: 106, asterisk: 106, multiply: 106, - // Num-Plus or + - '+': 107, 'plus': 107, - // Num-Subtract, or - - '-': 109, subtract: 109, - // Semicolon - ';': 186, semicolon:186, - // = or equals - '=': 187, 'equals': 187, - // Comma, or , - ',': 188, comma: 188, - //'-': 189, //??? - // Period, or ., or full-stop - '.': 190, period: 190, 'full-stop': 190, - // Slash, or /, or forward-slash - '/': 191, slash: 191, 'forward-slash': 191, - // Tick, or `, or back-quote - '`': 192, tick: 192, 'back-quote': 192, - // Open bracket, or [ - '[': 219, 'open-bracket': 219, - // Back slash, or \ - '\\': 220, 'back-slash': 220, - // Close backet, or ] - ']': 221, 'close-bracket': 221, - // Apostraphe, or Quote, or ' - '\'': 222, quote: 222, apostraphe: 222 +/* + * This code is licensed under the MIT license. + * + * Copyright © 2013, iD authors. + * + * Portions copyright © 2011, Keith Cirkel + * See https://github.com/keithamus/jwerty + * + */ +d3.keybinding = function(namespace) { + var bindings = []; + + function matches(binding, event) { + for (var p in binding.event) { + if (event[p] != binding.event[p]) + return false; } - }; - // To minimise code bloat, add all of the NUMPAD 0-9 keys in a loop - var i = 95, n = 0; - while (++i < 106) _keys.keys['num-' + n] = i; ++n; - // To minimise code bloat, add all of the top row 0-9 keys in a loop - i = 47, n = 0; - while (++i < 58) _keys.keys[n] = i; ++n; - // To minimise code bloat, add all of the F1-F25 keys in a loop - i = 111, n = 1; - while (++i < 136) _keys.keys['f' + n] = i; ++n; - // To minimise code bloat, add all of the letters of the alphabet in a loop - i = 64; - while(++i < 91) _keys.keys[String.fromCharCode(i).toLowerCase()] = i; - var pairs = d3.entries(_keys.keys), - event = d3.dispatch.apply(d3, d3.keys(_keys.keys)); - - function keys(selection) { - selection.on('keydown', function () { - var tagName = d3.select(d3.event.target).node().tagName; - if (tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA') { - return; - } - - var modifiers = ''; - if (d3.event.shiftKey) modifiers += '⇧'; - if (d3.event.ctrlKey) modifiers += '⌃'; - if (d3.event.altKey) modifiers += '⌥'; - if (d3.event.metaKey) modifiers += '⌘'; - - pairs.filter(function(d) { - return d.value === d3.event.keyCode; - }).forEach(function(d) { - event[d.key](d3.event, modifiers); - }); - }); + return (!binding.capture) === (event.eventPhase !== Event.CAPTURING_PHASE); } - return d3.rebind(keys, event, 'on'); + function capture() { + for (var i = 0; i < bindings.length; i++) { + var binding = bindings[i]; + if (matches(binding, d3.event)) { + binding.callback(); + } + } + } + + function bubble() { + var tagName = d3.select(d3.event.target).node().tagName; + if (tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA') { + return; + } + capture(); + } + + function keybinding(selection) { + selection = selection || d3.select(document); + selection.on('keydown.capture' + namespace, capture, true); + selection.on('keydown.bubble' + namespace, bubble, false); + return keybinding; + } + + keybinding.off = function(selection) { + selection = selection || d3.select(document); + selection.on('keydown.capture' + namespace, null); + selection.on('keydown.bubble' + namespace, null); + return keybinding; + }; + + keybinding.on = function(code, callback, capture) { + var binding = { + event: { + keyCode: 0, + shiftKey: false, + ctrlKey: false, + altKey: false, + metaKey: false + }, + capture: capture, + callback: callback + }; + + code = code.toLowerCase().match(/(?:(?:[^+])+|\+\+|^\+$)/g); + + for (var i = 0; i < code.length; i++) { + // Normalise matching errors + if (code[i] === '++') code[i] = '+'; + + if (code[i] in d3.keybinding.modifierCodes) { + binding.event[d3.keybinding.modifierProperties[d3.keybinding.modifierCodes[code[i]]]] = true; + } else if (code[i] in d3.keybinding.keyCodes) { + binding.event.keyCode = d3.keybinding.keyCodes[code[i]]; + } + } + + bindings.push(binding); + + return keybinding; + }; + + return keybinding; }; + +(function () { + d3.keybinding.modifierCodes = { + // Shift key, ⇧ + '⇧': 16, shift: 16, + // CTRL key, on Mac: ⌃ + '⌃': 17, ctrl: 17, + // ALT key, on Mac: ⌥ (Alt) + '⌥': 18, alt: 18, option: 18, + // META, on Mac: ⌘ (CMD), on Windows (Win), on Linux (Super) + '⌘': 91, meta: 91, cmd: 91, 'super': 91, win: 91 + }; + + d3.keybinding.modifierProperties = { + 16: 'shiftKey', + 17: 'ctrlKey', + 18: 'altKey', + 91: 'metaKey' + }; + + d3.keybinding.keyCodes = { + // Backspace key, on Mac: ⌫ (Backspace) + '⌫': 8, backspace: 8, + // Tab Key, on Mac: ⇥ (Tab), on Windows ⇥⇥ + '⇥': 9, '⇆': 9, tab: 9, + // Return key, ↩ + '↩': 13, 'return': 13, enter: 13, '⌅': 13, + // Pause/Break key + 'pause': 19, 'pause-break': 19, + // Caps Lock key, ⇪ + '⇪': 20, caps: 20, 'caps-lock': 20, + // Escape key, on Mac: ⎋, on Windows: Esc + '⎋': 27, escape: 27, esc: 27, + // Space key + space: 32, + // Page-Up key, or pgup, on Mac: ↖ + '↖': 33, pgup: 33, 'page-up': 33, + // Page-Down key, or pgdown, on Mac: ↘ + '↘': 34, pgdown: 34, 'page-down': 34, + // END key, on Mac: ⇟ + '⇟': 35, end: 35, + // HOME key, on Mac: ⇞ + '⇞': 36, home: 36, + // Insert key, or ins + ins: 45, insert: 45, + // Delete key, on Mac: ⌦ (Delete) + '⌦': 46, del: 46, 'delete': 46, + // Left Arrow Key, or ← + '←': 37, left: 37, 'arrow-left': 37, + // Up Arrow Key, or ↑ + '↑': 38, up: 38, 'arrow-up': 38, + // Right Arrow Key, or → + '→': 39, right: 39, 'arrow-right': 39, + // Up Arrow Key, or ↓ + '↓': 40, down: 40, 'arrow-down': 40, + // odities, printing characters that come out wrong: + // Num-Multiply, or * + '*': 106, star: 106, asterisk: 106, multiply: 106, + // Num-Plus or + + '+': 107, 'plus': 107, + // Num-Subtract, or - + '-': 109, subtract: 109, + // Semicolon + ';': 186, semicolon:186, + // = or equals + '=': 187, 'equals': 187, + // Comma, or , + ',': 188, comma: 188, + //'-': 189, //??? + // Period, or ., or full-stop + '.': 190, period: 190, 'full-stop': 190, + // Slash, or /, or forward-slash + '/': 191, slash: 191, 'forward-slash': 191, + // Tick, or `, or back-quote + '`': 192, tick: 192, 'back-quote': 192, + // Open bracket, or [ + '[': 219, 'open-bracket': 219, + // Back slash, or \ + '\\': 220, 'back-slash': 220, + // Close backet, or ] + ']': 221, 'close-bracket': 221, + // Apostrophe, or Quote, or ' + '\'': 222, quote: 222, apostrophe: 222 + }; + + // NUMPAD 0-9 + var i = 95, n = 0; + while (++i < 106) { + d3.keybinding.keyCodes['num-' + n] = i; + ++n; + } + + // 0-9 + i = 47; n = 0; + while (++i < 58) { + d3.keybinding.keyCodes[n] = i; + ++n; + } + + // F1-F25 + i = 111; n = 1; + while (++i < 136) { + d3.keybinding.keyCodes['f' + n] = i; + ++n; + } + + // a-z + i = 64; + while (++i < 91) { + d3.keybinding.keyCodes[String.fromCharCode(i).toLowerCase()] = i; + } +})(); diff --git a/test/index.html b/test/index.html index 425258d20..4099f2786 100644 --- a/test/index.html +++ b/test/index.html @@ -124,6 +124,8 @@ + + diff --git a/test/lib/happen.js b/test/lib/happen.js index 7c46d238d..8777d883a 100644 --- a/test/lib/happen.js +++ b/test/lib/happen.js @@ -19,10 +19,10 @@ evt = new Event(o.type); evt.keyCode = o.keyCode || 0; evt.charCode = o.charCode || 0; - evt.shift = o.shift || false; - evt.meta = o.meta || false; - evt.ctrl = o.ctrl || false; - evt.alt = o.alt || false; + evt.shiftKey = o.shiftKey || false; + evt.metaKey = o.metaKey || false; + evt.ctrlKey = o.ctrlKey || false; + evt.altKey = o.altKey || false; } else { evt = document.createEvent('KeyboardEvent'); // https://developer.mozilla.org/en/DOM/event.initKeyEvent @@ -33,10 +33,10 @@ true, // in boolean canBubbleArg, true, // in boolean cancelableArg, null, // in nsIDOMAbstractView viewArg, Specifies UIEvent.view. This value may be null. - o.ctrl || false, // in boolean ctrlKeyArg, - o.alt || false, // in boolean altKeyArg, - o.shift || false, // in boolean shiftKeyArg, - o.meta || false, // in boolean metaKeyArg, + o.ctrlKey || false, // in boolean ctrlKeyArg, + o.altKey || false, // in boolean altKeyArg, + o.shiftKey || false, // in boolean shiftKeyArg, + o.metaKey || false, // in boolean metaKeyArg, o.keyCode || 0, // in unsigned long keyCodeArg, o.charCode || 0 // in unsigned long charCodeArg); ); @@ -53,10 +53,10 @@ o.screenY || 0, // screenY o.clientX || 0, // clientX o.clientY || 0, // clientY - o.ctrl || 0, // ctrl - o.alt || false, // alt - o.shift || false, // shift - o.meta || false, // meta + o.ctrlKey || 0, // ctrl + o.altKey || false, // alt + o.shiftKey || false, // shift + o.metaKey || false, // meta o.button || false, // mouse button null // relatedTarget ); @@ -65,7 +65,8 @@ x.dispatchEvent(evt); }; - var shortcuts = ['click', 'mousedown', 'mouseup', 'mousemove', 'keydown', 'keyup', 'keypress'], + var shortcuts = ['click', 'mousedown', 'mouseup', 'mousemove', + 'mouseover', 'mouseout', 'keydown', 'keyup', 'keypress'], s, i = 0; while (s = shortcuts[i++]) { diff --git a/test/spec/lib/d3.keybinding.js b/test/spec/lib/d3.keybinding.js new file mode 100644 index 000000000..23e1a0e0c --- /dev/null +++ b/test/spec/lib/d3.keybinding.js @@ -0,0 +1,55 @@ +describe("d3.keybinding", function() { + var keybinding, spy, input; + + beforeEach(function () { + keybinding = d3.keybinding('keybinding-test'); + spy = sinon.spy(); + input = d3.select('body') + .append('input'); + }); + + afterEach(function () { + keybinding.off(d3.select(document)); + input.remove(); + }); + + describe("#on", function () { + it("returns self", function () { + expect(keybinding.on('a', spy)).to.equal(keybinding); + }); + + it("adds a binding for the specified bare key", function () { + d3.select(document).call(keybinding.on('A', spy)); + + happen.keydown(document, {keyCode: 65, metaKey: true}); + expect(spy).not.to.have.been.called; + + happen.keydown(document, {keyCode: 65}); + expect(spy).to.have.been.called; + }); + + it("adds a binding for the specified key combination", function () { + d3.select(document).call(keybinding.on('⌘+A', spy)); + + happen.keydown(document, {keyCode: 65}); + expect(spy).not.to.have.been.called; + + happen.keydown(document, {keyCode: 65, metaKey: true}); + expect(spy).to.have.been.called; + }); + + it("does not dispatch when focus is in input elements by default", function () { + d3.select(document).call(keybinding.on('A', spy)); + + happen.keydown(input.node(), {keyCode: 65}); + expect(spy).not.to.have.been.called; + }); + + it("dispatches when focus is in input elements when the capture flag was passed", function () { + d3.select(document).call(keybinding.on('A', spy, true)); + + happen.keydown(input.node(), {keyCode: 65}); + expect(spy).to.have.been.called; + }); + }); +}); From 814c3608db75d84ff3b8ce4bf30a1a67f7a6a45d Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 22 Jan 2013 14:42:29 -0500 Subject: [PATCH 7/8] Hook into undos in a different way This way doesn't depend on details of keybindings and the undo button. Also, implement this in DrawArea mode. --- js/id/graph/history.js | 4 +++- js/id/modes/draw_area.js | 14 +++++++++++--- js/id/modes/draw_line.js | 26 +++++++++++--------------- test/spec/graph/history.js | 15 +++++++++++++++ 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/js/id/graph/history.js b/js/id/graph/history.js index 3d027d042..142a9eeca 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -1,7 +1,7 @@ iD.History = function() { var stack, index, imagery_used = 'Bing', - dispatch = d3.dispatch('change'); + dispatch = d3.dispatch('change', 'undone', 'redone'); function perform(actions) { actions = Array.prototype.slice.call(actions); @@ -62,6 +62,7 @@ iD.History = function() { if (stack[index].annotation) break; } + dispatch.undone(); change(previous); }, @@ -73,6 +74,7 @@ iD.History = function() { if (stack[index].annotation) break; } + dispatch.redone(); change(previous); }, diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js index 2fe02f37c..026aa5bbc 100644 --- a/js/id/modes/draw_area.js +++ b/js/id/modes/draw_area.js @@ -112,16 +112,22 @@ iD.modes.DrawArea = function(wayId) { d3.select(document) .call(keybinding); + + history.on('undone.drawline', function () { + controller.enter(iD.modes.Browse()); + }); }; mode.exit = function() { - var surface = mode.map.surface; + var map = mode.map, + surface = map.surface, + history = mode.history; surface.selectAll('.way, .node') .classed('active', false); - mode.map.tail(false); - mode.map.fastEnable(true); + map.tail(false); + map.fastEnable(true); surface .on('mousemove.drawarea', null) @@ -129,6 +135,8 @@ iD.modes.DrawArea = function(wayId) { keybinding.off(); + history.on('undone.drawline', null); + window.setTimeout(function() { mode.map.dblclickEnable(true); }, 1000); diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js index e857bafe4..95719a6a9 100644 --- a/js/id/modes/draw_line.js +++ b/js/id/modes/draw_line.js @@ -126,11 +126,6 @@ iD.modes.DrawLine = function(wayId, direction) { controller.enter(iD.modes.Select(way, true)); } - function undo() { - history.undo(); - controller.enter(iD.modes.Browse()); - } - surface .on('mousemove.drawline', mousemove) .on('click.drawline', click); @@ -139,26 +134,27 @@ iD.modes.DrawLine = function(wayId, direction) { .on('⌫', backspace) .on('⌦', del) .on('⎋', ret) - .on('↩', ret) - .on('⌘-Z', undo) - .on('⌃-Z', undo); + .on('↩', ret); d3.select(document) .call(keybinding); - d3.select('#undo') - .on('click.drawline', undo); + history.on('undone.drawline', function () { + controller.enter(iD.modes.Browse()); + }); }; mode.exit = function() { - var surface = mode.map.surface; + var map = mode.map, + surface = map.surface, + history = mode.history; surface.selectAll('.way, .node') .classed('active', false); - mode.map.tail(false); - mode.map.fastEnable(true); - mode.map.minzoom(0); + map.tail(false); + map.fastEnable(true); + map.minzoom(0); surface .on('mousemove.drawline', null) @@ -166,7 +162,7 @@ iD.modes.DrawLine = function(wayId, direction) { keybinding.off(); - d3.select('#undo').on('click.drawline', null); + history.on('undone.drawline', null); window.setTimeout(function() { mode.map.dblclickEnable(true); diff --git a/test/spec/graph/history.js b/test/spec/graph/history.js index 554b22a82..309d396a1 100644 --- a/test/spec/graph/history.js +++ b/test/spec/graph/history.js @@ -83,6 +83,13 @@ describe("iD.History", function () { expect(history.redoAnnotation()).to.equal("annotation"); }); + it("emits an undone event", function () { + history.perform(action); + history.on('undone', spy); + history.undo(); + expect(spy).to.have.been.called; + }); + it("emits a change event", function () { history.perform(action); history.on('change', spy); @@ -92,6 +99,14 @@ describe("iD.History", function () { }); describe("#redo", function () { + it("emits an redone event", function () { + history.perform(action); + history.undo(); + history.on('change', spy); + history.redo(); + expect(spy).to.have.been.called; + }); + it("emits a change event", function () { history.perform(action); history.undo(); From 2e6c23d7ef6f3405ea2aeb33e9568b683b24b069 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 22 Jan 2013 14:58:45 -0500 Subject: [PATCH 8/8] Fix namespace --- js/id/modes/draw_area.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js index 026aa5bbc..a90d13c9c 100644 --- a/js/id/modes/draw_area.js +++ b/js/id/modes/draw_area.js @@ -113,7 +113,7 @@ iD.modes.DrawArea = function(wayId) { d3.select(document) .call(keybinding); - history.on('undone.drawline', function () { + history.on('undone.drawarea', function () { controller.enter(iD.modes.Browse()); }); }; @@ -135,7 +135,7 @@ iD.modes.DrawArea = function(wayId) { keybinding.off(); - history.on('undone.drawline', null); + history.on('undone.drawarea', null); window.setTimeout(function() { mode.map.dblclickEnable(true);