From 8cf59a6d5945e9a3ba45b5b0285e4a6a34a081c8 Mon Sep 17 00:00:00 2001 From: pedromml Date: Thu, 30 May 2024 15:29:26 -0300 Subject: [PATCH 1/8] Add source subfield to other fields --- css/80_app.css | 4 + data/core.yaml | 3 + dist/iD.css | 4 + modules/ui/field.js | 184 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 194 insertions(+), 1 deletion(-) diff --git a/css/80_app.css b/css/80_app.css index f124982a83..cfffb362fa 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2122,11 +2122,13 @@ input.date-selector { /* nested subfields for name in different languages or date in different formats */ .localized-multilingual, +.field-source, .date-edtf { padding: 0 10px; flex-basis: 100%; } .localized-multilingual .entry, +.field-source .entry, .date-edtf .entry { position: relative; overflow: hidden; @@ -2134,6 +2136,7 @@ input.date-selector { /* draws a little line connecting the multilingual field up to the name field */ .localized-multilingual .entry::before, +.field-source .entry::before, .date-edtf .entry::before { content: ""; display: block; @@ -2153,6 +2156,7 @@ input.date-selector { width: 100%; } .localized-multilingual .entry .localized-value, +.field-source .entry .field-source-value, .date-edtf .entry .date-value { border-top-width: 0; border-radius: 0 0 4px 4px; diff --git a/data/core.yaml b/data/core.yaml index 7659a9a2a8..a5edac7c94 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -798,6 +798,9 @@ en: edtf: Add EDTF date edtf_label: Extended Date/Time Format edtf_placeholder: 1849~, 1804?, 189X, 1906/1908, 1814-23, 1960-05-01T13:00... + field_source: Add Source + field_source_label: Source + field_source_placeholder: https://... max_length_reached: "This string is longer than the maximum length of {maxChars} characters. Anything exceeding that length will be truncated." set_today: "Sets the value to today." background: diff --git a/dist/iD.css b/dist/iD.css index ec34a3b6b3..5a2ef03510 100644 --- a/dist/iD.css +++ b/dist/iD.css @@ -5522,11 +5522,13 @@ /* nested subfields for name in different languages or date in different formats */ .ideditor .localized-multilingual, +.ideditor .field-source, .ideditor .date-edtf { padding: 0 10px; flex-basis: 100%; } .ideditor .localized-multilingual .entry, +.ideditor .field-source .entry, .ideditor .date-edtf .entry { position: relative; overflow: hidden; @@ -5534,6 +5536,7 @@ /* draws a little line connecting the multilingual field up to the name field */ .ideditor .localized-multilingual .entry::before, +.ideditor .field-source .entry::before, .ideditor .date-edtf .entry::before { content: ""; display: block; @@ -5553,6 +5556,7 @@ width: 100%; } .ideditor .localized-multilingual .entry .localized-value, +.ideditor .field-source .entry .field-source-value, .ideditor .date-edtf .entry .date-value { border-top-width: 0; border-radius: 0 0 4px 4px; diff --git a/modules/ui/field.js b/modules/ui/field.js index c7f0f2ca52..6281c9cf83 100644 --- a/modules/ui/field.js +++ b/modules/ui/field.js @@ -9,7 +9,7 @@ import { geoExtent } from '../geo/extent'; import { uiFieldHelp } from './field_help'; import { uiFields } from './fields'; import { uiTagReference } from './tag_reference'; -import { utilRebind, utilUniqueDomId } from '../util'; +import { utilGetSetValue, utilRebind, utilUniqueDomId } from '../util'; export function uiField(context, presetField, entityIDs, options) { @@ -27,6 +27,11 @@ export function uiField(context, presetField, entityIDs, options) { var _show = options.show; var _state = ''; var _tags = {}; + let sourceInput = d3_select(null); + let _sourceValue; + let sourceKey = field.key + ':source'; + + options.source = field.source !== undefined ? field.source : true; var _entityExtent; if (entityIDs && entityIDs.length) { @@ -125,6 +130,149 @@ export function uiField(context, presetField, entityIDs, options) { } + function renderSourceInput(selection) { + console.log('renderSourceInput'); + let entries = selection.selectAll('div.entry') + .data((typeof _sourceValue === 'string' || Array.isArray(_sourceValue)) ? [_sourceValue] : []); + + entries.exit() + .style('top', '0') + .style('max-height', '240px') + .transition() + .duration(200) + .style('opacity', '0') + .style('max-height', '0px') + .remove(); + + let entriesEnter = entries.enter() + .append('div') + .attr('class', 'entry') + .each(function() { + var wrap = d3_select(this); + + let domId = utilUniqueDomId('source-' + field.safeid); + let label = wrap + .append('label') + .attr('class', 'field-label') + .attr('for', domId); + + let text = label + .append('span') + .attr('class', 'label-text'); + + text + .append('span') + .attr('class', 'label-textvalue') + .call(t.append('inspector.field_source_label')); + + text + .append('span') + .attr('class', 'label-textannotation'); + + label + .append('button') + .attr('class', 'remove-icon-source') // 'remove-icon-edtf' + .attr('title', t('icons.remove')) + .on('click', function(d3_event) { + d3_event.preventDefault(); + + // remove the UI item manually + _sourceValue = undefined; + + if (sourceKey && sourceKey in _tags) { + delete _tags[sourceKey]; + // remove from entity tags + let t = {}; + t[sourceKey] = undefined; + dispatch.call('change', this, t); + return; + } + + renderSourceInput(selection); + }) + .call(svgIcon('#iD-operation-delete')); + + wrap + .append('input') + .attr('type', 'text') + .attr('class', 'field-source-value') + .on('blur', changeSourceValue) + .on('change', changeSourceValue); + }); + + entriesEnter + .style('margin-top', '0px') + .style('max-height', '0px') + .style('opacity', '0') + .transition() + .duration(200) + .style('margin-top', '10px') + .style('max-height', '240px') + .style('opacity', '1') + .on('end', function() { + d3_select(this) + .style('max-height', '') + .style('overflow', 'visible'); + }); + + entries = entries.merge(entriesEnter); + + entries.order(); + + // allow removing the entry UIs even if there isn't a tag to remove + entries.classed('present', true); + + utilGetSetValue(entries.select('.field-source-value'), function(d) { + return typeof d === 'string' ? d : ''; + }) + .attr('title', function(d) { + return Array.isArray(d) ? d.filter(Boolean).join('\n') : null; + }) + .attr('placeholder', function(d) { + return Array.isArray(d) ? t('inspector.multiple_values') : t('inspector.field_source_placeholder'); + }) + .classed('mixed', function(d) { + return Array.isArray(d); + }); + } + + function changeSourceValue(d3_event, d) { + console.log('changeSourceValue'); + let value = context.cleanTagValue(utilGetSetValue(d3_select(this))) || undefined; + console.log('sourceTagValue'); + console.log(value); + // don't override multiple values with blank string + if (!value && Array.isArray(d.value)) return; + + let t = {}; + t[sourceKey] = value; + d.value = value; + dispatch.call('change', this, t); + } + + function addSource(d3_event, d) { + console.log('addSource'); + d3_event.preventDefault(); + + if (typeof _sourceValue !== 'string' && !Array.isArray(_sourceValue)) { + _sourceValue = ''; + + sourceInput.call(renderSourceInput); + } + + } + + function calcSourceValue(tags) { + console.log('calcSourceValue'); + if (_sourceValue && !tags[sourceKey]) { + // Don't unset the variable based on deleted tags, since this makes the UI + // disappear unexpectedly when clearing values - #8164 + _sourceValue = ''; + } else { + _sourceValue = tags[sourceKey]; + } + } + field.render = function(selection) { var container = selection.selectAll('.form-field') .data([field]); @@ -171,6 +319,22 @@ export function uiField(context, presetField, entityIDs, options) { } } + if(options.source){ + let sourceButtonTip = uiTooltip() + .title(() => t.append('inspector.field_source')) + .placement('left'); + + labelEnter + .append('button') + .attr('class', 'source-icon') + .attr('title', 'source-button') + .call(sourceButtonTip) + .call(svgIcon('#iD-icon-note')); + } + + if (_tags && _sourceValue === undefined) { + calcSourceValue(_tags); + } // Update container = container @@ -182,6 +346,9 @@ export function uiField(context, presetField, entityIDs, options) { container.select('.field-label > .modified-icon') // propagate bound data .on('click', revert); + container.select('.field-label > .source-icon') // propagate bound data + .on('click', addSource); + container .each(function(d) { var selection = d3_select(this); @@ -258,6 +425,19 @@ export function uiField(context, presetField, entityIDs, options) { .attr('xlink:href', '#fas-lock'); container.call(_locked ? _lockedTip : _lockedTip.destroy); + + if(options.source){ + sourceInput = selection.selectChild().selectAll('.field-source') + .data([0]); + + sourceInput = sourceInput.enter() + .append('div') + .attr('class', 'field-source') + .merge(sourceInput); + + sourceInput + .call(renderSourceInput); + } }; @@ -280,6 +460,8 @@ export function uiField(context, presetField, entityIDs, options) { } } + calcSourceValue(_tags); + return field; }; From c3833ee8f9c611d6b94258daf5e86d79d1bad34d Mon Sep 17 00:00:00 2001 From: pedromml Date: Thu, 30 May 2024 15:35:06 -0300 Subject: [PATCH 2/8] Remove source subfield from source field --- modules/presets/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/presets/index.js b/modules/presets/index.js index 568304fd80..06501ad6a0 100644 --- a/modules/presets/index.js +++ b/modules/presets/index.js @@ -58,6 +58,7 @@ function addHistoricalFields(fields) { // A combo box would encourage mappers to choose one of the suggestions, but we want mappers to be as detailed as possible. if (fields.source) { fields.source.type = 'text'; + fields.source.source = false; } fields.license = { From 54811c863340f7f0a44b8d9232cf140572a402f0 Mon Sep 17 00:00:00 2001 From: pedromml Date: Tue, 4 Jun 2024 04:29:43 -0300 Subject: [PATCH 3/8] Separate logic in another file: uiSourceSubfield --- data/core.yaml | 2 +- modules/ui/field.js | 189 ++-------------------------------- modules/ui/index.js | 1 + modules/ui/source_subfield.js | 170 ++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 181 deletions(-) create mode 100644 modules/ui/source_subfield.js diff --git a/data/core.yaml b/data/core.yaml index a5edac7c94..ec41b0d1e3 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -800,7 +800,7 @@ en: edtf_placeholder: 1849~, 1804?, 189X, 1906/1908, 1814-23, 1960-05-01T13:00... field_source: Add Source field_source_label: Source - field_source_placeholder: https://... + field_source_placeholder: URL, newspaper article, book... max_length_reached: "This string is longer than the maximum length of {maxChars} characters. Anything exceeding that length will be truncated." set_today: "Sets the value to today." background: diff --git a/modules/ui/field.js b/modules/ui/field.js index 6281c9cf83..b84cb056a9 100644 --- a/modules/ui/field.js +++ b/modules/ui/field.js @@ -9,7 +9,8 @@ import { geoExtent } from '../geo/extent'; import { uiFieldHelp } from './field_help'; import { uiFields } from './fields'; import { uiTagReference } from './tag_reference'; -import { utilGetSetValue, utilRebind, utilUniqueDomId } from '../util'; +import { utilRebind, utilUniqueDomId } from '../util'; +import { uiSourceSubfield } from './source_subfield'; export function uiField(context, presetField, entityIDs, options) { @@ -27,9 +28,6 @@ export function uiField(context, presetField, entityIDs, options) { var _show = options.show; var _state = ''; var _tags = {}; - let sourceInput = d3_select(null); - let _sourceValue; - let sourceKey = field.key + ':source'; options.source = field.source !== undefined ? field.source : true; @@ -129,150 +127,6 @@ export function uiField(context, presetField, entityIDs, options) { dispatch.call('change', d, t); } - - function renderSourceInput(selection) { - console.log('renderSourceInput'); - let entries = selection.selectAll('div.entry') - .data((typeof _sourceValue === 'string' || Array.isArray(_sourceValue)) ? [_sourceValue] : []); - - entries.exit() - .style('top', '0') - .style('max-height', '240px') - .transition() - .duration(200) - .style('opacity', '0') - .style('max-height', '0px') - .remove(); - - let entriesEnter = entries.enter() - .append('div') - .attr('class', 'entry') - .each(function() { - var wrap = d3_select(this); - - let domId = utilUniqueDomId('source-' + field.safeid); - let label = wrap - .append('label') - .attr('class', 'field-label') - .attr('for', domId); - - let text = label - .append('span') - .attr('class', 'label-text'); - - text - .append('span') - .attr('class', 'label-textvalue') - .call(t.append('inspector.field_source_label')); - - text - .append('span') - .attr('class', 'label-textannotation'); - - label - .append('button') - .attr('class', 'remove-icon-source') // 'remove-icon-edtf' - .attr('title', t('icons.remove')) - .on('click', function(d3_event) { - d3_event.preventDefault(); - - // remove the UI item manually - _sourceValue = undefined; - - if (sourceKey && sourceKey in _tags) { - delete _tags[sourceKey]; - // remove from entity tags - let t = {}; - t[sourceKey] = undefined; - dispatch.call('change', this, t); - return; - } - - renderSourceInput(selection); - }) - .call(svgIcon('#iD-operation-delete')); - - wrap - .append('input') - .attr('type', 'text') - .attr('class', 'field-source-value') - .on('blur', changeSourceValue) - .on('change', changeSourceValue); - }); - - entriesEnter - .style('margin-top', '0px') - .style('max-height', '0px') - .style('opacity', '0') - .transition() - .duration(200) - .style('margin-top', '10px') - .style('max-height', '240px') - .style('opacity', '1') - .on('end', function() { - d3_select(this) - .style('max-height', '') - .style('overflow', 'visible'); - }); - - entries = entries.merge(entriesEnter); - - entries.order(); - - // allow removing the entry UIs even if there isn't a tag to remove - entries.classed('present', true); - - utilGetSetValue(entries.select('.field-source-value'), function(d) { - return typeof d === 'string' ? d : ''; - }) - .attr('title', function(d) { - return Array.isArray(d) ? d.filter(Boolean).join('\n') : null; - }) - .attr('placeholder', function(d) { - return Array.isArray(d) ? t('inspector.multiple_values') : t('inspector.field_source_placeholder'); - }) - .classed('mixed', function(d) { - return Array.isArray(d); - }); - } - - function changeSourceValue(d3_event, d) { - console.log('changeSourceValue'); - let value = context.cleanTagValue(utilGetSetValue(d3_select(this))) || undefined; - console.log('sourceTagValue'); - console.log(value); - // don't override multiple values with blank string - if (!value && Array.isArray(d.value)) return; - - let t = {}; - t[sourceKey] = value; - d.value = value; - dispatch.call('change', this, t); - } - - function addSource(d3_event, d) { - console.log('addSource'); - d3_event.preventDefault(); - - if (typeof _sourceValue !== 'string' && !Array.isArray(_sourceValue)) { - _sourceValue = ''; - - sourceInput.call(renderSourceInput); - } - - } - - function calcSourceValue(tags) { - console.log('calcSourceValue'); - if (_sourceValue && !tags[sourceKey]) { - // Don't unset the variable based on deleted tags, since this makes the UI - // disappear unexpectedly when clearing values - #8164 - _sourceValue = ''; - } else { - _sourceValue = tags[sourceKey]; - } - } - field.render = function(selection) { var container = selection.selectAll('.form-field') .data([field]); @@ -319,26 +173,15 @@ export function uiField(context, presetField, entityIDs, options) { } } - if(options.source){ - let sourceButtonTip = uiTooltip() - .title(() => t.append('inspector.field_source')) - .placement('left'); - - labelEnter - .append('button') - .attr('class', 'source-icon') - .attr('title', 'source-button') - .call(sourceButtonTip) - .call(svgIcon('#iD-icon-note')); - } - - if (_tags && _sourceValue === undefined) { - calcSourceValue(_tags); - } + var sourceSubfield = uiSourceSubfield(field, _tags, dispatch); // Update container = container - .merge(enter); + .merge(enter); + + if(options.source){ + sourceSubfield.button(labelEnter, container); + } container.select('.field-label > .remove-icon') // propagate bound data .on('click', remove); @@ -346,9 +189,6 @@ export function uiField(context, presetField, entityIDs, options) { container.select('.field-label > .modified-icon') // propagate bound data .on('click', revert); - container.select('.field-label > .source-icon') // propagate bound data - .on('click', addSource); - container .each(function(d) { var selection = d3_select(this); @@ -427,16 +267,7 @@ export function uiField(context, presetField, entityIDs, options) { container.call(_locked ? _lockedTip : _lockedTip.destroy); if(options.source){ - sourceInput = selection.selectChild().selectAll('.field-source') - .data([0]); - - sourceInput = sourceInput.enter() - .append('div') - .attr('class', 'field-source') - .merge(sourceInput); - - sourceInput - .call(renderSourceInput); + sourceSubfield.body(selection); } }; @@ -460,8 +291,6 @@ export function uiField(context, presetField, entityIDs, options) { } } - calcSourceValue(_tags); - return field; }; diff --git a/modules/ui/index.js b/modules/ui/index.js index 2ffbdd11b0..a7a362bda6 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -49,6 +49,7 @@ export { uiPresetList } from './preset_list'; export { uiRestore } from './restore'; export { uiScale } from './scale'; export { uiSidebar } from './sidebar'; +export { uiSourceSubfield } from './source_subfield'; export { uiSourceSwitch } from './source_switch'; export { uiSpinner } from './spinner'; export { uiSplash } from './splash'; diff --git a/modules/ui/source_subfield.js b/modules/ui/source_subfield.js new file mode 100644 index 0000000000..cb27f95af5 --- /dev/null +++ b/modules/ui/source_subfield.js @@ -0,0 +1,170 @@ +import { + select as d3_select +} from 'd3-selection'; + +import { t } from '../core/localizer'; +import { svgIcon } from '../svg/icon'; +import { uiTooltip } from './tooltip'; +import { utilGetSetValue, utilUniqueDomId } from '../util'; + +export function uiSourceSubfield(field, tags, dispatch) { + + var sourceSubfield = {}; + + let sourceInput = d3_select(null); + let sourceKey = field.key + ':source'; + let _sourceValue = tags[sourceKey]; + + function renderSourceInput(selection) { + let entries = selection.selectAll('div.entry') + .data((typeof _sourceValue === 'string' || Array.isArray(_sourceValue)) ? [_sourceValue] : []); + + entries.exit() + .style('top', '0') + .style('max-height', '240px') + .transition() + .duration(200) + .style('opacity', '0') + .style('max-height', '0px') + .remove(); + + let entriesEnter = entries.enter() + .append('div') + .attr('class', 'entry') + .each(function() { + var wrap = d3_select(this); + + let domId = utilUniqueDomId('source-' + field.safeid); + let label = wrap + .append('label') + .attr('class', 'field-label') + .attr('for', domId); + + let text = label + .append('span') + .attr('class', 'label-text'); + + text + .append('span') + .attr('class', 'label-textvalue') + .call(t.append('inspector.field_source_label')); + + text + .append('span') + .attr('class', 'label-textannotation'); + + label + .append('button') + .attr('class', 'remove-icon-source') + .attr('title', t('icons.remove')) + .on('click', function(d3_event) { + d3_event.preventDefault(); + + // remove the UI item manually + _sourceValue = undefined; + + let t = {}; + t[sourceKey] = undefined; + dispatch.call('change', this, t); + + renderSourceInput(selection); + }) + .call(svgIcon('#iD-operation-delete')); + + wrap + .append('input') + .attr('type', 'text') + .attr('class', 'field-source-value') + .on('blur', changeSourceValue) + .on('change', changeSourceValue); + }); + + entriesEnter + .style('margin-top', '0px') + .style('max-height', '0px') + .style('opacity', '0') + .transition() + .duration(200) + .style('margin-top', '10px') + .style('max-height', '240px') + .style('opacity', '1') + .on('end', function() { + d3_select(this) + .style('max-height', '') + .style('overflow', 'visible'); + }); + + entries = entries.merge(entriesEnter); + + entries.order(); + + // allow removing the entry UIs even if there isn't a tag to remove + entries.classed('present', true); + + utilGetSetValue(entries.select('.field-source-value'), function(d) { + return typeof d === 'string' ? d : ''; + }) + .attr('title', function(d) { + return Array.isArray(d) ? d.filter(Boolean).join('\n') : null; + }) + .attr('placeholder', function(d) { + return Array.isArray(d) ? t('inspector.multiple_values') : t('inspector.field_source_placeholder'); + }) + .classed('mixed', function(d) { + return Array.isArray(d); + }); + } + + function changeSourceValue(d3_event, d) { + let value = context.cleanTagValue(utilGetSetValue(d3_select(this))) || undefined; + // don't override multiple values with blank string + if (!value && Array.isArray(d.value)) return; + + let t = {}; + t[sourceKey] = value; + d.value = value; + dispatch.call('change', this, t); + } + + function addSource(d3_event, d) { + d3_event.preventDefault(); + + if (typeof _sourceValue !== 'string' && !Array.isArray(_sourceValue)) { + _sourceValue = ''; + } + sourceInput.call(renderSourceInput); + + } + + sourceSubfield.button = function(labelEnter, container) { + let sourceButtonTip = uiTooltip() + .title(() => t.append('inspector.field_source')) + .placement('left'); + + labelEnter + .append('button') + .attr('class', 'source-icon') + .attr('title', 'source-button') + .call(sourceButtonTip) + .call(svgIcon('#fas-book')); + + container.select('.field-label > .source-icon') // propagate bound data + .on('click', addSource); + }; + + + sourceSubfield.body = function(selection) { + sourceInput = selection.selectChild().selectAll('.field-source') + .data([0]); + + sourceInput = sourceInput.enter() + .append('div') + .attr('class', 'field-source') + .merge(sourceInput); + + sourceInput + .call(renderSourceInput); + }; + + return sourceSubfield; +} From cfc1ef2673b7fd888a471853fe6fd31e159ce047 Mon Sep 17 00:00:00 2001 From: pedromml Date: Thu, 13 Jun 2024 16:44:29 -0300 Subject: [PATCH 4/8] Add at icon and reference main field in source label --- data/core.yaml | 2 +- modules/ui/source_subfield.js | 4 ++-- scripts/build_data.js | 3 ++- svg/fontawesome/fas-at.svg | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 svg/fontawesome/fas-at.svg diff --git a/data/core.yaml b/data/core.yaml index ec41b0d1e3..b5856bcada 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -799,7 +799,7 @@ en: edtf_label: Extended Date/Time Format edtf_placeholder: 1849~, 1804?, 189X, 1906/1908, 1814-23, 1960-05-01T13:00... field_source: Add Source - field_source_label: Source + field_source_label: Source for {field_name} field_source_placeholder: URL, newspaper article, book... max_length_reached: "This string is longer than the maximum length of {maxChars} characters. Anything exceeding that length will be truncated." set_today: "Sets the value to today." diff --git a/modules/ui/source_subfield.js b/modules/ui/source_subfield.js index cb27f95af5..3d460768bc 100644 --- a/modules/ui/source_subfield.js +++ b/modules/ui/source_subfield.js @@ -47,7 +47,7 @@ export function uiSourceSubfield(field, tags, dispatch) { text .append('span') .attr('class', 'label-textvalue') - .call(t.append('inspector.field_source_label')); + .call(t.append('inspector.field_source_label', { field_name: field.title })); text .append('span') @@ -146,7 +146,7 @@ export function uiSourceSubfield(field, tags, dispatch) { .attr('class', 'source-icon') .attr('title', 'source-button') .call(sourceButtonTip) - .call(svgIcon('#fas-book')); + .call(svgIcon('#fas-at', 'inline')); container.select('.field-label > .source-icon') // propagate bound data .on('click', addSource); diff --git a/scripts/build_data.js b/scripts/build_data.js index 8a7c98f04c..603bfb51b1 100644 --- a/scripts/build_data.js +++ b/scripts/build_data.js @@ -71,7 +71,8 @@ function buildData() { 'fas-th-list', 'fas-user-cog', 'fas-calendar-days', - 'fas-rotate' + 'fas-rotate', + 'fas-at' ]); // add icons for QA integrations readQAIssueIcons(faIcons); diff --git a/svg/fontawesome/fas-at.svg b/svg/fontawesome/fas-at.svg new file mode 100644 index 0000000000..e90101f24e --- /dev/null +++ b/svg/fontawesome/fas-at.svg @@ -0,0 +1 @@ + \ No newline at end of file From 2a0895986be864648754cf3f17571a69d52ebc92 Mon Sep 17 00:00:00 2001 From: pedromml Date: Wed, 19 Jun 2024 19:06:26 -0300 Subject: [PATCH 5/8] add tests --- dist/img/fa-sprite.svg | 2 +- modules/ui/field.js | 16 ++++--- modules/ui/source_subfield.js | 13 ++++-- test/spec/ui/fields/source_subfield.js | 65 ++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 test/spec/ui/fields/source_subfield.js diff --git a/dist/img/fa-sprite.svg b/dist/img/fa-sprite.svg index aaa824d790..c80af3724d 100644 --- a/dist/img/fa-sprite.svg +++ b/dist/img/fa-sprite.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/modules/ui/field.js b/modules/ui/field.js index b84cb056a9..8635a5284d 100644 --- a/modules/ui/field.js +++ b/modules/ui/field.js @@ -28,7 +28,7 @@ export function uiField(context, presetField, entityIDs, options) { var _show = options.show; var _state = ''; var _tags = {}; - + options.source = field.source !== undefined ? field.source : true; var _entityExtent; @@ -128,6 +128,9 @@ export function uiField(context, presetField, entityIDs, options) { } field.render = function(selection) { + + var sourceSubfield = uiSourceSubfield(context, field, _tags, dispatch); + var container = selection.selectAll('.form-field') .data([field]); @@ -171,17 +174,16 @@ export function uiField(context, presetField, entityIDs, options) { .attr('title', t('icons.undo')) .call(svgIcon((localizer.textDirection() === 'rtl') ? '#iD-icon-redo' : '#iD-icon-undo')); } + + if (options.source){ + sourceSubfield.button(labelEnter, container); + } } - var sourceSubfield = uiSourceSubfield(field, _tags, dispatch); // Update container = container .merge(enter); - - if(options.source){ - sourceSubfield.button(labelEnter, container); - } container.select('.field-label > .remove-icon') // propagate bound data .on('click', remove); @@ -266,7 +268,7 @@ export function uiField(context, presetField, entityIDs, options) { container.call(_locked ? _lockedTip : _lockedTip.destroy); - if(options.source){ + if (options.source){ sourceSubfield.body(selection); } }; diff --git a/modules/ui/source_subfield.js b/modules/ui/source_subfield.js index 3d460768bc..944b2ca3f2 100644 --- a/modules/ui/source_subfield.js +++ b/modules/ui/source_subfield.js @@ -7,7 +7,7 @@ import { svgIcon } from '../svg/icon'; import { uiTooltip } from './tooltip'; import { utilGetSetValue, utilUniqueDomId } from '../util'; -export function uiSourceSubfield(field, tags, dispatch) { +export function uiSourceSubfield(context, field, tags, dispatch) { var sourceSubfield = {}; @@ -126,21 +126,21 @@ export function uiSourceSubfield(field, tags, dispatch) { dispatch.call('change', this, t); } - function addSource(d3_event, d) { + function addSource(d3_event) { d3_event.preventDefault(); - + if (typeof _sourceValue !== 'string' && !Array.isArray(_sourceValue)) { _sourceValue = ''; } sourceInput.call(renderSourceInput); - + } sourceSubfield.button = function(labelEnter, container) { let sourceButtonTip = uiTooltip() .title(() => t.append('inspector.field_source')) .placement('left'); - + labelEnter .append('button') .attr('class', 'source-icon') @@ -148,6 +148,9 @@ export function uiSourceSubfield(field, tags, dispatch) { .call(sourceButtonTip) .call(svgIcon('#fas-at', 'inline')); + container = container + .merge(labelEnter); + container.select('.field-label > .source-icon') // propagate bound data .on('click', addSource); }; diff --git a/test/spec/ui/fields/source_subfield.js b/test/spec/ui/fields/source_subfield.js new file mode 100644 index 0000000000..0e734fb1d7 --- /dev/null +++ b/test/spec/ui/fields/source_subfield.js @@ -0,0 +1,65 @@ +describe('iD.uiSourceSubfield', function() { + let entity, context, selection, field; + + beforeEach(function() { + entity = iD.osmNode({id: 'n12345'}); + context = iD.coreContext().assetPath('../dist/').init(); + context.history().merge([entity]); + selection = d3.select(document.createElement('div')); + field = iD.presetField('name', { key: 'name', type: 'text' }); + }); + + it('adds an source subfield when the @ button is clicked', function(done) { + var uiField = iD.uiField(context, field, ['n12345'], {show: true, wrap: true}); + window.setTimeout(function() { // async, so data will be available + selection.call(uiField.render); + happen.click(selection.selectAll('.source-icon').node()); + expect(selection.selectAll('.field-source').nodes().length).to.equal(1); + done(); + }, 20); + }); + + it('creates field:source tag after setting the value', function(done) { + var uiField = iD.uiField(context, field, ['n12345'], {show: true, wrap: true}); + window.setTimeout(function() { // async, so data will be available + selection.call(uiField.render); + happen.click(selection.selectAll('.source-icon').node()); + + iD.utilGetSetValue(selection.selectAll('.field-source-value'), 'Book 1'); + + uiField.on('change', function(tags) { + expect(tags).to.eql({'name:source': 'Book 1'}); + }); + happen.once(selection.selectAll('.field-source-value').node(), {type: 'change'}); + done(); + + }, 20); + }); + + +it('removes the tag when the value is emptied', function(done) { + var uiField = iD.uiField(context, field, ['n12345'], {show: true, wrap: true}); + window.setTimeout(function() { // async, so data will be available + selection.call(uiField.render); + happen.click(selection.selectAll('.source-icon').node()); + iD.utilGetSetValue(selection.selectAll('.field-source-value'), 'abc'); + + uiField.on('change', function(tags) { + expect(tags).to.eql({'name:source': undefined}); + }); + + iD.utilGetSetValue(selection.selectAll('.field-source-value'), ''); + happen.once(selection.selectAll('.field-source-value').node(), {type: 'change'}); + done(); + }, 20); + }); + + it('there is no @ button on main source field', function(done) { + var uiField = iD.uiField(context, {...field, source: false}, ['n12345'], {show: true, wrap: true}); + window.setTimeout(function() { // async, so data will be available + selection.call(uiField.render); + expect(selection.selectAll('.source-icon').nodes().length).to.equal(0); + done(); + }, 20); + }); +}); From b65ac4b594376a07cc468a16d6adfc691d5c5c34 Mon Sep 17 00:00:00 2001 From: pedromml Date: Thu, 20 Jun 2024 14:11:56 -0300 Subject: [PATCH 6/8] revert svg file --- dist/img/fa-sprite.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/img/fa-sprite.svg b/dist/img/fa-sprite.svg index c80af3724d..aaa824d790 100644 --- a/dist/img/fa-sprite.svg +++ b/dist/img/fa-sprite.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 244ae94330a23ba60ad284373153f7e652c61684 Mon Sep 17 00:00:00 2001 From: pedromml Date: Thu, 20 Jun 2024 14:27:49 -0300 Subject: [PATCH 7/8] Fix indentation on test file --- modules/ui/field.js | 2 +- test/spec/ui/fields/source_subfield.js | 67 +++++++++++++------------- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/modules/ui/field.js b/modules/ui/field.js index 8635a5284d..6fced76c66 100644 --- a/modules/ui/field.js +++ b/modules/ui/field.js @@ -183,7 +183,7 @@ export function uiField(context, presetField, entityIDs, options) { // Update container = container - .merge(enter); + .merge(enter); container.select('.field-label > .remove-icon') // propagate bound data .on('click', remove); diff --git a/test/spec/ui/fields/source_subfield.js b/test/spec/ui/fields/source_subfield.js index 0e734fb1d7..c083f05d9e 100644 --- a/test/spec/ui/fields/source_subfield.js +++ b/test/spec/ui/fields/source_subfield.js @@ -20,46 +20,45 @@ describe('iD.uiSourceSubfield', function() { }); it('creates field:source tag after setting the value', function(done) { - var uiField = iD.uiField(context, field, ['n12345'], {show: true, wrap: true}); - window.setTimeout(function() { // async, so data will be available - selection.call(uiField.render); - happen.click(selection.selectAll('.source-icon').node()); + var uiField = iD.uiField(context, field, ['n12345'], {show: true, wrap: true}); + window.setTimeout(function() { // async, so data will be available + selection.call(uiField.render); + happen.click(selection.selectAll('.source-icon').node()); - iD.utilGetSetValue(selection.selectAll('.field-source-value'), 'Book 1'); + iD.utilGetSetValue(selection.selectAll('.field-source-value'), 'Book 1'); - uiField.on('change', function(tags) { + uiField.on('change', function(tags) { expect(tags).to.eql({'name:source': 'Book 1'}); - }); - happen.once(selection.selectAll('.field-source-value').node(), {type: 'change'}); - done(); - - }, 20); - }); + }); + happen.once(selection.selectAll('.field-source-value').node(), {type: 'change'}); + done(); + }, 20); + }); -it('removes the tag when the value is emptied', function(done) { - var uiField = iD.uiField(context, field, ['n12345'], {show: true, wrap: true}); - window.setTimeout(function() { // async, so data will be available - selection.call(uiField.render); - happen.click(selection.selectAll('.source-icon').node()); - iD.utilGetSetValue(selection.selectAll('.field-source-value'), 'abc'); + it('removes the tag when the value is emptied', function(done) { + var uiField = iD.uiField(context, field, ['n12345'], {show: true, wrap: true}); + window.setTimeout(function() { // async, so data will be available + selection.call(uiField.render); + happen.click(selection.selectAll('.source-icon').node()); + iD.utilGetSetValue(selection.selectAll('.field-source-value'), 'abc'); - uiField.on('change', function(tags) { - expect(tags).to.eql({'name:source': undefined}); - }); + uiField.on('change', function(tags) { + expect(tags).to.eql({'name:source': undefined}); + }); - iD.utilGetSetValue(selection.selectAll('.field-source-value'), ''); - happen.once(selection.selectAll('.field-source-value').node(), {type: 'change'}); - done(); - }, 20); - }); + iD.utilGetSetValue(selection.selectAll('.field-source-value'), ''); + happen.once(selection.selectAll('.field-source-value').node(), {type: 'change'}); + done(); + }, 20); + }); - it('there is no @ button on main source field', function(done) { - var uiField = iD.uiField(context, {...field, source: false}, ['n12345'], {show: true, wrap: true}); - window.setTimeout(function() { // async, so data will be available - selection.call(uiField.render); - expect(selection.selectAll('.source-icon').nodes().length).to.equal(0); - done(); - }, 20); - }); + it('there is no @ button on main source field', function(done) { + var uiField = iD.uiField(context, {...field, source: false}, ['n12345'], {show: true, wrap: true}); + window.setTimeout(function() { // async, so data will be available + selection.call(uiField.render); + expect(selection.selectAll('.source-icon').nodes().length).to.equal(0); + done(); + }, 20); + }); }); From db97de9fa27ad2b7835816eaef348492751c2a42 Mon Sep 17 00:00:00 2001 From: pedromml Date: Thu, 20 Jun 2024 16:18:57 -0300 Subject: [PATCH 8/8] Update button tooltip. Add comments --- data/core.yaml | 2 +- modules/ui/field.js | 3 +++ modules/ui/source_subfield.js | 10 ++++------ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index b5856bcada..b17289a04d 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -798,7 +798,7 @@ en: edtf: Add EDTF date edtf_label: Extended Date/Time Format edtf_placeholder: 1849~, 1804?, 189X, 1906/1908, 1814-23, 1960-05-01T13:00... - field_source: Add Source + field_source: add source field_source_label: Source for {field_name} field_source_placeholder: URL, newspaper article, book... max_length_reached: "This string is longer than the maximum length of {maxChars} characters. Anything exceeding that length will be truncated." diff --git a/modules/ui/field.js b/modules/ui/field.js index 6fced76c66..be11fba3c2 100644 --- a/modules/ui/field.js +++ b/modules/ui/field.js @@ -29,6 +29,9 @@ export function uiField(context, presetField, entityIDs, options) { var _state = ''; var _tags = {}; + // This sets option.source to true if it has not been defined proviously + // This way, every field will have a source subfield unless explicitly stated otherwise + // Currently, only the main Sources field does not have a sources subfield options.source = field.source !== undefined ? field.source : true; var _entityExtent; diff --git a/modules/ui/source_subfield.js b/modules/ui/source_subfield.js index 944b2ca3f2..d79dbbcf6b 100644 --- a/modules/ui/source_subfield.js +++ b/modules/ui/source_subfield.js @@ -15,6 +15,7 @@ export function uiSourceSubfield(context, field, tags, dispatch) { let sourceKey = field.key + ':source'; let _sourceValue = tags[sourceKey]; + // Adapted from renderEDTF from modules/ui/fields/date.js function renderSourceInput(selection) { let entries = selection.selectAll('div.entry') .data((typeof _sourceValue === 'string' || Array.isArray(_sourceValue)) ? [_sourceValue] : []); @@ -137,15 +138,12 @@ export function uiSourceSubfield(context, field, tags, dispatch) { } sourceSubfield.button = function(labelEnter, container) { - let sourceButtonTip = uiTooltip() - .title(() => t.append('inspector.field_source')) - .placement('left'); - labelEnter .append('button') .attr('class', 'source-icon') - .attr('title', 'source-button') - .call(sourceButtonTip) + .attr('title', function() { + return t('inspector.field_source'); + }) .call(svgIcon('#fas-at', 'inline')); container = container