Skip to content

Commit

Permalink
Merge pull request #217 from pedromml/add-source-subfield-to-other-fi…
Browse files Browse the repository at this point in the history
…elds

Add source subfield to other fields
  • Loading branch information
1ec5 authored Jun 20, 2024
2 parents b3bacbb + db97de9 commit 06caacc
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 2 deletions.
4 changes: 4 additions & 0 deletions css/80_app.css
Original file line number Diff line number Diff line change
Expand Up @@ -2122,18 +2122,21 @@ 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;
}

/* 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;
Expand All @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions data/core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 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."
background:
Expand Down
4 changes: 4 additions & 0 deletions dist/iD.css
Original file line number Diff line number Diff line change
Expand Up @@ -5522,18 +5522,21 @@

/* 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;
}

/* 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;
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions modules/presets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
18 changes: 17 additions & 1 deletion modules/ui/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { uiFieldHelp } from './field_help';
import { uiFields } from './fields';
import { uiTagReference } from './tag_reference';
import { utilRebind, utilUniqueDomId } from '../util';
import { uiSourceSubfield } from './source_subfield';


export function uiField(context, presetField, entityIDs, options) {
Expand All @@ -28,6 +29,11 @@ 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;
if (entityIDs && entityIDs.length) {
_entityExtent = entityIDs.reduce(function(extent, entityID) {
Expand Down Expand Up @@ -124,8 +130,10 @@ export function uiField(context, presetField, entityIDs, options) {
dispatch.call('change', d, t);
}


field.render = function(selection) {

var sourceSubfield = uiSourceSubfield(context, field, _tags, dispatch);

var container = selection.selectAll('.form-field')
.data([field]);

Expand Down Expand Up @@ -169,6 +177,10 @@ 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);
}
}


Expand Down Expand Up @@ -258,6 +270,10 @@ export function uiField(context, presetField, entityIDs, options) {
.attr('xlink:href', '#fas-lock');

container.call(_locked ? _lockedTip : _lockedTip.destroy);

if (options.source){
sourceSubfield.body(selection);
}
};


Expand Down
1 change: 1 addition & 0 deletions modules/ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
171 changes: 171 additions & 0 deletions modules/ui/source_subfield.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
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(context, field, tags, dispatch) {

var sourceSubfield = {};

let sourceInput = d3_select(null);
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] : []);

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', { field_name: field.title }));

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) {
d3_event.preventDefault();

if (typeof _sourceValue !== 'string' && !Array.isArray(_sourceValue)) {
_sourceValue = '';
}
sourceInput.call(renderSourceInput);

}

sourceSubfield.button = function(labelEnter, container) {
labelEnter
.append('button')
.attr('class', 'source-icon')
.attr('title', function() {
return t('inspector.field_source');
})
.call(svgIcon('#fas-at', 'inline'));

container = container
.merge(labelEnter);

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;
}
3 changes: 2 additions & 1 deletion scripts/build_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions svg/fontawesome/fas-at.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions test/spec/ui/fields/source_subfield.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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);
});
});

0 comments on commit 06caacc

Please sign in to comment.