diff --git a/Makefile b/Makefile index 9c7e33783..125957615 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ all: \ js/lib/d3.keybinding.js \ js/lib/d3.one.js \ js/lib/d3.size.js \ + js/lib/d3.trigger.js \ js/lib/d3.typeahead.js \ js/lib/jxon.js \ js/lib/lodash.js \ diff --git a/css/app.css b/css/app.css index 8e4c019f3..e7f5d9466 100644 --- a/css/app.css +++ b/css/app.css @@ -291,6 +291,25 @@ button.delete:hover { button.save { background-color: #6bc641; + width: 120px; + position: relative; +} + +button.save.has-count { + padding-right:10px; +} + +button.save.has-count .count { + display: block; + position: absolute; + font-size:small; + top: 0; + right: 0; + background: #fff; + height: 37px; + border-radius: 0 3px 3px 0; + padding: 0 10px; + line-height: 40px; } button.save:hover { @@ -452,7 +471,7 @@ button.Browse .label { position:relative; } -.inspector-wrap a.permalink { +.inspector-inner.head a { text-decoration:none; margin-right: 10px; display: inline-block @@ -489,7 +508,7 @@ button.Browse .label { .tag-row input { width: 50%; - border-right: 0; + border-left: 0; } .tag-row input.key { @@ -500,9 +519,8 @@ button.Browse .label { border-top: 1px solid #ccc; } -.tag-row input.value { - position: relative; - border-right: 1px solid #ccc; +.tag-row input.key { + border-left: 1px solid #ccc; } .input-wrap::after { @@ -531,8 +549,17 @@ button.Browse .label { border-top: 1px solid #ccc; } -.tag-row-empty button { - display: none; +.inspector-inner .add-tag-row { + width: 100%; + padding-right: 70px; +} + +.inspector-inner .add-tag { + width: 50%; + height: 30px; + font-size: 100%; + border: 1px solid #ccc; + border-top: 0; } /* Map Controls */ @@ -572,6 +599,30 @@ button.Browse .label { top:210px; } +.layerswitcher-control .adjustments { + padding:5px; + opacity:0.2; +} + +.layerswitcher-control .adjustments:hover { + opacity:1; +} + +.layerswitcher-control .adjustments .reset { + height:20px; + font-size:10px; + font-weight:normal; + padding:0 5px; +} + +.layerswitcher-control .nudge { + height:20px; + width:20px; + font-size:10px; + margin-right:2px; + font-weight:normal; +} + .opacity-options-wrapper { padding: 10px 10px 0 10px; } @@ -630,6 +681,10 @@ button.Browse .label { margin: 4px; } +.geolocate-control { + top:260px; +} + /* Map ------------------------------------------------------- */ @@ -709,8 +764,8 @@ div.typeahead { box-shadow: 0 5px 10px 0 rgba(0,0,0,.2); margin-top: -1px; background: white; - max-height: 120px; - overflow: auto; + max-height: 180px; + overflow: hidden; border: 1px solid #ccc; } @@ -722,6 +777,7 @@ div.typeahead a { border-top:1px solid #ccc; background-color: #fff; padding:1px 4px; + white-space: nowrap; } div.typeahead a:hover, @@ -801,6 +857,14 @@ div.typeahead a:first-child { padding:5px 10px; } +.changeset-list li span.count { + font-size:10px; + color:#555; +} + +.changeset-list li span.count:before { content: '('; } +.changeset-list li span.count:after { content: ')'; } + .changeset-list li:first-child { border-top: 0;} .commit-modal .changeset-comment { @@ -823,6 +887,7 @@ div.typeahead a:first-child { left:70px; width:250px; height:50px; + padding:10px; background:#fff; font-size: 20px; font-weight: bold; diff --git a/css/map.css b/css/map.css index c8f8c676b..c093db4e4 100644 --- a/css/map.css +++ b/css/map.css @@ -10,30 +10,51 @@ g.point circle { fill:#fff; } -g.point.hover circle, -g.point.selected circle { - fill:#ffff00; - stroke-width:4; - stroke:#fff +g.point.hover circle.stroke, +g.point.selected circle.stroke { + fill:#333; + -webkit-transform:scale(1.2, 1.2); + -moz-transform:scale(1.2, 1.2); + transform:scale(1.2, 1.2); } /* interactive elements */ -circle.vertex { +g.vertex circle.fill { fill:white; - stroke:#333; fill-opacity:1; - stroke-width:2; - stroke-opacity: 1; } -circle.vertex.shared { +circle.stroke, +circle.fill { + -webkit-transition: -webkit-transform 50ms linear; + transition: transform 50ms linear; + -moz-transition: stroke 50ms linear; + -webkit-transform:scale(1, 1); + -moz-transform:scale(1, 1); + transform:scale(1, 1); +} + +g.vertex circle.stroke { + fill:#333; +} + +g.vertex.shared circle { fill:#aff; } -circle.vertex.hover { +g.vertex.hover circle.fill { + -webkit-transform:scale(2, 2); + -moz-transform:scale(2, 2); + transform:scale(2, 2); } -circle.vertex.selected { +g.vertex.hover circle.stroke { + -webkit-transform:scale(1.8, 1.8); + -moz-transform:scale(1.8, 1.8); + transform:scale(1.8, 1.8); +} + +g.vertex circle.selected { fill: #ffff00; } @@ -60,12 +81,12 @@ path.casing { path.casing.hover { stroke:#FF0F0F !important; - opacity:0.8; + stroke-opacity:0.8; } path.casing.selected { stroke:#E96666 !important; - opacity:1 !important; + stroke-opacity:1 !important; stroke-width:10 !important; } @@ -134,7 +155,7 @@ path.stroke.highway-residential { path.casing.highway-residential { stroke:#E8E8E8; stroke-width:10; - opacity:0.4; + stroke-opacity:0.4; } path.stroke.highway-unclassified, @@ -179,12 +200,12 @@ path.stroke.highway-motorway, path.stroke.highway-motorway_link { path.casing.highway-motorway, path.casing.highway-motorway_link { stroke:#809BC0; stroke-width:9; - opacity:0.4; + stroke-opacity:0.4; } path.stroke.highway-trunk, path.stroke.highway-trunk_link { stroke-width:7; - opacity:0.4; + stroke-opacity:0.4; stroke:#7FC97F; } path.casing.highway-trunk, path.casing.highway-trunk_link { @@ -198,7 +219,7 @@ path.stroke.highway-primary, path.stroke.highway-primary_link { } path.casing.highway-primary, path.casing.highway-primary_link { stroke:#FF6363; - opacity:0.4; + stroke-opacity:0.4; stroke-width:12; } @@ -207,7 +228,7 @@ path.stroke.highway-secondary, path.stroke.highway-secondary_link { stroke-width:4; } path.casing.highway-secondary, path.casing.highway-secondary_link { - opacity:0.4; + stroke-opacity:0.4; stroke:#FDBF6F; stroke-width:11; } diff --git a/index.html b/index.html index 676463cfc..b0153fd9a 100644 --- a/index.html +++ b/index.html @@ -20,6 +20,7 @@ + @@ -39,6 +40,7 @@ + diff --git a/js/id/connection.js b/js/id/connection.js index 9ccbb5255..a3cd28fd9 100644 --- a/js/id/connection.js +++ b/js/id/connection.js @@ -14,17 +14,19 @@ iD.Connection = function() { } function bboxFromAPI(box, tile, callback) { - loadFromURL(bboxUrl(box), function(err, parsed) { + function done(err, parsed) { loadedTiles[tile.toString()] = true; callback(err, parsed); - }); + } + loadFromURL(bboxUrl(box), done); } function loadFromURL(url, callback) { + function done(dom) { + return callback(null, parse(dom)); + } inflight.push(d3.xml(url).get() - .on('load', function(dom) { - return callback(null, parse(dom)); - })); + .on('load', done)); } function getNodes(obj) { @@ -67,9 +69,7 @@ iD.Connection = function() { tags: getTags(obj) }; for (var i = 0, l = obj.attributes.length; i < l; i++) { - var n = obj.attributes[i].nodeName; - var v = obj.attributes[i].nodeValue; - o[n] = v; + o[obj.attributes[i].nodeName] = obj.attributes[i].nodeValue; } if (o.lon && o.lat) { o.loc = [parseFloat(o.lon), parseFloat(o.lat)]; @@ -129,13 +129,14 @@ iD.Connection = function() { }; function userDetails(callback) { - oauth.xhr({ method: 'GET', path: '/api/0.6/user/details' }, function(err, user_details) { + function done(err, user_details) { var u = user_details.getElementsByTagName('user')[0]; callback(connection.user({ display_name: u.attributes.display_name.nodeValue, id: u.attributes.id.nodeValue }).user()); - }); + } + oauth.xhr({ method: 'GET', path: '/api/0.6/user/details' }, done); } function tileAlreadyLoaded(c) { return !loadedTiles[c.toString()]; } @@ -143,9 +144,10 @@ iD.Connection = function() { function abortRequest(i) { i.abort(); } function loadTile(e) { - bboxFromAPI(e.box, e.tile, function(err, g) { + function done(err, g) { event.load(err, g); - }); + } + bboxFromAPI(e.box, e.tile, done); } function loadTiles(projection) { @@ -211,10 +213,11 @@ iD.Connection = function() { }; connection.authenticate = function(callback) { - return oauth.authenticate(function(err, res) { + function done(err, res) { event.auth(); if (callback) callback(err, res); - }); + } + return oauth.authenticate(done); }; connection.bboxFromAPI = bboxFromAPI; diff --git a/js/id/graph/entity.js b/js/id/graph/entity.js index c94dcb2fe..58e454805 100644 --- a/js/id/graph/entity.js +++ b/js/id/graph/entity.js @@ -84,6 +84,28 @@ iD.Entity.prototype = { key != 'odbl' && key.indexOf('tiger:') !== 0; }); + }, + + friendlyName: function() { + // Generate a string such as 'river' or 'Fred's House' for an entity. + if (!this.tags || !Object.keys(this.tags).length) { return ''; } + + var mainkeys = ['highway','amenity','railway','waterway','natural'], + n = []; + + if (this.tags.name) n.push(this.tags.name); + if (this.tags.ref) n.push(this.tags.ref); + + if (!n.length) { + for (var k in this.tags) { + if (mainkeys.indexOf(k) !== -1) { + n.push(this.tags[k]); + break; + } + } + } + + return n.length === 0 ? 'unknown' : n.join('; '); } }; diff --git a/js/id/graph/node.js b/js/id/graph/node.js index 3a29cd8e6..9500991fa 100644 --- a/js/id/graph/node.js +++ b/js/id/graph/node.js @@ -3,5 +3,9 @@ iD.Node = iD.Entity.extend({ extent: function() { return [this.loc, this.loc]; + }, + + geometry: function() { + return this._poi ? 'point' : 'vertex'; } }); diff --git a/js/id/graph/way.js b/js/id/graph/way.js index a62afb8e1..77858d29a 100644 --- a/js/id/graph/way.js +++ b/js/id/graph/way.js @@ -36,5 +36,9 @@ iD.Way = iD.Entity.extend({ this.tags.area !== 'no' && !this.tags.highway && !this.tags.barrier); + }, + + geometry: function() { + return this.isArea() ? 'area' : 'line'; } }); diff --git a/js/id/id.js b/js/id/id.js index 914567cf2..3aa6802f3 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -107,10 +107,11 @@ window.iD = function(container) { .append('div') .attr('class', 'hello'); - bar.append('button') + var save_button = bar.append('button') .attr('class', 'save action wide') .html("Save") .attr('title', 'Save changes to OpenStreetMap, making them visible to other users') + .property('disabled', true) .call(bootstrap.tooltip() .placement('bottom')) .on('click', function() { @@ -121,6 +122,17 @@ window.iD = function(container) { l.remove(); history.reset(); map.flush().redraw(); + var modal = iD.modal(); + modal.select('.content') + .classed('success-modal', true) + .datum({ + id: changeset_id, + comment: e.comment + }) + .call(iD.success() + .on('cancel', function() { + modal.remove(); + })); }); } var changes = history.changes(); @@ -146,6 +158,24 @@ window.iD = function(container) { } }); + save_button.append('span') + .attr('class', 'count'); + + history.on('change.save-button', function() { + var changes = history.changes(), + num_changes = d3.sum(d3.values(changes).map(function(c) { + return c.length; + })); + + save_button.property('disabled', num_changes === 0); + + save_button + .classed('has-count', num_changes > 0); + + save_button.select('span.count') + .text(num_changes); + }); + bar.append('div') .attr('class', 'messages'); @@ -160,6 +190,21 @@ window.iD = function(container) { return d[0] + ' icon'; }); + function geolocateSuccess(position) { + map.center([position.coords.longitude, position.coords.latitude]); + } + function geolocateError() { } + if (navigator.geolocation) { + container.append('div') + .attr('class', 'geolocate-control map-control') + .append('button') + .attr('class', 'narrow') + .text('G') + .on('click', function() { + navigator.geolocation.getCurrentPosition(geolocateSuccess, geolocateError); + }); + } + var gc = container.append('div').attr('class', 'geocode-control map-control') .call(iD.geocoder().map(map)); diff --git a/js/id/modes/add_line.js b/js/id/modes/add_line.js index 0c3ef27aa..24a53a115 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) .hoverEnable(false) .hint('Click on the map to start drawing an road, path, or route.'); diff --git a/js/id/modes/add_point.js b/js/id/modes/add_point.js index 677830b32..06eefd00b 100644 --- a/js/id/modes/add_point.js +++ b/js/id/modes/add_point.js @@ -30,7 +30,7 @@ iD.modes.AddPoint = function() { }; mode.exit = function() { - map.hoverEnable(true); + 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/select.js b/js/id/modes/select.js index fe658eaf0..8defb17ba 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -66,14 +66,35 @@ iD.modes.Select = function (entity) { mode.controller.exit(); }); - surface.on('click.select', function () { + function click() { var datum = d3.select(d3.event.target).datum(); if (datum instanceof iD.Entity) { mode.controller.enter(iD.modes.Select(datum)); } else { mode.controller.enter(iD.modes.Browse()); } - }); + } + + function dblclick() { + var datum = d3.select(d3.event.target).datum(); + if (datum instanceof iD.Entity && + (datum.geometry() === 'area' || datum.geometry() === 'line')) { + var choice = iD.util.geo.chooseIndex(datum, + d3.mouse(mode.map.surface.node()), mode.map), + node = iD.Node({ loc: choice.loc }); + + mode.history.perform( + iD.actions.AddNode(node), + iD.actions.AddWayNode(datum.id, node.id, choice.index), + 'added a point to a road'); + + d3.event.preventDefault(); + d3.event.stopPropagation(); + } + } + + surface.on('click.select', click) + .on('dblclick.browse', dblclick); mode.map.keybinding().on('⌫.select', function(e) { remove(); diff --git a/js/id/oauth.js b/js/id/oauth.js index 5e29517e0..c310004e5 100644 --- a/js/id/oauth.js +++ b/js/id/oauth.js @@ -66,6 +66,10 @@ iD.OAuth = function() { var l = iD.loading('contacting openstreetmap...'); + // it would make more sense to have this code within the callback + // to oauth.xhr below. however, it needs to be directly within a + // browser event handler in order to open a popup without it being + // blocked. var w = 600, h = 550, settings = [ ['width', w], ['height', h], diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js index 54803980f..184f98401 100644 --- a/js/id/renderer/background.js +++ b/js/id/renderer/background.js @@ -2,6 +2,7 @@ iD.Background = function() { var tile = d3.geo.tile(), projection, cache = {}, + offset = [0, 0], transformProp = iD.util.prefixCSSProperty('Transform'), source = d3.functor(''); @@ -27,6 +28,10 @@ iD.Background = function() { return tiles; } + function tileSize(d, z) { + return Math.ceil(256 * Math.pow(2, z - d[2])) / 256; + } + // derive the tiles onscreen, remove those offscreen and position tiles // correctly for the currentstate of `projection` function background() { @@ -82,21 +87,29 @@ iD.Background = function() { .on('error', error) .on('load', load); - function tileSize(d) { - return Math.ceil(256 * Math.pow(2, z - d[2])) / 256; - } - image.style(transformProp, function(d) { var _ts = 256 * Math.pow(2, z - d[2]); - var scale = tileSize(d); + var scale = tileSize(d, z); return 'translate(' + - Math.round((d[0] * _ts) - tile_origin[0]) + 'px,' + - Math.round((d[1] * _ts) - tile_origin[1]) + 'px) scale(' + scale + ',' + scale + ')'; + (Math.round((d[0] * _ts) - tile_origin[0]) + offset[0]) + 'px,' + + (Math.round((d[1] * _ts) - tile_origin[1]) + offset[1]) + 'px) scale(' + scale + ',' + scale + ')'; }); if (Object.keys(cache).length > 100) cache = {}; } + background.offset = function(_) { + if (!arguments.length) return offset; + offset = _; + return background; + }; + + background.nudge = function(_) { + offset[0] += _[0]; + offset[1] += _[1]; + return background; + }; + background.projection = function(_) { if (!arguments.length) return projection; projection = _; diff --git a/js/id/renderer/background_source.js b/js/id/renderer/background_source.js index d0408557c..cfed23294 100644 --- a/js/id/renderer/background_source.js +++ b/js/id/renderer/background_source.js @@ -45,3 +45,7 @@ iD.BackgroundSource.Tiger2012 = iD.BackgroundSource.template( iD.BackgroundSource.OSM = iD.BackgroundSource.template( 'http://{t}.tile.openstreetmap.org/{z}/{x}/{y}.png', ['a', 'b', 'c'], [0, 18]); + +iD.BackgroundSource.MapBox = iD.BackgroundSource.template( + 'http://{t}.tiles.mapbox.com/v3/openstreetmap.map-4wvf9l0l/{z}/{x}/{y}.jpg70', + ['a', 'b', 'c'], [0, 16]); diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 66727cd06..4cc7de184 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -84,9 +84,9 @@ iD.Map = function() { filter = d3.functor(true); } else { var only = {}; - difference.forEach(function (id) { + difference.forEach(function buildDifference(id) { only[id] = graph.fetch(id); - graph.parentWays(id).forEach(function (parent) { + graph.parentWays(id).forEach(function buildOnly(parent) { only[parent.id] = graph.fetch(parent.id); }); }); @@ -94,7 +94,6 @@ iD.Map = function() { filter = function(d) { return d.accuracy ? d.way in only : d.id in only; }; } - if (all.length > 10000) return editOff(); else editOn(); @@ -112,9 +111,10 @@ iD.Map = function() { } } var parentStructure = graph.parentStructure(ways); - var wayAccuracyHandles = ways.reduce(function(mem, w) { - return mem.concat(accuracyHandles(w)); - }, []); + var wayAccuracyHandles = []; + for (i = 0; i < ways.length; i++) { + accuracyHandles(ways[i], wayAccuracyHandles); + } drawVertices(vertices, parentStructure, filter); drawAccuracyHandles(wayAccuracyHandles, filter); drawCasings(lines, filter); @@ -123,8 +123,8 @@ iD.Map = function() { drawPoints(points, filter); } - function accuracyHandles(way) { - var handles = []; + // updates handles by reference + function accuracyHandles(way, handles) { for (var i = 0; i < way.nodes.length - 1; i++) { if (iD.util.geo.dist(way.nodes[i].loc, way.nodes[i + 1].loc) > 0.0001) { handles.push({ @@ -135,32 +135,36 @@ iD.Map = function() { }); } } - return handles; + } + + function pointTransform(entity) { + return 'translate(' + iD.util.geo.roundCoords(projection(entity.loc)) + ')'; } function drawVertices(vertices, parentStructure, filter) { function shared(d) { return parentStructure[d.id] > 1; } - var circles = g.hit.selectAll('circle.vertex') + var circles = g.hit.selectAll('g.vertex') .filter(filter) .data(vertices, key); circles.exit().remove(); - circles.enter().insert('circle', ':first-child') + var cg = circles.enter() + .insert('g', ':first-child') .attr('class', 'node vertex'); - circles.attr('transform', function(entity) { - var p = projection(entity.loc); - return 'translate(' + [~~p[0], ~~p[1]] + - ')'; - }) + cg.append('circle') + .attr('class', 'stroke') + .attr('r', 6); + + cg.append('circle') + .attr('class', 'fill') + .attr('r', 4); + + circles.attr('transform', pointTransform) .classed('shared', shared) .classed('hover', classHover); - - circles.transition().duration(50).attr('r', function(d) { - return d.id === hover ? 8: 4; - }); } function drawAccuracyHandles(waynodes, filter) { @@ -170,10 +174,7 @@ iD.Map = function() { handles.exit().remove(); handles.enter().append('circle') .attr({ r: 3, 'class': 'accuracy-handle' }); - handles.attr('transform', function(entity) { - var p = projection(entity.loc); - return 'translate(' + [~~p[0], ~~p[1]] + ')'; - }); + handles.attr('transform', pointTransform); } function editOff() { @@ -209,20 +210,30 @@ iD.Map = function() { } function drawPoints(points, filter) { + var groups = g.hit.selectAll('g.point') .filter(filter) .data(points, key); + groups.exit().remove(); + var group = groups.enter().append('g') .attr('class', 'node point'); + group.append('circle') - .attr({ r: 10, cx: 8, cy: 8 }); + .attr('class', 'stroke') + .attr({ r: 10 }); + + group.append('circle') + .attr('class', 'fill') + .attr({ r: 10 }); + group.append('image') - .attr({ width: 16, height: 16 }); - groups.attr('transform', function(d) { - var pt = projection(d.loc); - return 'translate(' + [~~pt[0], ~~pt[1]] + ') translate(-8, -8)'; - }); + .attr({ width: 16, height: 16 }) + .attr('transform', 'translate(-8, -8)'); + + groups.attr('transform', pointTransform); + groups.classed('hover', classHover); groups.select('image').attr('xlink:href', iD.Style.pointImage); } @@ -299,11 +310,11 @@ iD.Map = function() { if (fast) { if (!translateStart) translateStart = d3.event.translate.slice(); var a = d3.event.translate, - b = translateStart; - tilegroup.style(transformProp, - 'translate(' + ~~(a[0] - b[0]) + 'px,' + ~~(a[1] - b[1]) + 'px)'); - surface.style(transformProp, - 'translate(' + ~~(a[0] - b[0]) + 'px,' + ~~(a[1] - b[1]) + 'px)'); + 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; diff --git a/js/id/taginfo.js b/js/id/taginfo.js index b8c77d168..051339d27 100644 --- a/js/id/taginfo.js +++ b/js/id/taginfo.js @@ -5,7 +5,7 @@ iD.taginfo = function() { taginfo.keys = function(parameters, callback) { d3.json(endpoint + 'db/keys?' + iD.util.qsString(_.extend({ - rp: 20, + rp: 6, sortname: 'count_all', sortorder: 'desc', page: 1 diff --git a/js/id/ui/commit.js b/js/id/ui/commit.js index b9320c0d7..e24cbc0dc 100644 --- a/js/id/ui/commit.js +++ b/js/id/ui/commit.js @@ -14,51 +14,77 @@ iD.commit = function() { .attr('class', 'changeset-comment') .attr('placeholder', 'Brief Description of your contributions'); - var buttonwrap = commit.append('div') - .attr('class', 'buttons'); + var buttonwrap = commit.append('div') + .attr('class', 'buttons'); - var savebutton = buttonwrap.append('button') - .attr('class', 'action wide') - .on('click.save', function() { - event.save({ - comment: d3.select('textarea.changeset-comment').node().value - }); - }); - savebutton.append('span').attr('class','icon save icon-pre-text'); - savebutton.append('span').attr('class','label').text('Save'); - var cancelbutton = buttonwrap.append('button') - .attr('class', 'cancel wide') - .on('click.cancel', function() { - event.cancel(); - }); - cancelbutton.append('span').attr('class','icon close icon-pre-text'); - cancelbutton.append('span').attr('class','label').text('Cancel'); + var savebutton = buttonwrap.append('button') + .attr('class', 'action wide') + .on('click.save', function() { + event.save({ + comment: d3.select('textarea.changeset-comment').node().value + }); + }); + savebutton.append('span').attr('class','icon save icon-pre-text'); + savebutton.append('span').attr('class','label').text('Save'); + + var cancelbutton = buttonwrap.append('button') + .attr('class', 'cancel wide') + .on('click.cancel', function() { + event.cancel(); + }); + cancelbutton.append('span').attr('class','icon close icon-pre-text'); + cancelbutton.append('span').attr('class','label').text('Cancel'); + + function changesLength(d) { return changes[d].length; } var section = body.selectAll('div.commit-section') - .data(['modified', 'deleted', 'created'].filter(function(d) { - return changes[d].length; - })) + .data(['modified', 'deleted', 'created'].filter(changesLength)) .enter() .append('div').attr('class', 'commit-section modal-section fillL2'); section.append('h3').text(String) .append('small') .attr('class', 'count') - .text(function(d) { return changes[d].length; }); + .text(changesLength); + + function zipSame(d) { + var c = [], n = -1; + for (var i = 0; i < d.length; i++) { + var desc = { + name: d[i].friendlyName(), + type: d[i].type, + count: 1, + tagText: iD.util.tagText(d[i]) + }; + if (c[n] && + c[n].name == desc.name && + c[n].tagText == desc.tagText) { + c[n].count++; + } else { + c[++n] = desc; + } + } + return c; + } var li = section.append('ul') .attr('class','changeset-list') .selectAll('li') - .data(function(d) { return changes[d]; }) + .data(function(d) { return zipSame(changes[d]); }) .enter() .append('li'); - li.append('strong').text(function(d) { return d.type + ' '; }); + li.append('strong').text(function(d) { + return (d.count > 1) ? d.type + 's ' : d.type + ' '; + }); li.append('span') - .text(function(d) { - return iD.util.friendlyName(d); - }) - .attr('title', iD.util.tagText); + .text(function(d) { return d.name; }) + .attr('title', function(d) { return d.tagText; }); + + li.filter(function(d) { return d.count > 1; }) + .append('span') + .attr('class', 'count') + .text(function(d) { return d.count; }); } return d3.rebind(commit, event, 'on'); diff --git a/js/id/ui/inspector.js b/js/id/ui/inspector.js index f9d133f6d..db9377d94 100644 --- a/js/id/ui/inspector.js +++ b/js/id/ui/inspector.js @@ -2,220 +2,237 @@ iD.Inspector = function() { var event = d3.dispatch('changeTags', 'changeWayDirection', 'update', 'remove', 'close', 'splitWay'), taginfo = iD.taginfo(), - inspectorwrap; + tagList; + + function inspector(selection) { + var entity = selection.datum(); + + selection.html("").append('button') + .attr('class', 'narrow close') + .html("") + .on('click', function() { + event.close(entity); + }); + + selection.append('div') + .attr('class', 'head inspector-inner') + .call(drawHead); + + var inspectorbody = selection.append('div') + .attr('class', 'inspector-body'); + + var inspectorwrap = inspectorbody.append('div') + .attr('class', 'inspector-inner tag-wrap fillL2'); + + inspectorwrap.append('h4') + .text('Edit tags'); + + tagList = inspectorwrap.append('ul'); + + inspectorwrap.append('div').attr('class', 'add-tag-row').append('button') + .attr('class', 'add-tag') + .text('+ Add New Tag') + .on('click', function() { + addTag(); + tagList.selectAll('li:last-child input.key').node().focus(); + }); + + var formsel = 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) { + var entity = selection.datum(); - function drawhead(selection) { - function osmLink(d) { - return 'http://www.openstreetmap.org/browse/' + d.type + '/' + d.osmId(); - } - function emitChangeDirection(d) { event.changeWayDirection(d); } - function emitSplitWay(d) { event.splitWay(d); } - selection.html(''); var h2 = selection.append('h2'); - h2.append('span').attr('class', function(d) { - var icons = { way: 'line', node: 'point' }; - return 'icon big icon-pre-text big-' + icons[d.type]; - }); - h2.append('span').text(iD.util.friendlyName(selection.datum())); + + h2.append('span') + .attr('class', 'icon big icon-pre-text big-' + entity.geometry()); + + h2.append('span') + .text(entity.friendlyName()); + selection.append('a') - .attr('class', 'permalink') - .attr('href', osmLink) + .attr('href', 'http://www.openstreetmap.org/browse/' + entity.type + '/' + entity.osmId()) .text('View on OSM'); - if (selection.datum().type === 'way') { + + if (entity.type === 'way') { selection.append('a') - .attr('class', 'permalink') .attr('href', '#') .text('Reverse Direction') - .on('click', emitChangeDirection); + .on('click', function() { event.changeWayDirection(entity); }); } - if (selection.datum().type === 'node' && !selection.datum()._poi) { + + if (entity.geometry() === 'vertex') { selection.append('a') - .attr('class', 'permalink') .attr('href', '#') .text('Split Way') - .on('click', emitSplitWay); + .on('click', function() { event.splitWay(entity); }); } } - function inspector(selection) { - selection.each(function(entity) { + function drawButtons(selection) { + selection.append('button') + .attr('class', 'apply wide action') + .html("Apply") + .on('click', apply); - function draw(tags) { + selection.append('button') + .attr('class', 'delete wide action') + .html("Delete") + .on('click', function(entity) { event.remove(entity); }); + } - function emptyTag(d) { - return d.key === ''; - } + function drawTags(tags) { + tags = d3.entries(tags); - function pushMore() { - if (d3.event.keyCode === 9) { - draw(inspector.tags()); - } - } + if (!tags.length) { + tags = [{key: '', value: ''}]; + } - function bindTypeahead() { - var row = d3.select(this), - key = row.selectAll('.key'), - value = row.selectAll('.value'); + var li = tagList.selectAll('li') + .data(tags, function(d) { return d.key; }); - key.call(d3.typeahead() - .data(function(_, callback) { - taginfo.keys({query: key.property('value')}, function(err, data) { - callback(data.data.map(function (d) { - return {value: d.key}; - })); - }); - })); + li.exit().remove(); - value.call(d3.typeahead() - .data(function(_, callback) { - 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}; - })); - }); - })); - } + var row = li.enter().append('li') + .attr('class', 'tag-row'); - tags = d3.entries(tags); - tags.push({ key: '', value: ''}); + var inputs = row.append('div') + .attr('class', 'input-wrap'); - var li = inspectorwrap.selectAll('li') - .data(tags, function(d) { return d.key; }); + inputs.append('input') + .property('type', 'text') + .attr('class', 'key') + .property('value', function(d) { return d.key; }) + .on('change', function(d) { d.key = this.value; }); - li.exit().remove(); + inputs.append('input') + .property('type', 'text') + .attr('class', 'value') + .property('value', function(d) { return d.value; }) + .on('change', function(d) { d.value = this.value; }) + .on('keydown.push-more', pushMore); - var row = li.enter().append('li').attr('class','tag-row'); - var inputs = row.append('div').attr('class','input-wrap'); + inputs.each(bindTypeahead); - li.classed('tag-row-empty', emptyTag); + var removeBtn = row.append('button') + .attr('tabindex', -1) + .attr('class','remove minor') + .on('click', removeTag); - inputs.append('input') - .property('type', 'text') - .attr('class', 'key') - .property('value', function(d) { return d.key; }); + removeBtn.append('span') + .attr('class', 'icon remove'); - inputs.append('input') - .property('type', 'text') - .attr('class', 'value') - .property('value', function(d) { return d.value; }) - .on('keydown.push-more', pushMore); - - inputs.each(bindTypeahead); - - var removeBtn = row.append('button') - .attr('tabindex', -1) - .attr('class','remove minor') - .on('click', removeTag); - - removeBtn.append('span').attr('class', 'icon remove'); - - var helpBtn = row.append('button') - .attr('tabindex', -1) - .attr('class', 'tag-help minor') - .append('a') - .attr('tabindex', -1) - .attr('target', '_blank') - .on('click', function(d) { - taginfo.docs(d, function(err, docs) { - var en = _.find(docs, function(d) { - return d.lang == 'en'; - }); - if (en) { - var types = []; - if (en.on_area) types.push('area'); - if (en.on_node) types.push('point'); - if (en.on_way) types.push('line'); - en.types = types; - var mod = iD.modal(); - mod.select('.content') - .datum(en) - .call(iD.tagReference); - } - }); - d3.event.preventDefault(); - }) - .attr('href', function(d) { - return 'http://taginfo.openstreetmap.org/keys/' + d.key; + var helpBtn = row.append('button') + .attr('tabindex', -1) + .attr('class', 'tag-help minor') + .append('a') + .attr('tabindex', -1) + .attr('target', '_blank') + .on('click', function(d) { + taginfo.docs(d, function(err, docs) { + var en = _.find(docs, function(d) { + return d.lang == 'en'; }); - - helpBtn.append('span').attr('class', 'icon inspect'); - - return li; - } - - function removeTag(d) { - var tags = inspector.tags(); - delete tags[d.key]; - draw(tags); - } - - function apply(entity) { - event.changeTags(entity, inspector.tags()); - event.close(entity); - } - - function drawbuttons(selection) { - selection.append('button') - .attr('class', 'apply wide action') - .html("Apply") - .on('click', apply); - selection.append('button') - .attr('class', 'delete wide action') - .html("Delete") - .on('click', function(entity) { event.remove(entity); }); - } - - selection.html("").append('button') - .attr('class', 'narrow close') - .html("") - .on('click', function() { - event.close(entity); + if (en) { + var types = []; + if (en.on_area) types.push('area'); + if (en.on_node) types.push('point'); + if (en.on_way) types.push('line'); + en.types = types; + var mod = iD.modal(); + mod.select('.content') + .datum(en) + .call(iD.tagReference); + } + }); + d3.event.preventDefault(); + }) + .attr('href', function(d) { + return 'http://taginfo.openstreetmap.org/keys/' + d.key; }); - selection.append('div') - .attr('class', 'head inspector-inner') - .call(drawhead); + helpBtn.append('span') + .attr('class', 'icon inspect'); - var inspectorbody = selection.append('div') - .attr('class', 'inspector-body'); + return li; + } - inspectorwrap = inspectorbody - .append('ul').attr('class', 'inspector-inner tag-wrap fillL2'); + function pushMore() { + if (d3.event.keyCode === 9 && tagList.selectAll('li:last-child input.value').node() === this) { + addTag(); + } + } - inspectorwrap.append('h4').text('Edit tags'); + function bindTypeahead() { + var row = d3.select(this), + key = row.selectAll('.key'), + value = row.selectAll('.value'); - - var formsel = draw(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'); + key.call(d3.typeahead() + .data(function(_, callback) { + taginfo.keys({query: key.property('value')}, function(err, data) { + callback(data.data.map(function (d) { + return {value: d.key}; + })); }); + })); - formsel.select('input').node().focus(); + value.call(d3.typeahead() + .data(function(_, callback) { + 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}; + })); + }); + })); + } - inspectortoggle.append('span') - .text('Details') - .attr('class','label'); - }); + function addTag() { + var tags = inspector.tags(); + tags[''] = ''; + drawTags(tags); + } + + function removeTag(d) { + var tags = inspector.tags(); + delete tags[d.key]; + drawTags(tags); + } + + function apply(entity) { + event.changeTags(entity, inspector.tags()); + event.close(entity); } inspector.tags = function () { var tags = {}; - inspectorwrap.selectAll('li').each(function() { + tagList.selectAll('li').each(function() { var row = d3.select(this), key = row.selectAll('.key').property('value'), value = row.selectAll('.value').property('value'); diff --git a/js/id/ui/layerswitcher.js b/js/id/ui/layerswitcher.js index f79ab169c..94d4956d9 100644 --- a/js/id/ui/layerswitcher.js +++ b/js/id/ui/layerswitcher.js @@ -12,6 +12,10 @@ iD.layerswitcher = function(map) { name: 'OSM', source: iD.BackgroundSource.OSM, description: 'The default OpenStreetMap layer.' + }, { + name: 'MapBox', + source: iD.BackgroundSource.MapBox, + description: 'Satellite and Aerial Imagery' }, { name: 'Custom', source: iD.BackgroundSource.Custom, @@ -121,6 +125,36 @@ iD.layerswitcher = function(map) { .insert('span') .attr('class','icon toggle'); + var adjustments = content + .append('div') + .attr('class', 'adjustments'); + + var directions = [ + ['←', [-1, 0]], + ['↑', [0, -1]], + ['→', [1, 0]], + ['↓', [0, 1]]]; + + function nudge(d) { + map.background.nudge(d[1]); + map.redraw(); + } + + adjustments.selectAll('button') + .data(directions).enter() + .append('button') + .attr('class', 'nudge') + .text(function(d) { return d[0]; }) + .on('click', nudge); + + adjustments.append('button') + .text('reset') + .attr('class', 'reset') + .on('click', function() { + map.background.offset([0, 0]); + map.redraw(); + }); + selection.call(clickoutside); selectLayer(map.background.source()); diff --git a/js/id/ui/notice.js b/js/id/ui/notice.js index 05f3c85b6..5467e3745 100644 --- a/js/id/ui/notice.js +++ b/js/id/ui/notice.js @@ -3,7 +3,7 @@ iD.notice = function(selection) { notice = {}; notice.message = function(_) { - selection.attr('class','inner'); + selection.attr('class', 'notice inner'); if (!arguments.length) return _; if (!message && _) { selection diff --git a/js/id/ui/success.js b/js/id/ui/success.js new file mode 100644 index 000000000..f81be1028 --- /dev/null +++ b/js/id/ui/success.js @@ -0,0 +1,43 @@ +iD.success = function() { + var event = d3.dispatch('cancel', 'save'); + + function success(selection) { + var changeset = selection.datum(), + header = selection.append('div').attr('class', 'header modal-section'), + body = selection.append('div').attr('class', 'body'); + + var section = body.append('div').attr('class','modal-section'); + + header.append('h2').text('You Just Edited OpenStreetMap!'); + header.append('p').text('You just improved the world\'s best free map'); + + var m = ''; + if (changeset.comment) { + m = '"' + changeset.comment.substring(0, 20) + '" '; + } + + var message = 'Edited OpenStreetMap! ' + m + + 'http://osm.org/browse/changeset/' + changeset.id; + + section.append('a') + .attr('href', function(d) { + return 'https://twitter.com/intent/tweet?source=webclient&text=' + + encodeURIComponent(message); + }) + .text('Tweet: ' + message); + + var buttonwrap = section.append('div') + .attr('class', 'buttons'); + + var okbutton = buttonwrap.append('button') + .attr('class', 'action wide') + .on('click.save', function() { + event.cancel(); + }); + + okbutton.append('span').attr('class','icon apply icon-pre-text'); + okbutton.append('span').attr('class','label').text('OK'); + } + + return d3.rebind(success, event, 'on'); +}; diff --git a/js/id/util.js b/js/id/util.js index f9c515ae5..91bbbb1c1 100644 --- a/js/id/util.js +++ b/js/id/util.js @@ -6,28 +6,6 @@ iD.util.trueObj = function(arr) { return o; }; -iD.util.friendlyName = function(entity) { - // Generate a string such as 'river' or 'Fred's House' for an entity. - if (!entity.tags || !Object.keys(entity.tags).length) { return ''; } - - var mainkeys = ['highway','amenity','railway','waterway','natural'], - n = []; - - if (entity.tags.name) n.push(entity.tags.name); - if (entity.tags.ref) n.push(entity.tags.ref); - - if (!n.length) { - for (var k in entity.tags) { - if (mainkeys.indexOf(k) !== -1) { - n.push(entity.tags[k]); - break; - } - } - } - - return n.length === 0 ? 'unknown' : n.join('; '); -}; - iD.util.codeWindow = function(content) { top.win = window.open('','contentWindow', 'width=350,height=350,menubar=0' + diff --git a/js/lib/d3.trigger.js b/js/lib/d3.trigger.js new file mode 100644 index 000000000..59b7cd5a5 --- /dev/null +++ b/js/lib/d3.trigger.js @@ -0,0 +1,7 @@ +d3.selection.prototype.trigger = function (type) { + this.each(function() { + var evt = document.createEvent('HTMLEvents'); + evt.initEvent(type, true, true); + this.dispatchEvent(evt); + }); +}; diff --git a/js/lib/d3.typeahead.js b/js/lib/d3.typeahead.js index a93da8877..554b89a60 100644 --- a/js/lib/d3.typeahead.js +++ b/js/lib/d3.typeahead.js @@ -14,35 +14,47 @@ d3.typeahead = function() { top: rect.bottom + 'px' }); selection - .on('keyup.typeahead', update); + .on('keyup.typeahead', key); hidden = false; } function hide() { - window.setTimeout(function() { - container.remove(); - idx = 0; - hidden = true; - }, 500); + container.remove(); + idx = 0; + hidden = true; + } + + function slowHide() { + window.setTimeout(hide, 150); } selection .on('focus.typeahead', setup) - .on('blur.typeahead', hide); + .on('blur.typeahead', slowHide); - function update() { - if (hidden) setup(); - - if (d3.event.keyCode === 40) idx++; - if (d3.event.keyCode === 38) idx--; - if (d3.event.keyCode === 13) { - selection.property('value', container.select('a.selected').datum().value); - hide(); - } + function key() { + if (d3.event.keyCode === 40) { + idx++; + return highlight(); + } else if (d3.event.keyCode === 38) { + idx--; + return highlight(); + } else if (d3.event.keyCode === 13) { + select(container.select('a.selected').datum()); + hide(); + } else { + update(); + } + } + function highlight() { container .selectAll('a') .classed('selected', function(d, i) { return i == idx; }); + } + + function update() { + if (hidden) setup(); data(selection, function(data) { container.style('display', function() { @@ -57,11 +69,21 @@ d3.typeahead = function() { .append('a') .text(function(d) { return d.value; }) .attr('title', function(d) { return d.title; }) - .on('click', function(d) { selection.property('value', d.value); }); + .on('click', select); options.exit().remove(); + + options + .classed('selected', function(d, i) { return i == idx; }); }); } + + function select(d) { + selection + .property('value', d.value) + .trigger('change'); + } + }; typeahead.data = function(_) { diff --git a/test/index.html b/test/index.html index 3d0a995c3..f45262f73 100644 --- a/test/index.html +++ b/test/index.html @@ -22,6 +22,7 @@ + diff --git a/test/spec/graph/entity.js b/test/spec/graph/entity.js index 6dd5cafa0..e5741104f 100644 --- a/test/spec/graph/entity.js +++ b/test/spec/graph/entity.js @@ -130,4 +130,18 @@ describe('iD.Entity', function () { expect(iD.Entity({tags: {'tiger:source': 'blah', 'tiger:foo': 'bar'}}).hasInterestingTags()).to.equal(false); }); }); + + describe("#friendlyName", function () { + it("returns the name", function () { + expect(iD.Entity({ tags: { name: 'hi' }}).friendlyName()).to.equal('hi'); + }); + + it("returns a highway tag value", function () { + expect(iD.Entity({ tags: { highway: 'Route 5' }}).friendlyName()).to.equal('Route 5'); + }); + + it("prefers the name to a highway tag value", function () { + expect(iD.Entity({ tags: { name: 'hi', highway: 'Route 5' }}).friendlyName()).to.equal('hi'); + }); + }); }); diff --git a/test/spec/graph/node.js b/test/spec/graph/node.js index c7ce0c406..929946b04 100644 --- a/test/spec/graph/node.js +++ b/test/spec/graph/node.js @@ -36,4 +36,14 @@ describe('iD.Node', function () { expect(iD.Node({loc: [0, 0]}).intersects([[100, 90], [180, -90]])).to.equal(false); }); }); + + describe("#geometry", function () { + it("returns 'vertex' if the node is not a point", function () { + expect(iD.Node().geometry()).to.equal('vertex'); + }); + + it("returns 'point' if the node is a point", function () { + expect(iD.Node({_poi: true}).geometry()).to.equal('point'); + }); + }); }); diff --git a/test/spec/graph/way.js b/test/spec/graph/way.js index 4f64fac67..3aa8045fb 100644 --- a/test/spec/graph/way.js +++ b/test/spec/graph/way.js @@ -110,4 +110,14 @@ describe('iD.Way', function() { expect(iD.Way({tags: { highway: 'residential' }, nodes: ['n1', 'n1']}).isArea()).to.equal(false); }); }); + + describe("#geometry", function() { + it("returns 'line' when the way is not an area", function () { + expect(iD.Way().geometry()).to.equal('line'); + }); + + it("returns 'area' when the way is an area", function () { + expect(iD.Way({tags: { area: 'yes' }}).geometry()).to.equal('area'); + }); + }); }); diff --git a/test/spec/modes/add_point.js b/test/spec/modes/add_point.js index c78370d42..26cfcd013 100644 --- a/test/spec/modes/add_point.js +++ b/test/spec/modes/add_point.js @@ -8,6 +8,8 @@ describe("iD.modes.AddPoint", function () { controller = iD.Controller(map, history); container.call(map); + container.append('div') + .attr('class', 'inspector-wrap'); mode = iD.modes.AddPoint(); controller.enter(mode); diff --git a/test/spec/taginfo.js b/test/spec/taginfo.js index ae1855484..0c9fec93f 100644 --- a/test/spec/taginfo.js +++ b/test/spec/taginfo.js @@ -26,7 +26,7 @@ describe("iD.taginfo", function() { server.respond(); expect(query(server.requests[0].url)).to.eql( - {query: "amen", page: "1", rp: "20", sortname: "count_all", sortorder: "desc"}); + {query: "amen", page: "1", rp: "6", sortname: "count_all", sortorder: "desc"}); expect(callback).to.have.been.calledWith(null, {"data":[{"count_all":5190337,"key":"amenity"}]}); }); diff --git a/test/spec/ui/inspector.js b/test/spec/ui/inspector.js index 5c6760354..6d4175ef3 100644 --- a/test/spec/ui/inspector.js +++ b/test/spec/ui/inspector.js @@ -1,15 +1,20 @@ describe("iD.Inspector", function () { var inspector, element, tags = {highway: 'residential'}, - entity = iD.Entity({type: 'node', id: "n12345", tags: tags}); + entity; - beforeEach(function () { + function render() { inspector = iD.Inspector(); element = d3.select('body') .append('div') .attr('id', 'inspector-wrap') .datum(entity) .call(inspector); + } + + beforeEach(function () { + entity = iD.Entity({type: 'node', id: "n12345", tags: tags}); + render(); }); afterEach(function () { @@ -22,9 +27,9 @@ describe("iD.Inspector", function () { }); it("returns updated tags when input values have changed", function () { - element.selectAll(".tag-row-empty input.key").property('value', 'k'); - element.selectAll(".tag-row-empty input.value").property('value', 'v'); - expect(inspector.tags()).to.eql({highway: 'residential', k: 'v'}); + element.selectAll("input.key").property('value', 'k'); + element.selectAll("input.value").property('value', 'v'); + expect(inspector.tags()).to.eql({k: 'v'}); }); }); @@ -33,17 +38,22 @@ describe("iD.Inspector", function () { expect(element.selectAll("input[value=residential]")).not.to.be.empty; }); - it("creates one trailing pair of empty input elements", function () { + it("creates a pair of empty input elements if the entity has no tags", function () { + element.remove(); + entity = entity.update({tags: {}}); + render(); + expect(element.selectAll("input.value").property('value')).to.be.empty; + expect(element.selectAll("input.key").property('value')).to.be.empty; + }); + + it("adds tags when clicking the add button", function () { + element.selectAll("button.add-tag").trigger('click'); expect(element.selectAll("input")[0][2].value).to.be.empty; expect(element.selectAll("input")[0][3].value).to.be.empty; }); - it("sets the 'tag-row-empty' class on the placeholder row", function () { - expect(element.selectAll(".tag-row:last-child").classed('tag-row-empty')).to.be.true; - }); - it("removes tags when clicking the remove button", function () { - happen.click(element.selectAll("button.remove").node()); + element.selectAll("button.remove").trigger('click'); expect(inspector.tags()).to.eql({}); }); @@ -51,7 +61,7 @@ describe("iD.Inspector", function () { var spy = sinon.spy(); inspector.on('close', spy); - happen.click(element.select('.close').node()); + element.select('.close').trigger('click'); expect(spy).to.have.been.calledWith(entity); }); @@ -60,7 +70,7 @@ describe("iD.Inspector", function () { var spy = sinon.spy(); inspector.on('changeTags', spy); - happen.click(element.select('.apply').node()); + element.select('.apply').trigger('click'); expect(spy).to.have.been.calledWith(entity, tags); }); @@ -69,7 +79,7 @@ describe("iD.Inspector", function () { var spy = sinon.spy(); inspector.on('remove', spy); - happen.click(element.select('.delete').node()); + element.select('.delete').trigger('click'); expect(spy).to.have.been.calledWith(entity); }); diff --git a/test/spec/util.js b/test/spec/util.js index 3b9b547a0..e6663ade2 100644 --- a/test/spec/util.js +++ b/test/spec/util.js @@ -24,12 +24,6 @@ describe('Util', function() { expect(iD.util.qsString({})).to.eql(''); }); - it('#friendlyName', function() { - expect(iD.util.friendlyName({ tags: { name: 'hi' }})).to.equal('hi'); - expect(iD.util.friendlyName({ tags: { highway: 'Route 5' }})).to.equal('Route 5'); - expect(iD.util.friendlyName({ tags: { name: 'hi', highway: 'Route 5' }})).to.equal('hi'); - }); - describe('geo', function() { describe('#roundCoords', function() { expect(iD.util.geo.roundCoords([0.1, 1])).to.eql([0, 1]);