Merge pull request #6302 from openstreetmap/text-raw-tag-editor

Text raw tag editor / Copy-paste tags
This commit is contained in:
Bryan Housel
2019-05-03 14:53:08 -04:00
committed by GitHub
11 changed files with 307 additions and 85 deletions
+12 -5
View File
@@ -17,7 +17,7 @@ import { uiTagReference } from './tag_reference';
import { uiPresetEditor } from './preset_editor';
import { uiEntityIssues } from './entity_issues';
import { uiTooltipHtml } from './tooltipHtml';
import { utilCleanTags, utilRebind } from '../util';
import { utilCallWhenIdle, utilCleanTags, utilRebind } from '../util';
export function uiEntityEditor(context) {
@@ -25,6 +25,7 @@ export function uiEntityEditor(context) {
var _state = 'select';
var _coalesceChanges = false;
var _modified = false;
var _scrolled = false;
var _base;
var _entityID;
var _activePreset;
@@ -83,7 +84,8 @@ export function uiEntityEditor(context) {
// Enter
var bodyEnter = body.enter()
.append('div')
.attr('class', 'inspector-body');
.attr('class', 'inspector-body')
.on('scroll.entity-editor', function() { _scrolled = true; });
bodyEnter
.append('div')
@@ -327,9 +329,14 @@ export function uiEntityEditor(context) {
_coalesceChanges = false;
// reset the scroll to the top of the inspector (warning: triggers reflow)
var body = d3_selectAll('.entity-editor-pane .inspector-body');
if (!body.empty()) {
body.node().scrollTop = 0;
if (_scrolled) {
utilCallWhenIdle(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);
+147 -23
View File
@@ -7,12 +7,18 @@ import { svgIcon } from '../svg/icon';
import { uiCombobox } from './combobox';
import { uiDisclosure } from './disclosure';
import { uiTagReference } from './tag_reference';
import { utilArrayDifference, utilGetSetValue, utilNoAuto, utilRebind } from '../util';
import { utilArrayDifference, utilGetSetValue, utilNoAuto, utilRebind, utilTagDiff } from '../util';
export function uiRawTagEditor(context) {
var taginfo = services.taginfo;
var dispatch = d3_dispatch('change');
var availableViews = [
{ id: 'text', icon: '#fas-i-cursor' },
{ id: 'list', icon: '#fas-th-list' }
];
var _tagView = (context.storage('raw-tag-editor-view') || 'list'); // 'list, 'text'
var _readOnlyTags = [];
var _indexedKeys = [];
var _showBlank = false;
@@ -75,13 +81,80 @@ export function uiRawTagEditor(context) {
rowData.push({ index: _indexedKeys.length, key: '', value: '' });
}
// List of tags
// View Options
var options = wrap.selectAll('.raw-tag-options')
.data([0]);
var optionsEnter = options.enter()
.append('div')
.attr('class', 'raw-tag-options');
var optionEnter = optionsEnter.selectAll('.raw-tag-option')
.data(availableViews, function(d) { return d.id; })
.enter();
optionEnter
.append('button')
.attr('class', function(d) {
return 'raw-tag-option raw-tag-option-' + d.id + (_tagView === d.id ? ' selected' : '');
})
.attr('title', function(d) { return d.id; })
.on('click', function(d) {
_tagView = d.id;
context.storage('raw-tag-editor-view', d.id);
wrap.selectAll('.raw-tag-option')
.classed('selected', function(datum) { return datum === d; });
wrap.selectAll('.tag-text')
.classed('hide', (d.id !== 'text'))
.each(setTextareaHeight);
wrap.selectAll('.tag-list, .add-row')
.classed('hide', (d.id !== 'list'));
})
.each(function(d) {
d3_select(this)
.call(svgIcon(d.icon));
});
// View as Text
var textData = rowsToText(rowData);
var textarea = wrap.selectAll('.tag-text')
.data([0]);
textarea = textarea.enter()
.append('textarea')
.attr('class', 'tag-text' + (_tagView !== 'text' ? ' hide' : ''))
.call(utilNoAuto)
.attr('spellcheck', 'false')
.merge(textarea);
textarea
.call(utilGetSetValue, textData)
.each(setTextareaHeight)
.on('input', setTextareaHeight)
.on('blur', textChanged)
.on('change', textChanged);
// If All Fields section is hidden, focus textarea and put cursor at end..
var fieldsExpanded = d3_select('.hide-toggle-preset_fields.expanded').size();
if (_state !== 'hover' && _tagView === 'text' && !fieldsExpanded) {
var element = textarea.node();
element.focus();
element.setSelectionRange(textData.length, textData.length);
}
// View as List
var list = wrap.selectAll('.tag-list')
.data([0]);
list = list.enter()
.append('ul')
.attr('class', 'tag-list')
.attr('class', 'tag-list' + (_tagView !== 'list' ? ' hide' : ''))
.merge(list);
@@ -90,7 +163,7 @@ export function uiRawTagEditor(context) {
.data([0])
.enter()
.append('div')
.attr('class', 'add-row');
.attr('class', 'add-row' + (_tagView !== 'list' ? ' hide' : ''));
addRowEnter
.append('button')
@@ -217,6 +290,75 @@ export function uiRawTagEditor(context) {
}
function setTextareaHeight() {
if (_tagView !== 'text') return;
var selection = d3_select(this);
selection.style('height', null);
selection.style('height', selection.node().scrollHeight + 5 + 'px');
}
function stringify(s) {
return JSON.stringify(s).slice(1, -1); // without leading/trailing "
}
function unstringify(s) {
var leading = '';
var trailing = '';
if (s.length < 1 || s.charAt(0) !== '"') {
leading = '"';
}
if (s.length < 2 || s.charAt(s.length - 1) !== '"' ||
(s.charAt(s.length - 1) === '"' && s.charAt(s.length - 2) === '\\')
) {
trailing = '"';
}
return JSON.parse(leading + s + trailing);
}
function rowsToText(rows) {
var str = rows
.filter(function(row) { return row.key && row.key.trim() !== ''; })
.map(function(row) { return stringify(row.key) + '=' + stringify(row.value); })
.join('\n');
return _state === 'hover' ? str : str + '\n';
}
function textChanged() {
var newText = this.value.trim();
var newTags = {};
newText.split('\n').forEach(function(row) {
var m = row.match(/^\s*([^=]+)=(.*)$/);
if (m !== null) {
var k = unstringify(m[1].trim());
var v = unstringify(m[2].trim());
newTags[k] = v;
}
});
var tagDiff = utilTagDiff(_tags, newTags);
if (!tagDiff.length) return;
_pendingChange = _pendingChange || {};
tagDiff.forEach(function(change) {
if (isReadOnly({ key: change.key })) return;
if (change.type === '-') {
_pendingChange[change.key] = undefined;
} else if (change.type === '+') {
_pendingChange[change.key] = change.newVal || '';
}
});
scheduleChange();
}
function pushMore() {
if (d3_event.keyCode === 9 && !d3_event.shiftKey &&
list.selectAll('li:last-child input.value').node() === this) {
@@ -317,25 +459,7 @@ export function uiRawTagEditor(context) {
_pendingChange[kOld] = undefined;
}
// if the key looks like "key=value key2=value2", split them up - #5024
var keys = (kNew.match(/[\w_]+=/g) || []).map(function (key) { return key.slice(0, -1); });
var vals = keys.length === 0
? []
: kNew
.split(new RegExp(keys.map(function (key) { return key.replace('_', '\\_'); }).join('|')))
.splice(1)
.map(function (val) { return val.slice(1).trim(); });
if (keys.length > 0) {
kNew = keys[0];
vNew = vals[0];
keys.forEach(function (key, i) {
_pendingChange[key] = vals[i];
});
} else {
_pendingChange[kNew] = vNew;
}
_pendingChange[kNew] = vNew;
d.key = kNew; // update datum to avoid exit/enter on tag update
d.value = vNew;
+1
View File
@@ -38,6 +38,7 @@ export { utilRebind } from './rebind';
export { utilSetTransform } from './util';
export { utilSessionMutex } from './session_mutex';
export { utilStringQs } from './util';
export { utilTagDiff } from './util';
export { utilTagText } from './util';
export { utilTiler } from './tiler';
export { utilTriggerEvent } from './trigger_event';
+34 -2
View File
@@ -1,8 +1,10 @@
import { t, textDirection } from './locale';
import { utilDetect } from './detect';
import { remove as removeDiacritics } from 'diacritics';
import { fixRTLTextForSvg, rtlRegex } from './svg_paths_rtl_fix';
import { t, textDirection } from './locale';
import { utilArrayUnion } from './array';
import { utilDetect } from './detect';
export function utilTagText(entity) {
var obj = (entity && entity.tags) || {};
@@ -12,6 +14,36 @@ export function utilTagText(entity) {
}
export function utilTagDiff(oldTags, newTags) {
var tagDiff = [];
var keys = utilArrayUnion(Object.keys(oldTags), Object.keys(newTags)).sort();
keys.forEach(function(k) {
var oldVal = oldTags[k];
var newVal = newTags[k];
if (oldVal && (!newVal || newVal !== oldVal)) {
tagDiff.push({
type: '-',
key: k,
oldVal: oldVal,
newVal: newVal,
display: '- ' + k + '=' + oldVal
});
}
if (newVal && (!oldVal || newVal !== oldVal)) {
tagDiff.push({
type: '+',
key: k,
oldVal: oldVal,
newVal: newVal,
display: '+ ' + k + '=' + newVal
});
}
});
return tagDiff;
}
export function utilEntitySelector(ids) {
return ids.length ? '.' + ids.join(',.') : 'nothing';
}
+4 -17
View File
@@ -3,7 +3,7 @@ import { actionChangePreset } from '../actions/change_preset';
import { actionChangeTags } from '../actions/change_tags';
import { actionUpgradeTags } from '../actions/upgrade_tags';
import { osmIsOldMultipolygonOuterMember, osmOldMultipolygonOuterMemberOfRelation } from '../osm/multipolygon';
import { utilArrayUnion, utilDisplayLabel } from '../util';
import { utilDisplayLabel, utilTagDiff } from '../util';
import { validationIssue, validationIssueFix } from '../core/validation';
@@ -48,20 +48,7 @@ export function validationOutdatedTags() {
}
// determine diff
var keys = utilArrayUnion(Object.keys(oldTags), Object.keys(newTags)).sort();
var tagDiff = [];
keys.forEach(function(k) {
var oldVal = oldTags[k];
var newVal = newTags[k];
if (oldVal && (!newVal || newVal !== oldVal)) {
tagDiff.push('- ' + k + '=' + oldVal);
}
if (newVal && (!oldVal || newVal !== oldVal)) {
tagDiff.push('+ ' + k + '=' + newVal);
}
});
var tagDiff = utilTagDiff(oldTags, newTags);
if (!tagDiff.length) return [];
return [new validationIssue({
@@ -113,10 +100,10 @@ export function validationOutdatedTags() {
.attr('class', 'tagDiff-row')
.append('td')
.attr('class', function(d) {
var klass = d.charAt(0) === '+' ? 'add' : 'remove';
var klass = d.type === '+' ? 'add' : 'remove';
return 'tagDiff-cell tagDiff-cell-' + klass;
})
.text(function(d) { return d; });
.text(function(d) { return d.display; });
}
}
+6 -9
View File
@@ -1,6 +1,6 @@
import { actionChangeTags } from '../actions/change_tags';
import { t } from '../util/locale';
import { utilDisplayLabel } from '../util';
import { utilDisplayLabel, utilTagDiff } from '../util';
import { validationIssue, validationIssueFix } from '../core/validation';
@@ -40,20 +40,17 @@ export function validationPrivateData() {
var validation = function checkPrivateData(entity, context) {
var tags = entity.tags;
var keepTags = {};
var tagDiff = [];
if (!tags.building || !privateBuildingValues[tags.building]) return [];
var keepTags = {};
for (var k in tags) {
if (publicKeys[k]) return []; // probably a public feature
if (personalTags[k]) {
tagDiff.push('- ' + k + '=' + tags[k]);
} else {
if (!personalTags[k]) {
keepTags[k] = tags[k];
}
}
var tagDiff = utilTagDiff(tags, keepTags);
if (!tagDiff.length) return [];
var fixID = tagDiff.length === 1 ? 'remove_tag' : 'remove_tags';
@@ -110,10 +107,10 @@ export function validationPrivateData() {
.attr('class', 'tagDiff-row')
.append('td')
.attr('class', function(d) {
var klass = d.charAt(0) === '+' ? 'add' : 'remove';
var klass = d.type === '+' ? 'add' : 'remove';
return 'tagDiff-cell tagDiff-cell-' + klass;
})
.text(function(d) { return d; });
.text(function(d) { return d.display; });
}
};