Files
iD/modules/ui/entity_editor.js
Bryan Housel bdb454e1b3 Remove utilCallWhenIdle, use requestIdleCallback/cancelIdleCallback
Places where a reset or connection switch would be problematic,
we can now cancel the callbacks.
2019-05-16 22:28:56 -04:00

364 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { dispatch as d3_dispatch } from 'd3-dispatch';
import {event as d3_event, selectAll as d3_selectAll } from 'd3-selection';
import deepEqual from 'fast-deep-equal';
import { t, textDirection } from '../util/locale';
import { tooltip } from '../util/tooltip';
import { actionChangeTags } from '../actions/change_tags';
import { modeBrowse } from '../modes/browse';
import { svgIcon } from '../svg/icon';
import { uiPresetFavoriteButton } from './preset_favorite_button';
import { uiPresetIcon } from './preset_icon';
import { uiQuickLinks } from './quick_links';
import { uiRawMemberEditor } from './raw_member_editor';
import { uiRawMembershipEditor } from './raw_membership_editor';
import { uiRawTagEditor } from './raw_tag_editor';
import { uiTagReference } from './tag_reference';
import { uiPresetEditor } from './preset_editor';
import { uiEntityIssues } from './entity_issues';
import { uiTooltipHtml } from './tooltipHtml';
import { utilCleanTags, utilRebind } from '../util';
export function uiEntityEditor(context) {
var dispatch = d3_dispatch('choose');
var _state = 'select';
var _coalesceChanges = false;
var _modified = false;
var _scrolled = false;
var _base;
var _entityID;
var _activePreset;
var _tagReference;
var _presetFavorite;
var entityIssues = uiEntityIssues(context);
var quickLinks = uiQuickLinks();
var presetEditor = uiPresetEditor(context).on('change', changeTags);
var rawTagEditor = uiRawTagEditor(context).on('change', changeTags);
var rawMemberEditor = uiRawMemberEditor(context);
var rawMembershipEditor = uiRawMembershipEditor(context);
function entityEditor(selection) {
var entity = context.entity(_entityID);
var tags = Object.assign({}, entity.tags); // shallow copy
// Header
var header = selection.selectAll('.header')
.data([0]);
// Enter
var headerEnter = header.enter()
.append('div')
.attr('class', 'header fillL cf');
headerEnter
.append('button')
.attr('class', 'fl preset-reset preset-choose')
.call(svgIcon((textDirection === 'rtl') ? '#iD-icon-forward' : '#iD-icon-backward'));
headerEnter
.append('button')
.attr('class', 'fr preset-close')
.on('click', function() { context.enter(modeBrowse(context)); })
.call(svgIcon(_modified ? '#iD-icon-apply' : '#iD-icon-close'));
headerEnter
.append('h3')
.text(t('inspector.edit'));
// Update
header = header
.merge(headerEnter);
header.selectAll('.preset-reset')
.on('click', function() {
dispatch.call('choose', this, _activePreset);
});
// Body
var body = selection.selectAll('.inspector-body')
.data([0]);
// Enter
var bodyEnter = body.enter()
.append('div')
.attr('class', 'inspector-body')
.on('scroll.entity-editor', function() { _scrolled = true; });
bodyEnter
.append('div')
.attr('class', 'preset-list-item inspector-inner')
.append('div')
.attr('class', 'preset-list-button-wrap')
.append('button')
.attr('class', 'preset-list-button preset-reset')
.call(tooltip().title(t('inspector.back_tooltip')).placement('bottom'))
.append('div')
.attr('class', 'label')
.append('div')
.attr('class', 'label-inner');
bodyEnter
.append('div')
.attr('class', 'preset-quick-links');
bodyEnter
.append('div')
.attr('class', 'entity-issues');
bodyEnter
.append('div')
.attr('class', 'preset-editor');
bodyEnter
.append('div')
.attr('class', 'raw-tag-editor inspector-inner');
bodyEnter
.append('div')
.attr('class', 'raw-member-editor inspector-inner');
bodyEnter
.append('div')
.attr('class', 'raw-membership-editor inspector-inner');
bodyEnter
.append('input')
.attr('type', 'text')
.attr('class', 'key-trap');
// Update
body = body
.merge(bodyEnter);
if (_presetFavorite) {
body.selectAll('.preset-list-button-wrap')
.call(_presetFavorite.button);
}
// update header
if (_tagReference) {
body.selectAll('.preset-list-button-wrap')
.call(_tagReference.button);
body.selectAll('.preset-list-item')
.call(_tagReference.body);
}
body.selectAll('.preset-reset')
.on('click', function() {
dispatch.call('choose', this, _activePreset);
});
body.select('.preset-list-item button')
.call(uiPresetIcon(context)
.geometry(context.geometry(_entityID))
.preset(_activePreset)
);
// NOTE: split on en-dash, not a hypen (to avoid conflict with hyphenated names)
var label = body.select('.label-inner');
var nameparts = label.selectAll('.namepart')
.data(_activePreset.name().split(' '), function(d) { return d; });
nameparts.exit()
.remove();
nameparts
.enter()
.append('div')
.attr('class', 'namepart')
.text(function(d) { return d; });
// update quick links
var choices = [{
id: 'zoom_to',
label: 'inspector.zoom_to.title',
tooltip: function() {
return uiTooltipHtml(t('inspector.zoom_to.tooltip_feature'), t('inspector.zoom_to.key'));
},
click: function zoomTo() {
context.mode().zoomToSelected();
}
}];
body.select('.preset-quick-links')
.call(quickLinks.choices(choices));
// update editor sections
body.select('.entity-issues')
.call(entityIssues
.entityID(_entityID)
);
body.select('.preset-editor')
.call(presetEditor
.preset(_activePreset)
.entityID(_entityID)
.tags(tags)
.state(_state)
);
body.select('.raw-tag-editor')
.call(rawTagEditor
.preset(_activePreset)
.entityID(_entityID)
.tags(tags)
.state(_state)
);
if (entity.type === 'relation') {
body.select('.raw-member-editor')
.style('display', 'block')
.call(rawMemberEditor
.entityID(_entityID)
);
} else {
body.select('.raw-member-editor')
.style('display', 'none');
}
body.select('.raw-membership-editor')
.call(rawMembershipEditor
.entityID(_entityID)
);
body.select('.key-trap')
.on('keydown.key-trap', function() {
// On tabbing, send focus back to the first field on the inspector-body
// (probably the `name` field) #4159
if (d3_event.keyCode === 9 && !d3_event.shiftKey) {
d3_event.preventDefault();
body.select('input').node().focus();
}
});
context.history()
.on('change.entity-editor', historyChanged);
function historyChanged(difference) {
if (_state === 'hide') return;
var significant = !difference ||
difference.didChange.properties ||
difference.didChange.addition ||
difference.didChange.deletion;
if (!significant) return;
var entity = context.hasEntity(_entityID);
var graph = context.graph();
if (!entity) return;
var match = context.presets().match(entity, graph);
var activePreset = entityEditor.preset();
var weakPreset = activePreset &&
Object.keys(activePreset.addTags || {}).length === 0;
// A "weak" preset doesn't set any tags. (e.g. "Address")
// Don't replace a weak preset with a fallback preset (e.g. "Point")
if (!(weakPreset && match.isFallback())) {
entityEditor.preset(match);
}
entityEditor.modified(_base !== graph);
entityEditor(selection);
}
}
// Tag changes that fire on input can all get coalesced into a single
// history operation when the user leaves the field. #2342
function changeTags(changed, onInput) {
var entity = context.entity(_entityID);
var annotation = t('operations.change_tags.annotation');
var tags = Object.assign({}, entity.tags); // shallow copy
for (var k in changed) {
if (!k) continue;
var v = changed[k];
if (v !== undefined || tags.hasOwnProperty(k)) {
tags[k] = v;
}
}
if (!onInput) {
tags = utilCleanTags(tags);
}
if (!deepEqual(entity.tags, tags)) {
if (_coalesceChanges) {
context.overwrite(actionChangeTags(_entityID, tags), annotation);
} else {
context.perform(actionChangeTags(_entityID, tags), annotation);
_coalesceChanges = !!onInput;
}
}
// if leaving field (blur event), rerun validation
if (!onInput) {
context.validator().validate();
}
}
entityEditor.modified = function(val) {
if (!arguments.length) return _modified;
_modified = val;
d3_selectAll('button.preset-close use')
.attr('xlink:href', (_modified ? '#iD-icon-apply' : '#iD-icon-close'));
return entityEditor;
};
entityEditor.state = function(val) {
if (!arguments.length) return _state;
_state = val;
return entityEditor;
};
entityEditor.entityID = function(val) {
if (!arguments.length) return _entityID;
if (_entityID === val) return entityEditor; // exit early if no change
_entityID = val;
_base = context.graph();
_coalesceChanges = false;
// reset the scroll to the top of the inspector (warning: triggers reflow)
if (_scrolled) {
window.requestIdleCallback(function() {
var body = d3_selectAll('.entity-editor-pane .inspector-body');
if (!body.empty()) {
_scrolled = false;
body.node().scrollTop = 0;
}
});
}
var presetMatch = context.presets().match(context.entity(_entityID), _base);
return entityEditor
.preset(presetMatch)
.modified(false);
};
entityEditor.preset = function(val) {
if (!arguments.length) return _activePreset;
if (val !== _activePreset) {
_activePreset = val;
_tagReference = uiTagReference(_activePreset.reference(context.geometry(_entityID)), context)
.showing(false);
}
_presetFavorite = uiPresetFavoriteButton(_activePreset, context.geometry(_entityID), context);
return entityEditor;
};
return utilRebind(entityEditor, dispatch, 'on');
}