From 6782947f5b2841a3b781924e4049c3c0195050cf Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Fri, 21 Feb 2020 15:22:54 -0800 Subject: [PATCH] Make inspector sections inherit from uiSection (re: #7368) --- css/80_app.css | 121 ++-- modules/index.js | 5 +- modules/ui/commit.js | 5 +- modules/ui/data_editor.js | 7 +- modules/ui/entity_editor.js | 314 ++------- modules/ui/index.js | 5 - modules/ui/intro/navigation.js | 2 +- modules/ui/panes/index.js | 5 + modules/ui/raw_membership_editor.js | 442 ------------ modules/ui/raw_tag_editor.js | 656 ------------------ modules/ui/section.js | 9 +- modules/ui/{ => sections}/entity_issues.js | 79 +-- modules/ui/sections/feature_type.js | 177 +++++ modules/ui/sections/index.js | 20 + .../preset_fields.js} | 59 +- .../ui/{ => sections}/raw_member_editor.js | 82 +-- modules/ui/sections/raw_membership_editor.js | 448 ++++++++++++ modules/ui/sections/raw_tag_editor.js | 623 +++++++++++++++++ modules/ui/{ => sections}/selection_list.js | 54 +- test/spec/ui/raw_tag_editor.js | 8 +- 20 files changed, 1522 insertions(+), 1599 deletions(-) create mode 100644 modules/ui/panes/index.js delete mode 100644 modules/ui/raw_membership_editor.js delete mode 100644 modules/ui/raw_tag_editor.js rename modules/ui/{ => sections}/entity_issues.js (84%) create mode 100644 modules/ui/sections/feature_type.js create mode 100644 modules/ui/sections/index.js rename modules/ui/{preset_editor.js => sections/preset_fields.js} (82%) rename modules/ui/{ => sections}/raw_member_editor.js (87%) create mode 100644 modules/ui/sections/raw_membership_editor.js create mode 100644 modules/ui/sections/raw_tag_editor.js rename modules/ui/{ => sections}/selection_list.js (75%) diff --git a/css/80_app.css b/css/80_app.css index a930b4c73..caebc843a 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -841,14 +841,12 @@ a.hide-toggle { .entity-editor-pane .inspector-body { top: 60px; } -/* preserve extra space at bottom of inspector to allow for dropdown options - #5280 */ -.entity-editor .section:last-child { - margin-bottom: 150px; +.entity-editor { + padding: 20px; } - -.inspector-inner { - padding: 20px 20px 5px 20px; - position: relative; +/* preserve extra space at bottom of inspector to allow for dropdown options - #5280 */ +.entity-editor > div:last-child { + margin-bottom: 150px; } #sidebar .search-header .icon { @@ -877,6 +875,10 @@ a.hide-toggle { font-weight: bold; } +.section { + margin-bottom: 30px; +} + /* Feature List / Search Results ------------------------------------------------------- */ @@ -958,13 +960,13 @@ a.hide-toggle { padding-left: 0; padding-right: 10px; } -.selected-features .feature-list { +.section-selected-features .feature-list { border: 1px solid #ccc; border-radius: 4px; overflow: hidden; margin-top: 5px; } -.selected-features .feature-list-item:last-child { +.section-selected-features .feature-list-item:last-child { border: none; } @@ -1268,24 +1270,13 @@ a.hide-toggle { /* Entity/Preset Editor ------------------------------------------------------- */ -.entity-issues, -.preset-editor { - overflow: hidden; - padding: 10px 0px 5px 0px; -} -.entity-issues a.hide-toggle, -.preset-editor a.hide-toggle { - margin: 0 20px 5px 20px; -} -.entity-issues .disclosure-wrap-entity_issues, -.preset-editor .form-fields-container { +.section .grouped-items-area { padding: 10px; - margin: 0 10px 10px 10px; + margin: 0 -10px 10px -10px; border-radius: 8px; background: #ececec; } -.entity-issues .disclosure-wrap-entity_issues:empty, -.preset-editor .form-fields-container:empty { +.section .grouped-items-area:empty { display: none; } .preset-list-item a.hide-toggle { @@ -2242,7 +2233,7 @@ div.combobox { /* More Fields dropdown ------------------------------------------------------- */ .more-fields { - padding: 0 20px 20px 20px; + margin-top: 10px; font-weight: bold; } .changeset-editor .more-fields { @@ -2478,43 +2469,47 @@ img.tag-reference-wiki-image { position: relative; width: 100%; } -.raw-tag-editor .tag-reference-body { +.section-raw-tag-editor .tag-reference-body { width: 100%; } -.raw-tag-editor .tag-row.readonly .tag-reference-body { +.section-raw-tag-editor .tag-row.readonly .tag-reference-body { background: #f6f6f6; color: #333; } -.raw-tag-editor .tag-row:not(:last-child) .tag-reference-body.expanded { +.section-raw-tag-editor .tag-row:not(:last-child) .tag-reference-body.expanded { border-bottom: 1px solid #ccc; } -.raw-tag-editor .tag-row.readonly .tag-reference-body.expanded { +.section-raw-tag-editor .tag-row.readonly .tag-reference-body.expanded { border-top: 1px solid #ccc; } /* Raw Member / Membership Editor ------------------------------------------------------- */ +.section-raw-member-editor .member-list:empty, +.section-raw-membership-editor .member-list:empty { + display: none; +} -.raw-member-editor .member-list, -.raw-membership-editor .member-list { +.section-raw-member-editor .member-list, +.section-raw-membership-editor .member-list { padding-top: 10px; } -.raw-member-editor .member-list li, -.raw-membership-editor .member-list li { +.section-raw-member-editor .member-list li, +.section-raw-membership-editor .member-list li { position: relative; border-radius: 4px; margin: 0; padding-bottom: 10px; } -.raw-member-editor .member-row .member-entity-name, -.raw-membership-editor .member-row .member-entity-name { +.section-raw-member-editor .member-row .member-entity-name, +.section-raw-membership-editor .member-row .member-entity-name { font-weight: normal; padding-left: 10px; } -[dir='rtl'] .raw-member-editor .member-row .member-entity-name, -[dir='rtl'] .raw-membership-editor .member-row .member-entity-name { +[dir='rtl'] .section-raw-member-editor .member-row .member-entity-name, +[dir='rtl'] .section-raw-membership-editor .member-row .member-entity-name { padding-left:0; padding-right: 10px; } @@ -2541,7 +2536,7 @@ img.tag-reference-wiki-image { border: 0; } -.raw-member-editor .member-row.dragging { +.section-raw-member-editor .member-row.dragging { opacity: 0.75; z-index: 3000; /* @@ -2815,11 +2810,11 @@ input.key-trap { } /* custom data editor - no info/delete buttons */ -.data-editor.raw-tag-editor .tag-row button { +.data-editor.section-raw-tag-editor .tag-row button { display: none; } -.data-editor.raw-tag-editor .tag-row .key-wrap, -.data-editor.raw-tag-editor .tag-row .value-wrap { +.data-editor.section-raw-tag-editor .tag-row .key-wrap, +.data-editor.section-raw-tag-editor .tag-row .value-wrap { width: 50%; } @@ -3280,42 +3275,42 @@ input.square-degrees-input { /* Entity Issues List */ -.entity-issues .issue-container .issue { +.section-entity-issues .issue-container .issue { border-radius: 4px; border: 1px solid #ccc; background: #f6f6f6; } -.entity-issues .issue-container:not(.active) .issue-text:hover, -.entity-issues .issue-container:not(.active) .issue-info-button:hover { +.section-entity-issues .issue-container:not(.active) .issue-text:hover, +.section-entity-issues .issue-container:not(.active) .issue-info-button:hover { background: #f1f1f1; } -.entity-issues .issue .issue-label .issue-text { +.section-entity-issues .issue .issue-label .issue-text { padding-right: 10px; } -[dir='rtl'] .entity-issues .issue .issue-label .issue-text { +[dir='rtl'] .section-entity-issues .issue .issue-label .issue-text { padding-right: unset; padding-left: 10px; } -.entity-issues .issue-container.active .issue-label .issue-text { +.section-entity-issues .issue-container.active .issue-label .issue-text { font-weight: bold; } -.entity-issues .issue-container:not(:last-of-type) { +.section-entity-issues .issue-container:not(:last-of-type) { margin-bottom: 5px; } -.entity-issues .issue-container.active:not(:first-of-type) { +.section-entity-issues .issue-container.active:not(:first-of-type) { margin-top: 10px; } -.entity-issues .issue-container.active:not(:last-of-type) { +.section-entity-issues .issue-container.active:not(:last-of-type) { margin-bottom: 10px; } /* fixes */ -.entity-issues .issue-fix-list { +.section-entity-issues .issue-fix-list { border-top: 1px solid; border-color: inherit; } -.entity-issues .issue-container.active .issue-fix-list:empty { +.section-entity-issues .issue-container.active .issue-fix-list:empty { display: none; } @@ -3596,7 +3591,7 @@ li.issue-fix-item:not(.actionable) .fix-icon { padding: 10px 20px 20px 40px; } -.pane-content > div { +.help-pane .pane-content > div { padding-bottom: 15px; } @@ -3705,7 +3700,7 @@ li.issue-fix-item:not(.actionable) .fix-icon { /* Inspector (hover styles) ------------------------------------------------------- */ -.inspector-hover .entity-issues .issue-container .issue .issue-label, +.inspector-hover .section-entity-issues .issue-container .issue .issue-label, .inspector-hover .form-field-input-wrap .label, .inspector-hover .form-field-input-multicombo .chiplist, .inspector-hover .form-field-button, @@ -3725,7 +3720,7 @@ li.issue-fix-item:not(.actionable) .fix-icon { .inspector-hover a, .inspector-hover .form-field-input-multicombo .chip, .inspector-hover .form-field-input-check span, -.inspector-hover .entity-issues .issue .icon { +.inspector-hover .section-entity-issues .issue .icon { color: #666; } @@ -3736,8 +3731,8 @@ li.issue-fix-item:not(.actionable) .fix-icon { /* no scrollbars */ .inspector-hover div { - overflow-x: hidden; - overflow-y: hidden; + overflow-x: visible; + overflow-y: visible; } /* hide and remove from layout */ @@ -3749,9 +3744,9 @@ li.issue-fix-item:not(.actionable) .fix-icon { .inspector-hover .form-field-input-radio label, .inspector-hover .form-field-input-radio label span, .inspector-hover .form-field-input-radio label.remove .icon, -.inspector-hover .inspector-inner .add-row, -.inspector-hover .entity-issues .issue-container .issue-fix-list, -.inspector-hover .entity-issues .issue-container .issue-info-button { +.inspector-hover .add-row, +.inspector-hover .section-entity-issues .issue-container .issue-fix-list, +.inspector-hover .section-entity-issues .issue-container .issue-info-button { display: none; } @@ -3769,17 +3764,17 @@ li.issue-fix-item:not(.actionable) .fix-icon { } /* Unstyle the active entity issue on hover */ -.inspector-hover .entity-issues .issue-container.active { +.inspector-hover .section-entity-issues .issue-container.active { margin-top: 1px; margin-bottom: 1px; } -.inspector-hover .entity-issues .issue-container * { +.inspector-hover .section-entity-issues .issue-container * { border-color: #ccc !important; } -.inspector-hover .entity-issues .issue-container.active .issue-label { +.inspector-hover .section-entity-issues .issue-container.active .issue-label { border-bottom: 0; } -.inspector-hover .entity-issues .issue-container.active .issue-label .issue-text { +.inspector-hover .section-entity-issues .issue-container.active .issue-label .issue-text { font-weight: normal; } diff --git a/modules/index.js b/modules/index.js index 1d3c98bab..fb581e3ac 100644 --- a/modules/index.js +++ b/modules/index.js @@ -15,6 +15,8 @@ export * from './svg/index'; export * from './ui/fields/index'; export * from './ui/intro/index'; export * from './ui/panels/index'; +export * from './ui/panes/index'; +export * from './ui/sections/index'; export * from './ui/settings/index'; export * from './ui/index'; export * from './util/index'; @@ -44,7 +46,6 @@ export { rendererFeatures as Features } from './renderer/features'; export { rendererMap as Map } from './renderer/map'; export { rendererTileLayer as TileLayer } from './renderer/tile_layer'; export { utilDetect as Detect } from './util/detect'; -export { uiPresetEditor as uiPreset } from './ui/preset_editor'; +export { uiSectionPresetFields as uiPreset } from './ui/sections/preset_fields'; export var debug = false; - diff --git a/modules/ui/commit.js b/modules/ui/commit.js index e3955ca9f..30abc4b01 100644 --- a/modules/ui/commit.js +++ b/modules/ui/commit.js @@ -11,7 +11,7 @@ import { tooltip } from '../util/tooltip'; import { uiChangesetEditor } from './changeset_editor'; import { uiCommitChanges } from './commit_changes'; import { uiCommitWarnings } from './commit_warnings'; -import { uiRawTagEditor } from './raw_tag_editor'; +import { uiSectionRawTagEditor } from './sections/raw_tag_editor'; import { utilArrayGroupBy, utilRebind } from '../util'; import { utilDetect } from '../util/detect'; @@ -44,7 +44,7 @@ export function uiCommit(context) { var changesetEditor = uiChangesetEditor(context) .on('change', changeTags); - var rawTagEditor = uiRawTagEditor(context) + var rawTagEditor = uiSectionRawTagEditor(context) .on('change', changeTags); var commitChanges = uiCommitChanges(context); var commitWarnings = uiCommitWarnings(context); @@ -399,6 +399,7 @@ export function uiCommit(context) { .expanded(expanded) .readOnlyTags(readOnlyTags) .tags(Object.assign({}, _changeset.tags)) // shallow copy + .render ); diff --git a/modules/ui/data_editor.js b/modules/ui/data_editor.js index 16943fb32..ed7640ce8 100644 --- a/modules/ui/data_editor.js +++ b/modules/ui/data_editor.js @@ -4,14 +4,14 @@ import { svgIcon } from '../svg/icon'; import { uiDataHeader } from './data_header'; import { uiQuickLinks } from './quick_links'; -import { uiRawTagEditor } from './raw_tag_editor'; +import { uiSectionRawTagEditor } from './sections/raw_tag_editor'; import { uiTooltipHtml } from './tooltipHtml'; export function uiDataEditor(context) { var dataHeader = uiDataHeader(); var quickLinks = uiQuickLinks(); - var rawTagEditor = uiRawTagEditor(context); + var rawTagEditor = uiSectionRawTagEditor(context); var _datum; @@ -74,13 +74,14 @@ export function uiDataEditor(context) { // enter/update rte.enter() .append('div') - .attr('class', 'raw-tag-editor inspector-inner data-editor') + .attr('class', 'raw-tag-editor data-editor') .merge(rte) .call(rawTagEditor .expanded(true) .readOnlyTags([/./]) .tags((_datum && _datum.properties) || {}) .state('hover') + .render ) .selectAll('textarea.tag-text') .attr('readonly', true) diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js index 03dceb452..9efffea13 100644 --- a/modules/ui/entity_editor.js +++ b/modules/ui/entity_editor.js @@ -3,24 +3,19 @@ import { event as d3_event, selectAll as d3_selectAll, select as d3_select } fro import deepEqual from 'fast-deep-equal'; import { t, textDirection } from '../util/locale'; -import { tooltip } from '../util/tooltip'; import { actionChangeTags } from '../actions/change_tags'; import { modeBrowse } from '../modes/browse'; import { svgIcon } from '../svg/icon'; -import { uiDisclosure } from './disclosure'; -import { uiPresetIcon } from './preset_icon'; -import { uiQuickLinks } from './quick_links'; -import { uiRawMemberEditor } from './raw_member_editor'; -import { uiRawMembershipEditor } from './raw_membership_editor'; -import { uiRawTagEditor } from './raw_tag_editor'; -import { uiTagReference } from './tag_reference'; -import { uiPresetEditor } from './preset_editor'; -import { uiEntityIssues } from './entity_issues'; -import { uiSelectionList } from './selection_list'; -import { uiTooltipHtml } from './tooltipHtml'; import { utilArrayIdentical } from '../util/array'; import { utilCleanTags, utilCombinedTags, utilRebind } from '../util'; +import { uiSectionEntityIssues } from './sections/entity_issues'; +import { uiSectionFeatureType } from './sections/feature_type'; +import { uiSectionPresetFields } from './sections/preset_fields'; +import { uiSectionRawMemberEditor } from './sections/raw_member_editor'; +import { uiSectionRawMembershipEditor } from './sections/raw_membership_editor'; +import { uiSectionRawTagEditor } from './sections/raw_tag_editor'; +import { uiSectionSelectionList } from './sections/selection_list'; export function uiEntityEditor(context) { var dispatch = d3_dispatch('choose'); @@ -30,22 +25,12 @@ export function uiEntityEditor(context) { var _base; var _entityIDs; var _activePresets = []; - var _tagReference; var _newFeature; - var selectionList = uiSelectionList(context); - var entityIssues = uiEntityIssues(context); - var quickLinks = uiQuickLinks(); - var presetEditor = uiPresetEditor(context).on('change', changeTags).on('revert', revertTags); - var rawTagEditor = uiRawTagEditor(context).on('change', changeTags); - var rawMemberEditor = uiRawMemberEditor(context); - var rawMembershipEditor = uiRawMembershipEditor(context); + var _sections; function entityEditor(selection) { - var singularEntityID = _entityIDs.length === 1 && _entityIDs[0]; - var singularEntity = singularEntityID && context.entity(singularEntityID); - var combinedTags = utilCombinedTags(_entityIDs, context.graph()); // Header @@ -76,7 +61,7 @@ export function uiEntityEditor(context) { .merge(headerEnter); header.selectAll('h3') - .text(singularEntityID ? t('inspector.edit') : t('inspector.edit_features')); + .text(_entityIDs.length === 1 ? t('inspector.edit') : t('inspector.edit_features')); header.selectAll('.preset-reset') .on('click', function() { @@ -96,231 +81,54 @@ export function uiEntityEditor(context) { body = body .merge(bodyEnter); - var sectionInfos = [ - { - klass: 'selected-features inspector-inner', - shouldHave: _entityIDs.length > 1, - update: function(section) { - section - .call(selectionList - .selectedIDs(_entityIDs) - ); - } - }, - { - klass: 'preset-list-item inspector-inner', - update: function(section) { + if (!_sections) { + _sections = [ + uiSectionSelectionList(context), + uiSectionFeatureType(context).on('choose', function(presets) { + dispatch.call('choose', this, presets); + }), + uiSectionEntityIssues(context), + uiSectionPresetFields(context).on('change', changeTags).on('revert', revertTags), + uiSectionRawTagEditor(context).on('change', changeTags), + uiSectionRawMemberEditor(context), + uiSectionRawMembershipEditor(context) + ]; + } - section.classed('mixed-types', _activePresets.length > 1); - - section - .call( - uiDisclosure(context, 'feature_type', true) - .title(t('inspector.feature_type')) - .content(renderFeatureType) - ); - - function renderFeatureType(selection) { - var presetButtonWrap = selection - .selectAll('.preset-list-button-wrap') - .data([0]) - .enter() - .append('div') - .attr('class', 'preset-list-button-wrap'); - - var presetButton = presetButtonWrap - .append('button') - .attr('class', 'preset-list-button preset-reset') - .call(tooltip() - .title(t('inspector.back_tooltip')) - .placement('bottom') - ); - - presetButton.append('div') - .attr('class', 'preset-icon-container'); - - presetButton - .append('div') - .attr('class', 'label') - .append('div') - .attr('class', 'label-inner'); - - presetButtonWrap.append('div') - .attr('class', 'accessory-buttons'); - - var tagReferenceBodyWrap = selection - .selectAll('.tag-reference-body-wrap') - .data([0]); - - tagReferenceBodyWrap = tagReferenceBodyWrap - .enter() - .append('div') - .attr('class', 'tag-reference-body-wrap') - .merge(tagReferenceBodyWrap); - - selection - .selectAll('.preset-quick-links') - .data([0]) - .enter() - .append('div') - .attr('class', 'preset-quick-links') - .call(quickLinks.choices([{ - id: 'zoom_to', - label: 'inspector.zoom_to.title', - tooltip: function() { - return uiTooltipHtml(t('inspector.zoom_to.tooltip_feature'), t('inspector.zoom_to.key')); - }, - click: function zoomTo() { - context.mode().zoomToSelected(); - } - }])); - - // update header - if (_tagReference) { - selection.selectAll('.preset-list-button-wrap .accessory-buttons') - .style('display', _activePresets.length === 1 ? null : 'none') - .call(_tagReference.button); - - tagReferenceBodyWrap - .style('display', _activePresets.length === 1 ? null : 'none') - .call(_tagReference.body); - } - - selection.selectAll('.preset-reset') - .on('click', function() { - dispatch.call('choose', this, _activePresets); - }) - .on('mousedown', function() { - d3_event.preventDefault(); - d3_event.stopPropagation(); - }) - .on('mouseup', function() { - d3_event.preventDefault(); - d3_event.stopPropagation(); - }); - - var geometries = entityGeometries(); - selection.select('.preset-list-item button') - .call(uiPresetIcon(context) - .geometry(_activePresets.length === 1 ? (geometries.length === 1 && geometries[0]) : null) - .preset(_activePresets.length === 1 ? _activePresets[0] : context.presets().item('point')) - ); - - // NOTE: split on en-dash, not a hypen (to avoid conflict with hyphenated names) - var names = _activePresets.length === 1 ? _activePresets[0].name().split(' – ') : [t('inspector.multiple_types')]; - - var label = selection.select('.label-inner'); - var nameparts = label.selectAll('.namepart') - .data(names, function(d) { return d; }); - - nameparts.exit() - .remove(); - - nameparts - .enter() - .append('div') - .attr('class', 'namepart') - .text(function(d) { return d; }); - } - } - }, - { - klass: 'entity-issues', - update: function(section) { - section - .call(entityIssues - .entityIDs(_entityIDs) - ); - } - }, { - klass: 'preset-editor', - update: function(section) { - section - .call(presetEditor - .presets(_activePresets) - .entityIDs(_entityIDs) - .tags(combinedTags) - .state(_state) - ); - } - }, { - klass: 'raw-tag-editor inspector-inner', - update: function(section) { - section - .call(rawTagEditor - .preset(_activePresets[0]) - .entityIDs(_entityIDs) - .tags(combinedTags) - .state(_state) - ); - } - }, { - klass: 'raw-member-editor inspector-inner', - shouldHave: singularEntity && singularEntity.type === 'relation', - update: function(section) { - section - .call(rawMemberEditor - .entityID(singularEntityID) - ); - } - }, { - klass: 'raw-membership-editor inspector-inner', - shouldHave: singularEntityID, - update: function(section) { - section - .call(rawMembershipEditor - .entityID(singularEntityID) - ); - } - }, { - klass: 'key-trap-wrap', - create: function(sectionEnter) { - sectionEnter - .append('input') - .attr('type', 'text') - .attr('class', 'key-trap') - .on('keydown.key-trap', function() { - // On tabbing, send focus back to the first field on the inspector-body - // (probably the `name` field) #4159 - if (d3_event.keyCode === 9 && !d3_event.shiftKey) { - d3_event.preventDefault(); - body.select('input').node().focus(); - } - }); - } + _sections.forEach(function(section) { + if (section.entityIDs) { + section.entityIDs(_entityIDs); } - ]; - - sectionInfos = sectionInfos.filter(function(info) { - return info.shouldHave === undefined || info.shouldHave; + if (section.presets) { + section.presets(_activePresets); + } + if (section.tags) { + section.tags(combinedTags); + } + if (section.state) { + section.state(_state); + } + body.call(section.render); }); - var sections = body.selectAll('.section') - .data(sectionInfos, function(d) { return d.klass; }); - - sections.exit().remove(); - - var sectionsEnter = sections.enter() + body + .selectAll('.key-trap-wrap') + .data([0]) + .enter() .append('div') - .attr('class', function(d) { - return 'section ' + d.klass; + .attr('class', 'key-trap-wrap') + .append('input') + .attr('type', 'text') + .attr('class', 'key-trap') + .on('keydown.key-trap', function() { + // On tabbing, send focus back to the first field on the inspector-body + // (probably the `name` field) #4159 + if (d3_event.keyCode === 9 && !d3_event.shiftKey) { + d3_event.preventDefault(); + body.select('input').node().focus(); + } }); - sectionsEnter.each(function(d) { - if (d.create) { - d.create(d3_select(this)); - } - }); - - sections = sectionsEnter - .merge(sections); - - sections.each(function(d) { - if (d.update) { - d.update(d3_select(this)); - } - }); - context.history() .on('change.entity-editor', historyChanged); @@ -535,32 +343,10 @@ export function uiEntityEditor(context) { // don't reload the same preset if (!utilArrayIdentical(val, _activePresets)) { - _activePresets = val; - - var geometries = entityGeometries(); - if (_activePresets.length === 1 && geometries.length) { - _tagReference = uiTagReference(_activePresets[0].reference(geometries[0]), context) - .showing(false); - } } return entityEditor; }; - function entityGeometries() { - - var counts = {}; - - for (var i in _entityIDs) { - var geometry = context.geometry(_entityIDs[i]); - if (!counts[geometry]) counts[geometry] = 0; - counts[geometry] += 1; - } - - return Object.keys(counts).sort(function(geom1, geom2) { - return counts[geom2] - counts[geom1]; - }); - } - return utilRebind(entityEditor, dispatch, 'on'); } diff --git a/modules/ui/index.js b/modules/ui/index.js index 256d57a7f..2e7b6d78a 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -43,17 +43,12 @@ export { uiNoteComments } from './note_comments'; export { uiNoteEditor } from './note_editor'; export { uiNoteHeader } from './note_header'; export { uiNoteReport } from './note_report'; -export { uiPresetEditor } from './preset_editor'; export { uiPresetIcon } from './preset_icon'; export { uiPresetList } from './preset_list'; export { uiQuickLinks } from './quick_links'; export { uiRadialMenu } from './radial_menu'; -export { uiRawMemberEditor } from './raw_member_editor'; -export { uiRawMembershipEditor } from './raw_membership_editor'; -export { uiRawTagEditor } from './raw_tag_editor'; export { uiRestore } from './restore'; export { uiScale } from './scale'; -export { uiSelectionList } from './selection_list'; export { uiSidebar } from './sidebar'; export { uiSourceSwitch } from './source_switch'; export { uiSpinner } from './spinner'; diff --git a/modules/ui/intro/navigation.js b/modules/ui/intro/navigation.js index 304bb972c..1beefcbde 100644 --- a/modules/ui/intro/navigation.js +++ b/modules/ui/intro/navigation.js @@ -304,7 +304,7 @@ export function uiIntroNavigation(context, reveal) { } }); - reveal('.inspector-body .preset-list-item.inspector-inner', + reveal('.entity-editor .preset-list-item', t('intro.navigation.preset_townhall', { preset: preset.name() }), { buttonText: t('intro.ok'), buttonCallback: onClick } ); diff --git a/modules/ui/panes/index.js b/modules/ui/panes/index.js new file mode 100644 index 000000000..15966d69e --- /dev/null +++ b/modules/ui/panes/index.js @@ -0,0 +1,5 @@ +export { uiPaneBackground } from './background'; +export { uiPaneHelp } from './help'; +export { uiPaneIssues } from './issues'; +export { uiPaneMapData } from './map_data'; +export { uiPanePreferences } from './preferences'; diff --git a/modules/ui/raw_membership_editor.js b/modules/ui/raw_membership_editor.js deleted file mode 100644 index 78422ec6b..000000000 --- a/modules/ui/raw_membership_editor.js +++ /dev/null @@ -1,442 +0,0 @@ -import { - event as d3_event, - select as d3_select -} from 'd3-selection'; - -import { t, textDirection } from '../util/locale'; - -import { actionAddEntity } from '../actions/add_entity'; -import { actionAddMember } from '../actions/add_member'; -import { actionChangeMember } from '../actions/change_member'; -import { actionDeleteMember } from '../actions/delete_member'; - -import { modeSelect } from '../modes/select'; -import { osmEntity, osmRelation } from '../osm'; -import { services } from '../services'; -import { svgIcon } from '../svg/icon'; -import { uiCombobox } from './combobox'; -import { uiDisclosure } from './disclosure'; -import { tooltip } from '../util/tooltip'; -import { utilArrayGroupBy, utilDisplayName, utilNoAuto, utilHighlightEntities } from '../util'; - - -export function uiRawMembershipEditor(context) { - var taginfo = services.taginfo; - var nearbyCombo = uiCombobox(context, 'parent-relation') - .minItems(1) - .fetcher(fetchNearbyRelations) - .itemsMouseEnter(function(d) { - if (d.relation) utilHighlightEntities([d.relation.id], true, context); - }) - .itemsMouseLeave(function(d) { - if (d.relation) utilHighlightEntities([d.relation.id], false, context); - }); - var _inChange = false; - var _entityID; - var _showBlank; - - - function selectRelation(d) { - d3_event.preventDefault(); - - // remove the hover-highlight styling - utilHighlightEntities([d.relation.id], false, context); - - context.enter(modeSelect(context, [d.relation.id])); - } - - - function changeRole(d) { - if (d === 0) return; // called on newrow (shoudn't happen) - if (_inChange) return; // avoid accidental recursive call #5731 - - var oldRole = d.member.role; - var newRole = d3_select(this).property('value'); - - if (oldRole !== newRole) { - _inChange = true; - context.perform( - actionChangeMember(d.relation.id, Object.assign({}, d.member, { role: newRole }), d.index), - t('operations.change_role.annotation') - ); - } - _inChange = false; - } - - - function addMembership(d, role) { - this.blur(); // avoid keeping focus on the button - _showBlank = false; - - var member = { id: _entityID, type: context.entity(_entityID).type, role: role }; - - if (d.relation) { - context.perform( - actionAddMember(d.relation.id, member), - t('operations.add_member.annotation') - ); - - } else { - var relation = osmRelation(); - context.perform( - actionAddEntity(relation), - actionAddMember(relation.id, member), - t('operations.add.annotation.relation') - ); - - context.enter(modeSelect(context, [relation.id]).newFeature(true)); - } - } - - - function deleteMembership(d) { - this.blur(); // avoid keeping focus on the button - if (d === 0) return; // called on newrow (shoudn't happen) - - // remove the hover-highlight styling - utilHighlightEntities([d.relation.id], false, context); - - context.perform( - actionDeleteMember(d.relation.id, d.index), - t('operations.delete_member.annotation') - ); - } - - - function fetchNearbyRelations(q, callback) { - var newRelation = { relation: null, value: t('inspector.new_relation') }; - - var result = []; - - var graph = context.graph(); - - function baseDisplayLabel(entity) { - var matched = context.presets().match(entity, graph); - var presetName = (matched && matched.name()) || t('inspector.relation'); - var entityName = utilDisplayName(entity) || ''; - - return presetName + ' ' + entityName; - } - - var explicitRelation = q && context.hasEntity(q.toLowerCase()); - if (explicitRelation && explicitRelation.type === 'relation' && explicitRelation.id !== _entityID) { - // loaded relation is specified explicitly, only show that - - result.push({ - relation: explicitRelation, - value: baseDisplayLabel(explicitRelation) + ' ' + explicitRelation.id - }); - } else { - - context.intersects(context.extent()).forEach(function(entity) { - if (entity.type !== 'relation' || entity.id === _entityID) return; - - var value = baseDisplayLabel(entity); - if (q && (value + ' ' + entity.id).toLowerCase().indexOf(q.toLowerCase()) === -1) return; - - result.push({ relation: entity, value: value }); - }); - - result.sort(function(a, b) { - return osmRelation.creationOrder(a.relation, b.relation); - }); - - // Dedupe identical names by appending relation id - see #2891 - var dupeGroups = Object.values(utilArrayGroupBy(result, 'value')) - .filter(function(v) { return v.length > 1; }); - - dupeGroups.forEach(function(group) { - group.forEach(function(obj) { - obj.value += ' ' + obj.relation.id; - }); - }); - } - - result.forEach(function(obj) { - obj.title = obj.value; - }); - - result.unshift(newRelation); - callback(result); - } - - - function rawMembershipEditor(selection) { - var entity = context.entity(_entityID); - var parents = context.graph().parentRelations(entity); - var memberships = []; - - parents.slice(0, 1000).forEach(function(relation) { - relation.members.forEach(function(member, index) { - if (member.id === entity.id) { - memberships.push({ relation: relation, member: member, index: index }); - } - }); - }); - - var gt = parents.length > 1000 ? '>' : ''; - selection.call(uiDisclosure(context, 'raw_membership_editor', true) - .title(t('inspector.relations_count', { count: gt + memberships.length })) - .expanded(true) - .updatePreference(false) - .on('toggled', function(expanded) { - if (expanded) { selection.node().parentNode.scrollTop += 200; } - }) - .content(content) - ); - - - function content(selection) { - var list = selection.selectAll('.member-list') - .data([0]); - - list = list.enter() - .append('ul') - .attr('class', 'member-list') - .merge(list); - - - var items = list.selectAll('li.member-row-normal') - .data(memberships, function(d) { - return osmEntity.key(d.relation) + ',' + d.index; - }); - - items.exit() - .each(unbind) - .remove(); - - // Enter - var itemsEnter = items.enter() - .append('li') - .attr('class', 'member-row member-row-normal form-field'); - - // highlight the relation in the map while hovering on the list item - itemsEnter.on('mouseover', function(d) { - utilHighlightEntities([d.relation.id], true, context); - }) - .on('mouseout', function(d) { - utilHighlightEntities([d.relation.id], false, context); - }); - - var labelEnter = itemsEnter - .append('label') - .attr('class', 'field-label') - .append('span') - .attr('class', 'label-text') - .append('a') - .attr('href', '#') - .on('click', selectRelation); - - labelEnter - .append('span') - .attr('class', 'member-entity-type') - .text(function(d) { - var matched = context.presets().match(d.relation, context.graph()); - return (matched && matched.name()) || t('inspector.relation'); - }); - - labelEnter - .append('span') - .attr('class', 'member-entity-name') - .text(function(d) { return utilDisplayName(d.relation); }); - - var wrapEnter = itemsEnter - .append('div') - .attr('class', 'form-field-input-wrap form-field-input-member'); - - wrapEnter - .append('input') - .attr('class', 'member-role') - .property('type', 'text') - .attr('maxlength', context.maxCharsForRelationRole()) - .attr('placeholder', t('inspector.role')) - .call(utilNoAuto) - .property('value', function(d) { return d.member.role; }) - .on('blur', changeRole) - .on('change', changeRole); - - wrapEnter - .append('button') - .attr('tabindex', -1) - .attr('class', 'remove form-field-button member-delete') - .call(svgIcon('#iD-operation-delete')) - .on('click', deleteMembership); - - if (taginfo) { - wrapEnter.each(bindTypeahead); - } - - - var newMembership = list.selectAll('.member-row-new') - .data(_showBlank ? [0] : []); - - // Exit - newMembership.exit() - .remove(); - - // Enter - var newMembershipEnter = newMembership.enter() - .append('li') - .attr('class', 'member-row member-row-new form-field'); - - newMembershipEnter - .append('label') - .attr('class', 'field-label') - .append('input') - .attr('placeholder', t('inspector.choose_relation')) - .attr('type', 'text') - .attr('class', 'member-entity-input') - .call(utilNoAuto); - - var newWrapEnter = newMembershipEnter - .append('div') - .attr('class', 'form-field-input-wrap form-field-input-member'); - - newWrapEnter - .append('input') - .attr('class', 'member-role') - .property('type', 'text') - .attr('maxlength', context.maxCharsForRelationRole()) - .attr('placeholder', t('inspector.role')) - .call(utilNoAuto); - - newWrapEnter - .append('button') - .attr('tabindex', -1) - .attr('class', 'remove form-field-button member-delete') - .call(svgIcon('#iD-operation-delete')) - .on('click', function() { - list.selectAll('.member-row-new') - .remove(); - }); - - // Update - newMembership = newMembership - .merge(newMembershipEnter); - - newMembership.selectAll('.member-entity-input') - .on('blur', cancelEntity) // if it wasn't accepted normally, cancel it - .call(nearbyCombo - .on('accept', acceptEntity) - .on('cancel', cancelEntity) - ); - - - // Container for the Add button - var addRow = selection.selectAll('.add-row') - .data([0]); - - // enter - var addRowEnter = addRow.enter() - .append('div') - .attr('class', 'add-row'); - - var addRelationButton = addRowEnter - .append('button') - .attr('class', 'add-relation'); - - addRelationButton - .call(svgIcon('#iD-icon-plus', 'light')); - addRelationButton - .call(tooltip().title(t('inspector.add_to_relation')).placement(textDirection === 'ltr' ? 'right' : 'left')); - - addRowEnter - .append('div') - .attr('class', 'space-value'); // preserve space - - addRowEnter - .append('div') - .attr('class', 'space-buttons'); // preserve space - - // update - addRow = addRow - .merge(addRowEnter); - - addRow.select('.add-relation') - .on('click', function() { - _showBlank = true; - content(selection); - list.selectAll('.member-entity-input').node().focus(); - }); - - - function acceptEntity(d) { - if (!d) { - cancelEntity(); - return; - } - // remove hover-higlighting - if (d.relation) utilHighlightEntities([d.relation.id], false, context); - - var role = list.selectAll('.member-row-new .member-role').property('value'); - addMembership(d, role); - } - - - function cancelEntity() { - var input = newMembership.selectAll('.member-entity-input'); - input.property('value', ''); - - // remove hover-higlighting - context.surface().selectAll('.highlighted') - .classed('highlighted', false); - } - - - function bindTypeahead(d) { - var row = d3_select(this); - var role = row.selectAll('input.member-role'); - var origValue = role.property('value'); - - function sort(value, data) { - var sameletter = []; - var other = []; - for (var i = 0; i < data.length; i++) { - if (data[i].value.substring(0, value.length) === value) { - sameletter.push(data[i]); - } else { - other.push(data[i]); - } - } - return sameletter.concat(other); - } - - role.call(uiCombobox(context, 'member-role') - .fetcher(function(role, callback) { - var rtype = d.relation.tags.type; - taginfo.roles({ - debounce: true, - rtype: rtype || '', - geometry: context.geometry(_entityID), - query: role - }, function(err, data) { - if (!err) callback(sort(role, data)); - }); - }) - .on('cancel', function() { - role.property('value', origValue); - }) - ); - } - - - function unbind() { - var row = d3_select(this); - - row.selectAll('input.member-role') - .call(uiCombobox.off); - } - } - } - - - rawMembershipEditor.entityID = function(val) { - if (!arguments.length) return _entityID; - _entityID = val; - _showBlank = false; - return rawMembershipEditor; - }; - - - return rawMembershipEditor; -} diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js deleted file mode 100644 index 8e3abdd93..000000000 --- a/modules/ui/raw_tag_editor.js +++ /dev/null @@ -1,656 +0,0 @@ -import { dispatch as d3_dispatch } from 'd3-dispatch'; -import { event as d3_event, select as d3_select } from 'd3-selection'; - -import { t } from '../util/locale'; -import { services } from '../services'; -import { svgIcon } from '../svg/icon'; -import { uiCombobox } from './combobox'; -import { uiDisclosure } from './disclosure'; -import { uiTagReference } from './tag_reference'; -import { utilArrayDifference, utilArrayIdentical } from '../util/array'; -import { 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 = []; - // the keys in the order we want them to display - var _orderedKeys = []; - var _showBlank = false; - var _updatePreference = true; - var _expanded = false; - var _pendingChange = null; - var _state; - var _preset; - var _tags; - var _entityIDs; - - - function rawTagEditor(selection) { - var count = Object.keys(_tags).filter(function(d) { return d; }).length; - - var disclosure = uiDisclosure(context, 'raw_tag_editor', false) - .title(t('inspector.tags_count', { count: count })) - .on('toggled', toggled) - .updatePreference(_updatePreference) - .content(content); - - // Sometimes we want to force the raw_tag_editor to be opened/closed.. - // When undefined, uiDisclosure will use the user's stored preference. - if (_expanded !== undefined) { - disclosure.expanded(_expanded); - } - - selection.call(disclosure); - - function toggled(expanded) { - _expanded = expanded; - if (expanded) { - selection.node().parentNode.scrollTop += 200; - } - } - } - - - function content(wrap) { - - // remove deleted keys - _orderedKeys = _orderedKeys.filter(function(key) { - return _tags[key] !== undefined; - }); - - // When switching to a different entity or changing the state (hover/select) - // reorder the keys alphabetically. - // We trigger this by emptying the `_orderedKeys` array, then it will be rebuilt here. - // Otherwise leave their order alone - #5857, #5927 - var all = Object.keys(_tags).sort(); - var missingKeys = utilArrayDifference(all, _orderedKeys); - for (var i in missingKeys) { - _orderedKeys.push(missingKeys[i]); - } - - // assemble row data - var rowData = _orderedKeys.map(function(key, i) { - return { index: i, key: key, value: _tags[key] }; - }); - - // append blank row last, if necessary - if (!rowData.length || _showBlank) { - _showBlank = false; - rowData.push({ index: rowData.length, key: '', value: '' }); - } - - - // View Options - var options = wrap.selectAll('.raw-tag-options') - .data([0]); - - options.exit() - .remove(); - - var optionsEnter = options.enter() - .insert('div', ':first-child') - .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 t('icons.' + 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('placeholder', t('inspector.key_value')) - .attr('spellcheck', 'false') - .merge(textarea); - - textarea - .call(utilGetSetValue, textData) - .each(setTextareaHeight) - .on('input', setTextareaHeight) - .on('blur', textChanged) - .on('change', textChanged); - - - // View as List - var list = wrap.selectAll('.tag-list') - .data([0]); - - list = list.enter() - .append('ul') - .attr('class', 'tag-list' + (_tagView !== 'list' ? ' hide' : '')) - .merge(list); - - - // Container for the Add button - var addRowEnter = wrap.selectAll('.add-row') - .data([0]) - .enter() - .append('div') - .attr('class', 'add-row' + (_tagView !== 'list' ? ' hide' : '')); - - addRowEnter - .append('button') - .attr('class', 'add-tag') - .call(svgIcon('#iD-icon-plus', 'light')) - .on('click', addTag); - - addRowEnter - .append('div') - .attr('class', 'space-value'); // preserve space - - addRowEnter - .append('div') - .attr('class', 'space-buttons'); // preserve space - - - // Tag list items - var items = list.selectAll('.tag-row') - .data(rowData, function(d) { return d.key; }); - - items.exit() - .each(unbind) - .remove(); - - - // Enter - var itemsEnter = items.enter() - .append('li') - .attr('class', 'tag-row') - .classed('readonly', isReadOnly); - - var innerWrap = itemsEnter.append('div') - .attr('class', 'inner-wrap'); - - innerWrap - .append('div') - .attr('class', 'key-wrap') - .append('input') - .property('type', 'text') - .attr('class', 'key') - .attr('maxlength', context.maxCharsForTagKey()) - .call(utilNoAuto) - .on('blur', keyChange) - .on('change', keyChange); - - innerWrap - .append('div') - .attr('class', 'value-wrap') - .append('input') - .property('type', 'text') - .attr('class', 'value') - .attr('maxlength', context.maxCharsForTagValue()) - .call(utilNoAuto) - .on('blur', valueChange) - .on('change', valueChange) - .on('keydown.push-more', pushMore); - - innerWrap - .append('button') - .attr('tabindex', -1) - .attr('class', 'form-field-button remove') - .attr('title', t('icons.remove')) - .call(svgIcon('#iD-operation-delete')); - - - // Update - items = items - .merge(itemsEnter) - .sort(function(a, b) { return a.index - b.index; }); - - items - .each(function(d) { - var row = d3_select(this); - var key = row.select('input.key'); // propagate bound data - var value = row.select('input.value'); // propagate bound data - - if (_entityIDs && taginfo && _state !== 'hover') { - bindTypeahead(key, value); - } - - var reference; - - if (typeof d.value !== 'string') { - reference = uiTagReference({ key: d.key }, context); - } else { - var isRelation = _entityIDs && _entityIDs.some(function(entityID) { - return context.entity(entityID).type === 'relation'; - }); - if (isRelation && d.key === 'type') { - reference = uiTagReference({ rtype: d.value }, context); - } else { - reference = uiTagReference({ key: d.key, value: d.value }, context); - } - } - - if (_state === 'hover') { - reference.showing(false); - } - - row.select('.inner-wrap') // propagate bound data - .call(reference.button); - - row.call(reference.body); - - row.select('button.remove'); // propagate bound data - }); - - items.selectAll('input.key') - .attr('title', function(d) { return d.key; }) - .call(utilGetSetValue, function(d) { return d.key; }) - .attr('readonly', function(d) { - return (isReadOnly(d) || (typeof d.value !== 'string')) || null; - }); - - items.selectAll('input.value') - .attr('title', function(d) { - return Array.isArray(d.value) ? d.value.filter(Boolean).join('\n') : d.value; - }) - .classed('mixed', function(d) { - return Array.isArray(d.value); - }) - .attr('placeholder', function(d) { - return typeof d.value === 'string' ? null : t('inspector.multiple_values'); - }) - .call(utilGetSetValue, function(d) { - return typeof d.value === 'string' ? d.value : ''; - }) - .attr('readonly', function(d) { - return isReadOnly(d) || null; - }); - - items.selectAll('button.remove') - .on('mousedown', removeTag); // 'click' fires too late - #5878 - - - - function isReadOnly(d) { - for (var i = 0; i < _readOnlyTags.length; i++) { - if (d.key.match(_readOnlyTags[i]) !== null) { - return true; - } - } - return false; - } - - - 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) { - var rawVal = row.value; - if (typeof rawVal !== 'string') rawVal = '*'; - var val = rawVal ? stringify(rawVal) : ''; - return stringify(row.key) + '=' + val; - }) - .join('\n'); - - if (_state !== 'hover' && str.length) { - return str + '\n'; - } - return str; - } - - - function textChanged() { - var newText = this.value.trim(); - var newTags = {}; - var maxKeyLength = context.maxCharsForTagKey(); - var maxValueLength = context.maxCharsForTagValue(); - newText.split('\n').forEach(function(row) { - var m = row.match(/^\s*([^=]+)=(.*)$/); - if (m !== null) { - var k = unstringify(m[1].trim()).substr(0, maxKeyLength); - var v = unstringify(m[2].trim()).substr(0, maxValueLength); - newTags[k] = v; - } - }); - - var tagDiff = utilTagDiff(_tags, newTags); - if (!tagDiff.length) return; - - _pendingChange = _pendingChange || {}; - - tagDiff.forEach(function(change) { - if (isReadOnly({ key: change.key })) return; - - // skip unchanged multiselection placeholders - if (change.newVal === '*' && typeof change.oldVal !== 'string') return; - - if (change.type === '-') { - _pendingChange[change.key] = undefined; - } else if (change.type === '+') { - _pendingChange[change.key] = change.newVal || ''; - } - }); - - if (Object.keys(_pendingChange).length === 0) { - _pendingChange = null; - return; - } - - scheduleChange(); - } - - - function pushMore() { - // if pressing Tab on the last value field with content, add a blank row - if (d3_event.keyCode === 9 && !d3_event.shiftKey && - list.selectAll('li:last-child input.value').node() === this && - utilGetSetValue(d3_select(this))) { - addTag(); - } - } - - - function bindTypeahead(key, value) { - if (isReadOnly(key.datum())) return; - - if (Array.isArray(value.datum().value)) { - value.call(uiCombobox(context, 'tag-value') - .minItems(1) - .fetcher(function(value, callback) { - var keyString = utilGetSetValue(key); - if (!_tags[keyString]) return; - var data = _tags[keyString].filter(Boolean).map(function(tagValue) { - return { - value: tagValue, - title: tagValue - }; - }); - callback(data); - })); - return; - } - - var geometry = context.geometry(_entityIDs[0]); - - key.call(uiCombobox(context, 'tag-key') - .fetcher(function(value, callback) { - taginfo.keys({ - debounce: true, - geometry: geometry, - query: value - }, function(err, data) { - if (!err) { - var filtered = data.filter(function(d) { return _tags[d.value] === undefined; }); - callback(sort(value, filtered)); - } - }); - })); - - value.call(uiCombobox(context, 'tag-value') - .fetcher(function(value, callback) { - taginfo.values({ - debounce: true, - key: utilGetSetValue(key), - geometry: geometry, - query: value - }, function(err, data) { - if (!err) callback(sort(value, data)); - }); - })); - - - function sort(value, data) { - var sameletter = []; - var other = []; - for (var i = 0; i < data.length; i++) { - if (data[i].value.substring(0, value.length) === value) { - sameletter.push(data[i]); - } else { - other.push(data[i]); - } - } - return sameletter.concat(other); - } - } - - - function unbind() { - var row = d3_select(this); - - row.selectAll('input.key') - .call(uiCombobox.off); - - row.selectAll('input.value') - .call(uiCombobox.off); - } - - - function keyChange(d) { - if (d3_select(this).attr('readonly')) return; - - var kOld = d.key; - var kNew = this.value.trim(); - var row = this.parentNode.parentNode; - var inputVal = d3_select(row).selectAll('input.value'); - var vNew = utilGetSetValue(inputVal); - - // allow no change if the key should be readonly - if (isReadOnly({ key: kNew })) { - this.value = kOld; - return; - } - - // switch focus if key is already in use - if (kNew && kNew !== kOld) { - if (_tags[kNew] !== undefined) { // new key is already in use - this.value = kOld; // reset the key - list.selectAll('input.value') - .each(function(d) { - if (d.key === kNew) { // send focus to that other value combo instead - var input = d3_select(this).node(); - input.focus(); - input.select(); - } - }); - return; - } - } - - _pendingChange = _pendingChange || {}; - - // exit if we are currently about to delete this row anyway - #6366 - if (_pendingChange.hasOwnProperty(d.key) && _pendingChange[d.key] === undefined) return; - - if (kOld) { - _pendingChange[kOld] = undefined; - } - - _pendingChange[kNew] = vNew; - - // update the ordered key index so this row doesn't change position - var existingKeyIndex = _orderedKeys.indexOf(kOld); - if (existingKeyIndex !== -1) _orderedKeys[existingKeyIndex] = kNew; - - d.key = kNew; // update datum to avoid exit/enter on tag update - d.value = vNew; - - this.value = kNew; - utilGetSetValue(inputVal, vNew); - scheduleChange(); - } - - - function valueChange(d) { - if (isReadOnly(d)) return; - - // exit if this is a multiselection and no value was entered - if (typeof d.value !== 'string' && !this.value) return; - - _pendingChange = _pendingChange || {}; - - // exit if we are currently about to delete this row anyway - #6366 - if (_pendingChange.hasOwnProperty(d.key) && _pendingChange[d.key] === undefined) return; - - _pendingChange[d.key] = this.value; - scheduleChange(); - } - - - function removeTag(d) { - if (isReadOnly(d)) return; - - if (d.key === '') { // removing the blank row - _showBlank = false; - content(wrap); - - } else { - // remove the key from the ordered key index - _orderedKeys = _orderedKeys.filter(function(key) { return key !== d.key; }); - - _pendingChange = _pendingChange || {}; - _pendingChange[d.key] = undefined; - scheduleChange(); - } - } - - - function addTag() { - // Delay render in case this click is blurring an edited combo. - // Without the setTimeout, the `content` render would wipe out the pending tag change. - window.setTimeout(function() { - _showBlank = true; - content(wrap); - list.selectAll('li:last-child input.key').node().focus(); - }, 20); - } - - - function scheduleChange() { - // Delay change in case this change is blurring an edited combo. - #5878 - window.setTimeout(function() { - dispatch.call('change', this, _pendingChange); - _pendingChange = null; - }, 10); - } - - } - - - rawTagEditor.state = function(val) { - if (!arguments.length) return _state; - if (_state !== val) { - _orderedKeys = []; - _state = val; - } - return rawTagEditor; - }; - - - rawTagEditor.preset = function(val) { - if (!arguments.length) return _preset; - _preset = val; - if (_preset && _preset.isFallback()) { - _expanded = true; - _updatePreference = false; - } else { - _expanded = undefined; - _updatePreference = true; - } - return rawTagEditor; - }; - - - rawTagEditor.tags = function(val) { - if (!arguments.length) return _tags; - _tags = val; - return rawTagEditor; - }; - - - rawTagEditor.entityIDs = function(val) { - if (!arguments.length) return _entityIDs; - if (!_entityIDs || !val || !utilArrayIdentical(_entityIDs, val)) { - _entityIDs = val; - _orderedKeys = []; - } - return rawTagEditor; - }; - - - rawTagEditor.expanded = function(val) { - if (!arguments.length) return _expanded; - _expanded = val; - _updatePreference = false; - return rawTagEditor; - }; - - - // pass an array of regular expressions to test against the tag key - rawTagEditor.readOnlyTags = function(val) { - if (!arguments.length) return _readOnlyTags; - _readOnlyTags = val; - return rawTagEditor; - }; - - - return utilRebind(rawTagEditor, dispatch, 'on'); -} diff --git a/modules/ui/section.js b/modules/ui/section.js index 9eb752dc3..b4567c52b 100644 --- a/modules/ui/section.js +++ b/modules/ui/section.js @@ -9,13 +9,13 @@ import { utilFunctor } from '../util'; // Can be labeled and collapsible. export function uiSection(id, context) { - var _disclosure; var _title; var _expandedByDefault = utilFunctor(true); var _shouldDisplay; var _content; var _disclosureContent; + var _disclosure; var _containerSelection = d3_select(null); var section = { @@ -80,6 +80,10 @@ export function uiSection(id, context) { return _containerSelection; }; + section.disclosure = function() { + return _disclosure; + }; + // may be called multiple times function renderContent(selection) { if (_shouldDisplay) { @@ -95,6 +99,9 @@ export function uiSection(id, context) { if (!_disclosure) { _disclosure = uiDisclosure(context, id.replace(/-/g, '_'), _expandedByDefault()) .title(_title || '') + /*.on('toggled', function(expanded) { + if (expanded) { selection.node().parentNode.scrollTop += 200; } + })*/ .content(_disclosureContent); } selection diff --git a/modules/ui/entity_issues.js b/modules/ui/sections/entity_issues.js similarity index 84% rename from modules/ui/entity_issues.js rename to modules/ui/sections/entity_issues.js index 8c7f08ee9..6e684cad9 100644 --- a/modules/ui/entity_issues.js +++ b/modules/ui/sections/entity_issues.js @@ -1,70 +1,54 @@ import { event as d3_event, select as d3_select } from 'd3-selection'; -import { svgIcon } from '../svg/icon'; -import { t } from '../util/locale'; -import { uiDisclosure } from './disclosure'; -import { utilArrayIdentical } from '../util/array'; -import { utilHighlightEntities } from '../util'; +import { svgIcon } from '../../svg/icon'; +import { utilArrayIdentical } from '../../util/array'; +import { t } from '../../util/locale'; +import { utilHighlightEntities } from '../../util'; +import { uiSection } from '../section'; +export function uiSectionEntityIssues(context) { -export function uiEntityIssues(context) { - var _selection = d3_select(null); - var _activeIssueID; var _entityIDs = []; + var _issues = []; + var _activeIssueID; + + var section = uiSection('entity-issues', context) + .shouldDisplay(function() { + return _issues.length > 0; + }) + .title(function() { + return t('issues.list_title', { count: _issues.length }); + }) + .disclosureContent(renderDisclosureContent); - // Refresh on validated events context.validator() .on('validated.entity_issues', function() { - _selection.selectAll('.disclosure-wrap-entity_issues') - .call(render); - - update(); + // Refresh on validated events + reloadIssues(); + section.reRender(); }) .on('focusedIssue.entity_issues', function(issue) { makeActiveIssue(issue.id); }); - - function entityIssues(selection) { - _selection = selection; - - selection - .call(uiDisclosure(context, 'entity_issues', true) - .content(render) - ); - - update(); - } - - function getIssues() { - return context.validator().getSharedEntityIssues(_entityIDs, { includeDisabledRules: true }); + function reloadIssues() { + _issues = context.validator().getSharedEntityIssues(_entityIDs, { includeDisabledRules: true }); } function makeActiveIssue(issueID) { _activeIssueID = issueID; - _selection.selectAll('.issue-container') + section.selection().selectAll('.issue-container') .classed('active', function(d) { return d.id === _activeIssueID; }); } - function update() { + function renderDisclosureContent(selection) { - var issues = getIssues(); - - _selection - .classed('hide', issues.length === 0); - - _selection.selectAll('.hide-toggle-entity_issues span') - .text(t('issues.list_title', { count: issues.length })); - } - - - function render(selection) { - var issues = getIssues(); - _activeIssueID = issues.length > 0 ? issues[0].id : null; + selection.classed('grouped-items-area', true); + _activeIssueID = _issues.length > 0 ? _issues[0].id : null; var containers = selection.selectAll('.issue-container') - .data(issues, function(d) { return d.id; }); + .data(_issues, function(d) { return d.id; }); // Exit containers.exit() @@ -265,16 +249,15 @@ export function uiEntityIssues(context) { }); } - - entityIssues.entityIDs = function(val) { + section.entityIDs = function(val) { if (!arguments.length) return _entityIDs; if (!_entityIDs || !val || !utilArrayIdentical(_entityIDs, val)) { _entityIDs = val; _activeIssueID = null; + reloadIssues(); } - return entityIssues; + return section; }; - - return entityIssues; + return section; } diff --git a/modules/ui/sections/feature_type.js b/modules/ui/sections/feature_type.js new file mode 100644 index 000000000..962291478 --- /dev/null +++ b/modules/ui/sections/feature_type.js @@ -0,0 +1,177 @@ +import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { + event as d3_event +} from 'd3-selection'; + +import { utilArrayIdentical } from '../../util/array'; +import { t } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { utilRebind } from '../../util'; +import { uiPresetIcon } from '../preset_icon'; +import { uiQuickLinks } from '../quick_links'; +import { uiSection } from '../section'; +import { uiTagReference } from '../tag_reference'; +import { uiTooltipHtml } from '../tooltipHtml'; + + +export function uiSectionFeatureType(context) { + + var dispatch = d3_dispatch('choose'); + + var _entityIDs = []; + var _presets = []; + + var _tagReference; + var _quickLinks = uiQuickLinks(); + + var section = uiSection('feature-type', context) + .title(t('inspector.feature_type')) + .disclosureContent(renderDisclosureContent); + + function renderDisclosureContent(selection) { + + selection.classed('preset-list-item', true); + selection.classed('mixed-types', _presets.length > 1); + + var presetButtonWrap = selection + .selectAll('.preset-list-button-wrap') + .data([0]) + .enter() + .append('div') + .attr('class', 'preset-list-button-wrap'); + + var presetButton = presetButtonWrap + .append('button') + .attr('class', 'preset-list-button preset-reset') + .call(tooltip() + .title(t('inspector.back_tooltip')) + .placement('bottom') + ); + + presetButton.append('div') + .attr('class', 'preset-icon-container'); + + presetButton + .append('div') + .attr('class', 'label') + .append('div') + .attr('class', 'label-inner'); + + presetButtonWrap.append('div') + .attr('class', 'accessory-buttons'); + + var tagReferenceBodyWrap = selection + .selectAll('.tag-reference-body-wrap') + .data([0]); + + tagReferenceBodyWrap = tagReferenceBodyWrap + .enter() + .append('div') + .attr('class', 'tag-reference-body-wrap') + .merge(tagReferenceBodyWrap); + + selection + .selectAll('.preset-quick-links') + .data([0]) + .enter() + .append('div') + .attr('class', 'preset-quick-links') + .call(_quickLinks.choices([{ + id: 'zoom_to', + label: 'inspector.zoom_to.title', + tooltip: function() { + return uiTooltipHtml(t('inspector.zoom_to.tooltip_feature'), t('inspector.zoom_to.key')); + }, + click: function zoomTo() { + context.mode().zoomToSelected(); + } + }])); + + // update header + if (_tagReference) { + selection.selectAll('.preset-list-button-wrap .accessory-buttons') + .style('display', _presets.length === 1 ? null : 'none') + .call(_tagReference.button); + + tagReferenceBodyWrap + .style('display', _presets.length === 1 ? null : 'none') + .call(_tagReference.body); + } + + selection.selectAll('.preset-reset') + .on('click', function() { + dispatch.call('choose', this, _presets); + }) + .on('mousedown', function() { + d3_event.preventDefault(); + d3_event.stopPropagation(); + }) + .on('mouseup', function() { + d3_event.preventDefault(); + d3_event.stopPropagation(); + }); + + var geometries = entityGeometries(); + selection.select('.preset-list-item button') + .call(uiPresetIcon(context) + .geometry(_presets.length === 1 ? (geometries.length === 1 && geometries[0]) : null) + .preset(_presets.length === 1 ? _presets[0] : context.presets().item('point')) + ); + + // NOTE: split on en-dash, not a hypen (to avoid conflict with hyphenated names) + var names = _presets.length === 1 ? _presets[0].name().split(' – ') : [t('inspector.multiple_types')]; + + var label = selection.select('.label-inner'); + var nameparts = label.selectAll('.namepart') + .data(names, function(d) { return d; }); + + nameparts.exit() + .remove(); + + nameparts + .enter() + .append('div') + .attr('class', 'namepart') + .text(function(d) { return d; }); + } + + section.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + _entityIDs = val; + return section; + }; + + section.presets = function(val) { + if (!arguments.length) return _presets; + + // don't reload the same preset + if (!utilArrayIdentical(val, _presets)) { + _presets = val; + + var geometries = entityGeometries(); + if (_presets.length === 1 && geometries.length) { + _tagReference = uiTagReference(_presets[0].reference(geometries[0]), context) + .showing(false); + } + } + + return section; + }; + + function entityGeometries() { + + var counts = {}; + + for (var i in _entityIDs) { + var geometry = context.geometry(_entityIDs[i]); + if (!counts[geometry]) counts[geometry] = 0; + counts[geometry] += 1; + } + + return Object.keys(counts).sort(function(geom1, geom2) { + return counts[geom2] - counts[geom1]; + }); + } + + return utilRebind(section, dispatch, 'on'); +} diff --git a/modules/ui/sections/index.js b/modules/ui/sections/index.js new file mode 100644 index 000000000..85ed8e252 --- /dev/null +++ b/modules/ui/sections/index.js @@ -0,0 +1,20 @@ +export { uiSectionBackgroundDisplayOptions } from './background_display_options'; +export { uiSectionBackgroundList } from './background_list'; +export { uiSectionBackgroundOffset } from './background_offset'; +export { uiSectionDataLayers } from './data_layers'; +export { uiSectionEntityIssues } from './entity_issues'; +export { uiSectionFeatureType } from './feature_type'; +export { uiSectionMapFeatures } from './map_features'; +export { uiSectionMapStyleOptions } from './map_style_options'; +export { uiSectionOverlayList } from './overlay_list'; +export { uiSectionPhotoOverlays } from './photo_overlays'; +export { uiSectionPresetFields } from './preset_fields'; +export { uiSectionPrivacy } from './privacy'; +export { uiSectionRawMemberEditor } from './raw_member_editor'; +export { uiSectionRawMembershipEditor } from './raw_membership_editor'; +export { uiSectionRawTagEditor } from './raw_tag_editor'; +export { uiSectionSelectionList } from './selection_list'; +export { uiSectionValidationIssues } from './validation_issues'; +export { uiSectionValidationOptions } from './validation_options'; +export { uiSectionValidationRules } from './validation_rules'; +export { uiSectionValidationStatus } from './validation_status'; diff --git a/modules/ui/preset_editor.js b/modules/ui/sections/preset_fields.js similarity index 82% rename from modules/ui/preset_editor.js rename to modules/ui/sections/preset_fields.js index a63e4a06a..64401b063 100644 --- a/modules/ui/preset_editor.js +++ b/modules/ui/sections/preset_fields.js @@ -1,20 +1,25 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; - import { event as d3_event, select as d3_select } from 'd3-selection'; -import { currentLocale, t } from '../util/locale'; -import { modeBrowse } from '../modes/browse'; -import { uiDisclosure } from './disclosure'; -import { uiField } from './field'; -import { uiFormFields } from './form_fields'; -import { utilArrayIdentical } from '../util/array'; -import { utilArrayUnion, utilRebind } from '../util'; +import { utilArrayIdentical } from '../../util/array'; +import { utilArrayUnion, utilRebind } from '../../util'; +import { currentLocale, t } from '../../util/locale'; +import { modeBrowse } from '../../modes/browse'; +import { uiField } from '../field'; +import { uiFormFields } from '../form_fields'; +import { uiSection } from '../section'; +export function uiSectionPresetFields(context) { + + var section = uiSection('preset-fields', context) + .title(function() { + return t('inspector.fields'); + }) + .disclosureContent(renderDisclosureContent); -export function uiPresetEditor(context) { var dispatch = d3_dispatch('change', 'revert'); var formFields = uiFormFields(context); var _state; @@ -23,16 +28,7 @@ export function uiPresetEditor(context) { var _tags; var _entityIDs; - - function presetEditor(selection) { - selection.call(uiDisclosure(context, 'preset_fields', true) - .title(t('inspector.fields')) - .content(render) - ); - } - - - function render(selection) { + function renderDisclosureContent(selection) { if (!_fieldsArr) { var graph = context.graph(); @@ -123,7 +119,7 @@ export function uiPresetEditor(context) { .call(formFields .fieldsArr(_fieldsArr) .state(_state) - .klass('inspector-inner fillL3') + .klass('grouped-items-area') ); @@ -136,41 +132,36 @@ export function uiPresetEditor(context) { }); } - - presetEditor.presets = function(val) { + section.presets = function(val) { if (!arguments.length) return _presets; if (!_presets || !val || !utilArrayIdentical(_presets, val)) { _presets = val; _fieldsArr = null; } - return presetEditor; + return section; }; - - presetEditor.state = function(val) { + section.state = function(val) { if (!arguments.length) return _state; _state = val; - return presetEditor; + return section; }; - - presetEditor.tags = function(val) { + section.tags = function(val) { if (!arguments.length) return _tags; _tags = val; // Don't reset _fieldsArr here. - return presetEditor; + return section; }; - - presetEditor.entityIDs = function(val) { + section.entityIDs = function(val) { if (!arguments.length) return _entityIDs; if (!val || !_entityIDs || !utilArrayIdentical(_entityIDs, val)) { _entityIDs = val; _fieldsArr = null; } - return presetEditor; + return section; }; - - return utilRebind(presetEditor, dispatch, 'on'); + return utilRebind(section, dispatch, 'on'); } diff --git a/modules/ui/raw_member_editor.js b/modules/ui/sections/raw_member_editor.js similarity index 87% rename from modules/ui/raw_member_editor.js rename to modules/ui/sections/raw_member_editor.js index bf6a616be..d810ae425 100644 --- a/modules/ui/raw_member_editor.js +++ b/modules/ui/sections/raw_member_editor.js @@ -4,24 +4,40 @@ import { select as d3_select } from 'd3-selection'; -import { t } from '../util/locale'; -import { actionChangeMember } from '../actions/change_member'; -import { actionDeleteMember } from '../actions/delete_member'; -import { actionMoveMember } from '../actions/move_member'; -import { modeBrowse } from '../modes/browse'; -import { modeSelect } from '../modes/select'; -import { osmEntity } from '../osm'; -import { svgIcon } from '../svg/icon'; -import { services } from '../services'; -import { uiCombobox } from './combobox'; -import { uiDisclosure } from './disclosure'; -import { utilDisplayName, utilDisplayType, utilHighlightEntities, utilNoAuto } from '../util'; +import { t } from '../../util/locale'; +import { actionChangeMember } from '../../actions/change_member'; +import { actionDeleteMember } from '../../actions/delete_member'; +import { actionMoveMember } from '../../actions/move_member'; +import { modeBrowse } from '../../modes/browse'; +import { modeSelect } from '../../modes/select'; +import { osmEntity } from '../../osm'; +import { svgIcon } from '../../svg/icon'; +import { services } from '../../services'; +import { uiCombobox } from '../combobox'; +import { uiSection } from '../section'; +import { utilDisplayName, utilDisplayType, utilHighlightEntities, utilNoAuto } from '../../util'; -export function uiRawMemberEditor(context) { +export function uiSectionRawMemberEditor(context) { + + var section = uiSection('raw-member-editor', context) + .shouldDisplay(function() { + if (!_entityIDs || _entityIDs.length !== 1) return false; + + var entity = context.hasEntity(_entityIDs[0]); + return entity && entity.type === 'relation'; + }) + .title(function() { + var entity = context.hasEntity(_entityIDs[0]); + if (!entity) return ''; + + var gt = entity.members.length > _maxMembers ? '>' : ''; + return t('inspector.members_count', { count: gt + entity.members.slice(0, _maxMembers).length }); + }) + .disclosureContent(renderDisclosureContent); + var taginfo = services.taginfo; - var _entityID; - var _contentSelection = d3_select(null); + var _entityIDs; var _maxMembers = 1000; function downloadMember(d) { @@ -30,7 +46,7 @@ export function uiRawMemberEditor(context) { // display the loading indicator d3_select(this.parentNode).classed('tag-reference-loading', true); context.loadEntity(d.id, function() { - updateDisclosureContent(_contentSelection); + section.reRender(); }); } @@ -91,11 +107,12 @@ export function uiRawMemberEditor(context) { } } - function updateDisclosureContent(selection) { - _contentSelection = selection; + function renderDisclosureContent(selection) { + + var entityID = _entityIDs[0]; var memberships = []; - var entity = context.entity(_entityID); + var entity = context.entity(entityID); entity.members.slice(0, _maxMembers).forEach(function(member, index) { memberships.push({ index: index, @@ -363,29 +380,12 @@ export function uiRawMemberEditor(context) { } } - function rawMemberEditor(selection) { - var entity = context.entity(_entityID); - - var gt = entity.members.length > _maxMembers ? '>' : ''; - selection.call(uiDisclosure(context, 'raw_member_editor', true) - .title(t('inspector.members_count', { count: gt + entity.members.slice(0, _maxMembers).length })) - .expanded(true) - .updatePreference(false) - .on('toggled', function(expanded) { - if (expanded) { - selection.node().parentNode.scrollTop += 200; - } - }) - .content(updateDisclosureContent) - ); - } - - rawMemberEditor.entityID = function(val) { - if (!arguments.length) return _entityID; - _entityID = val; - return rawMemberEditor; + section.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + _entityIDs = val; + return section; }; - return rawMemberEditor; + return section; } diff --git a/modules/ui/sections/raw_membership_editor.js b/modules/ui/sections/raw_membership_editor.js new file mode 100644 index 000000000..4db0a2f6f --- /dev/null +++ b/modules/ui/sections/raw_membership_editor.js @@ -0,0 +1,448 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { t, textDirection } from '../../util/locale'; + +import { actionAddEntity } from '../../actions/add_entity'; +import { actionAddMember } from '../../actions/add_member'; +import { actionChangeMember } from '../../actions/change_member'; +import { actionDeleteMember } from '../../actions/delete_member'; + +import { modeSelect } from '../../modes/select'; +import { osmEntity, osmRelation } from '../../osm'; +import { services } from '../../services'; +import { svgIcon } from '../../svg/icon'; +import { uiCombobox } from '../combobox'; +import { uiSection } from '../section'; +import { tooltip } from '../../util/tooltip'; +import { utilArrayGroupBy, utilDisplayName, utilNoAuto, utilHighlightEntities } from '../../util'; + + +export function uiSectionRawMembershipEditor(context) { + + var section = uiSection('raw-membership-editor', context) + .shouldDisplay(function() { + return _entityIDs && _entityIDs.length === 1; + }) + .title(function() { + var entity = context.hasEntity(_entityIDs[0]); + if (!entity) return ''; + + var parents = context.graph().parentRelations(entity); + var gt = parents.length > _maxMemberships ? '>' : ''; + return t('inspector.relations_count', { count: gt + parents.slice(0, _maxMemberships).length }); + }) + .disclosureContent(renderDisclosureContent); + + var taginfo = services.taginfo; + var nearbyCombo = uiCombobox(context, 'parent-relation') + .minItems(1) + .fetcher(fetchNearbyRelations) + .itemsMouseEnter(function(d) { + if (d.relation) utilHighlightEntities([d.relation.id], true, context); + }) + .itemsMouseLeave(function(d) { + if (d.relation) utilHighlightEntities([d.relation.id], false, context); + }); + var _inChange = false; + var _entityIDs = []; + var _showBlank; + var _maxMemberships = 1000; + + function selectRelation(d) { + d3_event.preventDefault(); + + // remove the hover-highlight styling + utilHighlightEntities([d.relation.id], false, context); + + context.enter(modeSelect(context, [d.relation.id])); + } + + + function changeRole(d) { + if (d === 0) return; // called on newrow (shoudn't happen) + if (_inChange) return; // avoid accidental recursive call #5731 + + var oldRole = d.member.role; + var newRole = d3_select(this).property('value'); + + if (oldRole !== newRole) { + _inChange = true; + context.perform( + actionChangeMember(d.relation.id, Object.assign({}, d.member, { role: newRole }), d.index), + t('operations.change_role.annotation') + ); + } + _inChange = false; + } + + + function addMembership(d, role) { + this.blur(); // avoid keeping focus on the button + _showBlank = false; + + var member = { id: _entityIDs[0], type: context.entity(_entityIDs[0]).type, role: role }; + + if (d.relation) { + context.perform( + actionAddMember(d.relation.id, member), + t('operations.add_member.annotation') + ); + + } else { + var relation = osmRelation(); + context.perform( + actionAddEntity(relation), + actionAddMember(relation.id, member), + t('operations.add.annotation.relation') + ); + + context.enter(modeSelect(context, [relation.id]).newFeature(true)); + } + } + + + function deleteMembership(d) { + this.blur(); // avoid keeping focus on the button + if (d === 0) return; // called on newrow (shoudn't happen) + + // remove the hover-highlight styling + utilHighlightEntities([d.relation.id], false, context); + + context.perform( + actionDeleteMember(d.relation.id, d.index), + t('operations.delete_member.annotation') + ); + } + + + function fetchNearbyRelations(q, callback) { + var newRelation = { relation: null, value: t('inspector.new_relation') }; + + var entityID = _entityIDs[0]; + + var result = []; + + var graph = context.graph(); + + function baseDisplayLabel(entity) { + var matched = context.presets().match(entity, graph); + var presetName = (matched && matched.name()) || t('inspector.relation'); + var entityName = utilDisplayName(entity) || ''; + + return presetName + ' ' + entityName; + } + + var explicitRelation = q && context.hasEntity(q.toLowerCase()); + if (explicitRelation && explicitRelation.type === 'relation' && explicitRelation.id !== entityID) { + // loaded relation is specified explicitly, only show that + + result.push({ + relation: explicitRelation, + value: baseDisplayLabel(explicitRelation) + ' ' + explicitRelation.id + }); + } else { + + context.intersects(context.extent()).forEach(function(entity) { + if (entity.type !== 'relation' || entity.id === entityID) return; + + var value = baseDisplayLabel(entity); + if (q && (value + ' ' + entity.id).toLowerCase().indexOf(q.toLowerCase()) === -1) return; + + result.push({ relation: entity, value: value }); + }); + + result.sort(function(a, b) { + return osmRelation.creationOrder(a.relation, b.relation); + }); + + // Dedupe identical names by appending relation id - see #2891 + var dupeGroups = Object.values(utilArrayGroupBy(result, 'value')) + .filter(function(v) { return v.length > 1; }); + + dupeGroups.forEach(function(group) { + group.forEach(function(obj) { + obj.value += ' ' + obj.relation.id; + }); + }); + } + + result.forEach(function(obj) { + obj.title = obj.value; + }); + + result.unshift(newRelation); + callback(result); + } + + function renderDisclosureContent(selection) { + + var entityID = _entityIDs[0]; + + var entity = context.entity(entityID); + var parents = context.graph().parentRelations(entity); + + var memberships = []; + + parents.slice(0, _maxMemberships).forEach(function(relation) { + relation.members.forEach(function(member, index) { + if (member.id === entity.id) { + memberships.push({ relation: relation, member: member, index: index }); + } + }); + }); + + var list = selection.selectAll('.member-list') + .data([0]); + + list = list.enter() + .append('ul') + .attr('class', 'member-list') + .merge(list); + + + var items = list.selectAll('li.member-row-normal') + .data(memberships, function(d) { + return osmEntity.key(d.relation) + ',' + d.index; + }); + + items.exit() + .each(unbind) + .remove(); + + // Enter + var itemsEnter = items.enter() + .append('li') + .attr('class', 'member-row member-row-normal form-field'); + + // highlight the relation in the map while hovering on the list item + itemsEnter.on('mouseover', function(d) { + utilHighlightEntities([d.relation.id], true, context); + }) + .on('mouseout', function(d) { + utilHighlightEntities([d.relation.id], false, context); + }); + + var labelEnter = itemsEnter + .append('label') + .attr('class', 'field-label') + .append('span') + .attr('class', 'label-text') + .append('a') + .attr('href', '#') + .on('click', selectRelation); + + labelEnter + .append('span') + .attr('class', 'member-entity-type') + .text(function(d) { + var matched = context.presets().match(d.relation, context.graph()); + return (matched && matched.name()) || t('inspector.relation'); + }); + + labelEnter + .append('span') + .attr('class', 'member-entity-name') + .text(function(d) { return utilDisplayName(d.relation); }); + + var wrapEnter = itemsEnter + .append('div') + .attr('class', 'form-field-input-wrap form-field-input-member'); + + wrapEnter + .append('input') + .attr('class', 'member-role') + .property('type', 'text') + .attr('maxlength', context.maxCharsForRelationRole()) + .attr('placeholder', t('inspector.role')) + .call(utilNoAuto) + .property('value', function(d) { return d.member.role; }) + .on('blur', changeRole) + .on('change', changeRole); + + wrapEnter + .append('button') + .attr('tabindex', -1) + .attr('class', 'remove form-field-button member-delete') + .call(svgIcon('#iD-operation-delete')) + .on('click', deleteMembership); + + if (taginfo) { + wrapEnter.each(bindTypeahead); + } + + + var newMembership = list.selectAll('.member-row-new') + .data(_showBlank ? [0] : []); + + // Exit + newMembership.exit() + .remove(); + + // Enter + var newMembershipEnter = newMembership.enter() + .append('li') + .attr('class', 'member-row member-row-new form-field'); + + newMembershipEnter + .append('label') + .attr('class', 'field-label') + .append('input') + .attr('placeholder', t('inspector.choose_relation')) + .attr('type', 'text') + .attr('class', 'member-entity-input') + .call(utilNoAuto); + + var newWrapEnter = newMembershipEnter + .append('div') + .attr('class', 'form-field-input-wrap form-field-input-member'); + + newWrapEnter + .append('input') + .attr('class', 'member-role') + .property('type', 'text') + .attr('maxlength', context.maxCharsForRelationRole()) + .attr('placeholder', t('inspector.role')) + .call(utilNoAuto); + + newWrapEnter + .append('button') + .attr('tabindex', -1) + .attr('class', 'remove form-field-button member-delete') + .call(svgIcon('#iD-operation-delete')) + .on('click', function() { + list.selectAll('.member-row-new') + .remove(); + }); + + // Update + newMembership = newMembership + .merge(newMembershipEnter); + + newMembership.selectAll('.member-entity-input') + .on('blur', cancelEntity) // if it wasn't accepted normally, cancel it + .call(nearbyCombo + .on('accept', acceptEntity) + .on('cancel', cancelEntity) + ); + + + // Container for the Add button + var addRow = selection.selectAll('.add-row') + .data([0]); + + // enter + var addRowEnter = addRow.enter() + .append('div') + .attr('class', 'add-row'); + + var addRelationButton = addRowEnter + .append('button') + .attr('class', 'add-relation'); + + addRelationButton + .call(svgIcon('#iD-icon-plus', 'light')); + addRelationButton + .call(tooltip().title(t('inspector.add_to_relation')).placement(textDirection === 'ltr' ? 'right' : 'left')); + + addRowEnter + .append('div') + .attr('class', 'space-value'); // preserve space + + addRowEnter + .append('div') + .attr('class', 'space-buttons'); // preserve space + + // update + addRow = addRow + .merge(addRowEnter); + + addRow.select('.add-relation') + .on('click', function() { + _showBlank = true; + section.reRender(); + list.selectAll('.member-entity-input').node().focus(); + }); + + + function acceptEntity(d) { + if (!d) { + cancelEntity(); + return; + } + // remove hover-higlighting + if (d.relation) utilHighlightEntities([d.relation.id], false, context); + + var role = list.selectAll('.member-row-new .member-role').property('value'); + addMembership(d, role); + } + + + function cancelEntity() { + var input = newMembership.selectAll('.member-entity-input'); + input.property('value', ''); + + // remove hover-higlighting + context.surface().selectAll('.highlighted') + .classed('highlighted', false); + } + + + function bindTypeahead(d) { + var row = d3_select(this); + var role = row.selectAll('input.member-role'); + var origValue = role.property('value'); + + function sort(value, data) { + var sameletter = []; + var other = []; + for (var i = 0; i < data.length; i++) { + if (data[i].value.substring(0, value.length) === value) { + sameletter.push(data[i]); + } else { + other.push(data[i]); + } + } + return sameletter.concat(other); + } + + role.call(uiCombobox(context, 'member-role') + .fetcher(function(role, callback) { + var rtype = d.relation.tags.type; + taginfo.roles({ + debounce: true, + rtype: rtype || '', + geometry: context.geometry(entityID), + query: role + }, function(err, data) { + if (!err) callback(sort(role, data)); + }); + }) + .on('cancel', function() { + role.property('value', origValue); + }) + ); + } + + + function unbind() { + var row = d3_select(this); + + row.selectAll('input.member-role') + .call(uiCombobox.off); + } + } + + + section.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + _entityIDs = val; + _showBlank = false; + return section; + }; + + + return section; +} diff --git a/modules/ui/sections/raw_tag_editor.js b/modules/ui/sections/raw_tag_editor.js new file mode 100644 index 000000000..e219b9627 --- /dev/null +++ b/modules/ui/sections/raw_tag_editor.js @@ -0,0 +1,623 @@ +import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { event as d3_event, select as d3_select } from 'd3-selection'; + +import { services } from '../../services'; +import { svgIcon } from '../../svg/icon'; +import { uiCombobox } from '../combobox'; +import { uiSection } from '../section'; +import { uiTagReference } from '../tag_reference'; +import { t } from '../../util/locale'; +import { utilArrayDifference, utilArrayIdentical } from '../../util/array'; +import { utilGetSetValue, utilNoAuto, utilRebind, utilTagDiff } from '../../util'; + +export function uiSectionRawTagEditor(context) { + + var section = uiSection('raw-tag-editor', context) + .title(function() { + var count = Object.keys(_tags).filter(function(d) { return d; }).length; + return t('inspector.tags_count', { count: count }); + }) + .expandedByDefault(false) + .disclosureContent(renderDisclosureContent); + + 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 = []; + // the keys in the order we want them to display + var _orderedKeys = []; + var _showBlank = false; + var _updatePreference = true; + var _expanded = false; + var _pendingChange = null; + var _state; + var _presets; + var _tags; + var _entityIDs; + + function renderDisclosureContent(wrap) { + + // remove deleted keys + _orderedKeys = _orderedKeys.filter(function(key) { + return _tags[key] !== undefined; + }); + + // When switching to a different entity or changing the state (hover/select) + // reorder the keys alphabetically. + // We trigger this by emptying the `_orderedKeys` array, then it will be rebuilt here. + // Otherwise leave their order alone - #5857, #5927 + var all = Object.keys(_tags).sort(); + var missingKeys = utilArrayDifference(all, _orderedKeys); + for (var i in missingKeys) { + _orderedKeys.push(missingKeys[i]); + } + + // assemble row data + var rowData = _orderedKeys.map(function(key, i) { + return { index: i, key: key, value: _tags[key] }; + }); + + // append blank row last, if necessary + if (!rowData.length || _showBlank) { + _showBlank = false; + rowData.push({ index: rowData.length, key: '', value: '' }); + } + + + // View Options + var options = wrap.selectAll('.raw-tag-options') + .data([0]); + + options.exit() + .remove(); + + var optionsEnter = options.enter() + .insert('div', ':first-child') + .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 t('icons.' + 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('placeholder', t('inspector.key_value')) + .attr('spellcheck', 'false') + .merge(textarea); + + textarea + .call(utilGetSetValue, textData) + .each(setTextareaHeight) + .on('input', setTextareaHeight) + .on('blur', textChanged) + .on('change', textChanged); + + + // View as List + var list = wrap.selectAll('.tag-list') + .data([0]); + + list = list.enter() + .append('ul') + .attr('class', 'tag-list' + (_tagView !== 'list' ? ' hide' : '')) + .merge(list); + + + // Container for the Add button + var addRowEnter = wrap.selectAll('.add-row') + .data([0]) + .enter() + .append('div') + .attr('class', 'add-row' + (_tagView !== 'list' ? ' hide' : '')); + + addRowEnter + .append('button') + .attr('class', 'add-tag') + .call(svgIcon('#iD-icon-plus', 'light')) + .on('click', addTag); + + addRowEnter + .append('div') + .attr('class', 'space-value'); // preserve space + + addRowEnter + .append('div') + .attr('class', 'space-buttons'); // preserve space + + + // Tag list items + var items = list.selectAll('.tag-row') + .data(rowData, function(d) { return d.key; }); + + items.exit() + .each(unbind) + .remove(); + + + // Enter + var itemsEnter = items.enter() + .append('li') + .attr('class', 'tag-row') + .classed('readonly', isReadOnly); + + var innerWrap = itemsEnter.append('div') + .attr('class', 'inner-wrap'); + + innerWrap + .append('div') + .attr('class', 'key-wrap') + .append('input') + .property('type', 'text') + .attr('class', 'key') + .attr('maxlength', context.maxCharsForTagKey()) + .call(utilNoAuto) + .on('blur', keyChange) + .on('change', keyChange); + + innerWrap + .append('div') + .attr('class', 'value-wrap') + .append('input') + .property('type', 'text') + .attr('class', 'value') + .attr('maxlength', context.maxCharsForTagValue()) + .call(utilNoAuto) + .on('blur', valueChange) + .on('change', valueChange) + .on('keydown.push-more', pushMore); + + innerWrap + .append('button') + .attr('tabindex', -1) + .attr('class', 'form-field-button remove') + .attr('title', t('icons.remove')) + .call(svgIcon('#iD-operation-delete')); + + + // Update + items = items + .merge(itemsEnter) + .sort(function(a, b) { return a.index - b.index; }); + + items + .each(function(d) { + var row = d3_select(this); + var key = row.select('input.key'); // propagate bound data + var value = row.select('input.value'); // propagate bound data + + if (_entityIDs && taginfo && _state !== 'hover') { + bindTypeahead(key, value); + } + + var reference; + + if (typeof d.value !== 'string') { + reference = uiTagReference({ key: d.key }, context); + } else { + var isRelation = _entityIDs && _entityIDs.some(function(entityID) { + return context.entity(entityID).type === 'relation'; + }); + if (isRelation && d.key === 'type') { + reference = uiTagReference({ rtype: d.value }, context); + } else { + reference = uiTagReference({ key: d.key, value: d.value }, context); + } + } + + if (_state === 'hover') { + reference.showing(false); + } + + row.select('.inner-wrap') // propagate bound data + .call(reference.button); + + row.call(reference.body); + + row.select('button.remove'); // propagate bound data + }); + + items.selectAll('input.key') + .attr('title', function(d) { return d.key; }) + .call(utilGetSetValue, function(d) { return d.key; }) + .attr('readonly', function(d) { + return (isReadOnly(d) || (typeof d.value !== 'string')) || null; + }); + + items.selectAll('input.value') + .attr('title', function(d) { + return Array.isArray(d.value) ? d.value.filter(Boolean).join('\n') : d.value; + }) + .classed('mixed', function(d) { + return Array.isArray(d.value); + }) + .attr('placeholder', function(d) { + return typeof d.value === 'string' ? null : t('inspector.multiple_values'); + }) + .call(utilGetSetValue, function(d) { + return typeof d.value === 'string' ? d.value : ''; + }) + .attr('readonly', function(d) { + return isReadOnly(d) || null; + }); + + items.selectAll('button.remove') + .on('mousedown', removeTag); // 'click' fires too late - #5878 + + } + + function isReadOnly(d) { + for (var i = 0; i < _readOnlyTags.length; i++) { + if (d.key.match(_readOnlyTags[i]) !== null) { + return true; + } + } + return false; + } + + 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) { + var rawVal = row.value; + if (typeof rawVal !== 'string') rawVal = '*'; + var val = rawVal ? stringify(rawVal) : ''; + return stringify(row.key) + '=' + val; + }) + .join('\n'); + + if (_state !== 'hover' && str.length) { + return str + '\n'; + } + return str; + } + + function textChanged() { + var newText = this.value.trim(); + var newTags = {}; + var maxKeyLength = context.maxCharsForTagKey(); + var maxValueLength = context.maxCharsForTagValue(); + newText.split('\n').forEach(function(row) { + var m = row.match(/^\s*([^=]+)=(.*)$/); + if (m !== null) { + var k = unstringify(m[1].trim()).substr(0, maxKeyLength); + var v = unstringify(m[2].trim()).substr(0, maxValueLength); + newTags[k] = v; + } + }); + + var tagDiff = utilTagDiff(_tags, newTags); + if (!tagDiff.length) return; + + _pendingChange = _pendingChange || {}; + + tagDiff.forEach(function(change) { + if (isReadOnly({ key: change.key })) return; + + // skip unchanged multiselection placeholders + if (change.newVal === '*' && typeof change.oldVal !== 'string') return; + + if (change.type === '-') { + _pendingChange[change.key] = undefined; + } else if (change.type === '+') { + _pendingChange[change.key] = change.newVal || ''; + } + }); + + if (Object.keys(_pendingChange).length === 0) { + _pendingChange = null; + return; + } + + scheduleChange(); + } + + function pushMore() { + // if pressing Tab on the last value field with content, add a blank row + if (d3_event.keyCode === 9 && !d3_event.shiftKey && + section.selection().selectAll('.tag-list li:last-child input.value').node() === this && + utilGetSetValue(d3_select(this))) { + addTag(); + } + } + + function bindTypeahead(key, value) { + if (isReadOnly(key.datum())) return; + + if (Array.isArray(value.datum().value)) { + value.call(uiCombobox(context, 'tag-value') + .minItems(1) + .fetcher(function(value, callback) { + var keyString = utilGetSetValue(key); + if (!_tags[keyString]) return; + var data = _tags[keyString].filter(Boolean).map(function(tagValue) { + return { + value: tagValue, + title: tagValue + }; + }); + callback(data); + })); + return; + } + + var geometry = context.geometry(_entityIDs[0]); + + key.call(uiCombobox(context, 'tag-key') + .fetcher(function(value, callback) { + taginfo.keys({ + debounce: true, + geometry: geometry, + query: value + }, function(err, data) { + if (!err) { + var filtered = data.filter(function(d) { return _tags[d.value] === undefined; }); + callback(sort(value, filtered)); + } + }); + })); + + value.call(uiCombobox(context, 'tag-value') + .fetcher(function(value, callback) { + taginfo.values({ + debounce: true, + key: utilGetSetValue(key), + geometry: geometry, + query: value + }, function(err, data) { + if (!err) callback(sort(value, data)); + }); + })); + + + function sort(value, data) { + var sameletter = []; + var other = []; + for (var i = 0; i < data.length; i++) { + if (data[i].value.substring(0, value.length) === value) { + sameletter.push(data[i]); + } else { + other.push(data[i]); + } + } + return sameletter.concat(other); + } + } + + function unbind() { + var row = d3_select(this); + + row.selectAll('input.key') + .call(uiCombobox.off); + + row.selectAll('input.value') + .call(uiCombobox.off); + } + + function keyChange(d) { + if (d3_select(this).attr('readonly')) return; + + var kOld = d.key; + var kNew = this.value.trim(); + var row = this.parentNode.parentNode; + var inputVal = d3_select(row).selectAll('input.value'); + var vNew = utilGetSetValue(inputVal); + + // allow no change if the key should be readonly + if (isReadOnly({ key: kNew })) { + this.value = kOld; + return; + } + + // switch focus if key is already in use + if (kNew && kNew !== kOld) { + if (_tags[kNew] !== undefined) { // new key is already in use + this.value = kOld; // reset the key + section.selection().selectAll('.tag-list input.value') + .each(function(d) { + if (d.key === kNew) { // send focus to that other value combo instead + var input = d3_select(this).node(); + input.focus(); + input.select(); + } + }); + return; + } + } + + _pendingChange = _pendingChange || {}; + + // exit if we are currently about to delete this row anyway - #6366 + if (_pendingChange.hasOwnProperty(d.key) && _pendingChange[d.key] === undefined) return; + + if (kOld) { + _pendingChange[kOld] = undefined; + } + + _pendingChange[kNew] = vNew; + + // update the ordered key index so this row doesn't change position + var existingKeyIndex = _orderedKeys.indexOf(kOld); + if (existingKeyIndex !== -1) _orderedKeys[existingKeyIndex] = kNew; + + d.key = kNew; // update datum to avoid exit/enter on tag update + d.value = vNew; + + this.value = kNew; + utilGetSetValue(inputVal, vNew); + scheduleChange(); + } + + function valueChange(d) { + if (isReadOnly(d)) return; + + // exit if this is a multiselection and no value was entered + if (typeof d.value !== 'string' && !this.value) return; + + _pendingChange = _pendingChange || {}; + + // exit if we are currently about to delete this row anyway - #6366 + if (_pendingChange.hasOwnProperty(d.key) && _pendingChange[d.key] === undefined) return; + + _pendingChange[d.key] = this.value; + scheduleChange(); + } + + function removeTag(d) { + if (isReadOnly(d)) return; + + if (d.key === '') { // removing the blank row + _showBlank = false; + section.reRender(); + + } else { + // remove the key from the ordered key index + _orderedKeys = _orderedKeys.filter(function(key) { return key !== d.key; }); + + _pendingChange = _pendingChange || {}; + _pendingChange[d.key] = undefined; + scheduleChange(); + } + } + + function addTag() { + // Delay render in case this click is blurring an edited combo. + // Without the setTimeout, the `content` render would wipe out the pending tag change. + window.setTimeout(function() { + _showBlank = true; + section.reRender(); + section.selection().selectAll('.tag-list li:last-child input.key').node().focus(); + }, 20); + } + + function scheduleChange() { + // Delay change in case this change is blurring an edited combo. - #5878 + window.setTimeout(function() { + dispatch.call('change', this, _pendingChange); + _pendingChange = null; + }, 10); + } + + + section.state = function(val) { + if (!arguments.length) return _state; + if (_state !== val) { + _orderedKeys = []; + _state = val; + } + return section; + }; + + + section.presets = function(val) { + if (!arguments.length) return _presets; + _presets = val; + if (_presets && _presets.length && _presets[0].isFallback()) { + _expanded = true; + _updatePreference = false; + } else { + _expanded = undefined; + _updatePreference = true; + } + return section; + }; + + + section.tags = function(val) { + if (!arguments.length) return _tags; + _tags = val; + return section; + }; + + + section.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + if (!_entityIDs || !val || !utilArrayIdentical(_entityIDs, val)) { + _entityIDs = val; + _orderedKeys = []; + } + return section; + }; + + + section.expanded = function(val) { + if (!arguments.length) return _expanded; + _expanded = val; + _updatePreference = false; + return section; + }; + + + // pass an array of regular expressions to test against the tag key + section.readOnlyTags = function(val) { + if (!arguments.length) return _readOnlyTags; + _readOnlyTags = val; + return section; + }; + + + return utilRebind(section, dispatch, 'on'); +} diff --git a/modules/ui/selection_list.js b/modules/ui/sections/selection_list.js similarity index 75% rename from modules/ui/selection_list.js rename to modules/ui/sections/selection_list.js index 2fcf281a5..329277613 100644 --- a/modules/ui/selection_list.js +++ b/modules/ui/sections/selection_list.js @@ -1,43 +1,36 @@ import { event as d3_event, select as d3_select } from 'd3-selection'; -import { modeSelect } from '../modes/select'; -import { osmEntity } from '../osm'; -import { svgIcon } from '../svg/icon'; -import { uiDisclosure } from './disclosure'; -import { t } from '../util/locale'; -import { utilDisplayName, utilHighlightEntities } from '../util'; +import { modeSelect } from '../../modes/select'; +import { osmEntity } from '../../osm'; +import { svgIcon } from '../../svg/icon'; +import { uiSection } from '../section'; +import { t } from '../../util/locale'; +import { utilDisplayName, utilHighlightEntities } from '../../util'; - -export function uiSelectionList(context) { +export function uiSectionSelectionList(context) { var _selectedIDs = []; - var _selection = d3_select(null); + + var section = uiSection('selected-features', context) + .shouldDisplay(function() { + return _selectedIDs.length > 1; + }) + .title(function() { + return t('inspector.features_count', { count: _selectedIDs.length }); + }) + .disclosureContent(renderDisclosureContent); context.history() .on('change.selectionList', function(difference) { if (difference) { - _selection.selectAll('.disclosure-wrap') - .call(render); - - updateTitle(); + section.reRender(); } }); - function selectionList(selection) { - _selection = selection; - - selection - .call(uiDisclosure(context, 'selected_features', true) - .content(render) - ); - - updateTitle(); - } - - selectionList.selectedIDs = function(val) { + section.entityIDs = function(val) { if (!arguments.length) return _selectedIDs; _selectedIDs = val; - return selectionList; + return section; }; function selectEntity(entity) { @@ -55,7 +48,7 @@ export function uiSelectionList(context) { } } - function render(selection) { + function renderDisclosureContent(selection) { var list = selection.selectAll('.feature-list') .data([0]); @@ -135,10 +128,5 @@ export function uiSelectionList(context) { }); } - function updateTitle() { - _selection.selectAll('.hide-toggle span') - .text(t('inspector.features_count', { count: _selectedIDs.length })); - } - - return selectionList; + return section; } diff --git a/test/spec/ui/raw_tag_editor.js b/test/spec/ui/raw_tag_editor.js index a6b6885d6..eaa84760b 100644 --- a/test/spec/ui/raw_tag_editor.js +++ b/test/spec/ui/raw_tag_editor.js @@ -1,17 +1,17 @@ -describe('iD.uiRawTagEditor', function() { +describe('iD.uiSectionRawTagEditor', function() { var taglist, element, entity, context; function render(tags) { - taglist = iD.uiRawTagEditor(context) + taglist = iD.uiSectionRawTagEditor(context) .entityIDs([entity.id]) - .preset({isFallback: function() { return false; }}) + .presets([{isFallback: function() { return false; }}]) .tags(tags) .expanded(true); element = d3.select('body') .append('div') .attr('class', 'ui-wrap') - .call(taglist); + .call(taglist.render); } beforeEach(function () {