text: 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');
  };
  