diff --git a/Makefile b/Makefile index 9752cec70..5aabf9bb8 100644 --- a/Makefile +++ b/Makefile @@ -232,6 +232,7 @@ dist/iD.js: \ js/id/ui/preset/address.js \ js/id/ui/preset/check.js \ js/id/ui/preset/combo.js \ + js/id/ui/preset/multiselect.js \ js/id/ui/preset/cycleway.js \ js/id/ui/preset/input.js \ js/id/ui/preset/localized.js \ diff --git a/css/app.css b/css/app.css index ea6d5bd59..c3b1f8a68 100644 --- a/css/app.css +++ b/css/app.css @@ -1193,6 +1193,58 @@ button.save.has-count .count::before { border-bottom-right-radius: 4px; } +/* preset form multiselect */ + +.form-field-multiselect { + 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-multiselect:focus { + border-bottom: 0px; +} + +.form-field-multiselect.active { + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; +} + +.form-field-multiselect li { + background-color: #eff2f7; + border: 1px solid #ccd5e3; + border-radius: 4px; + line-height: 25px; + display: inline-block; + padding: 2px 5px; + margin: 0 10px 5px 0; + height: 30px; +} + +.form-field-multiselect a { + font-family: Arial, Helvetica, sans-serif !important; + font-size: 16px !important; + line-height: 25px; + float: right; + margin: 1px 0 0 5px; + padding: 0; + cursor: pointer; + color: #a6b4ce; +} + +.form-field-multiselect input { + border: 0px; + width: 110px; +} + +.form-field-multiselect input:focus { + border-radius: 4px !important; +} + /* preset form cycleway */ .form-field-cycleway .preset-input-wrap li { diff --git a/data/presets.yaml b/data/presets.yaml index 8884e3cbf..c79e1c932 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -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/recycling_choices: + # 'recycling=*' + label: Accepts ref: # 'ref=*' label: Reference diff --git a/data/presets/fields.json b/data/presets/fields.json index d2aec1869..1c7bb7395 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -653,7 +653,7 @@ }, "internet_access": { "key": "internet_access", - "type": "combo", + "type": "multiselect", "label": "Internet Access", "strings": { "options": { @@ -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/recycling_choices": { + "key": "recycling", + "type": "multiselect", + "label": "Accepts" }, "ref": { "key": "ref", @@ -1270,7 +1234,7 @@ }, "sport_ice": { "key": "sport", - "type": "combo", + "type": "multiselect", "label": "Sport", "options": [ "skating", diff --git a/data/presets/fields/internet_access.json b/data/presets/fields/internet_access.json index a628dfd5f..ec5ca1f7c 100644 --- a/data/presets/fields/internet_access.json +++ b/data/presets/fields/internet_access.json @@ -1,6 +1,6 @@ { "key": "internet_access", - "type": "combo", + "type": "multiselect", "label": "Internet Access", "strings": { "options": { diff --git a/data/presets/fields/recycling/cans.json b/data/presets/fields/recycling/cans.json deleted file mode 100644 index 8829418c6..000000000 --- a/data/presets/fields/recycling/cans.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "key": "recycling:cans", - "type": "check", - "label": "Accepts Cans" -} diff --git a/data/presets/fields/recycling/clothes.json b/data/presets/fields/recycling/clothes.json deleted file mode 100644 index f90dfaac6..000000000 --- a/data/presets/fields/recycling/clothes.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "key": "recycling:clothes", - "type": "check", - "label": "Accepts Clothes" -} diff --git a/data/presets/fields/recycling/glass.json b/data/presets/fields/recycling/glass.json deleted file mode 100644 index df6b23073..000000000 --- a/data/presets/fields/recycling/glass.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "key": "recycling:glass", - "type": "check", - "label": "Accepts Glass" -} diff --git a/data/presets/fields/recycling/glass_bottles.json b/data/presets/fields/recycling/glass_bottles.json deleted file mode 100644 index ab4621f7b..000000000 --- a/data/presets/fields/recycling/glass_bottles.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "key": "recycling:glass_bottles", - "type": "check", - "label": "Accepts Glass Bottles" -} diff --git a/data/presets/fields/recycling/paper.json b/data/presets/fields/recycling/paper.json deleted file mode 100644 index 49e09c46e..000000000 --- a/data/presets/fields/recycling/paper.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "key": "recycling:paper", - "type": "check", - "label": "Accepts Paper" -} diff --git a/data/presets/fields/recycling/plastic.json b/data/presets/fields/recycling/plastic.json deleted file mode 100644 index 2bb43abd4..000000000 --- a/data/presets/fields/recycling/plastic.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "key": "recycling:plastic", - "type": "check", - "label": "Accepts Plastic" -} diff --git a/data/presets/fields/recycling/recycling_choices.json b/data/presets/fields/recycling/recycling_choices.json new file mode 100644 index 000000000..9500270e6 --- /dev/null +++ b/data/presets/fields/recycling/recycling_choices.json @@ -0,0 +1,5 @@ +{ + "key": "recycling", + "type": "multiselect", + "label": "Accepts" +} diff --git a/data/presets/fields/recycling/type.json b/data/presets/fields/recycling/type.json deleted file mode 100644 index e6d44574b..000000000 --- a/data/presets/fields/recycling/type.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "key": "recycling_type", - "type": "combo", - "label": "Recycling Type", - "strings": { - "options": { - "container": "Container", - "centre": "Recycling Center" - } - } -} diff --git a/data/presets/fields/sport_ice.json b/data/presets/fields/sport_ice.json index 7e5133bf9..1d42d5fb8 100644 --- a/data/presets/fields/sport_ice.json +++ b/data/presets/fields/sport_ice.json @@ -1,6 +1,6 @@ { "key": "sport", - "type": "combo", + "type": "multiselect", "label": "Sport", "options": [ "skating", diff --git a/data/presets/presets.json b/data/presets/presets.json index 88e740558..3b1318b06 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -1581,13 +1581,7 @@ "fields": [ "operator", "address", - "recycling/type", - "recycling/cans", - "recycling/glass_bottles", - "recycling/paper", - "recycling/glass", - "recycling/plastic", - "recycling/clothes" + "recycling/recycling_choices" ], "geometry": [ "point", diff --git a/data/presets/presets/amenity/recycling.json b/data/presets/presets/amenity/recycling.json index 5651c5774..23865fe5e 100644 --- a/data/presets/presets/amenity/recycling.json +++ b/data/presets/presets/amenity/recycling.json @@ -3,13 +3,7 @@ "fields": [ "operator", "address", - "recycling/type", - "recycling/cans", - "recycling/glass_bottles", - "recycling/paper", - "recycling/glass", - "recycling/plastic", - "recycling/clothes" + "recycling/recycling_choices" ], "geometry": [ "point", diff --git a/data/presets/schema/field.json b/data/presets/schema/field.json index e43366ce5..89ac31459 100644 --- a/data/presets/schema/field.json +++ b/data/presets/schema/field.json @@ -56,6 +56,7 @@ "defaultcheck", "text", "maxspeed", + "multiselect", "number", "tel", "email", diff --git a/dist/locales/en.json b/dist/locales/en.json index 726eb99a8..f12b6bb4e 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -1280,30 +1280,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/recycling_choices": { + "label": "Accepts" }, "ref": { "label": "Reference" diff --git a/index.html b/index.html index 80d184a2f..fc67bc970 100644 --- a/index.html +++ b/index.html @@ -129,6 +129,7 @@ + diff --git a/js/id/ui/preset/multiselect.js b/js/id/ui/preset/multiselect.js new file mode 100644 index 000000000..ad54d2230 --- /dev/null +++ b/js/id/ui/preset/multiselect.js @@ -0,0 +1,194 @@ +iD.ui.preset.multiselect = function(field, context) { + var dispatch = d3.dispatch('init', 'change'), + optstrings = field.strings && field.strings.options, + optarray = field.options, + strings = {}, + multiselectContainer, + combobox, + comboboxData, + input, + isInitialized; + + field.key += ':'; + + function getOptStringKey(val) { + if (optstrings) { + var match = _.find(strings, function(o) { + return o.value === val; + }); + return match && match.key; + } + } + + function getOptStringVal(key) { + if (optstrings) { + var match = _.find(strings, function(o) { + return o.key === key; + }); + return match && match.value; + } + } + + function objectDifference(a, b) { + var bObj = {}; + b.forEach(function(obj){ + bObj[obj.key] = obj; + }); + // Return all elements in a, unless in b + return a.filter(function(obj){ + return !(obj.key in bObj); + }); + } + + function multiselect(selection) { + isInitialized = false; + combobox = d3.combobox(); + + multiselectContainer = selection.selectAll('ul').data([0]); + + multiselectContainer.enter() + .append('ul') + .on('click', function() { + window.setTimeout(function(){input.node().focus();}, 100); + }) + .attr('class', 'form-field-multiselect'); + + input = multiselectContainer.selectAll('input') + .data([0]); + + var enter = input.enter() + .append('input') + .attr('type', 'text') + .attr('id', 'preset-input-' + field.id); + + if (optstrings) { enter.attr('readonly', 'readonly'); } + + input + .call(function() {combobox(input, selection);}) + .on('change', change) + .on('blur', change) + .on('focus', function() {multiselectContainer.classed('active', true);}) + .each(function() { + if (optstrings) { + strings = Object.keys(optstrings).map(function(k) { + return { + key: k, + value: field.t('options.' + k, { 'default': optstrings[k] }) + }; + }); + dispatch.init(); + isInitialized = true; + } else if (optarray) { + strings = optarray.map(function(k) {return {key: k, value: k};}); + dispatch.init(); + isInitialized = true; + } else if (context.taginfo()) { + context.taginfo().keys({query: field.key}, function(err, data) { + if (!err) { + strings = data.map(function(k) { + var d = k.value.replace(field.key, ''); + return { + key: d, + value: d + }; + }); + dispatch.init(); + isInitialized = true; + } + }); + } + }); + } + + function updateStrings(tagsData) { + comboboxData = objectDifference(strings, tagsData); + combobox.data(comboboxData.map(comboValues)); + input.attr('placeholder', field.placeholder() || + ( 'Type here')); + } + + + function update(data) { + var chips = multiselectContainer.selectAll('.chips').data(data); + + var chip = chips.enter() + .insert('li', 'input') + .attr('class', 'chips'); + + chip.append('span'); + chip.append('a'); + + chips.select('span').text(function(d) {return d.value;}); + + chips.select('a') + .on('click', removeKey) + .attr('class', 'remove') + .text('×'); + + chips.exit().remove(); + } + + function comboValues(d) { + return { + value: d.value, + title: d.value + }; + } + + function change() { + multiselectContainer.classed('active', false); + var key = getOptStringKey(input.value()) || input.value(); + if (key && key !== '') { + var t = {}; + t[field.key + key] = 'yes'; + input.value(''); + dispatch.change(t); + } + } + + function removeKey(d) { + d3.event.stopPropagation(); + var t = {}; + t[field.key + d.key] = undefined; + dispatch.change(t); + } + + multiselect.tags = function(tags) { + var tagsData = []; + Object.keys(tags).forEach(function(d) { + if (d.indexOf(field.key) > -1 && tags[d] === 'yes') { + var datum = d.replace(field.key, ''); + + if (!optstrings) { + return tagsData.push({ + key: datum, + value: datum + }); + } + // discards any pair not found in optstrings + if (optstrings && getOptStringVal(datum)) { + return tagsData.push({ + key: datum, + value: getOptStringVal(datum) + }); + } + } + }); + + update(tagsData); + + if (isInitialized) { + updateStrings(tagsData); + } else { + dispatch.on('init', function () { + updateStrings(tagsData); + }); + } + }; + + multiselect.focus = function() { + input.node().focus(); + }; + + return d3.rebind(multiselect, dispatch, 'on'); +}; diff --git a/js/lib/d3.combobox.js b/js/lib/d3.combobox.js index fbe4ca025..93f6b052f 100644 --- a/js/lib/d3.combobox.js +++ b/js/lib/d3.combobox.js @@ -14,7 +14,7 @@ d3.combobox = function() { })); }; - var combobox = function(input) { + var combobox = function(input, customBoundingRect) { var idx = -1, container = d3.select(document.body) .selectAll('div.combobox') @@ -223,7 +223,7 @@ d3.combobox = function() { options.exit() .remove(); - var rect = input.node().getBoundingClientRect(); + var rect = customBoundingRect ? customBoundingRect.node().getBoundingClientRect() : input.node().getBoundingClientRect(); container.style({ 'left': rect.left + 'px',