Files
iD/modules/ui/commit.js
Bryan Housel 439eed4b39 WIP: refactor uiCommit into sections, introduce uiChangesetEditor
uiCommit is getting kind of big as we add more to the commit pane.

I'm going to split it up and put the field rendering code into a separate
module, similar to how uiEntityEditor embeds uiPresetEditor for the fields.

This allows us to add a few more fields that users can set on their changesets
(like hashtags, source), and even hide them under a "Add field" dropdown.
2017-08-11 17:17:46 -04:00

487 lines
15 KiB
JavaScript

import * as d3 from 'd3';
import _ from 'lodash';
import { t } from '../util/locale';
import { d3combobox } from '../lib/d3.combobox.js';
import { osmChangeset } from '../osm';
import { modeSelect } from '../modes';
import { svgIcon } from '../svg';
import { tooltip } from '../util/tooltip';
import { uiChangesetEditor } from './changeset_editor';
import { uiRawTagEditor } from './raw_tag_editor';
import { utilDetect } from '../util/detect';
import {
utilDisplayName,
utilDisplayType,
utilEntityOrMemberSelector,
utilRebind,
utilTriggerEvent
} from '../util';
var changeset;
var readOnlyTags = ['created_by', 'imagery_used', 'host', 'locale'];
export function uiCommit(context) {
var dispatch = d3.dispatch('cancel', 'save');
function commit(selection) {
var osm = context.connection();
if (!osm) return;
if (!changeset) {
var detected = utilDetect(),
tags = {
created_by: ('iD ' + context.version).substr(0, 255),
imagery_used: context.history().imageryUsed().join(';').substr(0, 255),
host: detected.host.substr(0, 255),
locale: detected.locale.substr(0, 255)
};
changeset = new osmChangeset({ tags: tags });
}
var changesetEditor = uiChangesetEditor(context)
.on('change', changeTags);
var rawTagEditor = uiRawTagEditor(context)
.on('change', changeTags);
var changes = context.history().changes(),
summary = context.history().difference().summary(),
comment = context.storage('comment') || '',
commentDate = +context.storage('commentDate') || 0,
currDate = Date.now(),
cutoff = 2 * 86400 * 1000; // 2 days
// expire the stored comment if it is too old - #3947
if (commentDate > currDate || currDate - commentDate > cutoff) {
comment = '';
}
selection
.append('div')
.attr('class', 'header fillL')
.append('h3')
.text(t('commit.title'));
var body = selection
.append('div')
.attr('class', 'body');
body
.append('div')
.attr('class', 'modal-section changeset-editor')
.call(changesetEditor
.changeset(changeset)
.tags(tags)
);
// Fields
var fieldSection = body
.append('div')
.attr('class', 'modal-section form-field commit-form');
fieldSection
.append('label')
.attr('class', 'form-label')
.text(t('commit.message_label'));
var commentField = fieldSection
.append('textarea')
.attr('class', 'commit-form-comment')
.attr('placeholder', t('commit.description_placeholder'))
.attr('maxlength', 255)
.property('value', comment)
.on('input.save', change(true))
.on('change.save', change())
.on('blur.save', function() {
context.storage('comment', this.value);
context.storage('commentDate', Date.now());
});
commentField.node().select();
osm.userChangesets(function (err, changesets) {
if (err) return;
var comments = changesets.map(function(changeset) {
return {
title: changeset.tags.comment,
value: changeset.tags.comment
};
});
commentField
.call(d3combobox()
.container(context.container())
.caseSensitive(true)
.data(_.uniqBy(comments, 'title'))
);
});
var commentWarning = fieldSection.append('div')
.attr('class', 'field-warning comment-warning');
var changeSetInfo = fieldSection.append('div')
.attr('class', 'changeset-info');
changeSetInfo.append('a')
.attr('target', '_blank')
.attr('tabindex', -1)
.call(svgIcon('#icon-out-link', 'inline'))
.attr('href', t('commit.about_changeset_comments_link'))
.append('span')
.text(t('commit.about_changeset_comments'));
// Warnings
var warnings = body.selectAll('div.warning-section')
.data([context.history().validate(changes)]);
warnings = warnings.enter()
.append('div')
.attr('class', 'modal-section warning-section fillL2')
.style('display', function(d) { return _.isEmpty(d) ? 'none' : null; })
.merge(warnings);
warnings
.append('h3')
.text(t('commit.warnings'));
warnings
.append('ul')
.attr('class', 'changeset-list');
var warningLi = warnings.select('ul').selectAll('li')
.data(function(d) { return d; });
warningLi = warningLi.enter()
.append('li')
.on('mouseover', mouseover)
.on('mouseout', mouseout)
.on('click', warningClick)
.merge(warningLi);
warningLi
.call(svgIcon('#icon-alert', 'pre-text'));
warningLi
.append('strong')
.text(function(d) { return d.message; });
warningLi.filter(function(d) { return d.tooltip; })
.call(tooltip()
.title(function(d) { return d.tooltip; })
.placement('top')
);
// Upload Explanation
var saveSection = body
.append('div')
.attr('class','modal-section save-section fillL cf');
var prose = saveSection
.append('p')
.attr('class', 'commit-info')
.text(t('commit.upload_explanation'));
osm.userDetails(function(err, user) {
if (err) return;
var userLink = d3.select(document.createElement('div'));
if (user.image_url) {
userLink
.append('img')
.attr('src', user.image_url)
.attr('class', 'icon pre-text user-icon');
}
userLink
.append('a')
.attr('class','user-info')
.text(user.display_name)
.attr('href', osm.userURL(user.display_name))
.attr('tabindex', -1)
.attr('target', '_blank');
prose
.html(t('commit.upload_explanation_with_user', { user: userLink.html() }));
});
var requestReview = saveSection
.append('p')
.attr('class', 'request-review')
.text(t('commit.request_review'));
var requestReviewField = requestReview
.append('input')
.attr('type', 'checkbox')
.property('checked', isReviewRequested(changeset.tags))
.on('change', toggleRequestReview());
// Buttons
var buttonSection = saveSection
.append('div')
.attr('class', 'buttons fillL cf');
var cancelButton = buttonSection
.append('button')
.attr('class', 'secondary-action col5 button cancel-button')
.on('click.cancel', function() {
dispatch.call('cancel');
});
cancelButton
.append('span')
.attr('class', 'label')
.text(t('commit.cancel'));
var saveButton = buttonSection
.append('button')
.attr('class', 'action col5 button save-button')
.attr('disabled', function() {
var n = d3.select('.commit-form textarea').node();
return (n && n.value.length) ? null : true;
})
.on('click.save', function() {
dispatch.call('save', this, changeset);
});
saveButton
.append('span')
.attr('class', 'label')
.text(t('commit.save'));
// Raw Tag Editor
var tagSection = body
.append('div')
.attr('class', 'modal-section tag-section raw-tag-editor');
// Changes
var changeSection = body
.append('div')
.attr('class', 'commit-section modal-section fillL2');
changeSection.append('h3')
.text(t('commit.changes', { count: summary.length }));
var li = changeSection
.append('ul')
.attr('class', 'changeset-list')
.selectAll('li')
.data(summary);
li = li.enter()
.append('li')
.on('mouseover', mouseover)
.on('mouseout', mouseout)
.on('click', zoomToEntity)
.merge(li);
li.each(function(d) {
d3.select(this)
.call(svgIcon('#icon-' + d.entity.geometry(d.graph), 'pre-text ' + d.changeType));
});
li.append('span')
.attr('class', 'change-type')
.text(function(d) { return t('commit.' + d.changeType) + ' '; });
li.append('strong')
.attr('class', 'entity-type')
.text(function(d) {
var matched = context.presets().match(d.entity, d.graph);
return (matched && matched.name()) || utilDisplayType(d.entity.id);
});
li.append('span')
.attr('class', 'entity-name')
.text(function(d) {
var name = utilDisplayName(d.entity) || '',
string = '';
if (name !== '') string += ':';
return string += ' ' + name;
});
li.style('opacity', 0)
.transition()
.style('opacity', 1);
// Call change() off the bat, in case a changeset
// comment is recovered from localStorage
utilTriggerEvent(commentField, 'input');
function mouseover(d) {
if (d.entity) {
context.surface().selectAll(
utilEntityOrMemberSelector([d.entity.id], context.graph())
).classed('hover', true);
}
}
function mouseout() {
context.surface().selectAll('.hover')
.classed('hover', false);
}
function warningClick(d) {
if (d.entity) {
context.map().zoomTo(d.entity);
context.enter(modeSelect(context, [d.entity.id]));
}
}
function zoomToEntity(change) {
var entity = change.entity;
if (change.changeType !== 'deleted' &&
context.graph().entity(entity.id).geometry(context.graph()) !== 'vertex') {
context.map().zoomTo(entity);
context.surface().selectAll(utilEntityOrMemberSelector([entity.id], context.graph()))
.classed('hover', true);
}
}
function checkComment(comment) {
// Save button disabled if there is no comment..
d3.selectAll('.save-section .save-button')
.attr('disabled', (comment.length ? null : true));
// Warn if comment mentions Google..
var googleWarning = commentWarning
.html('')
.selectAll('a')
.data(comment.match(/google/i) ? [true] : []);
googleWarning.exit()
.remove();
googleWarning.enter()
.append('a')
.attr('target', '_blank')
.attr('tabindex', -1)
.call(svgIcon('#icon-alert', 'inline'))
.attr('href', t('commit.google_warning_link'))
.append('span')
.text(t('commit.google_warning'));
}
function change(onInput) {
return function() {
var comment = commentField.property('value').trim();
if (!onInput) {
commentField.property('value', comment);
}
checkComment(comment);
var changeset = updateChangeset({ comment: comment });
var expanded = !tagSection.selectAll('a.hide-toggle.expanded').empty();
tagSection
.call(rawTagEditor
.expanded(expanded)
.readOnlyTags(readOnlyTags)
.tags(_.clone(changeset.tags))
);
};
}
function toggleRequestReview() {
return function() {
var rr = requestReviewField.property('checked');
var changeset = updateChangeset({ review_requested: (rr ? 'yes' : undefined) });
var expanded = !tagSection.selectAll('a.hide-toggle.expanded').empty();
tagSection
.call(rawTagEditor
.expanded(expanded)
.readOnlyTags(readOnlyTags)
.tags(_.clone(changeset.tags))
);
};
}
function changeTags(changed) {
if (changed.hasOwnProperty('comment')) {
if (changed.comment === undefined) {
changed.comment = '';
}
changed.comment = changed.comment.trim();
commentField
.property('value', changed.comment);
}
if (changed.hasOwnProperty('review_requested')) {
if (changed.review_requested === undefined) {
requestReviewField
.property('checked', false);
} else {
changed.review_requested = changed.review_requested.trim();
requestReviewField
.property('checked', isReviewRequested(changed));
}
}
updateChangeset(changed);
utilTriggerEvent(commentField, 'input');
}
function isReviewRequested(tags) {
var rr = tags.review_requested;
if (rr === undefined) return false;
rr = rr.trim().toLowerCase();
return !(rr === '' || rr === 'no');
}
function updateChangeset(changed) {
var tags = _.clone(changeset.tags);
_.forEach(changed, function(v, k) {
k = k.trim().substr(0, 255);
if (readOnlyTags.indexOf(k) !== -1) return;
if (k !== '' && v !== undefined) {
tags[k] = v.trim().substr(0, 255);
} else {
delete tags[k];
}
});
if (!_.isEqual(changeset.tags, tags)) {
changeset = changeset.update({ tags: tags });
}
return changeset;
}
}
commit.reset = function() {
changeset = null;
};
return utilRebind(commit, dispatch, 'on');
}