diff --git a/css/80_app.css b/css/80_app.css index cfffb362fa..1291ba4232 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -1591,12 +1591,20 @@ input.date-selector { display: flex; flex-flow: row nowrap; } -.form-field ul.rows li.labeled-input > div { +.form-field ul.rows li.labeled-input > div, +.form-field ul.rows li.labeled-input-source > div { flex: 1 1 auto; width: 100%; border-radius: 0; line-height: 0.95rem; } +.form-field ul.rows li.labeled-input div > span, +.form-field ul.rows li.labeled-input-source div > span{ + vertical-align: middle; +} +.form-field ul.rows li.labeled-input-source > div { + width: auto; +} .form-field ul.rows li input { border-radius: 0; border-width: 0; @@ -1620,10 +1628,12 @@ input.date-selector { display: table; width: 100%; } -.form-field ul.rows.rows-table li.labeled-input { +.form-field ul.rows.rows-table li.labeled-input, +.form-field ul.rows.rows-table li.labeled-input-source { display: table-row; } -.form-field ul.rows.rows-table li.labeled-input > div:first-child { +.form-field ul.rows.rows-table li.labeled-input > div:first-child, +.form-field ul.rows.rows-table li.labeled-input-source > div:first-child { display: table-cell; width: auto; max-width: 170px; @@ -1631,7 +1641,8 @@ input.date-selector { text-overflow: ellipsis; overflow: hidden; } -.form-field ul.rows.rows-table li.labeled-input > div:nth-child(2) { +.form-field ul.rows.rows-table li.labeled-input > div:nth-child(2), +.form-field ul.rows.rows-table li.labeled-input-source > div:nth-child(2) { display: table-cell; width: auto; } @@ -2051,6 +2062,9 @@ input.date-selector { /* Field - date, roadheight, and roadspeed ------------------------------------------------------- */ +.form-field-input-source { + flex-direction: column; +} .form-field-input-date input.date-year, .form-field-input-date input.date-month, .form-field-input-roadheight input.roadheight-number, @@ -2960,6 +2974,10 @@ img.tag-reference-wiki-image { .add-row .space-value { flex: 1 1 50%; } +.add-row .add-subkey { + width: 32px; + flex: 0 1 auto; +} .add-row .space-buttons { flex: 0 0 62px; } diff --git a/data/core.yaml b/data/core.yaml index b17289a04d..ce0ab7d32d 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -801,6 +801,11 @@ en: field_source: add source field_source_label: Source for {field_name} field_source_placeholder: URL, newspaper article, book... + source: + main_input: Description + name: Name + url: https://example.com + date: Date the source was created 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: @@ -2535,6 +2540,8 @@ en: label: License placeholder: CC0 terms: copyleft, copyright + source: + label: Source presets: type/chronology: name: Chronology diff --git a/modules/presets/index.js b/modules/presets/index.js index 06501ad6a0..6462a591b2 100644 --- a/modules/presets/index.js +++ b/modules/presets/index.js @@ -57,8 +57,9 @@ 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.type = 'source'; fields.source.source = false; + fields.source.keys = ['source', 'source:url', 'source:name', 'source:date']; } fields.license = { diff --git a/modules/ui/fields/index.js b/modules/ui/fields/index.js index cf2c97c356..6545a84813 100644 --- a/modules/ui/fields/index.js +++ b/modules/ui/fields/index.js @@ -54,6 +54,7 @@ import { uiFieldLocalized } from './localized'; import { uiFieldRoadheight } from './roadheight'; import { uiFieldRoadspeed } from './roadspeed'; import { uiFieldRestrictions } from './restrictions'; +import { uiFieldSources } from './sources'; import { uiFieldTextarea } from './textarea'; import { uiFieldWikidata } from './wikidata'; import { uiFieldWikipedia } from './wikipedia'; @@ -82,6 +83,7 @@ export var uiFields = { radio: uiFieldRadio, restrictions: uiFieldRestrictions, semiCombo: uiFieldSemiCombo, + source: uiFieldSources, structureRadio: uiFieldStructureRadio, tel: uiFieldTel, text: uiFieldText, diff --git a/modules/ui/fields/sources.js b/modules/ui/fields/sources.js new file mode 100644 index 0000000000..f7e4a77074 --- /dev/null +++ b/modules/ui/fields/sources.js @@ -0,0 +1,134 @@ +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'; + +export function uiFieldSources(field, context) { + let dispatch = d3_dispatch('change'); + let items = d3_select(null); + let _tags = {}; + let _selection = d3_select(null); + let _pendingChange; + + const mainKey = 'source'; + const sourceHeader = mainKey + ':'; + + // Pre-selected subkeys to show + const possibleSourceSubkeys = [{key:'name'}, {key:'url'}, {key:'date'}]; + + function scheduleChange() { + if (!_pendingChange) return; + dispatch.call('change', this, _pendingChange); + _pendingChange = null; + _selection.call(sources); + } + + function valueChange(d3_event, d) { + // exit if this is a multiselection and no value was entered + if (typeof d.key !== 'string' && !this.value) return; + + var key = sourceHeader + d.key; + + _pendingChange = _pendingChange || {}; + + var value = context.cleanTagValue(this.value); + + _pendingChange[key] = value === '' ? undefined : value; + _tags[key] = value === '' ? undefined : value; + scheduleChange(); + } + + function mainChange() { + _pendingChange = _pendingChange || {}; + var value = context.cleanTagValue(this.value); + _pendingChange[mainKey] = value === '' ? undefined : value; + _tags[mainKey] = value === '' ? undefined : value; + scheduleChange(); + } + + function sources(selection) { + _selection = selection; + + var wrap = selection.selectAll('.form-field-input-wrap') + .data([0]); + + selection.exit() + .style('top', '0') + .style('max-height', '240px') + .transition() + .duration(200) + .style('opacity', '0') + .style('max-height', '0px') + .remove(); + + wrap = wrap.enter() + .append('div') + .attr('class', 'form-field-input-wrap form-field-input-' + field.type) + .merge(wrap); + + // source key + wrap.selectAll('input') + .data([0]) + .enter() + .append('input') + .attr('class', 'main-value') + .attr('type', 'text') + .attr('placeholder', t('inspector.source.main_input')) + .call(utilNoAuto) + .on('change', mainChange) + .on('blur', mainChange); + + var list = wrap.selectAll('ul') + .data([0]); + + list = list.enter() + .append('ul') + .attr('class', 'rows') + .merge(list); + + list = list.merge(list); + + // source:*= keys + items = list.selectAll('li.labeled-input-source') + .data(possibleSourceSubkeys); + + items = items.enter() + .append('li') + .attr('class', 'labeled-input-source'); + + items + .append('input') + .attr('type', 'text') + .attr('class', 'value') + .attr('placeholder', function(d) { + return t('inspector.source.' + d.key); + }) + .call(utilNoAuto) + .call(utilGetSetValue, function(d) { + return _tags[sourceHeader + d.key]; + }) + .on('change', valueChange) + .on('blur', valueChange); + + items.exit() + .remove(); + + utilGetSetValue(_selection.selectAll('.value'), function(d) { + return (_tags[sourceHeader + d.key] === undefined) ? '' : _tags[sourceHeader + d.key]; + }); + + utilGetSetValue(_selection.selectAll('.main-value'), function() { + return (_tags[mainKey] === undefined) ? '' : _tags[mainKey]; + }); + } + + sources.tags = function(tags){ + if (!arguments.length) return _tags; + _tags = tags; + + _selection.call(sources); + }; + + return utilRebind(sources, dispatch, 'on'); +} diff --git a/modules/ui/source_subfield.js b/modules/ui/source_subfield.js index d79dbbcf6b..0d18bbde8e 100644 --- a/modules/ui/source_subfield.js +++ b/modules/ui/source_subfield.js @@ -4,7 +4,6 @@ import { 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) {