iD.ui.EntityEditor = function(context) { var event = d3.dispatch('choose'), state = 'select', id, preset, reference; var presetEditor = iD.ui.preset(context) .on('change', changeTags); var rawTagEditor = iD.ui.RawTagEditor(context) .on('change', changeTags); function entityEditor(selection) { var entity = context.entity(id), tags = _.clone(entity.tags); var $header = selection.selectAll('.header') .data([0]); // Enter var $enter = $header.enter().append('div') .attr('class', 'header fillL cf'); $enter.append('button') .attr('class', 'fr preset-close') .append('span') .attr('class', 'icon close'); $enter.append('h3'); // Update $header.select('h3') .text(t('inspector.edit')); $header.select('.preset-close') .on('click', function() { context.enter(iD.modes.Browse(context)); }); var $body = selection.selectAll('.inspector-body') .data([0]); // Enter $enter = $body.enter().append('div') .attr('class', 'inspector-body'); $enter.append('div') .attr('class', 'preset-list-item inspector-inner') .append('div') .attr('class', 'preset-list-button-wrap') .append('button') .attr('class', 'preset-list-button preset-reset') .call(bootstrap.tooltip() .title(t('inspector.back_tooltip')) .placement('bottom')) .append('div') .attr('class', 'label'); $body.select('.preset-list-button-wrap') .call(reference.button); $body.select('.preset-list-item') .call(reference.body); $enter.append('div') .attr('class', 'inspector-border inspector-preset'); $enter.append('div') .attr('class', 'inspector-border raw-tag-editor inspector-inner'); $enter.append('div') .attr('class', 'inspector-border raw-member-editor inspector-inner'); $enter.append('div') .attr('class', 'raw-membership-editor inspector-inner'); selection.selectAll('.preset-reset') .on('click', function() { event.choose(preset); }); // Update $body.select('.preset-list-item button') .call(iD.ui.PresetIcon() .geometry(context.geometry(id)) .preset(preset)); $body.select('.preset-list-item .label') .text(preset.name()); $body.select('.inspector-preset') .call(presetEditor .preset(preset) .entityID(id) .tags(tags) .state(state)); $body.select('.raw-tag-editor') .call(rawTagEditor .preset(preset) .entityID(id) .tags(tags) .state(state)); if (entity.type === 'relation') { $body.select('.raw-member-editor') .style('display', 'block') .call(iD.ui.RawMemberEditor(context) .entityID(id)); } else { $body.select('.raw-member-editor') .style('display', 'none'); } $body.select('.raw-membership-editor') .call(iD.ui.RawMembershipEditor(context) .entityID(id)); function historyChanged() { if (state === 'hide') return; var entity = context.hasEntity(id); if (!entity) return; entityEditor.preset(context.presets().match(entity, context.graph())); entityEditor(selection); } context.history() .on('change.entity-editor', historyChanged); } function clean(o) { function cleanVal(k, v) { function keepSpaces(k) { var whitelist = ['opening_hours', 'service_times', 'collection_times', 'operating_times', 'smoking_hours', 'happy_hours']; return _.any(whitelist, function(s) { return k.indexOf(s) !== -1; }); } var blacklist = ['description', 'note', 'fixme']; if (_.any(blacklist, function(s) { return k.indexOf(s) !== -1; })) return v; var cleaned = v.split(';') .map(function(s) { return s.trim(); }) .join(keepSpaces(k) ? '; ' : ';'); // The code below is not intended to validate websites and emails. // It is only intended to prevent obvious copy-paste errors. (#2323) // clean website-like tags if (k.indexOf('website') !== -1 || cleaned.indexOf('http') === 0) { cleaned = cleaned .replace(/[\u200B-\u200F\uFEFF]/g, '') // strip LRM and other zero width chars .replace(/[^\w\+\-\.\/\?\[\]\(\)~!@#$%&*',:;=]/g, encodeURIComponent); // clean email-like tags } else if (k.indexOf('email') !== -1) { cleaned = cleaned .replace(/[\u200B-\u200F\uFEFF]/g, '') // strip LRM and other zero width chars .replace(/[^\w\+\-\.\/\?\|~!@#$%^&*'`{};=]/g, ''); // note: ';' allowed as OSM delimiter } return cleaned; } var out = {}, k, v; for (k in o) { if (k && (v = o[k]) !== undefined) { out[k] = cleanVal(k, v); } } return out; } function changeTags(changed) { var entity = context.entity(id), tags = clean(_.extend({}, entity.tags, changed)); if (!_.isEqual(entity.tags, tags)) { context.perform( iD.actions.ChangeTags(id, tags), t('operations.change_tags.annotation')); } } entityEditor.state = function(_) { if (!arguments.length) return state; state = _; return entityEditor; }; entityEditor.entityID = function(_) { if (!arguments.length) return id; id = _; entityEditor.preset(context.presets().match(context.entity(id), context.graph())); return entityEditor; }; entityEditor.preset = function(_) { if (!arguments.length) return preset; if (_ !== preset) { preset = _; reference = iD.ui.TagReference(preset.reference(context.geometry(id)), context) .showing(false); } return entityEditor; }; return d3.rebind(entityEditor, event, 'on'); };