modules/ui/field.js (266 lines of code) (raw):

import { dispatch as d3_dispatch } from 'd3-dispatch'; import { select as d3_select } from 'd3-selection'; import { utilUniqueString } from '@id-sdk/util'; import { t, localizer } from '../core/localizer'; import { locationManager } from '../core/locations'; import { svgIcon } from '../svg/icon'; import { uiTooltip } from './tooltip'; import { uiFieldHelp } from './field_help'; import { uiFields } from './fields'; import { uiTagReference } from './tag_reference'; import { utilRebind, utilTotalExtent } from '../util'; export function uiField(context, presetField, entityIDs, options) { options = Object.assign({ show: true, wrap: true, remove: true, revert: true, info: true }, options); // Don't show the remove and revert buttons if any of the entity IDs are FB features // with source=digitalglobe or source=maxar var someFbRoadsSelected = entityIDs ? entityIDs.some(function(entity) { return entity.__fbid__ && (entity.tags.source === 'maxar' || entity.tags.source === 'digitalglobe'); }) : false; if ( someFbRoadsSelected ) { options.remove = false; options.revert = false; } var dispatch = d3_dispatch('change', 'revert'); var field = Object.assign({}, presetField); // shallow copy field.domId = utilUniqueString('form-field-' + field.safeid); var _show = options.show; var _state = ''; var _tags = {}; var _entityExtent = null; if (entityIDs && entityIDs.length) { _entityExtent = utilTotalExtent(entityIDs, context.graph()); } var _locked = false; var _lockedTip = uiTooltip() .title(t.html('inspector.lock.suggestion', { label: field.label })) .placement('bottom'); field.keys = field.keys || [field.key]; // only create the fields that are actually being shown if (_show && !field.impl) { createField(); } // Creates the field.. This is done lazily, // once we know that the field will be shown. function createField() { field.impl = uiFields[field.type](field, context) .on('change', function(t, onInput) { dispatch.call('change', field, t, onInput); }); if (entityIDs) { field.entityIDs = entityIDs; // if this field cares about the entities, pass them along if (field.impl.entityIDs) { field.impl.entityIDs(entityIDs); } } } function isModified() { if (!entityIDs || !entityIDs.length) return false; return entityIDs.some(function(entityID) { var original = context.graph().base().entities[entityID]; var latest = context.graph().entity(entityID); return field.keys.some(function(key) { return original ? latest.tags[key] !== original.tags[key] : latest.tags[key]; }); }); } function tagsContainFieldKey() { return field.keys.some(function(key) { if (field.type === 'multiCombo') { for (var tagKey in _tags) { if (tagKey.indexOf(key) === 0) { return true; } } return false; } return _tags[key] !== undefined; }); } function revert(d3_event, d) { d3_event.stopPropagation(); d3_event.preventDefault(); if (!entityIDs || _locked) return; dispatch.call('revert', d, d.keys); } function remove(d3_event, d) { d3_event.stopPropagation(); d3_event.preventDefault(); if (_locked) return; var t = {}; d.keys.forEach(function(key) { t[key] = undefined; }); dispatch.call('change', d, t); } field.render = function(selection) { var container = selection.selectAll('.form-field') .data([field]); // Enter var enter = container.enter() .append('div') .attr('class', function(d) { return 'form-field form-field-' + d.safeid; }) .classed('nowrap', !options.wrap); if (options.wrap) { var labelEnter = enter .append('label') .attr('class', 'field-label') .attr('for', function(d) { return d.domId; }); var textEnter = labelEnter .append('span') .attr('class', 'label-text'); textEnter .append('span') .attr('class', 'label-textvalue') .html(function(d) { return d.label(); }); textEnter .append('span') .attr('class', 'label-textannotation'); if (options.remove) { labelEnter .append('button') .attr('class', 'remove-icon') .attr('title', t('icons.remove')) .call(svgIcon('#iD-operation-delete')); } if (options.revert) { labelEnter .append('button') .attr('class', 'modified-icon') .attr('title', t('icons.undo')) .call(svgIcon((localizer.textDirection() === 'rtl') ? '#iD-icon-redo' : '#iD-icon-undo')); } } // Update container = container .merge(enter); container.select('.field-label > .remove-icon') // propagate bound data .on('click', remove); container.select('.field-label > .modified-icon') // propagate bound data .on('click', revert); container .each(function(d) { var selection = d3_select(this); if (!d.impl) { createField(); } var reference, help; // instantiate field help if (options.wrap && field.type === 'restrictions') { help = uiFieldHelp(context, 'restrictions'); } // instantiate tag reference if (options.wrap && options.info) { var referenceKey = d.key || ''; if (d.type === 'multiCombo') { // lookup key without the trailing ':' referenceKey = referenceKey.replace(/:$/, ''); } reference = uiTagReference(d.reference || { key: referenceKey }, context); if (_state === 'hover') { reference.showing(false); } } selection .call(d.impl); // add field help components if (help) { selection .call(help.body) .select('.field-label') .call(help.button); } // add tag reference components if (reference) { selection .call(reference.body) .select('.field-label') .call(reference.button); } d.impl.tags(_tags); }); container .classed('locked', _locked) .classed('modified', isModified()) .classed('present', tagsContainFieldKey()); // show a tip and lock icon if the field is locked var annotation = container.selectAll('.field-label .label-textannotation'); var icon = annotation.selectAll('.icon') .data(_locked ? [0]: []); icon.exit() .remove(); icon.enter() .append('svg') .attr('class', 'icon') .append('use') .attr('xlink:href', '#fas-lock'); container.call(_locked ? _lockedTip : _lockedTip.destroy); }; field.state = function(val) { if (!arguments.length) return _state; _state = val; return field; }; field.tags = function(val) { if (!arguments.length) return _tags; _tags = val; if (tagsContainFieldKey() && !_show) { // always show a field if it has a value to display _show = true; if (!field.impl) { createField(); } } return field; }; field.locked = function(val) { if (!arguments.length) return _locked; _locked = val; return field; }; field.show = function() { _show = true; if (!field.impl) { createField(); } if (field.default && field.key && _tags[field.key] !== field.default) { var t = {}; t[field.key] = field.default; dispatch.call('change', this, t); } }; // A shown field has a visible UI, a non-shown field is in the 'Add field' dropdown field.isShown = function() { return _show; }; // An allowed field can appear in the UI or in the 'Add field' dropdown. // A non-allowed field is hidden from the user altogether field.isAllowed = function() { if (entityIDs && entityIDs.length > 1 && uiFields[field.type].supportsMultiselection === false) return false; if (field.geometry && !entityIDs.every(function(entityID) { return field.matchGeometry(context.graph().geometry(entityID)); })) return false; if (entityIDs && _entityExtent && field.locationSetID) { // is field allowed in this location? var validLocations = locationManager.locationsAt(_entityExtent.center()); if (!validLocations[field.locationSetID]) return false; } var prerequisiteTag = field.prerequisiteTag; if (entityIDs && !tagsContainFieldKey() && // ignore tagging prerequisites if a value is already present prerequisiteTag) { if (!entityIDs.every(function(entityID) { var entity = context.graph().entity(entityID); if (prerequisiteTag.key) { var value = entity.tags[prerequisiteTag.key]; if (!value) return false; if (prerequisiteTag.valueNot) { return prerequisiteTag.valueNot !== value; } if (prerequisiteTag.value) { return prerequisiteTag.value === value; } } else if (prerequisiteTag.keyNot) { if (entity.tags[prerequisiteTag.keyNot]) return false; } return true; })) return false; } return true; }; field.focus = function() { if (field.impl) { field.impl.focus(); } }; return utilRebind(field, dispatch, 'on'); }