Refactor combo.js and move @kepta's multiselect code into there.

This commit is contained in:
Bryan Housel
2016-04-29 17:02:53 -04:00
parent 03bb916287
commit 5752207c91
3 changed files with 199 additions and 52 deletions
+1 -1
View File
@@ -56,7 +56,7 @@
"defaultcheck",
"text",
"maxspeed",
"multiselect",
"multiCombo",
"number",
"tel",
"email",
+195 -49
View File
@@ -1,13 +1,24 @@
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 = {},
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, '_');
}
@@ -22,18 +33,76 @@ iD.ui.preset.typeCombo = function(field, context) {
.join(';');
}
function optString() {
return _.find(_.keys(strings), function(k) {
return strings[k] === input.value();
});
}
function initCombo(selection) {
var d;
// 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;
}
// 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 || '';
if (optstrings) {
var match = _.find(comboData, function(o) { return o.key === tval && o.value; });
if (match) {
return match.value;
}
}
if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {
return '';
}
return snake_case ? unsnake(tval) : tval;
}
function objectDifference(a, b) {
return _.reject(a, function(d1) {
return _.any(b, function(d2) { return d1.value === d2.value; });
});
}
function initCombo(selection, attachTo) {
if (optstrings) {
selection.attr('readonly', 'readonly');
d = Object.keys(optstrings).map(function(k) {
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,
@@ -41,11 +110,9 @@ iD.ui.preset.typeCombo = function(field, context) {
title: v
};
});
selection.call(d3.combobox().data(d));
setPlaceholders(d);
} else if (optarray) {
d = optarray.map(function(k) {
comboData = optarray.map(function(k) {
var v = snake_case ? unsnake(k) : k;
return {
key: k,
@@ -53,24 +120,24 @@ iD.ui.preset.typeCombo = function(field, context) {
title: v
};
});
selection.call(d3.combobox().data(d));
setPlaceholders(d);
} else if (context.taginfo()) {
selection.call(d3.combobox().fetcher(taginfoValues));
taginfoValues('', setPlaceholders);
}
combobox.data(objectDifference(comboData, multiData));
if (callback) callback(comboData);
}
function taginfoValues(q, callback) {
context.taginfo().values({
function setTaginfoValues(q, callback) {
var fn = isMulti ? 'multikeys' : 'values';
context.taginfo()[fn]({
debounce: true,
key: field.key,
geometry: context.geometry(entity.id),
query: q
query: (isMulti ? field.key : '') + q
}, function(err, data) {
if (err) return;
var d = _.pluck(data, 'value').map(function(k) {
comboData = _.pluck(data, 'value').map(function(k) {
if (isMulti) k = k.replace(field.key, '');
var v = snake_case ? unsnake(k) : k;
return {
key: k,
@@ -78,72 +145,151 @@ iD.ui.preset.typeCombo = function(field, context) {
title: v
};
});
callback(d);
comboData = objectDifference(comboData, multiData);
if (callback) callback(comboData);
});
}
function setPlaceholders(d) {
var vals = _.pluck(d, 'value').filter(function(s) { return s.length < 20; }),
placeholders = vals.length > 1 ? vals : _.pluck(d, 'key');
input.attr('placeholder', field.placeholder() ||
(placeholders.slice(0, 3).join(', ') + '…'));
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';
} else {
t[field.key] = val;
}
dispatch.change(t);
}
function removeMultikey(d) {
d3.event.stopPropagation();
var t = {};
t[field.key] = value || undefined;
t[d.key] = undefined;
dispatch.change(t);
}
function combo(selection) {
input = selection.selectAll('input')
if (isMulti) {
container = selection.selectAll('ul').data([0]);
container.enter()
.append('ul')
.attr('class', 'form-field-multiselect')
.on('click', function() {
window.setTimeout(function() { input.node().focus(); }, 100);
});
} else {
container = selection;
}
input = container.selectAll('input')
.data([0]);
input.enter()
.append('input')
.attr('type', 'text')
.attr('id', 'preset-input-' + field.id)
.call(initCombo);
.call(initCombo, selection);
input
.on('change', change)
.on('blur', change);
.on('blur', change)
.on('focus', function() {
if (isMulti) container.classed('active', true);
});
}
combo.tags = function(tags) {
var key = tags[field.key],
optstring = optString(),
value = strings[key] || key || '';
if (field.type === 'typeCombo' && value.toLowerCase() === 'yes') {
value = '';
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 there are no available multiselect options remaining..
container.selectAll('.combobox-input, .combobox-caret')
.classed('hide', !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]));
}
if (!optstring && snake_case) {
value = unsnake(value);
}
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');
};
+3 -2
View File
@@ -14,7 +14,7 @@ d3.combobox = function() {
}));
};
var combobox = function(input, customBoundingRect) {
var combobox = function(input, attachTo) {
var idx = -1,
container = d3.select(document.body)
.selectAll('div.combobox')
@@ -223,7 +223,8 @@ d3.combobox = function() {
options.exit()
.remove();
var rect = customBoundingRect ? customBoundingRect.node().getBoundingClientRect() : input.node().getBoundingClientRect();
var node = attachTo ? attachTo.node() : input.node(),
rect = node.getBoundingClientRect();
container.style({
'left': rect.left + 'px',