diff --git a/build_data.js b/build_data.js
index 1f2daac3a4..50d282f7a3 100644
--- a/build_data.js
+++ b/build_data.js
@@ -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
diff --git a/css/80_app.css b/css/80_app.css
index a99b480a3c..29398a4062 100644
--- a/css/80_app.css
+++ b/css/80_app.css
@@ -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;
}
@@ -2400,8 +2402,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%;
diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js
index 9b3ae46e1e..55204e4751 100644
--- a/modules/ui/entity_editor.js
+++ b/modules/ui/entity_editor.js
@@ -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);
diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js
index 72c48745fa..1353694966 100644
--- a/modules/ui/raw_tag_editor.js
+++ b/modules/ui/raw_tag_editor.js
@@ -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;
diff --git a/modules/util/index.js b/modules/util/index.js
index b7d51e668c..dde2a5614a 100644
--- a/modules/util/index.js
+++ b/modules/util/index.js
@@ -37,6 +37,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';
diff --git a/modules/util/util.js b/modules/util/util.js
index 2dc9c9b03a..a5bfc25343 100644
--- a/modules/util/util.js
+++ b/modules/util/util.js
@@ -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';
}
diff --git a/modules/validations/outdated_tags.js b/modules/validations/outdated_tags.js
index a17156a79c..e3336e68da 100644
--- a/modules/validations/outdated_tags.js
+++ b/modules/validations/outdated_tags.js
@@ -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({
@@ -112,10 +99,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; });
}
}
diff --git a/modules/validations/private_data.js b/modules/validations/private_data.js
index 53adef44cc..15c6ae5b79 100644
--- a/modules/validations/private_data.js
+++ b/modules/validations/private_data.js
@@ -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; });
}
};
diff --git a/svg/fontawesome/fas-i-cursor.svg b/svg/fontawesome/fas-i-cursor.svg
new file mode 100644
index 0000000000..8670a48aab
--- /dev/null
+++ b/svg/fontawesome/fas-i-cursor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-th-list.svg b/svg/fontawesome/fas-th-list.svg
new file mode 100644
index 0000000000..108272386a
--- /dev/null
+++ b/svg/fontawesome/fas-th-list.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/spec/util/util.js b/test/spec/util/util.js
index be330efaf9..63b9999552 100644
--- a/test/spec/util/util.js
+++ b/test/spec/util/util.js
@@ -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');