From 4cbee01be18c50d1fb6742915e7de9677d2d521d Mon Sep 17 00:00:00 2001 From: Neogeografen Date: Tue, 12 Feb 2013 23:18:45 +0100 Subject: [PATCH 01/17] Update locale/da.js Working on more danish translation - not finish yet --- locale/da.js | 78 ++++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/locale/da.js b/locale/da.js index 217f597f6..347a58962 100644 --- a/locale/da.js +++ b/locale/da.js @@ -34,27 +34,27 @@ locale.da = { operations: { add: { annotation: { - point: "Added a point.", - vertex: "Added a node to a way." + point: "Tilføjede et punkt.", + vertex: "Tilføjede en node til en vej." } }, start: { annotation: { - line: "Started a line.", - area: "Started an area." + line: "Startede en linje.", + area: "Startede et område." } }, - 'continue': { + 'Forsæt': { annotation: { - line: "Continued a line.", - area: "Continued an area." + line: "Forsatte en linje.", + area: "Forsatte et område." } }, cancel_draw: { - annotation: "Cancelled drawing." + annotation: "Annulleret indtegning." }, change_tags: { - annotation: "Changed tags." + annotation: "Ændret tags." }, circularize: { title: "Circularize", @@ -74,17 +74,17 @@ locale.da = { area: "Squared the corners of an area." } }, - 'delete': { - title: "Delete", - description: "Remove this from the map.", + 'slet': { + title: "Slet", + description: "Fjern dette fra kortet.", key: "⌫", annotation: { - point: "Deleted a point.", - vertex: "Deleted a node from a way.", - line: "Deleted a line.", - area: "Deleted an area.", - relation: "Deleted a relation.", - multiple: "Deleted {n} objects." + point: "Slettede et punkt.", + vertex: "Slettede en node fra en vej.", + line: "Slettede en linje.", + area: "Slettede et område.", + relation: "Sletede en relation.", + multiple: "Slettede {n} objekter." } }, connect: { @@ -108,14 +108,14 @@ locale.da = { annotation: "Merged {n} lines." }, move: { - title: "Move", - description: "Move this to a different location.", + title: "Flyt", + description: "Flyt dette til anden lokation.", key: "M", annotation: { - point: "Moved a point.", - vertex: "Moved a node in a way.", - line: "Moved a line.", - area: "Moved an area." + point: "Flyttede et punktMoved.", + vertex: "Flyttede en node i en vej.", + line: "Flyttede en linje.", + area: "Flyttede et område." } }, reverse: { @@ -125,10 +125,10 @@ locale.da = { annotation: "Reversed a line." }, split: { - title: "Split", - description: "Split this into two ways at this point.", + title: "Del op", + description: "Del op i to vej ved dette punkt.", key: "X", - annotation: "Split a way." + annotation: "Del op en vej." } }, @@ -140,17 +140,17 @@ locale.da = { deprecated_tags: "Deprecated tags: {tags}" }, - save: "Save", - unsaved_changes: "You have unsaved changes", - save_help: "Save changes to OpenStreetMap, making them visible to other users", - no_changes: "You don't have any changes to save.", - save_error: "An error occurred while trying to save", - uploading_changes: "Uploading changes to OpenStreetMap.", - just_edited: "You Just Edited OpenStreetMap!", - okay: "Okay", + save: "Gem", + unsaved_changes: "Du har ændringer der ikke er gemt endnu", + save_help: "Gem ændringer til OpenStreetMap gør dem synlige for andre brugere", + no_changes: "Du har ingen ændringer til at gemme endnu.", + save_error: "Der skete en fejl da du prøvede at gemme", + uploading_changes: "Gemmer nu ændringer til OpenStreetMap.", + just_edited: "Du har lige rettede i OpenStreetMap!", + okay: "Ok", - "zoom-in": "Zoom ind", - "zoom-out": "Zoom ud", + "zoom-ind": "Zoom ind", + "zoom-ud": "Zoom ud", nothing_to_undo: "Nothing to undo.", nothing_to_redo: "Nothing to redo.", @@ -158,8 +158,8 @@ locale.da = { browser_notice: "This editor is supported in Firefox, Chrome, Safari, Opera, and Internet Explorer 9 and above. Please upgrade your browser or use Potlatch 2 to edit the map.", inspector: { - no_documentation_combination: "This is no documentation available for this tag combination", - no_documentation_key: "This is no documentation available for this key", + no_documentation_combination: "Der er ingen dokumentation for denne tag kombination", + no_documentation_key: "Der er ingen dokumenation tilgængelig for denne nøgle", new_tag: "Nyt Tag" }, From 3f19a293aae2929a15390d1cabbe6cff87bd01b5 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 14:04:39 -0800 Subject: [PATCH 02/17] Make selection less laggy --- js/id/behavior/select.js | 61 ++-------------------------------------- js/id/modes/select.js | 7 ++++- 2 files changed, 9 insertions(+), 59 deletions(-) diff --git a/js/id/behavior/select.js b/js/id/behavior/select.js index 7369af97c..c0b942520 100644 --- a/js/id/behavior/select.js +++ b/js/id/behavior/select.js @@ -1,13 +1,6 @@ iD.behavior.Select = function(context) { - var behavior = function(selection) { - - var timeout = null, - // the position of the first mousedown - pos = null; - - function click(event) { - d3.event = event; + function click() { var datum = d3.event.target.__data__; if (datum instanceof iD.Entity) { if (d3.event.shiftKey) { @@ -20,59 +13,11 @@ iD.behavior.Select = function(context) { } } - function mousedown() { - var datum = d3.event.target.__data__; - pos = [d3.event.clientX, d3.event.clientY]; - if (datum instanceof iD.Entity || (datum && datum.type === 'midpoint')) { - selection - .on('mousemove.select', mousemove) - .on('touchmove.select', mousemove); - - // we've seen a mousedown within 400ms of this one, so ignore - // both because they will be a double click - if (timeout !== null) { - window.clearTimeout(timeout); - selection.on('mousemove.select', null); - timeout = null; - } else { - // queue the click handler to fire in 400ms if no other clicks - // are detected - timeout = window.setTimeout((function(event) { - return function() { - click(event); - timeout = null; - selection.on('mousemove.select', null); - }; - // save the event for the click handler - })(d3.event), 200); - } - } - } - - // allow mousemoves to cancel the click - function mousemove() { - if (iD.geo.dist([d3.event.clientX, d3.event.clientY], pos) > 4) { - window.clearTimeout(timeout); - timeout = null; - } - } - - function mouseup() { - selection.on('mousemove.select', null); - if (pos && d3.event.clientX === pos[0] && d3.event.clientY === pos[1] && - !(d3.event.target.__data__ instanceof iD.Entity)) { - context.enter(iD.modes.Browse(context)); - } - } - - selection - .on('mousedown.select', mousedown) - .on('mouseup.select', mouseup) - .on('touchstart.select', mousedown); + selection.on('click.select', click); }; behavior.off = function(selection) { - selection.on('mousedown.select', null); + selection.on('click.select', null); }; return behavior; diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 9b12c8134..db2b5f238 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -6,6 +6,7 @@ iD.modes.Select = function(context, selection, initial) { var inspector = iD.ui.inspector().initial(!!initial), keybinding = d3.keybinding('select'), + radialTime = null, behaviors = [ iD.behavior.Hover(), iD.behavior.Select(context), @@ -174,7 +175,9 @@ iD.modes.Select = function(context, selection, initial) { loc = entity.loc; } - context.surface().call(radialMenu, context.projection(loc)); + radialTime = window.setTimeout(function() { + context.surface().call(radialMenu, context.projection(loc)); + }, 300); } }; @@ -183,6 +186,8 @@ iD.modes.Select = function(context, selection, initial) { changeTags(singular(), inspector.tags()); } + if (radialTime) window.clearTimeout(radialTime); + context.container() .select('.inspector-wrap') .style('display', 'none') From 419aa088e33a865b87fb07cdb943528121072554 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 14:10:17 -0800 Subject: [PATCH 03/17] RadialMenu#center --- js/id/modes/select.js | 8 ++++---- js/id/ui/radial_menu.js | 11 +++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/js/id/modes/select.js b/js/id/modes/select.js index db2b5f238..f1a35d225 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -169,14 +169,14 @@ iD.modes.Select = function(context, selection, initial) { radialMenu = iD.ui.RadialMenu(operations); if (d3.event && !initial) { - var loc = context.map().mouseCoordinates(); - if (entity && entity.type === 'node') { - loc = entity.loc; + radialMenu.center(context.projection(entity.loc)); + } else { + radialMenu.center(d3.mouse(context.surface().node())); } radialTime = window.setTimeout(function() { - context.surface().call(radialMenu, context.projection(loc)); + context.surface().call(radialMenu); }, 300); } }; diff --git a/js/id/ui/radial_menu.js b/js/id/ui/radial_menu.js index 106a48196..e718cfe51 100644 --- a/js/id/ui/radial_menu.js +++ b/js/id/ui/radial_menu.js @@ -1,7 +1,8 @@ iD.ui.RadialMenu = function(operations) { - var menu; + var menu, + center = [0, 0]; - var radialMenu = function(selection, center) { + var radialMenu = function(selection) { if (!operations.length) return; @@ -94,5 +95,11 @@ iD.ui.RadialMenu = function(operations) { } }; + radialMenu.center = function(_) { + if (!arguments.length) return center; + center = _; + return radialMenu; + }; + return radialMenu; }; From 0dbdd7c79766658353a2374933bbfd3a423f057b Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 14:20:07 -0800 Subject: [PATCH 04/17] Shift-click deselects a selected entity --- js/id/behavior/select.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/js/id/behavior/select.js b/js/id/behavior/select.js index c0b942520..f3d52a71c 100644 --- a/js/id/behavior/select.js +++ b/js/id/behavior/select.js @@ -2,14 +2,20 @@ iD.behavior.Select = function(context) { var behavior = function(selection) { function click() { var datum = d3.event.target.__data__; - if (datum instanceof iD.Entity) { - if (d3.event.shiftKey) { - context.enter(iD.modes.Select(context, context.selection().concat([datum.id]))); - } else { - context.enter(iD.modes.Select(context, [datum.id])); - } - } else if (!d3.event.shiftKey) { + if (!(datum instanceof iD.Entity) && !d3.event.shiftKey) { context.enter(iD.modes.Browse(context)); + + } else if (!d3.event.shiftKey) { + context.enter(iD.modes.Select(context, [datum.id])); + + } else if (context.selection().indexOf(datum.id) >= 0) { + var selection = _.without(context.selection(), datum.id); + context.enter(selection.length ? + iD.modes.Select(context, selection) : + iD.modes.Browse(context)); + + } else { + context.enter(iD.modes.Select(context, context.selection().concat([datum.id]))); } } From 89fe4bff09695b0113e4380fb3b85b6f39e73c29 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 14:28:58 -0800 Subject: [PATCH 05/17] Only add vertex when double-clicking the selected entity Previously double-clicking would add a vertex to any way, as long as anything was selected. --- js/id/behavior/select.js | 4 +++- js/id/modes/select.js | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/js/id/behavior/select.js b/js/id/behavior/select.js index f3d52a71c..6b60b2da0 100644 --- a/js/id/behavior/select.js +++ b/js/id/behavior/select.js @@ -6,7 +6,9 @@ iD.behavior.Select = function(context) { context.enter(iD.modes.Browse(context)); } else if (!d3.event.shiftKey) { - context.enter(iD.modes.Select(context, [datum.id])); + // Avoid re-entering Select mode with same entity. + if (context.selection().length !== 1 || context.selection()[0] !== datum.id) + context.enter(iD.modes.Select(context, [datum.id])); } else if (context.selection().indexOf(datum.id) >= 0) { var selection = _.without(context.selection(), datum.id); diff --git a/js/id/modes/select.js b/js/id/modes/select.js index f1a35d225..c17a02ca1 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -6,7 +6,7 @@ iD.modes.Select = function(context, selection, initial) { var inspector = iD.ui.inspector().initial(!!initial), keybinding = d3.keybinding('select'), - radialTime = null, + timeout = null, behaviors = [ iD.behavior.Hover(), iD.behavior.Select(context), @@ -161,24 +161,27 @@ iD.modes.Select = function(context, selection, initial) { .call(keybinding); context.surface() - .on('dblclick.select', dblclick) .selectAll("*") .filter(selected) .classed('selected', true); radialMenu = iD.ui.RadialMenu(operations); + var showMenu = d3.event && !initial; - if (d3.event && !initial) { + if (showMenu) { if (entity && entity.type === 'node') { radialMenu.center(context.projection(entity.loc)); } else { radialMenu.center(d3.mouse(context.surface().node())); } - - radialTime = window.setTimeout(function() { - context.surface().call(radialMenu); - }, 300); } + + timeout = window.setTimeout(function() { + if (showMenu) context.surface().call(radialMenu); + + context.surface() + .on('dblclick.select', dblclick) + }, 300); }; mode.exit = function() { @@ -186,7 +189,7 @@ iD.modes.Select = function(context, selection, initial) { changeTags(singular(), inspector.tags()); } - if (radialTime) window.clearTimeout(radialTime); + if (timeout) window.clearTimeout(timeout); context.container() .select('.inspector-wrap') From 6bebb9197c9280103ff880d71b67e8df81f28631 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 14:58:54 -0800 Subject: [PATCH 06/17] 200ms --- js/id/modes/select.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/id/modes/select.js b/js/id/modes/select.js index c17a02ca1..ad1571d20 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -181,7 +181,7 @@ iD.modes.Select = function(context, selection, initial) { context.surface() .on('dblclick.select', dblclick) - }, 300); + }, 200); }; mode.exit = function() { From 3cce5b28dd2771240de3e5da26ba02e8651f3377 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 15:22:38 -0800 Subject: [PATCH 07/17] Fix, expand iD.behavior.Select tests --- js/id/behavior/select.js | 5 +++-- test/spec/behavior/select.js | 41 +++++++++++++++++++----------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/js/id/behavior/select.js b/js/id/behavior/select.js index 6b60b2da0..162535764 100644 --- a/js/id/behavior/select.js +++ b/js/id/behavior/select.js @@ -2,8 +2,9 @@ iD.behavior.Select = function(context) { var behavior = function(selection) { function click() { var datum = d3.event.target.__data__; - if (!(datum instanceof iD.Entity) && !d3.event.shiftKey) { - context.enter(iD.modes.Browse(context)); + if (!(datum instanceof iD.Entity)) { + if (!d3.event.shiftKey) + context.enter(iD.modes.Browse(context)); } else if (!d3.event.shiftKey) { // Avoid re-entering Select mode with same entity. diff --git a/test/spec/behavior/select.js b/test/spec/behavior/select.js index 5e9bf9ea3..ecbb7e7b4 100644 --- a/test/spec/behavior/select.js +++ b/test/spec/behavior/select.js @@ -20,6 +20,8 @@ describe("iD.behavior.Select", function() { .enter().append('circle') .attr('class', function(d) { return d.id; }); + context.enter(iD.modes.Browse(context)); + behavior = iD.behavior.Select(context); context.install(behavior); }); @@ -30,32 +32,33 @@ describe("iD.behavior.Select", function() { container.remove(); }); - specify("click on entity selects the entity", function(done) { - happen.mousedown(context.surface().select('.' + a.id).node()); - window.setTimeout(function() { - expect(context.selection()).to.eql([a.id]); - done(); - }, 600); + specify("click on entity selects the entity", function() { + happen.click(context.surface().select('.' + a.id).node()); + expect(context.selection()).to.eql([a.id]); }); - specify("click on empty space clears the selection", function(done) { + specify("click on empty space clears the selection", function() { context.enter(iD.modes.Select(context, [a.id])); happen.click(context.surface().node()); - happen.mousedown(context.surface().node()); - happen.mouseup(context.surface().node()); - window.setTimeout(function() { - expect(context.selection()).to.eql([]); - done(); - }, 600); + expect(context.mode().id).to.eql('browse'); }); - specify("shift-click on entity adds the entity to the selection", function(done) { + specify("shift-click on unselected entity adds it to the selection", function() { context.enter(iD.modes.Select(context, [a.id])); - happen.mousedown(context.surface().select('.' + b.id).node(), {shiftKey: true}); - window.setTimeout(function() { - expect(context.selection()).to.eql([a.id, b.id]); - done(); - }, 600); + happen.click(context.surface().select('.' + b.id).node(), {shiftKey: true}); + expect(context.selection()).to.eql([a.id, b.id]); + }); + + specify("shift-click on selected entity removes it from the selection", function() { + context.enter(iD.modes.Select(context, [a.id, b.id])); + happen.click(context.surface().select('.' + b.id).node(), {shiftKey: true}); + expect(context.selection()).to.eql([a.id]); + }); + + specify("shift-click on last selected entity clears the selection", function() { + context.enter(iD.modes.Select(context, [a.id])); + happen.click(context.surface().select('.' + a.id).node(), {shiftKey: true}); + expect(context.mode().id).to.eql('browse'); }); specify("shift-click on empty space leaves the selection unchanged", function() { From f071e9cf189e0e657fa7d302174d4f082d6bc6d1 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 15:28:14 -0800 Subject: [PATCH 08/17] Fix da translation --- locale/da.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locale/da.js b/locale/da.js index 347a58962..342273223 100644 --- a/locale/da.js +++ b/locale/da.js @@ -44,7 +44,7 @@ locale.da = { area: "Startede et område." } }, - 'Forsæt': { + 'continue': { annotation: { line: "Forsatte en linje.", area: "Forsatte et område." @@ -74,7 +74,7 @@ locale.da = { area: "Squared the corners of an area." } }, - 'slet': { + 'delete': { title: "Slet", description: "Fjern dette fra kortet.", key: "⌫", @@ -149,8 +149,8 @@ locale.da = { just_edited: "Du har lige rettede i OpenStreetMap!", okay: "Ok", - "zoom-ind": "Zoom ind", - "zoom-ud": "Zoom ud", + "zoom-in": "Zoom ind", + "zoom-out": "Zoom ud", nothing_to_undo: "Nothing to undo.", nothing_to_redo: "Nothing to redo.", From dc2dbbe183dcb327a08488a9bd3854fe4e68e086 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 15:45:12 -0800 Subject: [PATCH 09/17] Extract iD.ui.Zoom, add tooltips with key hint --- index.html | 1 + js/id/id.js | 2 ++ js/id/ui.js | 21 +++------------------ js/id/ui/zoom.js | 40 ++++++++++++++++++++++++++++++++++++++++ locale/da.js | 8 +++++--- locale/de.js | 8 +++++--- locale/en.js | 8 +++++--- locale/es.js | 8 +++++--- locale/fr.js | 8 +++++--- locale/ja.js | 8 +++++--- locale/lv.js | 8 +++++--- locale/tr.js | 8 +++++--- 12 files changed, 86 insertions(+), 42 deletions(-) create mode 100644 js/id/ui/zoom.js diff --git a/index.html b/index.html index 4ea9d9721..009ee4133 100644 --- a/index.html +++ b/index.html @@ -82,6 +82,7 @@ + diff --git a/js/id/id.js b/js/id/id.js index f0747ad02..5db609593 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -85,6 +85,8 @@ window.iD = function () { context.projection = map.projection; context.tail = map.tail; context.redraw = map.redraw; + context.zoomIn = map.zoomIn; + context.zoomOut = map.zoomOut; context.container = function(_) { if (!arguments.length) return container; diff --git a/js/id/ui.js b/js/id/ui.js index 535c825eb..7ae2c9e28 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -37,20 +37,9 @@ iD.ui = function(context) { .attr('class', 'button-wrap col1') .call(iD.ui.Save(context)); - var zoom = container.append('div') + container.append('div') .attr('class', 'zoombuttons map-control') - .selectAll('button') - .data([['zoom-in', '+', map.zoomIn, t('zoom-in')], ['zoom-out', '-', map.zoomOut, t('zoom-out')]]) - .enter() - .append('button') - .attr('tabindex', -1) - .attr('class', function(d) { return d[0]; }) - .attr('title', function(d) { return d[3]; }) - .on('click.editor', function(d) { return d[2](); }) - .append('span') - .attr('class', function(d) { - return d[0] + ' icon'; - }); + .call(iD.ui.Zoom(context)); if (navigator.geolocation) { container.append('div') @@ -139,11 +128,7 @@ iD.ui = function(context) { .on('←', pan([pa, 0])) .on('↑', pan([0, pa])) .on('→', pan([-pa, 0])) - .on('↓', pan([0, -pa])) - .on('⇧=', function() { map.zoomIn(); }) - .on('+', function() { map.zoomIn(); }) - .on('-', function() { map.zoomOut(); }) - .on('dash', function() { map.zoomOut(); }); + .on('↓', pan([0, -pa])); d3.select(document) .call(keybinding); diff --git a/js/id/ui/zoom.js b/js/id/ui/zoom.js new file mode 100644 index 000000000..823bc120f --- /dev/null +++ b/js/id/ui/zoom.js @@ -0,0 +1,40 @@ +iD.ui.Zoom = function(context) { + var zooms = [{ + id: 'zoom-in', + title: t('zoom.in'), + action: context.zoomIn, + key: '+' + }, { + id: 'zoom-out', + title: t('zoom.out'), + action: context.zoomOut, + key: '-' + }]; + + return function(selection) { + var button = selection.selectAll('button') + .data(zooms) + .enter().append('button') + .attr('tabindex', -1) + .attr('class', function(d) { return d.id; }) + .on('click.editor', function(d) { d.action(); }) + .call(bootstrap.tooltip() + .placement('right') + .html(true) + .title(function(d) { + return iD.ui.tooltipHtml(d.title, d.key); + })); + + button.append('span') + .attr('class', function(d) { return d.id + ' icon'; }); + + var keybinding = d3.keybinding('zoom') + .on('+', function() { context.zoomIn(); }) + .on('-', function() { context.zoomOut(); }) + .on('⇧=', function() { context.zoomIn(); }) + .on('dash', function() { context.zoomOut(); }); + + d3.select(document) + .call(keybinding); + } +}; diff --git a/locale/da.js b/locale/da.js index 342273223..29e23c6f2 100644 --- a/locale/da.js +++ b/locale/da.js @@ -149,9 +149,6 @@ locale.da = { just_edited: "Du har lige rettede i OpenStreetMap!", okay: "Ok", - "zoom-in": "Zoom ind", - "zoom-out": "Zoom ud", - nothing_to_undo: "Nothing to undo.", nothing_to_redo: "Nothing to redo.", @@ -197,5 +194,10 @@ locale.da = { source_switch: { live: "live", dev: "dev" + }, + + zoom: { + in: "Zoom ind", + out: "Zoom ud" } }; diff --git a/locale/de.js b/locale/de.js index 543eaab2d..f1f3ef850 100644 --- a/locale/de.js +++ b/locale/de.js @@ -145,9 +145,6 @@ locale.de = { just_edited: "Sie haben gerade OpenStreetMap editiert!", okay: "OK", - "zoom-in": "Hineinzoomen", - "zoom-out": "Herauszoomen", - nothing_to_undo: "Nichts zum Rückgängigmachen.", nothing_to_redo: "Nichts zum Wiederherstellen.", @@ -193,5 +190,10 @@ locale.de = { source_switch: { live: "live", dev: "dev" + }, + + zoom: { + in: "Hineinzoomen", + out: "Herauszoomen" } }; diff --git a/locale/en.js b/locale/en.js index 007c8e5be..501f36a37 100644 --- a/locale/en.js +++ b/locale/en.js @@ -145,9 +145,6 @@ locale.en = { just_edited: "You Just Edited OpenStreetMap!", okay: "Okay", - "zoom-in": "Zoom In", - "zoom-out": "Zoom Out", - nothing_to_undo: "Nothing to undo.", nothing_to_redo: "Nothing to redo.", @@ -193,5 +190,10 @@ locale.en = { source_switch: { live: "live", dev: "dev" + }, + + zoom: { + in: "Zoom In", + out: "Zoom Out" } }; diff --git a/locale/es.js b/locale/es.js index 4bb5f3959..0468405b5 100644 --- a/locale/es.js +++ b/locale/es.js @@ -145,9 +145,6 @@ locale.es = { just_edited: "Acabas de editar OpenStreetMap!", //"You Just Edited OpenStreetMap!", okay: "OK", //"Okay", - "zoom-in": "Aumentar", // "Zoom In", - "zoom-out": "Alejar", //"Zoom Out", - nothing_to_undo: "Nada para deshacer", //"Nothing to undo.", nothing_to_redo: "Nada para rehacer", //"Nothing to redo.", @@ -193,5 +190,10 @@ locale.es = { source_switch: { live: "en vivo", //"live", dev: "dev" + }, + + zoom: { + in: "Aumentar", // "Zoom In", + out: "Alejar" //"Zoom Out", } }; diff --git a/locale/fr.js b/locale/fr.js index adf4a2d26..4530924a0 100644 --- a/locale/fr.js +++ b/locale/fr.js @@ -145,9 +145,6 @@ locale.fr = { just_edited: "Vous venez de participer à OpenStreetMap!", okay: "Okay", - "zoom-in": "Zoomer", - "zoom-out": "Dézoomer", - nothing_to_undo: "Rien à annuler.", nothing_to_redo: "Rien à refaire.", @@ -193,5 +190,10 @@ locale.fr = { source_switch: { live: "live", dev: "dev" + }, + + zoom: { + in: "Zoomer", + out: "Dézoomer" } }; diff --git a/locale/ja.js b/locale/ja.js index b002f54bf..ada737bd1 100644 --- a/locale/ja.js +++ b/locale/ja.js @@ -145,9 +145,6 @@ locale.ja = { just_edited: "OpenStreetMap編集完了!", okay: "OK", - "zoom-in": "ズームイン", - "zoom-out": "ズームアウト", - nothing_to_undo: "やり直す変更点がありません", nothing_to_redo: "やり直した変更点がありません", @@ -193,5 +190,10 @@ locale.ja = { source_switch: { live: "本番サーバ", dev: "開発サーバ" + }, + + zoom: { + in: "ズームイン", + out: "ズームアウト" } }; diff --git a/locale/lv.js b/locale/lv.js index 1d55450f8..fd8be7cfe 100644 --- a/locale/lv.js +++ b/locale/lv.js @@ -145,9 +145,6 @@ locale.lv = { just_edited: "Jūs nupat rediģējāt OpenStreetMap", okay: "Labi", - "zoom-in": "Pietuvināt", - "zoom-out": "Attālināt", - nothing_to_undo: "Nav nekā, ko atcelt", nothing_to_redo: "Nav nekā, ko atsaukt", @@ -193,5 +190,10 @@ locale.lv = { source_switch: { live: "live", dev: "dev" + }, + + zoom: { + in: "Pietuvināt", + out: "Attālināt" } }; diff --git a/locale/tr.js b/locale/tr.js index d14f08ecb..a9b3e4b47 100644 --- a/locale/tr.js +++ b/locale/tr.js @@ -145,9 +145,6 @@ locale.tr = { just_edited: "Şu an OpenStreetMap'de bir değişiklik yaptınız!", okay: "Tamam", - "zoom-in": "Yaklaş", - "zoom-out": "Uzaklaş", - nothing_to_undo: "Geri alınacak birşey yok.", nothing_to_redo: "Tekrar yapılacak birşey yok.", @@ -193,5 +190,10 @@ locale.tr = { source_switch: { live: "canlı", dev: "geliştirme" + }, + + zoom: { + in: "Yaklaş", + out: "Uzaklaş" } }; From b9860f222fdbfab04f77313db7e66673a5d209f1 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 15:52:17 -0800 Subject: [PATCH 10/17] Cleanup; consistent tooltips on remaining buttons --- css/app.css | 4 ++-- js/id/ui.js | 19 ++++++++++--------- js/id/ui/geocoder.js | 10 +++++++--- js/id/ui/geolocate.js | 25 ++++++++++++++----------- js/id/ui/layerswitcher.js | 9 ++++++--- 5 files changed, 39 insertions(+), 28 deletions(-) diff --git a/css/app.css b/css/app.css index 29f5b82b5..9f9e54c01 100644 --- a/css/app.css +++ b/css/app.css @@ -849,13 +849,13 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} margin: 4px; } -.geocode-control div { +.geocode-control div.content { top: 50px; width: 340px; margin: 4px; padding: 5px; } -.geocode-control div span { +.geocode-control div.content span { display: inline-block; border-bottom: 1px solid #333; } diff --git a/js/id/ui.js b/js/id/ui.js index 7ae2c9e28..e1daad3a1 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -38,20 +38,21 @@ iD.ui = function(context) { .call(iD.ui.Save(context)); container.append('div') - .attr('class', 'zoombuttons map-control') + .attr('class', 'map-control zoombuttons') .call(iD.ui.Zoom(context)); - if (navigator.geolocation) { - container.append('div') - .call(iD.ui.geolocate(map)); - } + container.append('div') + .attr('class', 'map-control geocode-control') + .call(iD.ui.Geocoder(context)); - container.append('div').attr('class', 'geocode-control map-control') - .call(iD.ui.geocoder(context)); - - container.append('div').attr('class', 'map-control layerswitcher-control') + container.append('div') + .attr('class', 'map-control layerswitcher-control') .call(iD.ui.layerswitcher(context)); + container.append('div') + .attr('class', 'map-control geolocate-control') + .call(iD.ui.Geolocate(map)); + container.append('div') .style('display', 'none') .attr('class', 'inspector-wrap fr col5'); diff --git a/js/id/ui/geocoder.js b/js/id/ui/geocoder.js index 7b79c2158..d0479e61d 100644 --- a/js/id/ui/geocoder.js +++ b/js/id/ui/geocoder.js @@ -1,4 +1,4 @@ -iD.ui.geocoder = function(context) { +iD.ui.Geocoder = function(context) { function resultExtent(bounds) { return new iD.geo.Extent( [parseFloat(bounds[3]), parseFloat(bounds[0])], @@ -75,8 +75,12 @@ iD.ui.geocoder = function(context) { var button = selection.append('button') .attr('tabindex', -1) .attr('title', t('geocoder.title')) - .html('') - .on('click', toggle); + .on('click', toggle) + .call(bootstrap.tooltip() + .placement('right')); + + button.append('span') + .attr('class', 'icon geocode'); var gcForm = selection.append('form'); diff --git a/js/id/ui/geolocate.js b/js/id/ui/geolocate.js index 14b060e8a..5356039dd 100644 --- a/js/id/ui/geolocate.js +++ b/js/id/ui/geolocate.js @@ -1,4 +1,8 @@ -iD.ui.geolocate = function(map) { +iD.ui.Geolocate = function(map) { + function click() { + navigator.geolocation.getCurrentPosition( + success, error); + } function success(position) { map.center([position.coords.longitude, position.coords.latitude]); @@ -7,17 +11,16 @@ iD.ui.geolocate = function(map) { function error() { } return function(selection) { - selection - .attr('class', 'geolocate-control map-control') - .append('button') + if (!navigator.geolocation) return; + + var button = selection.append('button') .attr('tabindex', -1) .attr('title', 'Show My Location') - .on('click', function() { - navigator.geolocation.getCurrentPosition( - success, error); - }) - .append('span') - .attr('class','icon geolocate'); - }; + .on('click', click) + .call(bootstrap.tooltip() + .placement('right')); + button.append('span') + .attr('class', 'icon geolocate'); + }; }; diff --git a/js/id/ui/layerswitcher.js b/js/id/ui/layerswitcher.js index 9ae4f3fbe..60fd674e0 100644 --- a/js/id/ui/layerswitcher.js +++ b/js/id/ui/layerswitcher.js @@ -23,10 +23,13 @@ iD.ui.layerswitcher = function(context) { .attr('tabindex', -1) .attr('class', 'fillD') .attr('title', t('layerswitcher.description')) - .html("") - .on('click.layerswitcher-toggle', toggle); + .on('click.layerswitcher-toggle', toggle) + .call(bootstrap.tooltip() + .placement('right')); + + button.append('span') + .attr('class', 'layers icon'); - function show() { setVisible(true); } function hide() { setVisible(false); } function toggle() { setVisible(content.classed('hide')); } From 8d0225e9389c9cf81e145676c7be5af9018ed541 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 16:11:22 -0800 Subject: [PATCH 11/17] i18n for geolocate --- js/id/ui/geolocate.js | 2 +- locale/en.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/js/id/ui/geolocate.js b/js/id/ui/geolocate.js index 5356039dd..d1d3f3772 100644 --- a/js/id/ui/geolocate.js +++ b/js/id/ui/geolocate.js @@ -15,7 +15,7 @@ iD.ui.Geolocate = function(map) { var button = selection.append('button') .attr('tabindex', -1) - .attr('title', 'Show My Location') + .attr('title', t('geolocate.title')) .on('click', click) .call(bootstrap.tooltip() .placement('right')); diff --git a/locale/en.js b/locale/en.js index 501f36a37..7581b5fcb 100644 --- a/locale/en.js +++ b/locale/en.js @@ -168,6 +168,10 @@ locale.en = { no_results: "Couldn't locate a place named '{name}'" }, + geolocate: { + title: "Show My Location" + }, + description: "Description", logout: "logout", From 1c5a894f1e37bb5ebbeb66eaf2938df678610d0b Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 16:16:52 -0800 Subject: [PATCH 12/17] Add to translation README --- locale/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/locale/README.md b/locale/README.md index b1610b311..0feb1f631 100644 --- a/locale/README.md +++ b/locale/README.md @@ -22,6 +22,10 @@ Let's look at an example line from `en.js`: no_results: "Couldn't locate a place named '{name}'" ``` +`no_results` is the translation _key_, and should not be translated. +The text to the right of the colon, `"Couldn't locate a place named '{name}'"`, +is the string to be translated. + The word in brackets, `{name}`, should **not** be translated into a new language: it's replaced with a place name when iD presents the text. So a French translation would look like @@ -30,6 +34,15 @@ a French translation would look like no_results: "Impossible de localiser l'endroit nommé '{name}'" ``` +For technical reasons, a few translation keys are quoted. For example: + +``` +'delete': "Delete" +``` + +Only translate the value to the right of the colon, not the quoted key on +the left. + ## License Contributions to translations are under the same liberal From 5c9832e2ef5a28f89d9a364b5608fc9d9e16fc6e Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 16:18:55 -0800 Subject: [PATCH 13/17] Fix test --- test/spec/ui/geocoder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/spec/ui/geocoder.js b/test/spec/ui/geocoder.js index 54a7e571c..91e107ee2 100644 --- a/test/spec/ui/geocoder.js +++ b/test/spec/ui/geocoder.js @@ -1,6 +1,6 @@ -describe("iD.ui.geocoder", function () { +describe("iD.ui.Geocoder", function () { it('can be instantiated', function () { - var geocoder = iD.ui.geocoder(); + var geocoder = iD.ui.Geocoder(); expect(geocoder).to.be.ok; }); }); From 50e01150a7379b91867747e3c3eb77c16f0137c8 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 16:25:39 -0800 Subject: [PATCH 14/17] Fix global leak --- js/id/ui/tag_reference.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/id/ui/tag_reference.js b/js/id/ui/tag_reference.js index cdb22b505..6b7b9fb9f 100644 --- a/js/id/ui/tag_reference.js +++ b/js/id/ui/tag_reference.js @@ -19,7 +19,7 @@ iD.ui.tagReference = function(selection) { header.append('span') .text(g('title')); - referenceBody = selection.append('div') + var referenceBody = selection.append('div') .attr('class','modal-section fillL2'); referenceBody From 1e60b0b7fa7c2057cc7f22478fe2e71f16aada3a Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 16:28:02 -0800 Subject: [PATCH 15/17] iD constructors are camel case --- js/id/behavior/lasso.js | 2 +- js/id/modes/select.js | 2 +- js/id/ui.js | 6 +++--- js/id/ui/commit.js | 2 +- js/id/ui/contributors.js | 2 +- js/id/ui/geocoder.js | 2 +- js/id/ui/inspector.js | 4 ++-- js/id/ui/lasso.js | 6 +++--- js/id/ui/layerswitcher.js | 4 ++-- js/id/ui/save.js | 4 ++-- js/id/ui/success.js | 2 +- js/id/ui/toggle.js | 2 +- js/id/ui/userpanel.js | 2 +- test/spec/ui/inspector.js | 4 ++-- 14 files changed, 22 insertions(+), 22 deletions(-) diff --git a/js/id/behavior/lasso.js b/js/id/behavior/lasso.js index 0a26f311d..6196cbf2e 100644 --- a/js/id/behavior/lasso.js +++ b/js/id/behavior/lasso.js @@ -12,7 +12,7 @@ iD.behavior.Lasso = function(context) { pos = [d3.event.clientX, d3.event.clientY]; - lasso = iD.ui.lasso().a(d3.mouse(context.surface().node())); + lasso = iD.ui.Lasso().a(d3.mouse(context.surface().node())); context.surface().call(lasso); diff --git a/js/id/modes/select.js b/js/id/modes/select.js index ad1571d20..ac6ad4943 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -4,7 +4,7 @@ iD.modes.Select = function(context, selection, initial) { button: 'browse' }; - var inspector = iD.ui.inspector().initial(!!initial), + var inspector = iD.ui.Inspector().initial(!!initial), keybinding = d3.keybinding('select'), timeout = null, behaviors = [ diff --git a/js/id/ui.js b/js/id/ui.js index e1daad3a1..48e5682bf 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -47,7 +47,7 @@ iD.ui = function(context) { container.append('div') .attr('class', 'map-control layerswitcher-control') - .call(iD.ui.layerswitcher(context)); + .call(iD.ui.LayerSwitcher(context)); container.append('div') .attr('class', 'map-control geolocate-control') @@ -103,7 +103,7 @@ iD.ui = function(context) { linkList.append('li') .attr('id', 'user-list') - .call(iD.ui.contributors(context)); + .call(iD.ui.Contributors(context)); window.onbeforeunload = function() { history.save(); @@ -142,7 +142,7 @@ iD.ui = function(context) { map.centerZoom([-77.02271, 38.90085], 20); } - userContainer.call(iD.ui.userpanel(connection) + userContainer.call(iD.ui.UserPanel(connection) .on('logout.editor', connection.logout) .on('login.editor', connection.authenticate)); diff --git a/js/id/ui/commit.js b/js/id/ui/commit.js index 8d724a732..6708ece49 100644 --- a/js/id/ui/commit.js +++ b/js/id/ui/commit.js @@ -1,4 +1,4 @@ -iD.ui.commit = function(context) { +iD.ui.Commit = function(context) { var event = d3.dispatch('cancel', 'save', 'fix'); function zipSame(d) { diff --git a/js/id/ui/contributors.js b/js/id/ui/contributors.js index 596c576ad..2389f266a 100644 --- a/js/id/ui/contributors.js +++ b/js/id/ui/contributors.js @@ -1,4 +1,4 @@ -iD.ui.contributors = function(context) { +iD.ui.Contributors = function(context) { function update(selection) { var users = {}, limit = 3, diff --git a/js/id/ui/geocoder.js b/js/id/ui/geocoder.js index d0479e61d..cc0ce205c 100644 --- a/js/id/ui/geocoder.js +++ b/js/id/ui/geocoder.js @@ -64,7 +64,7 @@ iD.ui.Geocoder = function(context) { function setVisible(show) { if (show !== shown) { button.classed('active', show); - gcForm.call(iD.ui.toggle(show)); + gcForm.call(iD.ui.Toggle(show)); if (!show) resultsList.classed('hide', !show); if (show) inputNode.node().focus(); else inputNode.node().blur(); diff --git a/js/id/ui/inspector.js b/js/id/ui/inspector.js index 836f73cff..173594101 100644 --- a/js/id/ui/inspector.js +++ b/js/id/ui/inspector.js @@ -1,4 +1,4 @@ -iD.ui.inspector = function() { +iD.ui.Inspector = function() { var event = d3.dispatch('changeTags', 'close'), taginfo = iD.taginfo(), initial = false, @@ -43,7 +43,7 @@ iD.ui.inspector = function() { .attr('class', 'inspector-buttons pad1 fillD') .call(drawButtons); - inspector.call(iD.ui.toggle(true)); + inspector.call(iD.ui.Toggle(true)); } function drawHead(selection) { diff --git a/js/id/ui/lasso.js b/js/id/ui/lasso.js index 511df2a7e..4644364c3 100644 --- a/js/id/ui/lasso.js +++ b/js/id/ui/lasso.js @@ -1,4 +1,4 @@ -iD.ui.lasso = function() { +iD.ui.Lasso = function() { var center, box, group, @@ -13,7 +13,7 @@ iD.ui.lasso = function() { box = group.append('rect') .attr('class', 'lasso-box'); - group.call(iD.ui.toggle(true)); + group.call(iD.ui.Toggle(true)); } @@ -50,7 +50,7 @@ iD.ui.lasso = function() { lasso.close = function(selection) { if (group) { - group.call(iD.ui.toggle(false, function() { + group.call(iD.ui.Toggle(false, function() { d3.select(this).remove(); })); } diff --git a/js/id/ui/layerswitcher.js b/js/id/ui/layerswitcher.js index 60fd674e0..6724715e0 100644 --- a/js/id/ui/layerswitcher.js +++ b/js/id/ui/layerswitcher.js @@ -1,4 +1,4 @@ -iD.ui.layerswitcher = function(context) { +iD.ui.LayerSwitcher = function(context) { var event = d3.dispatch('cancel', 'save'), opacities = [1, 0.5, 0]; @@ -36,7 +36,7 @@ iD.ui.layerswitcher = function(context) { function setVisible(show) { if (show !== shown) { button.classed('active', show); - content.call(iD.ui.toggle(show)); + content.call(iD.ui.Toggle(show)); shown = show; } } diff --git a/js/id/ui/save.js b/js/id/ui/save.js index b8f9d619a..b4eed1dd0 100644 --- a/js/id/ui/save.js +++ b/js/id/ui/save.js @@ -17,7 +17,7 @@ iD.ui.Save = function(context) { modal.select('.content') .classed('commit-modal', true) .datum(changes) - .call(iD.ui.commit(context) + .call(iD.ui.Commit(context) .on('cancel', function() { modal.remove(); }) @@ -60,7 +60,7 @@ iD.ui.Save = function(context) { id: changeset_id, comment: e.comment }) - .call(iD.ui.success(connection) + .call(iD.ui.Success(connection) .on('cancel', function() { modal.remove(); })); diff --git a/js/id/ui/success.js b/js/id/ui/success.js index fffa7b6ab..17a577fb3 100644 --- a/js/id/ui/success.js +++ b/js/id/ui/success.js @@ -1,4 +1,4 @@ -iD.ui.success = function(connection) { +iD.ui.Success = function(connection) { var event = d3.dispatch('cancel', 'save'); function success(selection) { diff --git a/js/id/ui/toggle.js b/js/id/ui/toggle.js index 3a7500023..c3c299905 100644 --- a/js/id/ui/toggle.js +++ b/js/id/ui/toggle.js @@ -2,7 +2,7 @@ // hide class, which sets display=none, and a d3 transition for opacity. // this will cause blinking when called repeatedly, so check that the // value actually changes between calls. -iD.ui.toggle = function(show, callback) { +iD.ui.Toggle = function(show, callback) { return function(selection) { selection.style('opacity', show ? 0 : 1) .classed('hide', false) diff --git a/js/id/ui/userpanel.js b/js/id/ui/userpanel.js index fda2fc463..3dab2a5bd 100644 --- a/js/id/ui/userpanel.js +++ b/js/id/ui/userpanel.js @@ -1,4 +1,4 @@ -iD.ui.userpanel = function(connection) { +iD.ui.UserPanel = function(connection) { var event = d3.dispatch('logout', 'login'); function user(selection) { diff --git a/test/spec/ui/inspector.js b/test/spec/ui/inspector.js index e92604a7a..17160ddb4 100644 --- a/test/spec/ui/inspector.js +++ b/test/spec/ui/inspector.js @@ -1,10 +1,10 @@ -describe("iD.ui.inspector", function () { +describe("iD.ui.Inspector", function () { var inspector, element, tags = {highway: 'residential'}, entity, graph, context; function render() { - inspector = iD.ui.inspector().context(context); + inspector = iD.ui.Inspector().context(context); element = d3.select('body') .append('div') .attr('id', 'inspector-wrap') From 902ae8026709fdb9467a87a8764b521c44584781 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 16:51:31 -0800 Subject: [PATCH 16/17] Include all locale files --- Makefile | 4 ++-- index.html | 11 ++++++----- {locale => js/lib}/locale.js | 0 3 files changed, 8 insertions(+), 7 deletions(-) rename {locale => js/lib}/locale.js (100%) diff --git a/Makefile b/Makefile index 826337109..bb6844399 100644 --- a/Makefile +++ b/Makefile @@ -52,8 +52,8 @@ all: \ js/id/ui/*.js \ js/id/validate.js \ js/id/end.js \ - locale/locale.js \ - locale/en.js + js/lib/locale.js \ + locale/*.js iD.js: Makefile @rm -f $@ diff --git a/index.html b/index.html index 009ee4133..e79c178e4 100644 --- a/index.html +++ b/index.html @@ -147,14 +147,15 @@ - - - - + + + + + + - diff --git a/locale/locale.js b/js/lib/locale.js similarity index 100% rename from locale/locale.js rename to js/lib/locale.js From 211431bdb87287808328490d57ae04f72eced161 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 12 Feb 2013 16:58:44 -0800 Subject: [PATCH 17/17] Fix "Browse" button in non-English locales --- css/app.css | 2 +- js/id/ui/modes.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/css/app.css b/css/app.css index 9f9e54c01..765c58f1f 100644 --- a/css/app.css +++ b/css/app.css @@ -327,7 +327,7 @@ button.centered { border-radius:0 4px 4px 0; } -button.Browse .label { display: none;} +button.browse .label { display: none;} button.action { background: #7092ff; diff --git a/js/id/ui/modes.js b/js/id/ui/modes.js index b382ab295..d3db0074a 100644 --- a/js/id/ui/modes.js +++ b/js/id/ui/modes.js @@ -11,7 +11,7 @@ iD.ui.Modes = function(context) { buttons.enter().append('button') .attr('tabindex', -1) - .attr('class', function(mode) { return mode.title + ' add-button col3'; }) + .attr('class', function(mode) { return mode.id + ' add-button col3'; }) .on('click.mode-buttons', function(mode) { context.enter(mode); }) .call(bootstrap.tooltip() .placement('bottom')