mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-16 22:03:37 +02:00
Merge pull request #3080 from openstreetmap/kepta-chips
Add multiselect preset
This commit is contained in:
+70
-3
@@ -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 {
|
||||
|
||||
@@ -261,6 +261,7 @@ en:
|
||||
check:
|
||||
"yes": "Yes"
|
||||
"no": "No"
|
||||
add: Add
|
||||
none: None
|
||||
node: Node
|
||||
way: Way
|
||||
|
||||
+3
-26
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"key": "recycling:cans",
|
||||
"type": "check",
|
||||
"label": "Accepts Cans"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"key": "recycling:clothes",
|
||||
"type": "check",
|
||||
"label": "Accepts Clothes"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"key": "recycling:paper",
|
||||
"type": "check",
|
||||
"label": "Accepts Paper"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"key": "recycling:plastic",
|
||||
"type": "check",
|
||||
"label": "Accepts Plastic"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"defaultcheck",
|
||||
"text",
|
||||
"maxspeed",
|
||||
"multiCombo",
|
||||
"number",
|
||||
"tel",
|
||||
"email",
|
||||
|
||||
Vendored
+3
-24
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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');
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user