Merge pull request #3080 from openstreetmap/kepta-chips

Add multiselect preset
This commit is contained in:
Bryan Housel
2016-04-30 00:21:30 -04:00
25 changed files with 475 additions and 243 deletions
+70 -3
View File
@@ -298,6 +298,7 @@ ul li { list-style: none;}
.al { left: 0; }
.ar { right: 0; }
input.hide,
div.hide,
form.hide,
button.hide,
@@ -1018,7 +1019,7 @@ button.save.has-count .count::before {
}
.form-label button {
border-left: 1px solid #CCC;
border-left: 1px solid #ccc;
width: 10%;
height: 100%;
border-radius: 0;
@@ -1041,7 +1042,7 @@ button.save.has-count .count::before {
.form-field > input,
.form-field > textarea,
.form-field .preset-input-wrap {
border: 1px solid #CCC;
border: 1px solid #ccc;
min-height: 30px;
border-top: 0;
border-radius: 0 0 4px 4px;
@@ -1053,23 +1054,30 @@ button.save.has-count .count::before {
}
.inspector-border {
border-bottom: 1px solid #CCC
border-bottom: 1px solid #ccc
}
/* Preset form (hover mode) */
.inspector-hover .checkselect label:last-of-type,
.inspector-hover .preset-input-wrap .label,
.inspector-hover .form-field-multicombo,
.inspector-hover input,
.inspector-hover label {
background: #ececec;
}
.inspector-hover a,
.inspector-hover .form-field-multicombo .chips,
.inspector-hover .checkselect label:last-of-type {
color: #666;
}
.inspector-hover .form-field-multicombo .chips {
background: #eee;
border: 1px solid #ccc;
}
/* hide and remove from layout */
.inspector-hidden,
.inspector-hover label input[type="checkbox"],
@@ -1078,6 +1086,7 @@ button.save.has-count .count::before {
.inspector-hover .toggle-list label span,
.inspector-hover .inspector-inner .add-tag,
.inspector-hover .inspector-inner .add-relation,
.inspector-hover .form-field-multicombo .combobox-input,
.inspector-hover .toggle-list label.remove .icon {
height: 0;
width: 0;
@@ -1093,6 +1102,7 @@ button.save.has-count .count::before {
.inspector-hover .combobox-caret,
.inspector-hover .entity-editor-pane .header button,
.inspector-hover .spin-control,
.inspector-hover .form-field-multicombo .chips .remove,
.inspector-hover .hide-toggle:before,
.inspector-hover .more-fields,
.inspector-hover .form-label-button-wrap,
@@ -1193,6 +1203,63 @@ button.save.has-count .count::before {
border-bottom-right-radius: 4px;
}
/* preset form multicombo */
.form-field-multicombo {
border: 1px solid #cfcfcf;
border-top: 0px;
padding: 5px 0 5px 10px;
background: #fff;
display: block;
border-radius: 0 0 4px 4px;
overflow: hidden;
}
.form-field-multicombo:focus {
border-bottom: 0px;
}
.form-field-multicombo.active {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.form-field-multicombo li {
background-color: #eff2f7;
border: 1px solid #ccd5e3;
border-radius: 4px;
line-height: 25px;
display: inline-block;
padding: 2px 5px;
margin: 3px;
height: 30px;
}
.form-field-multicombo a {
font-family: Arial, Helvetica, sans-serif !important;
font-size: 16px !important;
line-height: 24px;
float: right;
margin: 1px 0 0 5px;
padding: 0;
cursor: pointer;
color: #a6b4ce;
}
.form-field-multicombo input {
border: 1px solid #ddd;
width: 100px;
margin: 3px;
}
.form-field-multicombo .combobox-caret {
margin: 3px 3px 3px -30px;
}
.form-field-multicombo input:focus {
border-radius: 4px !important;
}
/* preset form cycleway */
.form-field-cycleway .preset-input-wrap li {
+1
View File
@@ -261,6 +261,7 @@ en:
check:
"yes": "Yes"
"no": "No"
add: Add
none: None
node: Node
way: Way
+3 -26
View File
@@ -773,32 +773,9 @@ en:
railway:
# 'railway=*'
label: Type
recycling/cans:
# 'recycling:cans=*'
label: Accepts Cans
recycling/clothes:
# 'recycling:clothes=*'
label: Accepts Clothes
recycling/glass:
# 'recycling:glass=*'
label: Accepts Glass
recycling/glass_bottles:
# 'recycling:glass_bottles=*'
label: Accepts Glass Bottles
recycling/paper:
# 'recycling:paper=*'
label: Accepts Paper
recycling/plastic:
# 'recycling:plastic=*'
label: Accepts Plastic
recycling/type:
# 'recycling_type=*'
label: Recycling Type
options:
# recycling_type=centre
centre: Recycling Center
# recycling_type=container
container: Container
recycling_accepts:
# 'recycling:=*'
label: Accepts
ref:
# 'ref=*'
label: Reference
+4 -40
View File
@@ -1020,46 +1020,10 @@
"type": "typeCombo",
"label": "Type"
},
"recycling/cans": {
"key": "recycling:cans",
"type": "check",
"label": "Accepts Cans"
},
"recycling/clothes": {
"key": "recycling:clothes",
"type": "check",
"label": "Accepts Clothes"
},
"recycling/glass": {
"key": "recycling:glass",
"type": "check",
"label": "Accepts Glass"
},
"recycling/glass_bottles": {
"key": "recycling:glass_bottles",
"type": "check",
"label": "Accepts Glass Bottles"
},
"recycling/paper": {
"key": "recycling:paper",
"type": "check",
"label": "Accepts Paper"
},
"recycling/plastic": {
"key": "recycling:plastic",
"type": "check",
"label": "Accepts Plastic"
},
"recycling/type": {
"key": "recycling_type",
"type": "combo",
"label": "Recycling Type",
"strings": {
"options": {
"container": "Container",
"centre": "Recycling Center"
}
}
"recycling_accepts": {
"key": "recycling:",
"type": "multiCombo",
"label": "Accepts"
},
"ref": {
"key": "ref",
-5
View File
@@ -1,5 +0,0 @@
{
"key": "recycling:cans",
"type": "check",
"label": "Accepts Cans"
}
@@ -1,5 +0,0 @@
{
"key": "recycling:clothes",
"type": "check",
"label": "Accepts Clothes"
}
-5
View File
@@ -1,5 +0,0 @@
{
"key": "recycling:glass",
"type": "check",
"label": "Accepts Glass"
}
@@ -1,5 +0,0 @@
{
"key": "recycling:glass_bottles",
"type": "check",
"label": "Accepts Glass Bottles"
}
-5
View File
@@ -1,5 +0,0 @@
{
"key": "recycling:paper",
"type": "check",
"label": "Accepts Paper"
}
@@ -1,5 +0,0 @@
{
"key": "recycling:plastic",
"type": "check",
"label": "Accepts Plastic"
}
-11
View File
@@ -1,11 +0,0 @@
{
"key": "recycling_type",
"type": "combo",
"label": "Recycling Type",
"strings": {
"options": {
"container": "Container",
"centre": "Recycling Center"
}
}
}
@@ -0,0 +1,5 @@
{
"key": "recycling:",
"type": "multiCombo",
"label": "Accepts"
}
+1 -7
View File
@@ -1600,13 +1600,7 @@
"fields": [
"operator",
"address",
"recycling/type",
"recycling/cans",
"recycling/glass_bottles",
"recycling/paper",
"recycling/glass",
"recycling/plastic",
"recycling/clothes"
"recycling_accepts"
],
"geometry": [
"point",
+1 -7
View File
@@ -3,13 +3,7 @@
"fields": [
"operator",
"address",
"recycling/type",
"recycling/cans",
"recycling/glass_bottles",
"recycling/paper",
"recycling/glass",
"recycling/plastic",
"recycling/clothes"
"recycling_accepts"
],
"geometry": [
"point",
+1
View File
@@ -56,6 +56,7 @@
"defaultcheck",
"text",
"maxspeed",
"multiCombo",
"number",
"tel",
"email",
+3 -24
View File
@@ -316,6 +316,7 @@
"yes": "Yes",
"no": "No"
},
"add": "Add",
"none": "None",
"node": "Node",
"way": "Way",
@@ -1280,30 +1281,8 @@
"railway": {
"label": "Type"
},
"recycling/cans": {
"label": "Accepts Cans"
},
"recycling/clothes": {
"label": "Accepts Clothes"
},
"recycling/glass": {
"label": "Accepts Glass"
},
"recycling/glass_bottles": {
"label": "Accepts Glass Bottles"
},
"recycling/paper": {
"label": "Accepts Paper"
},
"recycling/plastic": {
"label": "Accepts Plastic"
},
"recycling/type": {
"label": "Recycling Type",
"options": {
"container": "Container",
"centre": "Recycling Center"
}
"recycling_accepts": {
"label": "Accepts"
},
"ref": {
"label": "Reference"
+36 -8
View File
@@ -34,14 +34,24 @@ iD.services.taginfo = function() {
return _.omit(parameters, 'geometry', 'debounce');
}
function popularKeys(parameters) {
var pop_field = 'count_all';
if (parameters.filter) pop_field = 'count_' + parameters.filter;
return function(d) { return parseFloat(d[pop_field]) > 5000 || d.in_wiki; };
function filterKeys(type) {
var count_type = type ? 'count_' + type : 'count_all';
return function(d) {
return parseFloat(d[count_type]) > 2500 || d.in_wiki;
};
}
function popularValues() {
return function(d) { return parseFloat(d.fraction) > 0.01 || d.in_wiki; };
function filterMultikeys() {
return function(d) {
return (d.key.match(/:/g) || []).length === 1; // exactly one ':'
};
}
function filterValues() {
return function(d) {
if (d.value.match(/[A-Z*;,]/) !== null) return false; // exclude some punctuation, uppercase letters
return parseFloat(d.fraction) > 0.0 || d.in_wiki;
};
}
function valKey(d) {
@@ -95,7 +105,24 @@ iD.services.taginfo = function() {
page: 1
}, parameters)), debounce, function(err, d) {
if (err) return callback(err);
callback(null, d.data.filter(popularKeys(parameters)).sort(sortKeys).map(valKey));
var f = filterKeys(parameters.filter);
callback(null, d.data.filter(f).sort(sortKeys).map(valKey));
});
};
taginfo.multikeys = function(parameters, callback) {
var debounce = parameters.debounce;
parameters = clean(setSort(parameters));
request(endpoint + 'keys/all?' +
iD.util.qsString(_.extend({
rp: 25,
sortname: 'count_all',
sortorder: 'desc',
page: 1
}, parameters)), debounce, function(err, d) {
if (err) return callback(err);
var f = filterMultikeys();
callback(null, d.data.filter(f).map(valKey));
});
};
@@ -110,7 +137,8 @@ iD.services.taginfo = function() {
page: 1
}, parameters)), debounce, function(err, d) {
if (err) return callback(err);
callback(null, d.data.filter(popularValues()).map(valKeyDescription), parameters);
var f = filterValues();
callback(null, d.data.filter(f).map(valKeyDescription));
});
};
+3 -1
View File
@@ -125,6 +125,7 @@ iD.ui.preset = function(context) {
wrap.append('button')
.attr('class', 'remove-icon')
.attr('tabindex', -1)
.call(iD.svg.Icon('#operation-delete'));
wrap.append('button')
@@ -159,7 +160,8 @@ iD.ui.preset = function(context) {
.call(field.input)
.selectAll('input')
.on('keydown', function() {
if (d3.event.keyCode === 13) { // enter
// if user presses enter, and combobox is not active, accept edits..
if (d3.event.keyCode === 13 && d3.select('.combobox').empty()) {
context.enter(iD.modes.Browse(context));
}
})
+255 -73
View File
@@ -1,11 +1,23 @@
iD.ui.preset.combo =
iD.ui.preset.typeCombo = function(field, context) {
iD.ui.preset.typeCombo =
iD.ui.preset.multiCombo = function(field, context) {
var dispatch = d3.dispatch('change'),
isMulti = (field.type === 'multiCombo'),
optstrings = field.strings && field.strings.options,
optarray = field.options,
snake_case = (field.snake_case || (field.snake_case === undefined)),
strings = {},
input;
combobox = d3.combobox().minItems(isMulti ? 1 : 2),
comboData = [],
multiData = [],
container,
input,
entity;
// ensure multiCombo field.key ends with a ':'
if (isMulti && field.key.match(/:$/) === null) {
field.key += ':';
}
function snake(s) {
return s.replace(/\s+/g, '_');
@@ -21,103 +33,273 @@ iD.ui.preset.typeCombo = function(field, context) {
.join(';');
}
function optString() {
return _.find(_.keys(strings), function(k) {
return strings[k] === input.value();
});
// returns the tag value for a display value
// (for multiCombo, dval should be the key suffix, not the entire key)
function tagValue(dval) {
dval = clean(dval || '');
if (optstrings) {
var match = _.find(comboData, function(o) { return o.value === dval && o.key; });
if (match) {
return match.key;
}
}
if (field.type === 'typeCombo' && !dval) {
return 'yes';
}
return (snake_case ? snake(dval) : dval) || undefined;
}
function combo(selection) {
var combobox = d3.combobox();
input = selection.selectAll('input')
.data([0]);
// returns the display value for a tag value
// (for multiCombo, tval should be the key suffix, not the entire key)
function displayValue(tval) {
tval = tval || '';
var enter = input.enter()
.append('input')
.attr('type', 'text')
.attr('id', 'preset-input-' + field.id);
if (optstrings) {
var match = _.find(comboData, function(o) { return o.key === tval && o.value; });
if (match) {
return match.value;
}
}
if (optstrings) { enter.attr('readonly', 'readonly'); }
if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {
return '';
}
input
.call(combobox)
.on('change', change)
.on('blur', change)
.each(function() {
if (optstrings) {
_.each(optstrings, function(v, k) {
strings[k] = field.t('options.' + k, { 'default': v });
});
stringsLoaded();
} else if (optarray) {
_.each(optarray, function(k) {
strings[k] = (snake_case ? unsnake(k) : k);
});
stringsLoaded();
} else if (context.taginfo()) {
context.taginfo().values({key: field.key}, function(err, data) {
if (!err) {
_.each(_.pluck(data, 'value'), function(k) {
strings[k] = (snake_case ? unsnake(k) : k);
});
stringsLoaded();
}
});
}
});
return snake_case ? unsnake(tval) : tval;
}
function stringsLoaded() {
var keys = _.keys(strings),
strs = [],
placeholders;
combobox.data(keys.map(function(k) {
var s = strings[k],
o = {};
o.title = o.value = s;
if (s.length < 20) { strs.push(s); }
return o;
}));
function objectDifference(a, b) {
return _.reject(a, function(d1) {
return _.any(b, function(d2) { return d1.value === d2.value; });
});
}
placeholders = strs.length > 1 ? strs : keys;
input.attr('placeholder', field.placeholder() ||
(placeholders.slice(0, 3).join(', ') + '...'));
function initCombo(selection, attachTo) {
if (optstrings) {
selection.attr('readonly', 'readonly');
selection.call(combobox, attachTo);
setStaticValues(setPlaceholder);
} else if (optarray) {
selection.call(combobox, attachTo);
setStaticValues(setPlaceholder);
} else if (context.taginfo()) {
selection.call(combobox.fetcher(setTaginfoValues), attachTo);
setTaginfoValues('', setPlaceholder);
}
}
function setStaticValues(callback) {
if (!(optstrings || optarray)) return;
if (optstrings) {
comboData = Object.keys(optstrings).map(function(k) {
var v = field.t('options.' + k, { 'default': optstrings[k] });
return {
key: k,
value: v,
title: v
};
});
} else if (optarray) {
comboData = optarray.map(function(k) {
var v = snake_case ? unsnake(k) : k;
return {
key: k,
value: v,
title: v
};
});
}
combobox.data(objectDifference(comboData, multiData));
if (callback) callback(comboData);
}
function setTaginfoValues(q, callback) {
var fn = isMulti ? 'multikeys' : 'values';
context.taginfo()[fn]({
debounce: true,
key: field.key,
geometry: context.geometry(entity.id),
query: (isMulti ? field.key : '') + q
}, function(err, data) {
if (err) return;
comboData = _.pluck(data, 'value').map(function(k) {
if (isMulti) k = k.replace(field.key, '');
var v = snake_case ? unsnake(k) : k;
return {
key: k,
value: v,
title: v
};
});
comboData = objectDifference(comboData, multiData);
if (callback) callback(comboData);
});
}
function setPlaceholder(d) {
var ph;
if (isMulti) {
ph = field.placeholder() || t('inspector.add');
} else {
var vals = _.pluck(d, 'value').filter(function(s) { return s.length < 20; }),
placeholders = vals.length > 1 ? vals : _.pluck(d, 'key');
ph = field.placeholder() || placeholders.slice(0, 3).join(', ');
}
input.attr('placeholder', ph + '…');
}
function change() {
var value = optString() || clean(input.value());
var val = tagValue(input.value()),
t = {};
if (snake_case) {
value = snake(value);
}
if (field.type === 'typeCombo' && !value) {
value = 'yes';
if (isMulti) {
if (!val) return;
container.classed('active', false);
input.value('');
field.keys.push(field.key + val);
t[field.key + val] = 'yes';
window.setTimeout(function() { input.node().focus(); }, 10);
} else {
t[field.key] = val;
}
var t = {};
t[field.key] = value || undefined;
dispatch.change(t);
}
combo.tags = function(tags) {
var key = tags[field.key],
optstring = optString(),
value = strings[key] || key || '';
if (field.type === 'typeCombo' && value.toLowerCase() === 'yes') {
value = '';
function removeMultikey(d) {
d3.event.stopPropagation();
var t = {};
t[d.key] = undefined;
dispatch.change(t);
}
function combo(selection) {
if (isMulti) {
container = selection.selectAll('ul').data([0]);
container.enter()
.append('ul')
.attr('class', 'form-field-multicombo')
.on('click', function() {
window.setTimeout(function() { input.node().focus(); }, 10);
});
} else {
container = selection;
}
if (!optstring && snake_case) {
value = unsnake(value);
input = container.selectAll('input')
.data([0]);
input.enter()
.append('input')
.attr('type', 'text')
.attr('id', 'preset-input-' + field.id)
.call(initCombo, selection);
input
.on('change', change)
.on('blur', change);
if (isMulti) {
combobox
.on('accept', function() {
input.node().blur();
input.node().focus();
});
input
.on('focus', function() { container.classed('active', true); });
}
}
combo.tags = function(tags) {
if (isMulti) {
multiData = [];
// Build multiData array containing keys already set..
Object.keys(tags).forEach(function(key) {
if (key.indexOf(field.key) !== 0 || tags[key].toLowerCase() !== 'yes') return;
var suffix = key.substring(field.key.length);
multiData.push({
key: key,
value: displayValue(suffix)
});
});
// Set keys for form-field modified (needed for undo and reset buttons)..
field.keys = _.pluck(multiData, 'key');
// Exclude existing multikeys from combo options..
var available = objectDifference(comboData, multiData);
combobox.data(available);
// Hide "Add" button if this field uses fixed set of
// translateable optstrings and they're all currently used..
container.selectAll('.combobox-input, .combobox-caret')
.classed('hide', optstrings && !available.length);
// Render chips
var chips = container.selectAll('.chips').data(multiData);
var enter = chips.enter()
.insert('li', 'input')
.attr('class', 'chips');
enter.append('span');
enter.append('a');
chips.select('span')
.text(function(d) { return d.value; });
chips.select('a')
.on('click', removeMultikey)
.attr('class', 'remove')
.text('×');
chips.exit()
.remove();
} else {
input.value(displayValue(tags[field.key]));
}
input.value(value);
};
combo.focus = function() {
input.node().focus();
};
combo.entity = function(_) {
if (!arguments.length) return entity;
entity = _;
return combo;
};
return d3.rebind(combo, dispatch, 'on');
};
+4 -2
View File
@@ -32,11 +32,13 @@ iD.ui.preset.url = function(field) {
enter.append('button')
.datum(1)
.attr('class', 'increment');
.attr('class', 'increment')
.attr('tabindex', -1);
enter.append('button')
.datum(-1)
.attr('class', 'decrement');
.attr('class', 'decrement')
.attr('tabindex', -1);
spinControl.selectAll('button')
.on('click', function(d) {
+1
View File
@@ -32,6 +32,7 @@ iD.ui.preset.localized = function(field, context) {
translateButton.enter()
.append('button')
.attr('class', 'button-input-action localized-add minor')
.attr('tabindex', -1)
.call(iD.svg.Icon('#icon-plus'))
.call(bootstrap.tooltip()
.title(t('translate.translate'))
+1
View File
@@ -62,6 +62,7 @@ iD.ui.preset.wikipedia = function(field, context) {
link.enter().append('a')
.attr('class', 'wiki-link button-input-action minor')
.attr('tabindex', -1)
.attr('target', '_blank')
.call(iD.svg.Icon('#icon-out-link', 'inline'));
}
+1
View File
@@ -69,6 +69,7 @@ iD.ui.TagReference = function(tag, context) {
body
.append('a')
.attr('target', '_blank')
.attr('tabindex', -1)
.attr('href', 'https://wiki.openstreetmap.org/wiki/' + docs.title)
.call(iD.svg.Icon('#icon-out-link', 'inline'))
.append('span')
+4 -2
View File
@@ -14,7 +14,7 @@ d3.combobox = function() {
}));
};
var combobox = function(input) {
var combobox = function(input, attachTo) {
var idx = -1,
container = d3.select(document.body)
.selectAll('div.combobox')
@@ -154,6 +154,7 @@ d3.combobox = function() {
}
function nav(dir) {
if (!suggestions.length) return;
idx = Math.max(Math.min(idx + dir, suggestions.length - 1), 0);
input.property('value', suggestions[idx].value);
render();
@@ -223,7 +224,8 @@ d3.combobox = function() {
options.exit()
.remove();
var rect = input.node().getBoundingClientRect();
var node = attachTo ? attachTo.node() : input.node(),
rect = node.getBoundingClientRect();
container.style({
'left': rect.left + 'px',
+81 -9
View File
@@ -29,7 +29,7 @@ describe("iD.services.taginfo", function() {
expect(callback).to.have.been.calledWith(null, [{"title":"amenity", "value":"amenity"}]);
});
it("filters only popular keys", function() {
it("includes popular keys", function() {
var callback = sinon.spy();
taginfo.keys({query: "amen"}, callback);
@@ -42,9 +42,8 @@ describe("iD.services.taginfo", function() {
expect(callback).to.have.been.calledWith(null, [{"title":"amenity", "value":"amenity"}]);
});
it("filters only popular keys with an entity type filter", function() {
it("includes popular keys with an entity type filter", function() {
var callback = sinon.spy();
taginfo.keys({query: "amen", filter: "nodes"}, callback);
server.respondWith("GET", new RegExp("https://taginfo.openstreetmap.org/api/4/keys/all"),
@@ -56,9 +55,24 @@ describe("iD.services.taginfo", function() {
expect(callback).to.have.been.calledWith(null, [{"title":"amenity", "value":"amenity"}]);
});
it("includes unpopular keys with a wiki page", function() {
var callback = sinon.spy();
taginfo.keys({query: "amen"}, callback);
server.respondWith("GET", new RegExp("https://taginfo.openstreetmap.org/api/4/keys/all"),
[200, { "Content-Type": "application/json" },
'{"data":[{"count_all":5190337,"key":"amenity","count_all_fraction":1.0, "count_nodes_fraction":1.0},\
{"count_all":1,"key":"amenityother","count_all_fraction":0.0, "count_nodes_fraction":0.0, "in_wiki": true}]}']);
server.respond();
expect(callback).to.have.been.calledWith(null, [
{"title":"amenity", "value":"amenity"}
{"title":"amenityother", "value":"amenityother"}
]);
});
it("sorts keys with ':' below keys without ':'", function() {
var callback = sinon.spy();
taginfo.keys({query: "ref"}, callback);
server.respondWith("GET", new RegExp("https://taginfo.openstreetmap.org/api/4/keys/all"),
@@ -71,10 +85,38 @@ describe("iD.services.taginfo", function() {
});
});
describe("#multikeys", function() {
it("calls the given callback with the results of the multikeys query", function() {
var callback = sinon.spy();
taginfo.multikeys({query: "recycling:"}, callback);
server.respondWith("GET", new RegExp("https://taginfo.openstreetmap.org/api/4/keys/all"),
[200, { "Content-Type": "application/json" },
'{"data":[{"count_all":69593,"key":"recycling:glass","count_all_fraction":0.0}]}']);
server.respond();
expect(query(server.requests[0].url)).to.eql(
{query: "recycling:", page: "1", rp: "25", sortname: "count_all", sortorder: "desc"});
expect(callback).to.have.been.calledWith(null, [{"title":"recycling:glass", "value":"recycling:glass"}]);
});
it("excludes multikeys with extra colons", function() {
var callback = sinon.spy();
taginfo.multikeys({query: "recycling:"}, callback);
server.respondWith("GET", new RegExp("https://taginfo.openstreetmap.org/api/4/keys/all"),
[200, { "Content-Type": "application/json" },
'{"data":[{"count_all":69593,"key":"recycling:glass","count_all_fraction":0.0},\
{"count_all":22,"key":"recycling:glass:color","count_all_fraction":0.0}]}']);
server.respond();
expect(callback).to.have.been.calledWith(null, [{"title":"recycling:glass", "value":"recycling:glass"}]);
});
});
describe("#values", function() {
it("calls the given callback with the results of the values query", function() {
var callback = sinon.spy();
taginfo.values({key: "amenity", query: "par"}, callback);
server.respondWith("GET", new RegExp("https://taginfo.openstreetmap.org/api/4/key/values"),
@@ -87,15 +129,46 @@ describe("iD.services.taginfo", function() {
expect(callback).to.have.been.calledWith(null, [{"value":"parking","title":"A place for parking cars"}]);
});
it("filters popular values", function() {
it("includes popular values", function() {
var callback = sinon.spy();
taginfo.values({key: "amenity", query: "par"}, callback);
server.respondWith("GET", new RegExp("https://taginfo.openstreetmap.org/api/4/key/values"),
[200, { "Content-Type": "application/json" },
'{"data":[{"value":"parking","description":"A place for parking cars", "fraction":1.0},\
{"value":"party","description":"A place for partying", "fraction":0.0}]}']);
{"value":"party","description":"A place for partying", "fraction":0.0}]}']);
server.respond();
expect(callback).to.have.been.calledWith(null, [{"value":"parking","title":"A place for parking cars"}]);
});
it("includes unpopular values with a wiki page", function() {
var callback = sinon.spy();
taginfo.values({key: "amenity", query: "par"}, callback);
server.respondWith("GET", new RegExp("https://taginfo.openstreetmap.org/api/4/key/values"),
[200, { "Content-Type": "application/json" },
'{"data":[{"value":"parking","description":"A place for parking cars", "fraction":1.0},\
{"value":"party","description":"A place for partying", "fraction":0.0, "in_wiki": true}]}']);
server.respond();
expect(callback).to.have.been.calledWith(null, [
{"value":"parking","title":"A place for parking cars"},
{"value":"party","title":"A place for partying"}
]);
});
it("excludes values with capital letters and some punctuation", function() {
var callback = sinon.spy();
taginfo.values({key: "amenity", query: "par"}, callback);
server.respondWith("GET", new RegExp("https://taginfo.openstreetmap.org/api/4/key/values"),
[200, { "Content-Type": "application/json" },
'{"data":[{"value":"parking","description":"A place for parking cars", "fraction":0.2},\
{"value":"PArking","description":"A common mispelling", "fraction":0.2},\
{"value":"parking;partying","description":"A place for parking cars *and* partying", "fraction":0.2},\
{"value":"parking, partying","description":"A place for parking cars *and* partying", "fraction":0.2},\
{"value":"*","description":"", "fraction":0.2}]}']);
server.respond();
expect(callback).to.have.been.calledWith(null, [{"value":"parking","title":"A place for parking cars"}]);
@@ -105,7 +178,6 @@ describe("iD.services.taginfo", function() {
describe("#docs", function() {
it("calls the given callback with the results of the docs query", function() {
var callback = sinon.spy();
taginfo.docs({key: "amenity", value: "parking"}, callback);
server.respondWith("GET", new RegExp("https://taginfo.openstreetmap.org/api/4/tag/wiki_page"),