diff --git a/css/80_app.css b/css/80_app.css index af3e8280d3..480b74adc4 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -1077,6 +1077,7 @@ button.save.has-count .count::before { .inspector-hover .checkselect label:last-of-type, .inspector-hover .preset-input-wrap .label, .inspector-hover .form-field-multicombo, +.inspector-hover .structure-extras-wrap, .inspector-hover input, .inspector-hover label { background: #ececec; @@ -1195,21 +1196,29 @@ button.save.has-count .count::before { width: 50%; } -/* preset form access */ - .preset-input-wrap .label { height: 30px; background: #F6F6F6; padding: 5px 10px; } + +/* preset form access */ +/* preset form cycleway */ + +.form-field-structure .structure-extras-wrap li, +.form-field-cycleway .preset-input-wrap li, .form-field-access .preset-input-wrap li { - border-bottom: 1px solid #CCC; + border-bottom: 1px solid #ccc; } +.form-field-structure .structure-extras-wrap li:last-child, +.form-field-cycleway .preset-input-wrap li:last-child, .form-field-access .preset-input-wrap li:last-child { border-bottom: 0; } - +.structure-input-type-wrap input, +.structure-input-layer-wrap input, +.preset-input-cycleway-wrap input, .preset-input-access-wrap input { border-radius: 0; border-width: 0; @@ -1220,6 +1229,16 @@ button.save.has-count .count::before { border-bottom-right-radius: 4px; } +.structure-extras-wrap { + padding: 10px 10px; + background: #fff; +} +.structure-extras-wrap ul { + border: 1px solid #ccc; + border-radius: 4px; +} + + /* preset form multicombo */ .form-field-multicombo { @@ -1277,20 +1296,6 @@ button.save.has-count .count::before { border-radius: 4px !important; } -/* preset form cycleway */ - -.form-field-cycleway .preset-input-wrap li { - border-bottom: 1px solid #CCC; -} -.form-field-cycleway .preset-input-wrap li:last-child { - border-bottom: 0; -} - -.preset-input-cycleway-wrap input { - border-radius: 0; - border-width: 0; - border-left-width: 1px; -} /* preset form numbers */ diff --git a/data/core.yaml b/data/core.yaml index bba2ed2674..12cfc5c986 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -324,6 +324,11 @@ en: "yes": "Yes" "no": "No" reverser: "Change Direction" + radio: + structure: + type: Type + default: Default + layer: Layer add: Add none: None node: Node diff --git a/data/presets/README.md b/data/presets/README.md index e45f202ed8..c154d41825 100644 --- a/data/presets/README.md +++ b/data/presets/README.md @@ -121,6 +121,7 @@ The complete JSON schema for fields can be found in [`data/presets/schema/field. **Radio Buttons** * `radio` - Multiple choice radio button field +* `structureRadio` - Multiple choice structure radio button field, with extra input for bridge/tunnel level **Special** * `access` - Block of dropdowns for defining the `access=*` tags on a highway diff --git a/data/presets/fields.json b/data/presets/fields.json index d55f912b79..e68af6cc14 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -1583,7 +1583,7 @@ } }, "structure": { - "type": "radio", + "type": "structureRadio", "keys": [ "bridge", "tunnel", diff --git a/data/presets/fields/structure.json b/data/presets/fields/structure.json index 6d96428192..095400a9e1 100644 --- a/data/presets/fields/structure.json +++ b/data/presets/fields/structure.json @@ -1,5 +1,5 @@ { - "type": "radio", + "type": "structureRadio", "keys": [ "bridge", "tunnel", diff --git a/data/presets/schema/field.json b/data/presets/schema/field.json index 92698150b9..504bdc4423 100644 --- a/data/presets/schema/field.json +++ b/data/presets/schema/field.json @@ -65,6 +65,7 @@ "radio", "restrictions", "semiCombo", + "structureRadio", "tel", "textarea", "text", diff --git a/dist/locales/en.json b/dist/locales/en.json index 0802e9e10f..7bf594b963 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -405,6 +405,13 @@ "no": "No", "reverser": "Change Direction" }, + "radio": { + "structure": { + "type": "Type", + "default": "Default", + "layer": "Layer" + } + }, "add": "Add", "none": "None", "node": "Node", diff --git a/modules/ui/fields/index.js b/modules/ui/fields/index.js index b92f2bf06c..27950023a4 100644 --- a/modules/ui/fields/index.js +++ b/modules/ui/fields/index.js @@ -34,13 +34,17 @@ import { uiFieldUrl } from './input'; +import { + uiFieldRadio, + uiFieldStructureRadio +} from './radio'; + import { uiFieldAccess } from './access'; import { uiFieldAddress } from './address'; import { uiFieldCycleway } from './cycleway'; import { uiFieldLanes } from './lanes'; import { uiFieldLocalized } from './localized'; import { uiFieldMaxspeed } from './maxspeed'; -import { uiFieldRadio } from './radio'; import { uiFieldRestrictions } from './restrictions'; import { uiFieldTextarea } from './textarea'; import { uiFieldWikipedia } from './wikipedia'; @@ -63,6 +67,7 @@ export var uiFields = { radio: uiFieldRadio, restrictions: uiFieldRestrictions, semiCombo: uiFieldSemiCombo, + structureRadio: uiFieldStructureRadio, tel: uiFieldTel, text: uiFieldText, textarea: uiFieldTextarea, diff --git a/modules/ui/fields/radio.js b/modules/ui/fields/radio.js index 3017df23c3..5e58133ca1 100644 --- a/modules/ui/fields/radio.js +++ b/modules/ui/fields/radio.js @@ -1,19 +1,69 @@ import * as d3 from 'd3'; import { t } from '../../util/locale'; -import { utilRebind } from '../../util/rebind'; +import { d3combobox } from '../../lib/d3.combobox.js'; +import { services } from '../../services/index'; + +import { + utilGetSetValue, + utilNoAuto, + utilRebind +} from '../../util'; + + +export { uiFieldRadio as uiFieldStructureRadio }; export function uiFieldRadio(field) { var dispatch = d3.dispatch('change'), + taginfo = services.taginfo, placeholder = d3.select(null), + wrap = d3.select(null), labels = d3.select(null), - radios = d3.select(null); + radios = d3.select(null), + typeInput = d3.select(null), + layerInput = d3.select(null), + oldType = {}, + entity; + + + + function selectedKey() { + var selector = '.form-field-structure .toggle-list label.active input', + node = d3.selectAll(selector); + return !node.empty() && node.datum(); + } + + // returns the tag value for a display value + function tagValue(dispVal) { + dispVal = snake(clean(dispVal || '')); + return dispVal.toLowerCase() || 'yes'; + } + + // returns the display value for a tag value + function displayValue(tagVal) { + tagVal = tagVal || ''; + return tagVal.toLowerCase() === 'yes' ? '' : unsnake(tagVal); + } + + function snake(s) { + return s.replace(/\s+/g, '_'); + } + + function unsnake(s) { + return s.replace(/_+/g, ' '); + } + + function clean(s) { + return s.split(';') + .map(function(s) { return s.trim(); }) + .join(';'); + } function radio(selection) { selection.classed('preset-radio', true); - var wrap = selection.selectAll('.preset-input-wrap') + wrap = selection.selectAll('.preset-input-wrap') .data([0]); var enter = wrap.enter() @@ -36,34 +86,215 @@ export function uiFieldRadio(field) { enter = labels.enter() .append('label'); - enter.append('input') + enter + .append('input') .attr('type', 'radio') .attr('name', field.id) .attr('value', function(d) { return field.t('options.' + d, { 'default': d }); }) .attr('checked', false); - enter.append('span') + enter + .append('span') .text(function(d) { return field.t('options.' + d, { 'default': d }); }); labels = labels .merge(enter); radios = labels.selectAll('input') - .on('change', change); + .on('change', changeRadio); } - function change() { - var t = {}; - if (field.key) t[field.key] = undefined; + function structureExtras(selection) { + var selected = selectedKey(); + + var extrasWrap = selection.selectAll('.structure-extras-wrap') + .data(selected ? [0] : []); + + extrasWrap.exit() + .remove(); + + extrasWrap = extrasWrap.enter() + .append('div') + .attr('class', 'structure-extras-wrap') + .merge(extrasWrap); + + var list = extrasWrap.selectAll('ul') + .data([0]); + + list = list.enter() + .append('ul') + .merge(list); + + + // Type + var typeItem = list.selectAll('.structure-type-item') + .data([0]); + + var typeEnter = typeItem.enter() + .append('li') + .attr('class', 'cf structure-type-item'); + + typeEnter + .append('span') + .attr('class', 'col6 label structure-label-type') + .attr('for', 'structure-input-type') + .text(t('inspector.radio.structure.type')); + + typeEnter + .append('div') + .attr('class', 'col6 structure-input-type-wrap') + .append('input') + .attr('type', 'text') + .attr('class', 'structure-input-type') + .attr('placeholder', t('inspector.radio.structure.default')) + .call(utilNoAuto); + + typeItem = typeItem + .merge(typeEnter); + + typeInput = typeItem.selectAll('.structure-input-type'); + + if (taginfo) { + typeInput + .call(d3combobox().fetcher(typeFetcher)); + } + + typeInput + .on('change', changeType) + .on('blur', changeType); + + + // Layer + var showLayer = (selected === 'bridge' || selected === 'tunnel'); + + var layerItem = list.selectAll('.structure-layer-item') + .data(showLayer ? [0] : []); + + layerItem.exit() + .remove(); + + var layerEnter = layerItem.enter() + .append('li') + .attr('class', 'cf structure-layer-item'); + + layerEnter + .append('span') + .attr('class', 'col6 label structure-label-layer') + .attr('for', 'structure-input-layer') + .text(t('inspector.radio.structure.layer')); + + layerEnter + .append('div') + .attr('class', 'col6 structure-input-layer-wrap') + .append('input') + .attr('type', 'text') + .attr('class', 'structure-input-layer') + .attr('placeholder', '0') + .call(utilNoAuto); + + var spin = layerEnter + .append('div') + .attr('class', 'spin-control'); + + spin + .append('button') + .datum(1) + .attr('class', 'increment') + .attr('tabindex', -1); + + spin + .append('button') + .datum(-1) + .attr('class', 'decrement') + .attr('tabindex', -1); + + layerItem = layerItem + .merge(layerEnter); + + layerInput = layerItem.selectAll('.structure-input-layer') + .on('change', changeLayer) + .on('blur', changeLayer); + + layerItem.selectAll('button') + .on('click', function(d) { + d3.event.preventDefault(); + var num = parseInt(layerInput.node().value || 0, 10); + if (!isNaN(num)) layerInput.node().value = num + d; + changeLayer(); + }); + + } + + + function typeFetcher(q, callback) { + taginfo.values({ + debounce: true, + key: selectedKey(), + query: q + }, function(err, data) { + if (err) return; + var comboData = data.map(function(d) { + return { + key: d.value, + value: unsnake(d.value), + title: d.title + }; + }); + if (callback) callback(comboData); + }); + } + + + function changeType() { + var key = selectedKey(), + t = {}; + + if (!key) return; + var val = tagValue(utilGetSetValue(typeInput)); + t[key] = val; + if (val !== 'no') oldType[key] = val; + dispatch.call('change', this, t); + } + + + function changeLayer() { + // note: don't use utilGetSetValue here because we want 0 to be falsy. + var t = { layer: layerInput.node().value || undefined }; + dispatch.call('change', this, t); + } + + + function changeRadio() { + var t = {}, + activeKey; + + if (field.key) { + t[field.key] = undefined; + } + radios.each(function(d) { var active = d3.select(this).property('checked'); + if (active) activeKey = d; + if (field.key) { if (active) t[field.key] = d; } else { - t[d] = active ? 'yes' : undefined; + var val = oldType[activeKey] || 'yes'; + t[d] = active ? val : undefined; } }); + + if (field.type === 'structureRadio') { + if (activeKey === 'bridge') { + t.layer = '1'; + } else if (activeKey === 'tunnel') { + t.layer = '-1'; + } else { + t.layer = undefined; + } + } + dispatch.call('change', this, t); } @@ -73,17 +304,27 @@ export function uiFieldRadio(field) { if (field.key) { return tags[field.key] === d; } else { - return !!(tags[d] && tags[d] !== 'no'); + return !!(tags[d] && tags[d].toLowerCase() !== 'no'); } } labels.classed('active', checked); radios.property('checked', checked); + var selection = radios.filter(function() { return this.checked; }); + var typeVal = ''; + if (selection.empty()) { placeholder.text(t('inspector.none')); } else { placeholder.text(selection.attr('value')); + typeVal = oldType[selection.datum()] = tags[selection.datum()]; + } + + if (field.type === 'structureRadio') { + wrap.call(structureExtras); + utilGetSetValue(typeInput, displayValue(typeVal) || ''); + utilGetSetValue(layerInput, tags.layer || ''); } }; @@ -93,5 +334,13 @@ export function uiFieldRadio(field) { }; + radio.entity = function(_) { + if (!arguments.length) return entity; + entity = _; + oldType = {}; + return radio; + }; + + return utilRebind(radio, dispatch, 'on'); } diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index e67674f4c2..a36dd727ea 100644 --- a/modules/ui/raw_tag_editor.js +++ b/modules/ui/raw_tag_editor.js @@ -158,13 +158,11 @@ export function uiRawTagEditor(context) { .attr('title', function(d) { return d.key; }) .call(utilGetSetValue, function(d) { return d.key; }) .property('disabled', isReadOnly); - // .classed('deemphasize', isReadOnly); items.selectAll('input.value') .attr('title', function(d) { return d.value; }) .call(utilGetSetValue, function(d) { return d.value; }) .property('disabled', isReadOnly); - // .classed('deemphasize', isReadOnly); items.selectAll('button.remove') .on('click', removeTag);