From ae063e9616e62b1b70f6d8bdacd24ba48e37a390 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Nov 2018 23:44:52 -0500 Subject: [PATCH] Make name and brand fields readonly on suggestion presets (re #5515) --- css/80_app.css | 48 ++++--- modules/ui/fields/input.js | 27 +++- modules/ui/fields/localized.js | 250 ++++++++++++++++++--------------- 3 files changed, 187 insertions(+), 138 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index e9b3c0ef0..0ca40e025 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -353,13 +353,6 @@ button:hover { background-color: #ececec; } -button[disabled], -button.disabled { - background-color: rgba(255,255,255,.25); - color: rgba(0,0,0,.4); - cursor: auto; -} - button.active { background: #7092ff; } @@ -380,6 +373,15 @@ button.minor:hover { background-color: #f1f1f1; } +button[disabled], +button.disabled, +button.minor[disabled], +button.minor.disabled { + background-color: rgba(255,255,255,.25); + color: rgba(0,0,0,.4); + cursor: not-allowed; +} + .joined button { border-radius: 0; border-right: 1px solid rgba(0,0,0,.5); @@ -1235,9 +1237,10 @@ a.hide-toggle { .form-label { position: relative; font-weight: bold; + color: #333; + background: #f6f6f6; border: 1px solid #cfcfcf; padding: 5px 0 5px 10px; - background: #f6f6f6; display: block; border-radius: 4px 4px 0 0; overflow: hidden; @@ -1774,15 +1777,15 @@ input[type=number] { /* Field - Localized Name ------------------------------------------------------- */ -.form-field .localized-main { +.localized-main { padding-right: 12%; } -[dir='rtl'] .form-field .localized-main { +[dir='rtl'] .localized-main { padding-left: 12%; padding-right: 10px; } -.form-field .button-input-action { +button.localized-add { position: relative; right: 1px; width: 32px; @@ -1794,22 +1797,22 @@ input[type=number] { height: 30px; vertical-align: top; } -[dir='rtl'] .form-field .button-input-action { +[dir='rtl'] button.localized-add { margin-left: 0; margin-right: -32px; border-right-width: 1px; border-radius: 0 0 0 4px; } -.form-field .localized-wrap { +.localized-wrap { padding: 0 10px; } -.form-field .localized-wrap .entry { +.localized-wrap .entry { position: relative; overflow: hidden; } -.form-field .localized-wrap .entry::before { +.localized-wrap .entry::before { content: ""; display: block; position: absolute; @@ -1822,18 +1825,27 @@ input[type=number] { margin: auto; } -.form-field .localized-wrap .entry .localized-lang { +.localized-wrap .entry .localized-lang { border-radius: 0; border-top-width: 0; } -.form-field .localized-wrap .entry .localized-value { +.localized-wrap .entry .localized-value { border-top-width: 0; border-radius: 0 0 4px 4px; } -.form-field .localized-wrap .form-label button { +.localized-wrap .form-label button { border-top-right-radius: 3px; } +button.localized-add[disabled], +.localized-wrap .form-label button[disabled], +.localized-wrap .localized-lang[disabled], +.localized-wrap .localized-value[disabled] { + color: #777; + background-color: #eee; + cursor: not-allowed; +} + /* Field - Address ------------------------------------------------------- */ diff --git a/modules/ui/fields/input.js b/modules/ui/fields/input.js index b8c98df56..d4c0b2057 100644 --- a/modules/ui/fields/input.js +++ b/modules/ui/fields/input.js @@ -23,10 +23,13 @@ export function uiFieldText(field, context) { var dispatch = d3_dispatch('change'); var nominatim = services.geocoder; var input; - var entity; + var _entity; function i(selection) { + var preset = _entity && context.presets().match(_entity, context.graph()); + var isSuggestion = preset && preset.suggestion && field.id === 'brand'; + var fieldId = 'preset-input-' + field.safeid; input = selection.selectAll('input') @@ -41,12 +44,14 @@ export function uiFieldText(field, context) { .merge(input); input + .property('disabled', !!isSuggestion) .on('input', change(true)) .on('blur', change()) .on('change', change()); - if (field.type === 'tel' && nominatim && entity) { - var center = entity.extent(context.graph()).center(); + + if (field.type === 'tel' && nominatim && _entity) { + var center = _entity.extent(context.graph()).center(); nominatim.countryCode(center, function (err, countryCode) { if (err || !dataPhoneFormats[countryCode]) return; selection.selectAll('#' + fieldId) @@ -86,6 +91,16 @@ export function uiFieldText(field, context) { input.node().value = parsed(input.node().value) + d; change()(); }); + + } else if (preset && field.id === 'brand') { + var pTag = preset.id.split('/', 2); + var pKey = pTag[0]; + if (isSuggestion) { + // A "suggestion" preset (brand name) + // Put suggestion keys in `field.keys` so delete button can remove them all. + field.keys = Object.keys(preset.removeTags) + .filter(function(k) { return k !== pKey; }); + } } } @@ -124,9 +139,9 @@ export function uiFieldText(field, context) { } - i.entity = function(_) { - if (!arguments.length) return entity; - entity = _; + i.entity = function(val) { + if (!arguments.length) return _entity; + _entity = val; return i; }; diff --git a/modules/ui/fields/localized.js b/modules/ui/fields/localized.js index 6344a35ab..f783a5210 100644 --- a/modules/ui/fields/localized.js +++ b/modules/ui/fields/localized.js @@ -28,11 +28,15 @@ export function uiFieldLocalized(field, context) { var wikipedia = services.wikipedia; var input = d3_select(null); var localizedInputs = d3_select(null); - var wikiTitles; + var _wikiTitles; var _entity; function localized(selection) { + var presets = context.presets(); + var preset = _entity && presets.match(_entity, context.graph()); + var isSuggestion = preset && preset.suggestion && field.id === 'name'; + input = selection.selectAll('.localized-main') .data([0]); @@ -46,58 +50,60 @@ export function uiFieldLocalized(field, context) { .call(utilNoAuto) .merge(input); - var presets = context.presets(); - var preset = presets.match(_entity, context.graph()); - var isSuggestion = preset.suggestion; - - if (field.id === 'name' && isSuggestion) { - // field.keys = Object.keys(preset.removeTags) - - } else if (field.id === 'name' && !isSuggestion) { - var isFallback = preset.isFallback(); + if (preset && field.id === 'name') { var pTag = preset.id.split('/', 2); var pKey = pTag[0]; var pValue = pTag[1]; - var allSuggestions = presets.collection.filter(function(p) { - return p.suggestion === true; - }); + if (isSuggestion) { + // A "suggestion" preset (brand name) + // Put suggestion keys in `field.keys` so delete button can remove them all. + field.keys = Object.keys(preset.removeTags) + .filter(function(k) { return k !== pKey; }); - // This code attempts to determine if the matched preset is the - // kind of preset that even can benefit from name suggestions.. - // - true = shops, cafes, hotels, etc. (also generic and fallback presets) - // - false = churches, parks, hospitals, etc. (things not in the index) - var goodSuggestions = allSuggestions.filter(function(s) { - if (isFallback) return true; - var sTag = s.id.split('/', 2); - var sKey = sTag[0]; - var sValue = sTag[1]; - return pKey === sKey && (!pValue || pValue === sValue); - }); + } else { + // Not a suggestion preset - Add a suggestions dropdown if it makes sense to. + var allSuggestions = presets.collection.filter(function(p) { + return p.suggestion === true; + }); - // Show the suggestions.. If the user picks one, change the tags.. - if (allSuggestions.length && goodSuggestions.length) { - input - .call(d3_combobox() - .container(context.container()) - .fetcher(suggestNames(preset, allSuggestions)) - .minItems(1) - .on('accept', function(d) { - var tags = _entity.tags; - var geometry = _entity.geometry(context.graph()); - var removed = preset.unsetTags(tags, geometry); - for (var k in tags) { - tags[k] = removed[k]; // set removed tags to `undefined` - } - tags = d.suggestion.setTags(tags, geometry); - dispatch.call('change', this, tags); - }) - ); + // This code attempts to determine if the matched preset is the + // kind of preset that even can benefit from name suggestions.. + // - true = shops, cafes, hotels, etc. (also generic and fallback presets) + // - false = churches, parks, hospitals, etc. (things not in the index) + var isFallback = preset.isFallback(); + var goodSuggestions = allSuggestions.filter(function(s) { + if (isFallback) return true; + var sTag = s.id.split('/', 2); + var sKey = sTag[0]; + var sValue = sTag[1]; + return pKey === sKey && (!pValue || pValue === sValue); + }); + + // Show the suggestions.. If the user picks one, change the tags.. + if (allSuggestions.length && goodSuggestions.length) { + input + .call(d3_combobox() + .container(context.container()) + .fetcher(suggestNames(preset, allSuggestions)) + .minItems(1) + .on('accept', function(d) { + var tags = _entity.tags; + var geometry = _entity.geometry(context.graph()); + var removed = preset.unsetTags(tags, geometry); + for (var k in tags) { + tags[k] = removed[k]; // set removed tags to `undefined` + } + tags = d.suggestion.setTags(tags, geometry); + dispatch.call('change', this, tags); + }) + ); + } } } input - .property('disabled', isSuggestion) + .property('disabled', !!isSuggestion) .on('input', change(true)) .on('blur', change()) .on('change', change()); @@ -117,73 +123,82 @@ export function uiFieldLocalized(field, context) { .merge(translateButton); translateButton + .property('disabled', !!isSuggestion) .on('click', addNew); localizedInputs = selection.selectAll('.localized-wrap') .data([0]); - localizedInputs = localizedInputs.enter().append('div') + localizedInputs = localizedInputs.enter() + .append('div') .attr('class', 'localized-wrap') .merge(localizedInputs); - } + + localizedInputs.selectAll('button, input') + .property('disabled', !!isSuggestion); - function suggestNames(preset, suggestions) { - var pTag = preset.id.split('/', 2); - var pKey = pTag[0]; - var pValue = pTag[1]; + function suggestNames(preset, suggestions) { + var pTag = preset.id.split('/', 2); + var pKey = pTag[0]; + var pValue = pTag[1]; - return function(value, callback) { - var results = []; - if (value && value.length > 2) { - for (var i = 0; i < suggestions.length; i++) { - var s = suggestions[i]; - var sTag = s.id.split('/', 2); - var sKey = sTag[0]; - var sValue = sTag[1]; - var name = s.name(); - var dist = utilEditDistance(value, name.substring(0, value.length)); - var matchesPreset = (pKey === sKey && (!pValue || pValue === sValue)); + return function(value, callback) { + var results = []; + if (value && value.length > 2) { + for (var i = 0; i < suggestions.length; i++) { + var s = suggestions[i]; + var sTag = s.id.split('/', 2); + var sKey = sTag[0]; + var sValue = sTag[1]; + var name = s.name(); + var dist = utilEditDistance(value, name.substring(0, value.length)); + var matchesPreset = (pKey === sKey && (!pValue || pValue === sValue)); - if (dist < 1 || (matchesPreset && dist < 3)) { - var obj = { - title: name, - value: name, - suggestion: s, - dist: dist + (matchesPreset ? 0 : 1) // penalize if not matched preset - }; - results.push(obj); + if (dist < 1 || (matchesPreset && dist < 3)) { + var obj = { + title: name, + value: name, + suggestion: s, + dist: dist + (matchesPreset ? 0 : 1) // penalize if not matched preset + }; + results.push(obj); + } } + results.sort(function(a, b) { return a.dist - b.dist; }); } - results.sort(function(a, b) { return a.dist - b.dist; }); - } - results = results.slice(0, 10); - callback(results); - }; - } - - - function addNew() { - d3_event.preventDefault(); - var data = localizedInputs.selectAll('div.entry').data(); - var defaultLang = utilDetect().locale.toLowerCase().split('-')[0]; - var langExists = _find(data, function(datum) { return datum.lang === defaultLang;}); - var isLangEn = defaultLang.indexOf('en') > -1; - if (isLangEn || langExists) { - defaultLang = ''; + results = results.slice(0, 10); + callback(results); + }; } - data.push({ lang: defaultLang, value: '' }); - localizedInputs.call(render, data); - } - function change(onInput) { - return function() { - var t = {}; - t[field.key] = utilGetSetValue(d3_select(this)) || undefined; - dispatch.call('change', this, t, onInput); - }; + function addNew() { + d3_event.preventDefault(); + if (isSuggestion) return; + + var data = localizedInputs.selectAll('div.entry').data(); + var defaultLang = utilDetect().locale.toLowerCase().split('-')[0]; + var langExists = _find(data, function(datum) { return datum.lang === defaultLang;}); + var isLangEn = defaultLang.indexOf('en') > -1; + if (isLangEn || langExists) { + defaultLang = ''; + } + data.push({ lang: defaultLang, value: '' }); + + localizedInputs + .call(renderMultilingual, data); + } + + + function change(onInput) { + return function() { + var t = {}; + t[field.key] = utilGetSetValue(d3_select(this)) || undefined; + dispatch.call('change', this, t, onInput); + }; + } } @@ -211,8 +226,8 @@ export function uiFieldLocalized(field, context) { if (lang && value) { t[key(lang)] = value; - } else if (lang && wikiTitles && wikiTitles[d.lang]) { - t[key(lang)] = wikiTitles[d.lang]; + } else if (lang && _wikiTitles && _wikiTitles[d.lang]) { + t[key(lang)] = _wikiTitles[d.lang]; } d.lang = lang; @@ -241,16 +256,20 @@ export function uiFieldLocalized(field, context) { } - function render(selection, data) { + function renderMultilingual(selection, data) { + var presets = context.presets(); + var preset = _entity && presets.match(_entity, context.graph()); + var isSuggestion = preset && preset.suggestion && field.id === 'name'; + var wraps = selection.selectAll('div.entry') .data(data, function(d) { return d.lang; }); wraps.exit() .transition() .duration(200) - .style('max-height','0px') + .style('max-height', '0px') .style('opacity', '0') - .style('top','-10px') + .style('top', '-10px') .remove(); var innerWrap = wraps.enter() @@ -267,24 +286,26 @@ export function uiFieldLocalized(field, context) { var label = wrap .append('label') - .attr('class','form-label') + .attr('class', 'form-label') .text(t('translate.localized_translation_label')) - .attr('for','localized-lang'); + .attr('for', 'localized-lang'); label .append('button') .attr('class', 'minor remove') - .on('click', function(d){ + .property('disabled', !!isSuggestion) + .on('click', function(d) { + if (isSuggestion) return; d3_event.preventDefault(); var t = {}; t[key(d.lang)] = undefined; dispatch.call('change', this, t); d3_select(this.parentNode.parentNode) - .style('top','0') - .style('max-height','240px') + .style('top', '0') + .style('max-height', '240px') .transition() .style('opacity', '0') - .style('max-height','0px') + .style('max-height', '0px') .remove(); }) .call(svgIcon('#iD-operation-delete')); @@ -293,18 +314,20 @@ export function uiFieldLocalized(field, context) { .append('input') .attr('class', 'localized-lang') .attr('type', 'text') - .attr('placeholder',t('translate.localized_translation_language')) + .attr('placeholder', t('translate.localized_translation_language')) + .property('disabled', !!isSuggestion) .on('blur', changeLang) .on('change', changeLang) .call(langcombo); wrap .append('input') - .on('blur', changeValue) - .on('change', changeValue) .attr('type', 'text') .attr('placeholder', t('translate.localized_translation_name')) - .attr('class', 'localized-value'); + .attr('class', 'localized-value') + .property('disabled', !!isSuggestion) + .on('blur', changeValue) + .on('change', changeValue); }); innerWrap @@ -337,13 +360,11 @@ export function uiFieldLocalized(field, context) { localized.tags = function(tags) { // Fetch translations from wikipedia - if (tags.wikipedia && !wikiTitles) { - wikiTitles = {}; + if (tags.wikipedia && !_wikiTitles) { + _wikiTitles = {}; var wm = tags.wikipedia.match(/([^:]+):(.+)/); if (wm && wm[0] && wm[1]) { - wikipedia.translations(wm[1], wm[2], function(d) { - wikiTitles = d; - }); + wikipedia.translations(wm[1], wm[2], function(d) { _wikiTitles = d; }); } } @@ -357,7 +378,8 @@ export function uiFieldLocalized(field, context) { } } - localizedInputs.call(render, postfixed.reverse()); + localizedInputs + .call(renderMultilingual, postfixed.reverse()); };