From 674421eec7632f419b9448a897e13f39e6867dd5 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Tue, 4 Oct 2022 18:28:14 +0200 Subject: [PATCH 1/3] implement fetching string references see https://github.com/openstreetmap/id-tagging-schema/pull/598 --- modules/presets/field.js | 25 ++++++++++++----- modules/presets/index.js | 2 +- modules/presets/preset.js | 51 ++++++++++++++++++++++++----------- modules/ui/fields/access.js | 3 ++- modules/ui/fields/check.js | 3 ++- modules/ui/fields/combo.js | 10 ++++--- modules/ui/fields/cycleway.js | 3 ++- modules/ui/fields/radio.js | 5 ++-- 8 files changed, 71 insertions(+), 31 deletions(-) diff --git a/modules/presets/field.js b/modules/presets/field.js index 5e7a60a73..04da44353 100644 --- a/modules/presets/field.js +++ b/modules/presets/field.js @@ -6,7 +6,8 @@ import { utilSafeClassName } from '../util/util'; // `presetField` decorates a given `field` Object // with some extra methods for searching and matching geometry // -export function presetField(fieldID, field) { +export function presetField(fieldID, field, allFields) { + allFields = allFields || {}; let _this = Object.assign({}, field); // shallow copy _this.id = fieldID; @@ -25,17 +26,29 @@ export function presetField(fieldID, field) { _this.t.append = (scope, options) => t.append(`_tagging.presets.fields.${fieldID}.${scope}`, options); _this.hasTextForStringId = (scope) => localizer.hasTextForStringId(`_tagging.presets.fields.${fieldID}.${scope}`); - _this.title = () => _this.overrideLabel || _this.t('label', { 'default': fieldID }); + _this.resolveReference = which => { + const referenceRegex = /^\{(.*)\}$/; + const match = (field[which] || '').match(referenceRegex); + if (match) { + const field = allFields[match[1]]; + if (field) { + return field; + } + console.error(`Unable to resolve referenced field: ${match[1]}`); // eslint-disable-line no-console + } + return _this; + }; + + _this.title = () => _this.overrideLabel || _this.resolveReference('label').t('label', { 'default': fieldID }); _this.label = () => _this.overrideLabel ? selection => selection.text(_this.overrideLabel) : - _this.t.append('label', { 'default': fieldID }); + _this.resolveReference('label').t.append('label', { 'default': fieldID }); - const _placeholder = _this.placeholder; - _this.placeholder = () => _this.t('placeholder', { 'default': _placeholder }); + _this.placeholder = () => _this.resolveReference('placeholder').t('placeholder', { 'default': '' }); _this.originalTerms = (_this.terms || []).join(); - _this.terms = () => _this.t('terms', { 'default': _this.originalTerms }) + _this.terms = () => _this.resolveReference('label').t('terms', { 'default': _this.originalTerms }) .toLowerCase().trim().split(/\s*,+\s*/); _this.increment = _this.type === 'number' ? (_this.increment || 1) : undefined; diff --git a/modules/presets/index.js b/modules/presets/index.js index c3ae1f63e..1a46e5219 100644 --- a/modules/presets/index.js +++ b/modules/presets/index.js @@ -96,7 +96,7 @@ export function presetIndex() { let f = d.fields[fieldID]; if (f) { // add or replace - f = presetField(fieldID, f); + f = presetField(fieldID, f, _fields); if (f.locationSet) newLocationSets.push(f); _fields[fieldID] = f; diff --git a/modules/presets/preset.js b/modules/presets/preset.js index 4cb5c9f62..332757bee 100644 --- a/modules/presets/preset.js +++ b/modules/presets/preset.js @@ -11,15 +11,17 @@ import { utilSafeClassName } from '../util/util'; export function presetPreset(presetID, preset, addable, allFields, allPresets) { allFields = allFields || {}; allPresets = allPresets || {}; - let _this = Object.assign({}, preset); // shallow copy + let _this = Object.assign({}, preset); // shallow copy let _addable = addable || false; - let _resolvedFields; // cache - let _resolvedMoreFields; // cache - let _searchName; // cache - let _searchNameStripped; // cache - let _searchAliases; // cache + let _resolvedFields; // cache + let _resolvedMoreFields; // cache + let _searchName; // cache + let _searchNameStripped; // cache + let _searchAliases; // cache let _searchAliasesStripped; // cache + const referenceRegex = /^\{(.*)\}$/; + _this.id = presetID; _this.safeid = utilSafeClassName(presetID); // for use in css classes, selectors, element ids @@ -38,9 +40,9 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) { _this.originalMoreFields = (_this.moreFields || []); - _this.fields = () => _resolvedFields || (_resolvedFields = resolve('fields')); + _this.fields = () => _resolvedFields || (_resolvedFields = resolveFields('fields')); - _this.moreFields = () => _resolvedMoreFields || (_resolvedMoreFields = resolve('moreFields')); + _this.moreFields = () => _resolvedMoreFields || (_resolvedMoreFields = resolveFields('moreFields')); _this.resetFields = () => _resolvedFields = _resolvedMoreFields = null; @@ -99,12 +101,27 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) { return t.append(textID, options); }; + function resolveReference(which) { + const match = (_this[which] || '').match(referenceRegex); + if (match) { + const preset = allPresets[match[1]]; + if (preset) { + return preset; + } + console.error(`Unable to resolve referenced preset: ${match[1]}`); // eslint-disable-line no-console + } + return _this; + } _this.name = () => { - return _this.t('name', { 'default': _this.originalName }); + return resolveReference('originalName') + .t('name', { 'default': _this.originalName || presetID }); }; - _this.nameLabel = () => _this.t.append('name', { 'default': _this.originalName }); + _this.nameLabel = () => { + return resolveReference('originalName') + .t.append('name', { 'default': _this.originalName || presetID }); + }; _this.subtitle = () => { if (_this.suggestion) { @@ -125,11 +142,15 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) { }; _this.aliases = () => { - return _this.t('aliases', { 'default': _this.originalAliases }).trim().split(/\s*[\r\n]+\s*/); + return resolveReference('originalName') + .t('aliases', { 'default': _this.originalAliases }).trim().split(/\s*[\r\n]+\s*/); }; - _this.terms = () => _this.t('terms', { 'default': _this.originalTerms }) - .toLowerCase().trim().split(/\s*,+\s*/); + _this.terms = () => { + return resolveReference('originalName') + .t('terms', { 'default': _this.originalTerms }) + .toLowerCase().trim().split(/\s*,+\s*/); + }; _this.searchName = () => { if (!_searchName) { @@ -267,12 +288,12 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) { // For a preset without fields, use the fields of the parent preset. // Replace {preset} placeholders with the fields of the specified presets. - function resolve(which) { + function resolveFields(which) { const fieldIDs = (which === 'fields' ? _this.originalFields : _this.originalMoreFields); let resolved = []; fieldIDs.forEach(fieldID => { - const match = fieldID.match(/\{(.*)\}/); + const match = fieldID.match(referenceRegex); if (match !== null) { // a presetID wrapped in braces {} resolved = resolved.concat(inheritFields(match[1], which)); } else if (allFields[fieldID]) { // a normal fieldID diff --git a/modules/ui/fields/access.js b/modules/ui/fields/access.js index 9ec529578..9e60e44e8 100644 --- a/modules/ui/fields/access.js +++ b/modules/ui/fields/access.js @@ -98,9 +98,10 @@ export function uiFieldAccess(field, context) { options.splice(options.length - 4, 0, 'dismount'); } + var stringsField = field.resolveReference('stringsCrossReference'); return options.map(function(option) { return { - title: field.t('options.' + option + '.description'), + title: stringsField.t('options.' + option + '.description'), value: option }; }); diff --git a/modules/ui/fields/check.js b/modules/ui/fields/check.js index 0563547c6..b11e0a70d 100644 --- a/modules/ui/fields/check.js +++ b/modules/ui/fields/check.js @@ -33,10 +33,11 @@ export function uiFieldCheck(field, context) { if (options) { + var stringsField = field.resolveReference('stringsCrossReference'); for (var i in options) { var v = options[i]; values.push(v === 'undefined' ? undefined : v); - texts.push(field.t.html('options.' + v, { 'default': v })); + texts.push(stringsField.t.html('options.' + v, { 'default': v })); } } else { values = [undefined, 'yes']; diff --git a/modules/ui/fields/combo.js b/modules/ui/fields/combo.js index 404ba93df..3ecd9d3ad 100644 --- a/modules/ui/fields/combo.js +++ b/modules/ui/fields/combo.js @@ -90,8 +90,9 @@ export function uiFieldCombo(field, context) { function displayValue(tval) { tval = tval || ''; - if (field.hasTextForStringId('options.' + tval)) { - return field.t('options.' + tval, { default: tval }); + var stringsField = field.resolveReference('stringsCrossReference'); + if (stringsField.hasTextForStringId('options.' + tval)) { + return stringsField.t('options.' + tval, { default: tval }); } if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') { @@ -107,8 +108,9 @@ export function uiFieldCombo(field, context) { function renderValue(tval) { tval = tval || ''; - if (field.hasTextForStringId('options.' + tval)) { - return field.t.append('options.' + tval, { default: tval }); + var stringsField = field.resolveReference('stringsCrossReference'); + if (stringsField.hasTextForStringId('options.' + tval)) { + return stringsField.t.append('options.' + tval, { default: tval }); } if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') { diff --git a/modules/ui/fields/cycleway.js b/modules/ui/fields/cycleway.js index 5976dbea9..1cd94e747 100644 --- a/modules/ui/fields/cycleway.js +++ b/modules/ui/fields/cycleway.js @@ -115,9 +115,10 @@ export function uiFieldCycleway(field, context) { cycleway.options = function() { + var stringsField = field.resolveReference('stringsCrossReference'); return field.options.map(function(option) { return { - title: field.t('options.' + option + '.description'), + title: stringsField.t('options.' + option + '.description'), value: option }; }); diff --git a/modules/ui/fields/radio.js b/modules/ui/fields/radio.js index 7da6b3a28..c7cd14edb 100644 --- a/modules/ui/fields/radio.js +++ b/modules/ui/fields/radio.js @@ -55,16 +55,17 @@ export function uiFieldRadio(field, context) { enter = labels.enter() .append('label'); + var stringsField = field.resolveReference('stringsCrossReference'); enter .append('input') .attr('type', 'radio') .attr('name', field.id) - .attr('value', function(d) { return field.t('options.' + d, { 'default': d }); }) + .attr('value', function(d) { return stringsField.t('options.' + d, { 'default': d }); }) .attr('checked', false); enter .append('span') - .html(function(d) { return field.t.html('options.' + d, { 'default': d }); }); + .each(function(d) { stringsField.t.append('options.' + d, { 'default': d })(d3_select(this)); }); labels = labels .merge(enter); From 52ae374ceee690c1add71facf85aa6a11d7ba369 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Mon, 10 Oct 2022 13:17:19 +0200 Subject: [PATCH 2/3] add tests --- test/spec/presets/field.js | 61 +++++++++++++++++++++++++++++++++++++ test/spec/presets/preset.js | 32 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 test/spec/presets/field.js diff --git a/test/spec/presets/field.js b/test/spec/presets/field.js new file mode 100644 index 000000000..57b2e7bfb --- /dev/null +++ b/test/spec/presets/field.js @@ -0,0 +1,61 @@ +describe('iD.presetField', function() { + describe('#references', function() { + it('references label and terms of another field', function() { + var allFields = {}; + var other = iD.presetField('other', {}, allFields); + var field = iD.presetField('test', {label: '{other}'}, allFields); + allFields.other = other; + allFields.preset = field; + + // mock localizer + sinon.spy(other, 't'); + sinon.spy(field, 't'); + + field.title(); + expect(other.t).to.have.been.calledOnce; + expect(field.t).not.to.have.been.called; + + other.t.resetHistory(); + field.t.resetHistory(); + + field.terms(); + expect(other.t).to.have.been.calledOnce; + expect(field.t).not.to.have.been.called; + }); + + it('references placeholder of another field', function() { + var allFields = {}; + var other = iD.presetField('other', {}, allFields); + var field = iD.presetField('test', {placeholder: '{other}'}, allFields); + allFields.other = other; + allFields.preset = field; + + // mock localizer + sinon.spy(other, 't'); + sinon.spy(field, 't'); + + field.placeholder(); + expect(other.t).to.have.been.calledOnce; + expect(field.t).not.to.have.been.called; + }); + + it('references string options of another field', function() { + var allFields = {}; + var other = iD.presetField('other', {}, allFields); + var field = iD.presetField('test', {stringsCrossReference: '{other}', options: ['v'], key: 'k'}, allFields); + allFields.other = other; + allFields.preset = field; + + // mock localizer + sinon.spy(other, 't'); + sinon.spy(field, 't'); + sinon.stub(other, 'hasTextForStringId').returns(true); + + var context = iD.coreContext().assetPath('../dist/').init(); + var uiField = iD.uiFieldCombo(field, context); + uiField.tags({k: 'v'}); + expect(field.t).not.to.have.been.called; + expect(other.t).to.have.been.calledOnce; + }); + }); +}); diff --git a/test/spec/presets/preset.js b/test/spec/presets/preset.js index 11093f62b..b8f89c16a 100644 --- a/test/spec/presets/preset.js +++ b/test/spec/presets/preset.js @@ -228,4 +228,36 @@ describe('iD.presetPreset', function() { expect(preset.addable()).to.be.true; }); }); + + describe('#references', function() { + it('references name, aliases and terms of another preset', function() { + var allPresets = {}; + var other = iD.presetPreset('other', {}, undefined, undefined, allPresets); + var preset = iD.presetPreset('test', {name: '{other}'}, undefined, undefined, allPresets); + allPresets.other = other; + allPresets.preset = preset; + + // mock localizer + sinon.spy(other, 't'); + sinon.spy(preset, 't'); + + preset.name(); + expect(other.t).to.have.been.calledOnce; + expect(preset.t).not.to.have.been.called; + + other.t.resetHistory(); + preset.t.resetHistory(); + + preset.aliases(); + expect(other.t).to.have.been.calledOnce; + expect(preset.t).not.to.have.been.called; + + other.t.resetHistory(); + preset.t.resetHistory(); + + preset.terms(); + expect(other.t).to.have.been.calledOnce; + expect(preset.t).not.to.have.been.called; + }); + }); }); From f5dc3b95552c8167fdb9c95741a9af59ef126e06 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 13 Oct 2022 13:52:33 +0200 Subject: [PATCH 3/3] add changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e65a0cf48..b6a1aeffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,10 +43,13 @@ _Breaking developer changes, which may affect downstream projects or sites that #### :white_check_mark: Validation #### :bug: Bugfixes #### :rocket: Presets +* add support for tagging schema v5 ([#9320]) * Render `natural=strait` features in blue color ([#9294]) #### :hammer: Development [#9294]: https://github.com/openstreetmap/iD/issues/9294 +[#9320]: https://github.com/openstreetmap/iD/pull/9320 + # 2.22.0 ##### 2022-Sep-27