Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add source subfield to other fields #217

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -3675,7 +3675,7 @@

/* Utility Classes
------------------------------------------------------- */
.ideditor .fillL {

Check failure on line 3678 in dist/iD.css

View workflow job for this annotation

GitHub Actions / Check for spelling errors

fillL ==> fill
background: #fff;
color: #333;
}
Expand All @@ -3687,7 +3687,7 @@
background: #ececec;
color: #333;
}
.ideditor .fillD {

Check failure on line 3690 in dist/iD.css

View workflow job for this annotation

GitHub Actions / Check for spelling errors

fillD ==> filled, filed, fill
background: rgba(0,0,0,.5);
color: #fff;
}
Expand Down Expand Up @@ -3764,7 +3764,7 @@
border-right: none;
}

.ideditor .fillL .joined > * {

Check failure on line 3767 in dist/iD.css

View workflow job for this annotation

GitHub Actions / Check for spelling errors

fillL ==> fill
border-right: 1px solid #fff;
}
.ideditor .joined > *:first-child {
Expand Down 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;
1ec5 marked this conversation as resolved.
Show resolved Hide resolved

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) {
1ec5 marked this conversation as resolved.
Show resolved Hide resolved
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);
});
});
Loading