From b8bb52cc9b16439591c9a3382d7d4bb1e5667b80 Mon Sep 17 00:00:00 2001 From: shyam pandey Date: Wed, 31 Dec 2025 19:48:38 +0530 Subject: [PATCH 1/4] Add inline informational validation for regex-based fields --- modules/ui/fields/textarea.js | 157 ++++++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 7 deletions(-) diff --git a/modules/ui/fields/textarea.js b/modules/ui/fields/textarea.js index 223121daebd..99ced018fd9 100644 --- a/modules/ui/fields/textarea.js +++ b/modules/ui/fields/textarea.js @@ -8,18 +8,20 @@ import { utilRebind } from '../../util'; import { uiLengthIndicator } from '..'; +import { svgIcon } from '../../svg/icon'; // ← ADDED export function uiFieldTextarea(field, context) { var dispatch = d3_dispatch('change'); var input = d3_select(null); + var wrap = d3_select(null); // ← ADDED var _lengthIndicator = uiLengthIndicator(context.maxCharsForTagValue()) .silent(field.usage === 'changeset' && field.key === 'comment'); var _tags; function textarea(selection) { - var wrap = selection.selectAll('.form-field-input-wrap') + wrap = selection.selectAll('.form-field-input-wrap') .data([0]); wrap = wrap.enter() @@ -36,15 +38,24 @@ export function uiFieldTextarea(field, context) { .attr('dir', 'auto') .attr('id', field.domId) .call(utilNoAuto) - .on('input', change(true)) - .on('blur', change()) - .on('change', change()) + .on('input', function () { + change(true)(); + updatePatternValidation(); // ← ADDED + }) + .on('blur', function () { + change()(); + updatePatternValidation(); // ← ADDED + }) + .on('change', function () { + change()(); + updatePatternValidation(); // ← ADDED + }) .merge(input); wrap.call(_lengthIndicator); function change(onInput) { - return function() { + return function () { var val = utilGetSetValue(input); if (!onInput) val = context.cleanTagValue(val); @@ -60,7 +71,50 @@ export function uiFieldTextarea(field, context) { } - textarea.tags = function(tags) { + // ============================ + // INLINE REGEX VALIDATION (FIX) + // ============================ + function updatePatternValidation() { + if (!field.pattern || !wrap || wrap.empty()) return; + + const value = utilGetSetValue(input).trim(); + + if (!value) { + wrap.selectAll('.form-field-pattern-info').remove(); + return; + } + + let isInvalid = false; + try { + const regex = new RegExp(field.pattern); + isInvalid = !regex.test(value); + } catch { + // invalid regex → fail silently + return; + } + + const info = wrap.selectAll('.form-field-pattern-info') + .data(isInvalid ? [0] : []); + + const enter = info.enter() + .append('div') + .attr('class', 'form-field-pattern-info'); + + enter + .append('span') + .call(svgIcon('#iD-icon-info')); + + enter + .append('span') + .attr('class', 'form-field-pattern-info-text') + .text(t('inspector.invalid_format')); + + info.exit().remove(); + } + // ============================ + + + textarea.tags = function (tags) { _tags = tags; var isMixed = Array.isArray(tags[field.key]); @@ -73,13 +127,102 @@ export function uiFieldTextarea(field, context) { if (!isMixed) { _lengthIndicator.update(tags[field.key]); } + + updatePatternValidation(); // ← ADDED (initial render) }; - textarea.focus = function() { + textarea.focus = function () { input.node().focus(); }; return utilRebind(textarea, dispatch, 'on'); } + + +// import { dispatch as d3_dispatch } from 'd3-dispatch'; +// import { select as d3_select } from 'd3-selection'; + +// import { t } from '../../core/localizer'; +// import { +// utilGetSetValue, +// utilNoAuto, +// utilRebind +// } from '../../util'; +// import { uiLengthIndicator } from '..'; + + +// export function uiFieldTextarea(field, context) { +// var dispatch = d3_dispatch('change'); +// var input = d3_select(null); +// var _lengthIndicator = uiLengthIndicator(context.maxCharsForTagValue()) +// .silent(field.usage === 'changeset' && field.key === 'comment'); +// var _tags; + + +// function textarea(selection) { +// var wrap = selection.selectAll('.form-field-input-wrap') +// .data([0]); + +// wrap = wrap.enter() +// .append('div') +// .attr('class', 'form-field-input-wrap form-field-input-' + field.type) +// .style('position', 'relative') +// .merge(wrap); + +// input = wrap.selectAll('textarea') +// .data([0]); + +// input = input.enter() +// .append('textarea') +// .attr('dir', 'auto') +// .attr('id', field.domId) +// .call(utilNoAuto) +// .on('input', change(true)) +// .on('blur', change()) +// .on('change', change()) +// .merge(input); + +// wrap.call(_lengthIndicator); + +// function change(onInput) { +// return function() { + +// var val = utilGetSetValue(input); +// if (!onInput) val = context.cleanTagValue(val); + +// // don't override multiple values with blank string +// if (!val && Array.isArray(_tags[field.key])) return; + +// var t = {}; +// t[field.key] = val || undefined; +// dispatch.call('change', this, t, onInput); +// }; +// } +// } + + +// textarea.tags = function(tags) { +// _tags = tags; + +// var isMixed = Array.isArray(tags[field.key]); + +// utilGetSetValue(input, !isMixed && tags[field.key] ? tags[field.key] : '') +// .attr('title', isMixed ? tags[field.key].filter(Boolean).join('\n') : undefined) +// .attr('placeholder', isMixed ? t('inspector.multiple_values') : (field.placeholder() || t('inspector.unknown'))) +// .classed('mixed', isMixed); + +// if (!isMixed) { +// _lengthIndicator.update(tags[field.key]); +// } +// }; + + +// textarea.focus = function() { +// input.node().focus(); +// }; + + +// return utilRebind(textarea, dispatch, 'on'); +// } From 48accd493907cf58c83d080eaaf5848eac0aaa7c Mon Sep 17 00:00:00 2001 From: shyam pandey Date: Sun, 4 Jan 2026 12:07:27 +0530 Subject: [PATCH 2/4] WIP: save crossing_ways fixes --- css/30_highways.css | 4 ++- modules/validations/crossing_ways.js | 45 +++++++++++++++++----------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/css/30_highways.css b/css/30_highways.css index 21221358890..05f18df0679 100644 --- a/css/30_highways.css +++ b/css/30_highways.css @@ -1,13 +1,15 @@ -preset-icon-container/* highways */ +/* preset-icon-container highways */ /* defaults */ .preset-icon .icon.tag-highway.other-line { color: #fff; fill: #777; } + path.line.casing.tag-highway { stroke: #444; } + path.line.stroke.tag-highway { stroke: #ccc; } diff --git a/modules/validations/crossing_ways.js b/modules/validations/crossing_ways.js index 3f2e27123fa..05e23d91054 100644 --- a/modules/validations/crossing_ways.js +++ b/modules/validations/crossing_ways.js @@ -794,7 +794,7 @@ export function validationCrossingWays(context) { return fix; } - function makeChangeLayerFix(higherOrLower) { + function makeChangeLayerFix(higherOrLower) { return new validationIssueFix({ icon: 'iD-icon-' + (higherOrLower === 'higher' ? 'up' : 'down'), title: t.append('issues.fix.tag_this_as_' + higherOrLower + '.title'), @@ -807,29 +807,39 @@ export function validationCrossingWays(context) { if (selectedIDs.length !== 1) return; var selectedID = selectedIDs[0]; - if (!this.issue.entityIds.some(function(entityId) { - return entityId === selectedID; - })) return; + if (!this.issue.entityIds.includes(selectedID)) return; var entity = context.hasEntity(selectedID); if (!entity) return; - var tags = Object.assign({}, entity.tags); // shallow copy - var layer = tags.layer && Number(tags.layer); - if (layer && !isNaN(layer)) { - if (higherOrLower === 'higher') { - layer += 1; - } else { - layer -= 1; - } + // Clone tags (never mutate original) + var tags = Object.assign({}, entity.tags); + + // Determine feature type + var featureType = getFeatureType(entity, context.graph()); + + // Compute new layer value + var layer = Number(tags.layer); + if (!isNaN(layer)) { + layer += (higherOrLower === 'higher' ? 1 : -1); } else { - if (higherOrLower === 'higher') { - layer = 1; - } else { - layer = -1; - } + layer = (higherOrLower === 'higher' ? 1 : -1); } tags.layer = layer.toString(); + + // Apply bridge/tunnel ONLY to allowed linear features + if (featureType && allowsBridge(featureType) && higherOrLower === 'higher') { + tags.bridge = 'yes'; + delete tags.tunnel; + } else if (featureType && allowsTunnel(featureType) && higherOrLower === 'lower') { + tags.tunnel = 'yes'; + delete tags.bridge; + } else { + // Buildings or unsupported features + delete tags.bridge; + delete tags.tunnel; + } + context.perform( actionChangeTags(entity.id, tags), t('operations.change_tags.annotation') @@ -838,6 +848,7 @@ export function validationCrossingWays(context) { }); } + validation.type = type; return validation; From b39f94bf9ff437b79b818dd26bb4584376027145 Mon Sep 17 00:00:00 2001 From: shyam pandey Date: Wed, 7 Jan 2026 12:08:24 +0530 Subject: [PATCH 3/4] After removing commented code --- modules/ui/fields/textarea.js | 97 +---------------------------------- 1 file changed, 2 insertions(+), 95 deletions(-) diff --git a/modules/ui/fields/textarea.js b/modules/ui/fields/textarea.js index 99ced018fd9..999be808955 100644 --- a/modules/ui/fields/textarea.js +++ b/modules/ui/fields/textarea.js @@ -19,7 +19,6 @@ export function uiFieldTextarea(field, context) { .silent(field.usage === 'changeset' && field.key === 'comment'); var _tags; - function textarea(selection) { wrap = selection.selectAll('.form-field-input-wrap') .data([0]); @@ -69,11 +68,7 @@ export function uiFieldTextarea(field, context) { }; } } - - - // ============================ - // INLINE REGEX VALIDATION (FIX) - // ============================ + function updatePatternValidation() { if (!field.pattern || !wrap || wrap.empty()) return; @@ -89,7 +84,6 @@ export function uiFieldTextarea(field, context) { const regex = new RegExp(field.pattern); isInvalid = !regex.test(value); } catch { - // invalid regex → fail silently return; } @@ -128,7 +122,7 @@ export function uiFieldTextarea(field, context) { _lengthIndicator.update(tags[field.key]); } - updatePatternValidation(); // ← ADDED (initial render) + updatePatternValidation(); }; @@ -139,90 +133,3 @@ export function uiFieldTextarea(field, context) { return utilRebind(textarea, dispatch, 'on'); } - - -// import { dispatch as d3_dispatch } from 'd3-dispatch'; -// import { select as d3_select } from 'd3-selection'; - -// import { t } from '../../core/localizer'; -// import { -// utilGetSetValue, -// utilNoAuto, -// utilRebind -// } from '../../util'; -// import { uiLengthIndicator } from '..'; - - -// export function uiFieldTextarea(field, context) { -// var dispatch = d3_dispatch('change'); -// var input = d3_select(null); -// var _lengthIndicator = uiLengthIndicator(context.maxCharsForTagValue()) -// .silent(field.usage === 'changeset' && field.key === 'comment'); -// var _tags; - - -// function textarea(selection) { -// var wrap = selection.selectAll('.form-field-input-wrap') -// .data([0]); - -// wrap = wrap.enter() -// .append('div') -// .attr('class', 'form-field-input-wrap form-field-input-' + field.type) -// .style('position', 'relative') -// .merge(wrap); - -// input = wrap.selectAll('textarea') -// .data([0]); - -// input = input.enter() -// .append('textarea') -// .attr('dir', 'auto') -// .attr('id', field.domId) -// .call(utilNoAuto) -// .on('input', change(true)) -// .on('blur', change()) -// .on('change', change()) -// .merge(input); - -// wrap.call(_lengthIndicator); - -// function change(onInput) { -// return function() { - -// var val = utilGetSetValue(input); -// if (!onInput) val = context.cleanTagValue(val); - -// // don't override multiple values with blank string -// if (!val && Array.isArray(_tags[field.key])) return; - -// var t = {}; -// t[field.key] = val || undefined; -// dispatch.call('change', this, t, onInput); -// }; -// } -// } - - -// textarea.tags = function(tags) { -// _tags = tags; - -// var isMixed = Array.isArray(tags[field.key]); - -// utilGetSetValue(input, !isMixed && tags[field.key] ? tags[field.key] : '') -// .attr('title', isMixed ? tags[field.key].filter(Boolean).join('\n') : undefined) -// .attr('placeholder', isMixed ? t('inspector.multiple_values') : (field.placeholder() || t('inspector.unknown'))) -// .classed('mixed', isMixed); - -// if (!isMixed) { -// _lengthIndicator.update(tags[field.key]); -// } -// }; - - -// textarea.focus = function() { -// input.node().focus(); -// }; - - -// return utilRebind(textarea, dispatch, 'on'); -// } From 4cd2415e8d8a318533bc792944327c8bf41a8e02 Mon Sep 17 00:00:00 2001 From: shyam pandey Date: Thu, 8 Jan 2026 16:20:39 +0530 Subject: [PATCH 4/4] final fixing of Regex validation --- modules/ui/fields/textarea.js | 1 - modules/validations/crossing_ways.js | 47 +++++++++++----------------- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/modules/ui/fields/textarea.js b/modules/ui/fields/textarea.js index 999be808955..019480363f3 100644 --- a/modules/ui/fields/textarea.js +++ b/modules/ui/fields/textarea.js @@ -10,7 +10,6 @@ import { import { uiLengthIndicator } from '..'; import { svgIcon } from '../../svg/icon'; // ← ADDED - export function uiFieldTextarea(field, context) { var dispatch = d3_dispatch('change'); var input = d3_select(null); diff --git a/modules/validations/crossing_ways.js b/modules/validations/crossing_ways.js index 05e23d91054..5ca3aa149ce 100644 --- a/modules/validations/crossing_ways.js +++ b/modules/validations/crossing_ways.js @@ -794,7 +794,7 @@ export function validationCrossingWays(context) { return fix; } - function makeChangeLayerFix(higherOrLower) { + function makeChangeLayerFix(higherOrLower) { return new validationIssueFix({ icon: 'iD-icon-' + (higherOrLower === 'higher' ? 'up' : 'down'), title: t.append('issues.fix.tag_this_as_' + higherOrLower + '.title'), @@ -807,39 +807,29 @@ export function validationCrossingWays(context) { if (selectedIDs.length !== 1) return; var selectedID = selectedIDs[0]; - if (!this.issue.entityIds.includes(selectedID)) return; + if (!this.issue.entityIds.some(function(entityId) { + return entityId === selectedID; + })) return; var entity = context.hasEntity(selectedID); if (!entity) return; - // Clone tags (never mutate original) - var tags = Object.assign({}, entity.tags); - - // Determine feature type - var featureType = getFeatureType(entity, context.graph()); - - // Compute new layer value - var layer = Number(tags.layer); - if (!isNaN(layer)) { - layer += (higherOrLower === 'higher' ? 1 : -1); + var tags = Object.assign({}, entity.tags); // shallow copy + var layer = tags.layer && Number(tags.layer); + if (layer && !isNaN(layer)) { + if (higherOrLower === 'higher') { + layer += 1; + } else { + layer -= 1; + } } else { - layer = (higherOrLower === 'higher' ? 1 : -1); + if (higherOrLower === 'higher') { + layer = 1; + } else { + layer = -1; + } } tags.layer = layer.toString(); - - // Apply bridge/tunnel ONLY to allowed linear features - if (featureType && allowsBridge(featureType) && higherOrLower === 'higher') { - tags.bridge = 'yes'; - delete tags.tunnel; - } else if (featureType && allowsTunnel(featureType) && higherOrLower === 'lower') { - tags.tunnel = 'yes'; - delete tags.bridge; - } else { - // Buildings or unsupported features - delete tags.bridge; - delete tags.tunnel; - } - context.perform( actionChangeTags(entity.id, tags), t('operations.change_tags.annotation') @@ -848,8 +838,7 @@ export function validationCrossingWays(context) { }); } - validation.type = type; return validation; -} +} \ No newline at end of file