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

View File

@@ -55,9 +55,12 @@ module.exports = function buildData() {
// Font Awesome icons used
var faIcons = {
'fas-long-arrow-alt-right': {}
'fas-i-cursor': {},
'fas-long-arrow-alt-right': {},
'fas-th-list': {}
};
// The Noun Project icons used
var tnpIcons = {};
// Start clean

View File

@@ -259,10 +259,12 @@ table.tags, table.tags td, table.tags th {
.ar { right: 0; }
input.hide,
textarea.hide,
div.hide,
form.hide,
button.hide,
a.hide,
ul.hide,
li.hide {
display: none;
}
@@ -2401,8 +2403,56 @@ div.combobox {
/* Raw Tag Editor
------------------------------------------------------- */
.raw-tag-options {
display: flex;
flex-flow: row nowrap;
flex-direction: row-reverse;
margin-top: -25px;
padding: 0 3px;
}
button.raw-tag-option {
flex: 0 0 20px;
height: 20px;
width: 20px;
background: #aaa;
color: #eee;
margin: 0 3px;
}
button.raw-tag-option:focus,
button.raw-tag-option:hover,
button.raw-tag-option.active {
color: #fff;
background: #597be7;
}
button.raw-tag-option.selected {
color: #fff;
background: #7092ff;
}
button.raw-tag-option svg.icon {
width: 14px;
height: 14px;
vertical-align: text-bottom;
}
[dir='ltr'] button.raw-tag-option-list {
-moz-transform: scaleX(-1);
-o-transform: scaleX(-1);
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
filter: FlipH;
-ms-filter: "FlipH";
}
.tag-text {
width: 100%;
height: 100%;
font-family: monospace;
white-space: pre;
}
.tag-text,
.tag-list {
padding-top: 10px;
margin-top: 10px;
}
.tag-row {
width: 100%;

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);

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;

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';

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';
}

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; });
}
}

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; });
}
};

View File

@@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="i-cursor" class="svg-inline--fa fa-i-cursor fa-w-8" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><path fill="currentColor" d="M256 52.048V12.065C256 5.496 250.726.148 244.158.066 211.621-.344 166.469.011 128 37.959 90.266.736 46.979-.114 11.913.114 5.318.157 0 5.519 0 12.114v39.645c0 6.687 5.458 12.078 12.145 11.998C38.111 63.447 96 67.243 96 112.182V224H60c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h36v112c0 44.932-56.075 48.031-83.95 47.959C5.404 447.942 0 453.306 0 459.952v39.983c0 6.569 5.274 11.917 11.842 11.999 32.537.409 77.689.054 116.158-37.894 37.734 37.223 81.021 38.073 116.087 37.845 6.595-.043 11.913-5.405 11.913-12V460.24c0-6.687-5.458-12.078-12.145-11.998C217.889 448.553 160 444.939 160 400V288h36c6.627 0 12-5.373 12-12v-40c0-6.627-5.373-12-12-12h-36V112.182c0-44.932 56.075-48.213 83.95-48.142 6.646.018 12.05-5.346 12.05-11.992z"></path></svg>

After

Width:  |  Height:  |  Size: 970 B

View File

@@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="th-list" class="svg-inline--fa fa-th-list fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M149.333 216v80c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24v-80c0-13.255 10.745-24 24-24h101.333c13.255 0 24 10.745 24 24zM0 376v80c0 13.255 10.745 24 24 24h101.333c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H24c-13.255 0-24 10.745-24 24zM125.333 32H24C10.745 32 0 42.745 0 56v80c0 13.255 10.745 24 24 24h101.333c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24zm80 448H488c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H205.333c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24zm-24-424v80c0 13.255 10.745 24 24 24H488c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24H205.333c-13.255 0-24 10.745-24 24zm24 264H488c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H205.333c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24z"></path></svg>

After

Width:  |  Height:  |  Size: 1004 B

View File

@@ -2,57 +2,76 @@ describe('iD.util', function() {
describe('utilGetAllNodes', function() {
it('gets all descendant nodes of a way', function() {
var a = iD.osmNode({ id: 'a' }),
b = iD.osmNode({ id: 'b' }),
w = iD.osmWay({ id: 'w', nodes: ['a','b','a'] }),
graph = iD.coreGraph([a, b, w]),
result = iD.utilGetAllNodes(['w'], graph);
var a = iD.osmNode({ id: 'a' });
var b = iD.osmNode({ id: 'b' });
var w = iD.osmWay({ id: 'w', nodes: ['a','b','a'] });
var graph = iD.coreGraph([a, b, w]);
var result = iD.utilGetAllNodes(['w'], graph);
expect(result).to.have.members([a, b]);
expect(result).to.have.lengthOf(2);
});
it('gets all descendant nodes of a relation', function() {
var a = iD.osmNode({ id: 'a' }),
b = iD.osmNode({ id: 'b' }),
c = iD.osmNode({ id: 'c' }),
w = iD.osmWay({ id: 'w', nodes: ['a','b','a'] }),
r = iD.osmRelation({ id: 'r', members: [{id: 'w'}, {id: 'c'}] }),
graph = iD.coreGraph([a, b, c, w, r]),
result = iD.utilGetAllNodes(['r'], graph);
var a = iD.osmNode({ id: 'a' });
var b = iD.osmNode({ id: 'b' });
var c = iD.osmNode({ id: 'c' });
var w = iD.osmWay({ id: 'w', nodes: ['a','b','a'] });
var r = iD.osmRelation({ id: 'r', members: [{id: 'w'}, {id: 'c'}] });
var graph = iD.coreGraph([a, b, c, w, r]);
var result = iD.utilGetAllNodes(['r'], graph);
expect(result).to.have.members([a, b, c]);
expect(result).to.have.lengthOf(3);
});
it('gets all descendant nodes of multiple ids', function() {
var a = iD.osmNode({ id: 'a' }),
b = iD.osmNode({ id: 'b' }),
c = iD.osmNode({ id: 'c' }),
d = iD.osmNode({ id: 'd' }),
e = iD.osmNode({ id: 'e' }),
w1 = iD.osmWay({ id: 'w1', nodes: ['a','b','a'] }),
w2 = iD.osmWay({ id: 'w2', nodes: ['c','b','a','c'] }),
r = iD.osmRelation({ id: 'r', members: [{id: 'w1'}, {id: 'd'}] }),
graph = iD.coreGraph([a, b, c, d, e, w1, w2, r]),
result = iD.utilGetAllNodes(['r', 'w2', 'e'], graph);
var a = iD.osmNode({ id: 'a' });
var b = iD.osmNode({ id: 'b' });
var c = iD.osmNode({ id: 'c' });
var d = iD.osmNode({ id: 'd' });
var e = iD.osmNode({ id: 'e' });
var w1 = iD.osmWay({ id: 'w1', nodes: ['a','b','a'] });
var w2 = iD.osmWay({ id: 'w2', nodes: ['c','b','a','c'] });
var r = iD.osmRelation({ id: 'r', members: [{id: 'w1'}, {id: 'd'}] });
var graph = iD.coreGraph([a, b, c, d, e, w1, w2, r]);
var result = iD.utilGetAllNodes(['r', 'w2', 'e'], graph);
expect(result).to.have.members([a, b, c, d, e]);
expect(result).to.have.lengthOf(5);
});
it('handles recursive relations', function() {
var a = iD.osmNode({ id: 'a' }),
r1 = iD.osmRelation({ id: 'r1', members: [{id: 'r2'}] }),
r2 = iD.osmRelation({ id: 'r2', members: [{id: 'r1'}, {id: 'a'}] }),
graph = iD.coreGraph([a, r1, r2]),
result = iD.utilGetAllNodes(['r1'], graph);
var a = iD.osmNode({ id: 'a' });
var r1 = iD.osmRelation({ id: 'r1', members: [{id: 'r2'}] });
var r2 = iD.osmRelation({ id: 'r2', members: [{id: 'r1'}, {id: 'a'}] });
var graph = iD.coreGraph([a, r1, r2]);
var result = iD.utilGetAllNodes(['r1'], graph);
expect(result).to.have.members([a]);
expect(result).to.have.lengthOf(1);
});
});
it('utilTagDiff', function() {
var oldTags = { a: 'one', b: 'two', c: 'three' };
var newTags = { a: 'one', b: 'three', d: 'four' };
var diff = iD.utilTagDiff(oldTags, newTags);
expect(diff).to.have.length(4);
expect(diff[0]).to.eql({
type: '-', key: 'b', oldVal: 'two', newVal: 'three', display: '- b=two' // delete-modify
});
expect(diff[1]).to.eql({
type: '+', key: 'b', oldVal: 'two', newVal: 'three', display: '+ b=three' // insert-modify
});
expect(diff[2]).to.eql({
type: '-', key: 'c', oldVal: 'three', newVal: undefined, display: '- c=three' // delete
});
expect(diff[3]).to.eql({
type: '+', key: 'd', oldVal: undefined, newVal: 'four', display: '+ d=four' // insert
});
});
it('utilTagText', function() {
expect(iD.utilTagText({})).to.eql('');
expect(iD.utilTagText({tags:{foo:'bar'}})).to.eql('foo=bar');