Skip to content

Commit

Permalink
Merge pull request #6302 from openstreetmap/text-raw-tag-editor
Browse files Browse the repository at this point in the history
Text raw tag editor / Copy-paste tags
  • Loading branch information
bhousel authored May 3, 2019
2 parents c99ddf8 + 16ec257 commit ddc9d16
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 85 deletions.
5 changes: 4 additions & 1 deletion build_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 51 additions & 1 deletion css/80_app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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%;
Expand Down
17 changes: 12 additions & 5 deletions modules/ui/entity_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ 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) {
var dispatch = d3_dispatch('choose');
var _state = 'select';
var _coalesceChanges = false;
var _modified = false;
var _scrolled = false;
var _base;
var _entityID;
var _activePreset;
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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);
Expand Down
170 changes: 147 additions & 23 deletions modules/ui/raw_tag_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);


Expand All @@ -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')
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions modules/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading

0 comments on commit ddc9d16

Please sign in to comment.