diff --git a/css/80_app.css b/css/80_app.css index edd94ac5b..fc334eaa1 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -968,7 +968,7 @@ button.save.has-count .count::before { position: absolute; top: 0; right: 0; - width: 10%; + width: 32px; background: #fafafa; } @@ -1024,22 +1024,22 @@ button.save.has-count .count::before { /* preset form basics */ -.inspector-preset { +.preset-editor { overflow: hidden; padding-bottom: 10px; } -.inspector-preset a.hide-toggle { +.preset-editor a.hide-toggle { margin: 0 20px 10px 20px; } -.inspector-preset .preset-form { +.preset-editor .preset-form { padding: 10px; margin: 0 10px 10px 10px; border-radius: 8px; } -.inspector-preset .preset-form:empty { +.preset-editor .preset-form:empty { display: none; } @@ -1056,7 +1056,8 @@ button.save.has-count .count::before { transition: margin-bottom 200ms; } -.form-field:last-child { +.form-field.nowrap, +.wrap-form-field:last-child .form-field { margin-bottom: 0; } @@ -1097,7 +1098,7 @@ button.save.has-count .count::before { .form-label button { border-left: 1px solid #ccc; - width: 10%; + width: 32px; height: 100%; border-radius: 0; background: #f6f6f6; @@ -1106,6 +1107,7 @@ button.save.has-count .count::before { border-left: none; border-right: 1px solid #CCC; border-radius: 4px 0 0 0; + width: 31px; } .form-label button:hover { background: #f1f1f1; @@ -1146,6 +1148,7 @@ button.save.has-count .count::before { .inspector-hover .form-field-multicombo, .inspector-hover .structure-extras-wrap, .inspector-hover input, +.inspector-hover textarea, .inspector-hover label { background: #ececec; } @@ -1296,6 +1299,7 @@ button.save.has-count .count::before { /* preset form access */ /* preset form cycleway */ +/* preset form structure extras */ .form-field-structure .structure-extras-wrap li, .form-field-cycleway .preset-input-wrap li, @@ -1335,6 +1339,34 @@ button.save.has-count .count::before { border: 1px solid #ccc; border-radius: 4px; } +.structure-extras-wrap li:first-child span { + border-top-left-radius: 4px; +} +.structure-extras-wrap li:first-child input { + border-top-right-radius: 4px; +} +.structure-extras-wrap li:last-child span { + border-bottom-left-radius: 4px; +} +.structure-extras-wrap li:last-child input { + border-bottom-right-radius: 4px; +} +[dir='rtl'] .structure-extras-wrap li:first-child span { + border-top-left-radius: 0; + border-top-right-radius: 4px; +} +[dir='rtl'] .structure-extras-wrap li:first-child input { + border-top-right-radius: 0; + border-top-left-radius: 4px; +} +[dir='rtl'] .structure-extras-wrap li:last-child span { + border-bottom-left-radius: 0; + border-bottom-right-radius: 4px; +} +[dir='rtl'] .structure-extras-wrap li:last-child input { + border-bottom-right-radius: 0; + border-bottom-left-radius: 4px; +} /* preset form multicombo */ @@ -1403,28 +1435,43 @@ input[type=number] { } .spin-control { - width: 20%; - height: 29px; + width: 64px; + height: 30px; display: inline-block; - margin-left: -20%; + margin-left: -64px; margin-bottom: -11px; position: relative; } +[dir='rtl'] .spin-control{ + margin-left: 0; + margin-right: -64px; +} .spin-control button { right: 1px; position: relative; float: left; height: 100%; - width: 50%; + width: 32px; border-left: 1px solid #CCC; border-radius: 0; background: rgba(0, 0, 0, 0); } +[dir='rtl'] .spin-control button{ + border-left: 0; + border-right: 1px solid #CCC; +} .spin-control button.decrement { border-bottom-right-radius: 3px; } +[dir='rtl'] .spin-control button.decrement { + border-bottom-right-radius: 0; +} +[dir='rtl'] .spin-control button.increment { + border-bottom-left-radius: 3px; + right: 0; +} .spin-control button.decrement::after, .spin-control button.increment::after { @@ -1447,6 +1494,7 @@ input[type=number] { border-right: 5px solid transparent; } + /* preset form checkbox */ .checkselect label:last-of-type { @@ -1530,11 +1578,11 @@ input[type=number] { } .form-field .wiki-title ~ .combobox-caret { - right: 10%; + right: 32px; } [dir='rtl'] .form-field .wiki-title ~ .combobox-caret { right: auto; - left: 10%; + left: 32px; } /* Localized field */ @@ -1549,8 +1597,8 @@ input[type=number] { .form-field .button-input-action { position: relative; right: 1px; - width: 10%; - margin-left: -10%; + width: 32px; + margin-left: -32px; border: 1px solid #CCC; border-top-width: 0; border-right-width: 0; @@ -1560,7 +1608,7 @@ input[type=number] { } [dir='rtl'] .form-field .button-input-action { margin-left: 0; - margin-right: -10%; + margin-right: -32px; border-right-width: 1px; border-radius: 0 0 0 4px; } @@ -2708,6 +2756,9 @@ img.tile-removing { background: none; color: #ddd; } +[dir='rtl'] .panel-title button.close { + float: left; +} .panel-title button.close:hover { color: #fff; @@ -4165,23 +4216,6 @@ li.hide + li.version .badge .tooltip .tooltip-arrow { border-radius: 4px 0 0 4px; } - -/* increment / decrement control - code by Naoufel Razouane */ - -[dir='rtl'] .spin-control{ - margin-left: 0; - margin-right: -20%; -} -[dir='rtl'] .spin-control button{ - border-left: 0; - border-right: 1px solid #CCC; -} -[dir='rtl'] .spin-control button.decrement{ - border-bottom-right-radius: 0; -} -[dir='rtl'] .spin-control button.increment{ - border-bottom-left-radius: 3px; -} /* modal */ [dir='rtl'] .modal > button { position: absolute; diff --git a/data/presets.yaml b/data/presets.yaml index 952b018a0..42225fcf8 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -246,6 +246,11 @@ en: brand: # brand=* label: Brand + bridge: + # bridge=* + label: Type + # bridge field placeholder + placeholder: Default building: # building=* label: Building @@ -369,6 +374,11 @@ en: currency_multi: # 'currency:=*' label: Currency Types + cutting: + # cutting=* + label: Type + # cutting field placeholder + placeholder: Default cycle_network: # cycle_network=* label: Network @@ -458,6 +468,11 @@ en: label: Email # email field placeholder placeholder: example@example.com + embankment: + # embankment=* + label: Type + # embankment field placeholder + placeholder: Default emergency: # emergency=* label: Emergency @@ -505,6 +520,11 @@ en: fixme: # fixme=* label: Fix Me + ford: + # ford=* + label: Type + # ford field placeholder + placeholder: Default fuel: # fuel=* label: Fuel @@ -634,6 +654,8 @@ en: layer: # layer=* label: Layer + # layer field placeholder + placeholder: '0' leaf_cycle: # leaf_cycle=* label: Leaf Cycle @@ -1361,6 +1383,11 @@ en: # trees=* label: Trees tunnel: + # tunnel=* + label: Type + # tunnel field placeholder + placeholder: Default + tunnel_waterway: # tunnel=* label: Tunnel vending: diff --git a/data/presets/fields.json b/data/presets/fields.json index f2f53c732..e099f5416 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -329,6 +329,12 @@ "type": "text", "label": "Brand" }, + "bridge": { + "key": "bridge", + "type": "typeCombo", + "label": "Type", + "placeholder": "Default" + }, "building_area": { "key": "building", "type": "combo", @@ -488,6 +494,12 @@ "type": "multiCombo", "label": "Currency Types" }, + "cutting": { + "key": "cutting", + "type": "typeCombo", + "label": "Type", + "placeholder": "Default" + }, "cycle_network": { "key": "cycle_network", "type": "networkCombo", @@ -631,6 +643,12 @@ "universal": true, "label": "Email" }, + "embankment": { + "key": "embankment", + "type": "typeCombo", + "label": "Type", + "placeholder": "Default" + }, "emergency": { "key": "emergency", "type": "check", @@ -695,6 +713,12 @@ "label": "Fix Me", "universal": true }, + "ford": { + "key": "ford", + "type": "typeCombo", + "label": "Type", + "placeholder": "Default" + }, "fuel_multi": { "key": "fuel:", "type": "multiCombo", @@ -883,8 +907,9 @@ }, "layer": { "key": "layer", - "type": "combo", - "label": "Layer" + "type": "number", + "label": "Layer", + "placeholder": "0" }, "leaf_cycle_singular": { "key": "leaf_cycle", @@ -1842,11 +1867,17 @@ "type": "semiCombo", "label": "Trees" }, - "tunnel": { + "tunnel_waterway": { "key": "tunnel", "type": "combo", "label": "Tunnel" }, + "tunnel": { + "key": "tunnel", + "type": "typeCombo", + "label": "Type", + "placeholder": "Default" + }, "vending": { "key": "vending", "type": "combo", diff --git a/data/presets/fields/bridge.json b/data/presets/fields/bridge.json new file mode 100644 index 000000000..bf0e98a04 --- /dev/null +++ b/data/presets/fields/bridge.json @@ -0,0 +1,6 @@ +{ + "key": "bridge", + "type": "typeCombo", + "label": "Type", + "placeholder": "Default" +} diff --git a/data/presets/fields/cutting.json b/data/presets/fields/cutting.json new file mode 100644 index 000000000..72314c8ff --- /dev/null +++ b/data/presets/fields/cutting.json @@ -0,0 +1,6 @@ +{ + "key": "cutting", + "type": "typeCombo", + "label": "Type", + "placeholder": "Default" +} diff --git a/data/presets/fields/embankment.json b/data/presets/fields/embankment.json new file mode 100644 index 000000000..a9818c5ba --- /dev/null +++ b/data/presets/fields/embankment.json @@ -0,0 +1,6 @@ +{ + "key": "embankment", + "type": "typeCombo", + "label": "Type", + "placeholder": "Default" +} diff --git a/data/presets/fields/ford.json b/data/presets/fields/ford.json new file mode 100644 index 000000000..3755f5925 --- /dev/null +++ b/data/presets/fields/ford.json @@ -0,0 +1,6 @@ +{ + "key": "ford", + "type": "typeCombo", + "label": "Type", + "placeholder": "Default" +} diff --git a/data/presets/fields/layer.json b/data/presets/fields/layer.json index d66d60c0a..29039f46a 100644 --- a/data/presets/fields/layer.json +++ b/data/presets/fields/layer.json @@ -1,5 +1,6 @@ { "key": "layer", - "type": "combo", - "label": "Layer" -} \ No newline at end of file + "type": "number", + "label": "Layer", + "placeholder": "0" +} diff --git a/data/presets/fields/tunnel.json b/data/presets/fields/tunnel.json index 2c4f7751f..e349219b4 100644 --- a/data/presets/fields/tunnel.json +++ b/data/presets/fields/tunnel.json @@ -1,5 +1,6 @@ { "key": "tunnel", - "type": "combo", - "label": "Tunnel" + "type": "typeCombo", + "label": "Type", + "placeholder": "Default" } diff --git a/data/presets/fields/tunnel_waterway.json b/data/presets/fields/tunnel_waterway.json new file mode 100644 index 000000000..2c4f7751f --- /dev/null +++ b/data/presets/fields/tunnel_waterway.json @@ -0,0 +1,5 @@ +{ + "key": "tunnel", + "type": "combo", + "label": "Tunnel" +} diff --git a/data/presets/presets.json b/data/presets/presets.json index 92c966b3c..e87820226 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -14776,7 +14776,7 @@ "waterway/ditch": { "icon": "waterway-ditch", "fields": [ - "tunnel" + "tunnel_waterway" ], "geometry": [ "line" @@ -14812,7 +14812,7 @@ "waterway/drain": { "icon": "waterway-stream", "fields": [ - "tunnel" + "tunnel_waterway" ], "geometry": [ "line" @@ -14850,7 +14850,7 @@ "icon": "waterway-river", "fields": [ "name", - "tunnel", + "tunnel_waterway", "width" ], "geometry": [ @@ -14921,7 +14921,7 @@ "icon": "waterway-stream", "fields": [ "name", - "tunnel", + "tunnel_waterway", "width" ], "geometry": [ diff --git a/data/presets/presets/waterway/ditch.json b/data/presets/presets/waterway/ditch.json index 0f61421df..9373764b1 100644 --- a/data/presets/presets/waterway/ditch.json +++ b/data/presets/presets/waterway/ditch.json @@ -1,7 +1,7 @@ { "icon": "waterway-ditch", "fields": [ - "tunnel" + "tunnel_waterway" ], "geometry": [ "line" diff --git a/data/presets/presets/waterway/drain.json b/data/presets/presets/waterway/drain.json index c0b72fdac..71f63d634 100644 --- a/data/presets/presets/waterway/drain.json +++ b/data/presets/presets/waterway/drain.json @@ -1,7 +1,7 @@ { "icon": "waterway-stream", "fields": [ - "tunnel" + "tunnel_waterway" ], "geometry": [ "line" diff --git a/data/presets/presets/waterway/river.json b/data/presets/presets/waterway/river.json index bc09c8dab..3125600e2 100644 --- a/data/presets/presets/waterway/river.json +++ b/data/presets/presets/waterway/river.json @@ -2,7 +2,7 @@ "icon": "waterway-river", "fields": [ "name", - "tunnel", + "tunnel_waterway", "width" ], "geometry": [ diff --git a/data/presets/presets/waterway/stream.json b/data/presets/presets/waterway/stream.json index 87a1317a8..802924646 100644 --- a/data/presets/presets/waterway/stream.json +++ b/data/presets/presets/waterway/stream.json @@ -2,7 +2,7 @@ "icon": "waterway-stream", "fields": [ "name", - "tunnel", + "tunnel_waterway", "width" ], "geometry": [ diff --git a/dist/locales/en.json b/dist/locales/en.json index 586169767..31e2b1edd 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -1282,6 +1282,10 @@ "brand": { "label": "Brand" }, + "bridge": { + "label": "Type", + "placeholder": "Default" + }, "building_area": { "label": "Building" }, @@ -1384,6 +1388,10 @@ "currency_multi": { "label": "Currency Types" }, + "cutting": { + "label": "Type", + "placeholder": "Default" + }, "cycle_network": { "label": "Network" }, @@ -1469,6 +1477,10 @@ "label": "Email", "placeholder": "example@example.com" }, + "embankment": { + "label": "Type", + "placeholder": "Default" + }, "emergency": { "label": "Emergency" }, @@ -1509,6 +1521,10 @@ "fixme": { "label": "Fix Me" }, + "ford": { + "label": "Type", + "placeholder": "Default" + }, "fuel_multi": { "label": "Fuel Types" }, @@ -1624,7 +1640,8 @@ "placeholder": "1, 2, 3..." }, "layer": { - "label": "Layer" + "label": "Layer", + "placeholder": "0" }, "leaf_cycle_singular": { "label": "Leaf Cycle", @@ -2216,9 +2233,13 @@ "trees": { "label": "Trees" }, - "tunnel": { + "tunnel_waterway": { "label": "Tunnel" }, + "tunnel": { + "label": "Type", + "placeholder": "Default" + }, "vending": { "label": "Type of Goods" }, diff --git a/modules/index.js b/modules/index.js index 7b79ecf9f..e14719f0e 100644 --- a/modules/index.js +++ b/modules/index.js @@ -37,6 +37,7 @@ 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 var debug = false; diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js index 4f2e0e2fc..cb642fe8e 100644 --- a/modules/ui/entity_editor.js +++ b/modules/ui/entity_editor.js @@ -10,7 +10,7 @@ import { uiRawMemberEditor } from './raw_member_editor'; import { uiRawMembershipEditor } from './raw_membership_editor'; import { uiRawTagEditor } from './raw_tag_editor'; import { uiTagReference } from './tag_reference'; -import { uiPreset } from './preset'; +import { uiPresetEditor } from './preset_editor'; import { utilRebind } from '../util'; @@ -20,18 +20,18 @@ export function uiEntityEditor(context) { coalesceChanges = false, modified = false, base, - id, + entityId, activePreset, reference; - var presetEditor = uiPreset(context) + var presetEditor = uiPresetEditor(context) .on('change', changeTags); var rawTagEditor = uiRawTagEditor(context) .on('change', changeTags); function entityEditor(selection) { - var entity = context.entity(id), + var entity = context.entity(entityId), tags = _.clone(entity.tags); // Header @@ -63,7 +63,9 @@ export function uiEntityEditor(context) { .merge(enter); header.selectAll('.preset-reset') - .on('click', function() { dispatch.call('choose', this, activePreset); }); + .on('click', function() { + dispatch.call('choose', this, activePreset); + }); // Body @@ -88,7 +90,7 @@ export function uiEntityEditor(context) { enter .append('div') - .attr('class', 'inspector-border inspector-preset'); + .attr('class', 'inspector-border preset-editor'); enter .append('div') @@ -119,35 +121,41 @@ export function uiEntityEditor(context) { .call(reference.body); body.selectAll('.preset-reset') - .on('click', function() { dispatch.call('choose', this, activePreset); }); + .on('click', function() { + dispatch.call('choose', this, activePreset); + }); body.select('.preset-list-item button') .call(uiPresetIcon() - .geometry(context.geometry(id)) - .preset(activePreset)); + .geometry(context.geometry(entityId)) + .preset(activePreset) + ); body.select('.preset-list-item .label') .text(activePreset.name()); - body.select('.inspector-preset') + body.select('.preset-editor') .call(presetEditor .preset(activePreset) - .entityID(id) + .entityID(entityId) .tags(tags) - .state(state)); + .state(state) + ); body.select('.raw-tag-editor') .call(rawTagEditor .preset(activePreset) - .entityID(id) + .entityID(entityId) .tags(tags) - .state(state)); + .state(state) + ); if (entity.type === 'relation') { body.select('.raw-member-editor') .style('display', 'block') .call(uiRawMemberEditor(context) - .entityID(id)); + .entityID(entityId) + ); } else { body.select('.raw-member-editor') .style('display', 'none'); @@ -155,7 +163,8 @@ export function uiEntityEditor(context) { body.select('.raw-membership-editor') .call(uiRawMembershipEditor(context) - .entityID(id)); + .entityID(entityId) + ); body.select('.key-trap') .on('keydown.key-trap', function() { @@ -174,7 +183,7 @@ export function uiEntityEditor(context) { function historyChanged() { if (state === 'hide') return; - var entity = context.hasEntity(id), + var entity = context.hasEntity(entityId), graph = context.graph(); if (!entity) return; @@ -226,7 +235,7 @@ export function uiEntityEditor(context) { // Tag changes that fire on input can all get coalesced into a single // history operation when the user leaves the field. #2342 function changeTags(changed, onInput) { - var entity = context.entity(id), + var entity = context.entity(entityId), annotation = t('operations.change_tags.annotation'), tags = _.clone(entity.tags); @@ -242,9 +251,9 @@ export function uiEntityEditor(context) { if (!_.isEqual(entity.tags, tags)) { if (coalesceChanges) { - context.overwrite(actionChangeTags(id, tags), annotation); + context.overwrite(actionChangeTags(entityId, tags), annotation); } else { - context.perform(actionChangeTags(id, tags), annotation); + context.perform(actionChangeTags(entityId, tags), annotation); coalesceChanges = !!onInput; } } @@ -267,10 +276,10 @@ export function uiEntityEditor(context) { entityEditor.entityID = function(_) { - if (!arguments.length) return id; - id = _; + if (!arguments.length) return entityId; + entityId = _; base = context.graph(); - entityEditor.preset(context.presets().match(context.entity(id), base)); + entityEditor.preset(context.presets().match(context.entity(entityId), base)); entityEditor.modified(false); coalesceChanges = false; return entityEditor; @@ -281,7 +290,7 @@ export function uiEntityEditor(context) { if (!arguments.length) return activePreset; if (_ !== activePreset) { activePreset = _; - reference = uiTagReference(activePreset.reference(context.geometry(id)), context) + reference = uiTagReference(activePreset.reference(context.geometry(entityId)), context) .showing(false); } return entityEditor; diff --git a/modules/ui/field.js b/modules/ui/field.js new file mode 100644 index 000000000..148369f1d --- /dev/null +++ b/modules/ui/field.js @@ -0,0 +1,182 @@ +import * as d3 from 'd3'; +import _ from 'lodash'; +import { textDirection } from '../util/locale'; +import { svgIcon } from '../svg'; +import { uiFields } from './fields'; +import { uiTagReference } from './tag_reference'; +import { utilRebind } from '../util'; + + +export function uiField(context, presetField, entity, options) { + options = _.extend({ + show: true, + wrap: true + }, options); + + var dispatch = d3.dispatch('change'), + field = _.clone(presetField), + state = '', + tags = {}; + + + field.impl = uiFields[field.type](field, context) + .on('change', function(t, onInput) { + dispatch.call('change', field, t, onInput); + }); + + if (field.impl.entity) { + field.impl.entity(entity); + } + + field.keys = field.keys || [field.key]; + + field.show = options.show; + + + function isModified() { + var original = context.graph().base().entities[entity.id]; + return _.some(field.keys, function(key) { + return original ? tags[key] !== original.tags[key] : tags[key]; + }); + } + + + function isPresent() { + return _.some(field.keys, function(key) { + return tags[key]; + }); + } + + + function revert(d) { + d3.event.stopPropagation(); + d3.event.preventDefault(); + + var original = context.graph().base().entities[entity.id], + t = {}; + d.keys.forEach(function(key) { + t[key] = original ? original.tags[key] : undefined; + }); + + dispatch.call('change', d, t); + } + + + function remove(d) { + d3.event.stopPropagation(); + d3.event.preventDefault(); + + var t = {}; + d.keys.forEach(function(key) { + t[key] = undefined; + }); + + dispatch.call('change', d, t); + } + + + field.render = function(selection) { + var container = selection.selectAll('.form-field') + .data([field]); + + // Enter + var enter = container.enter() + .append('div') + .attr('class', function(d) { return 'form-field form-field-' + d.id; }) + .classed('nowrap', !options.wrap); + + if (options.wrap) { + var label = enter + .append('label') + .attr('class', 'form-label') + .attr('for', function(d) { return 'preset-input-' + d.id; }) + .text(function(d) { return d.label(); }); + + var wrap = label + .append('div') + .attr('class', 'form-label-button-wrap'); + + wrap + .append('button') + .attr('class', 'remove-icon') + .attr('tabindex', -1) + .call(svgIcon('#operation-delete')); + + wrap + .append('button') + .attr('class', 'modified-icon') + .attr('tabindex', -1) + .call( + (textDirection === 'rtl') ? svgIcon('#icon-redo') : svgIcon('#icon-undo') + ); + } + + + // Update + container = container + .merge(enter); + + container.selectAll('.form-label-button-wrap .remove-icon') + .on('click', remove); + + container.selectAll('.form-label-button-wrap .modified-icon') + .on('click', revert); + + container + .classed('modified', isModified()) + .classed('present', isPresent()) + .each(function(d) { + if (options.wrap) { + var referenceKey = d.key; + if (d.type === 'multiCombo') { // lookup key without the trailing ':' + referenceKey = referenceKey.replace(/:$/, ''); + } + var reference = uiTagReference(d.reference || { key: referenceKey }, context); + + if (state === 'hover') { + reference.showing(false); + } + } + + d3.select(this) + .call(d.impl); + + if (options.wrap) { + d3.select(this) + .call(reference.body) + .select('.form-label-button-wrap') + .call(reference.button); + } + + d.impl.tags(tags); + }); + }; + + + field.state = function(_) { + if (!arguments.length) return state; + state = _; + return field; + }; + + + field.tags = function(_) { + if (!arguments.length) return tags; + tags = _; + return field; + }; + + + field.isShown = function() { + return field.show || _.some(field.keys, function(key) { return !!tags[key]; }); + }; + + + field.focus = function() { + field.impl.focus(); + }; + + + return utilRebind(field, dispatch, 'on'); +} + diff --git a/modules/ui/fields/input.js b/modules/ui/fields/input.js index 4cbed223a..c60f9361e 100644 --- a/modules/ui/fields/input.js +++ b/modules/ui/fields/input.js @@ -1,7 +1,7 @@ import * as d3 from 'd3'; -import { t } from '../../util/locale'; -import { dataPhoneFormats } from '../../../data/index'; -import { services } from '../../services/index'; +import { t, textDirection } from '../../util/locale'; +import { dataPhoneFormats } from '../../../data'; +import { services } from '../../services'; import { utilGetSetValue, utilNoAuto, @@ -52,6 +52,8 @@ export function uiFieldText(field, context) { }); } else if (field.type === 'number') { + var rtl = (textDirection === 'rtl'); + input.attr('type', 'text'); var spinControl = selection.selectAll('.spin-control') @@ -63,14 +65,14 @@ export function uiFieldText(field, context) { enter .append('button') - .datum(-1) - .attr('class', 'decrement') + .datum(rtl ? 1 : -1) + .attr('class', rtl ? 'increment' : 'decrement') .attr('tabindex', -1); enter .append('button') - .datum(1) - .attr('class', 'increment') + .datum(rtl ? -1 : 1) + .attr('class', rtl ? 'decrement' : 'increment') .attr('tabindex', -1); spinControl = spinControl diff --git a/modules/ui/fields/radio.js b/modules/ui/fields/radio.js index eca522493..fa4fc4090 100644 --- a/modules/ui/fields/radio.js +++ b/modules/ui/fields/radio.js @@ -1,13 +1,7 @@ import * as d3 from 'd3'; import { t } from '../../util/locale'; -import { d3combobox } from '../../lib/d3.combobox.js'; -import { services } from '../../services/index'; - -import { - utilGetSetValue, - utilNoAuto, - utilRebind -} from '../../util'; +import { uiField } from '../field'; +import { utilRebind } from '../../util'; export { uiFieldRadio as uiFieldStructureRadio }; @@ -15,13 +9,12 @@ export { uiFieldRadio as uiFieldStructureRadio }; export function uiFieldRadio(field, context) { var dispatch = d3.dispatch('change'), - taginfo = services.taginfo, placeholder = d3.select(null), wrap = d3.select(null), labels = d3.select(null), radios = d3.select(null), - typeInput = d3.select(null), - layerInput = d3.select(null), + typeField, + layerField, oldType = {}, entity; @@ -32,32 +25,6 @@ export function uiFieldRadio(field, context) { return !node.empty() && node.datum(); } - // returns the tag value for a display value - function tagValue(dispVal) { - dispVal = snake(clean(dispVal || '')); - return dispVal.toLowerCase() || 'yes'; - } - - // returns the display value for a tag value - function displayValue(tagVal) { - tagVal = tagVal || ''; - return tagVal.toLowerCase() === 'yes' ? '' : unsnake(tagVal); - } - - function snake(s) { - return s.replace(/\s+/g, '_'); - } - - function unsnake(s) { - return s.replace(/_+/g, ' '); - } - - function clean(s) { - return s.split(';') - .map(function(s) { return s.trim(); }) - .join(';'); - } - function radio(selection) { selection.classed('preset-radio', true); @@ -101,11 +68,16 @@ export function uiFieldRadio(field, context) { radios = labels.selectAll('input') .on('change', changeRadio); + } - function structureExtras(selection) { - var selected = selectedKey(); + function structureExtras(selection, tags) { + var selected = selectedKey(), + type = context.presets().field(selected), + layer = context.presets().field('layer'), + showLayer = (selected === 'bridge' || selected === 'tunnel'); + var extrasWrap = selection.selectAll('.structure-extras-wrap') .data(selected ? [0] : []); @@ -127,55 +99,67 @@ export function uiFieldRadio(field, context) { // Type - var typeItem = list.selectAll('.structure-type-item') - .data([0]); + if (type) { + if (!typeField || typeField.id !== selected) { + typeField = uiField(context, type, entity, { wrap: false }) + .on('change', changeType); + } + typeField.tags(tags); + } else { + typeField = null; + } + var typeItem = list.selectAll('.structure-type-item') + .data(typeField ? [typeField] : [], function(d) { return d.id; }); + + // Exit + typeItem.exit() + .remove(); + + // Enter var typeEnter = typeItem.enter() - .append('li') + .insert('li', ':first-child') .attr('class', 'cf structure-type-item'); typeEnter .append('span') .attr('class', 'col6 label structure-label-type') - .attr('for', 'structure-input-type') + .attr('for', 'preset-input-' + selected) .text(t('inspector.radio.structure.type')); typeEnter .append('div') - .attr('class', 'col6 structure-input-type-wrap') - .append('input') - .attr('type', 'text') - .attr('class', 'structure-input-type') - .attr('placeholder', t('inspector.radio.structure.default')) - .call(utilNoAuto); + .attr('class', 'col6 structure-input-type-wrap'); + // Update typeItem = typeItem .merge(typeEnter); - typeInput = typeItem.selectAll('.structure-input-type'); - - if (taginfo) { - typeInput - .call(d3combobox() - .container(context.container()) - .fetcher(typeFetcher) - ); + if (typeField) { + typeItem.selectAll('.structure-input-type-wrap') + .call(typeField.render); } - typeInput - .on('change', changeType) - .on('blur', changeType); - // Layer - var showLayer = (selected === 'bridge' || selected === 'tunnel'); + if (layer && showLayer) { + if (!layerField) { + layerField = uiField(context, layer, entity, { wrap: false }) + .on('change', changeLayer); + } + layerField.tags(tags); + } else { + layerField = null; + } var layerItem = list.selectAll('.structure-layer-item') - .data(showLayer ? [0] : []); + .data(layerField ? [layerField] : []); + // Exit layerItem.exit() .remove(); + // Enter var layerEnter = layerItem.enter() .append('li') .attr('class', 'cf structure-layer-item'); @@ -183,87 +167,39 @@ export function uiFieldRadio(field, context) { layerEnter .append('span') .attr('class', 'col6 label structure-label-layer') - .attr('for', 'structure-input-layer') + .attr('for', 'preset-input-layer') .text(t('inspector.radio.structure.layer')); layerEnter .append('div') - .attr('class', 'col6 structure-input-layer-wrap') - .append('input') - .attr('type', 'text') - .attr('class', 'structure-input-layer') - .attr('placeholder', '0') - .call(utilNoAuto); - - var spin = layerEnter - .append('div') - .attr('class', 'spin-control'); - - spin - .append('button') - .datum(-1) - .attr('class', 'decrement') - .attr('tabindex', -1); - - spin - .append('button') - .datum(1) - .attr('class', 'increment') - .attr('tabindex', -1); + .attr('class', 'col6 structure-input-layer-wrap'); + // Update layerItem = layerItem .merge(layerEnter); - layerInput = layerItem.selectAll('.structure-input-layer') - .on('change', changeLayer) - .on('blur', changeLayer); - - layerItem.selectAll('button') - .on('click', function(d) { - d3.event.preventDefault(); - var num = parseInt(layerInput.node().value || 0, 10); - if (!isNaN(num)) layerInput.node().value = num + d; - changeLayer(); - }); - + if (layerField) { + layerItem.selectAll('.structure-input-layer-wrap') + .call(layerField.render); + } } - function typeFetcher(q, callback) { - taginfo.values({ - debounce: true, - key: selectedKey(), - query: q - }, function(err, data) { - if (err) return; - var comboData = data.map(function(d) { - return { - key: d.value, - value: unsnake(d.value), - title: d.title - }; - }); - if (callback) callback(comboData); - }); - } - - - function changeType() { - var key = selectedKey(), - t = {}; - + function changeType(t, onInput) { + var key = selectedKey(); if (!key) return; - var val = tagValue(utilGetSetValue(typeInput)); - t[key] = val; + + var val = t[key]; if (val !== 'no') oldType[key] = val; - dispatch.call('change', this, t); + dispatch.call('change', this, t, onInput); } - function changeLayer() { - // note: don't use utilGetSetValue here because we want 0 to be falsy. - var t = { layer: layerInput.node().value || undefined }; - dispatch.call('change', this, t); + function changeLayer(t, onInput) { + if (t.layer === '0') { + t.layer = undefined; + } + dispatch.call('change', this, t, onInput); } @@ -314,19 +250,16 @@ export function uiFieldRadio(field, context) { radios.property('checked', checked); var selection = radios.filter(function() { return this.checked; }); - var typeVal = ''; if (selection.empty()) { placeholder.text(t('inspector.none')); } else { placeholder.text(selection.attr('value')); - typeVal = oldType[selection.datum()] = tags[selection.datum()]; + oldType[selection.datum()] = tags[selection.datum()]; } if (field.type === 'structureRadio') { - wrap.call(structureExtras); - utilGetSetValue(typeInput, displayValue(typeVal) || ''); - utilGetSetValue(layerInput, tags.layer || ''); + wrap.call(structureExtras, tags); } }; diff --git a/modules/ui/index.js b/modules/ui/index.js index 76dd5fd31..3abe1ebb2 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -13,6 +13,7 @@ export { uiEditMenu } from './edit_menu'; export { uiEntityEditor } from './entity_editor'; export { uiFeatureInfo } from './feature_info'; export { uiFeatureList } from './feature_list'; +export { uiField } from './field'; export { uiFlash } from './flash'; export { uiFullScreen } from './full_screen'; export { uiGeolocate } from './geolocate'; @@ -26,7 +27,7 @@ export { uiMapInMap } from './map_in_map'; export { uiModal } from './modal'; export { uiModes } from './modes'; export { uiNotice } from './notice'; -export { uiPreset } from './preset'; +export { uiPresetEditor } from './preset_editor'; export { uiPresetIcon } from './preset_icon'; export { uiPresetList } from './preset_list'; export { uiRadialMenu } from './radial_menu'; diff --git a/modules/ui/intro/navigation.js b/modules/ui/intro/navigation.js index 003aa3c15..e3f93222c 100644 --- a/modules/ui/intro/navigation.js +++ b/modules/ui/intro/navigation.js @@ -326,7 +326,7 @@ export function uiIntroNavigation(context, reveal) { var onClick = function() { continueTo(closeTownHall); }; - reveal('.inspector-body .inspector-preset', + reveal('.inspector-body .preset-editor', t('intro.navigation.fields_townhall'), { buttonText: t('intro.ok'), buttonCallback: onClick } ); diff --git a/modules/ui/preset.js b/modules/ui/preset.js deleted file mode 100644 index fd1defff1..000000000 --- a/modules/ui/preset.js +++ /dev/null @@ -1,327 +0,0 @@ -import * as d3 from 'd3'; -import _ from 'lodash'; -import { d3combobox } from '../lib/d3.combobox.js'; -import { t, textDirection } from '../util/locale'; -import { modeBrowse } from '../modes/index'; -import { svgIcon } from '../svg/index'; -import { uiDisclosure } from './disclosure'; -import { uiFields } from './fields/index'; -import { uiTagReference } from './tag_reference'; -import { - utilGetSetValue, - utilNoAuto, - utilRebind -} from '../util'; - - -export function uiPreset(context) { - var dispatch = d3.dispatch('change'), - expandedPreference = (context.storage('preset_fields.expanded') !== 'false'), - state, - fieldsArr, - preset, - tags, - id; - - - // Field Constructor - function UIField(field, entity, show) { - field = _.clone(field); - - field.input = uiFields[field.type](field, context) - .on('change', function(t, onInput) { - dispatch.call('change', field, t, onInput); - }); - - if (field.input.entity) field.input.entity(entity); - - field.keys = field.keys || [field.key]; - - field.show = show; - - field.shown = function() { - return field.show || _.some(field.keys, function(key) { return !!tags[key]; }); - }; - - field.modified = function() { - var original = context.graph().base().entities[entity.id]; - return _.some(field.keys, function(key) { - return original ? tags[key] !== original.tags[key] : tags[key]; - }); - }; - - field.revert = function() { - var original = context.graph().base().entities[entity.id], - t = {}; - field.keys.forEach(function(key) { - t[key] = original ? original.tags[key] : undefined; - }); - return t; - }; - - field.present = function() { - return _.some(field.keys, function(key) { - return tags[key]; - }); - }; - - field.remove = function() { - var t = {}; - field.keys.forEach(function(key) { - t[key] = undefined; - }); - return t; - }; - - return field; - } - - - function fieldKey(field) { - return field.id; - } - - - function presets(selection) { - selection.call(uiDisclosure() - .title(t('inspector.all_fields')) - .expanded(expandedPreference) - .on('toggled', toggled) - .content(content) - ); - - function toggled(expanded) { - expandedPreference = expanded; - context.storage('preset_fields.expanded', expanded); - } - } - - - function content(selection) { - if (!fieldsArr) { - var entity = context.entity(id), - geometry = context.geometry(id), - presets = context.presets(); - - fieldsArr = []; - - preset.fields.forEach(function(field) { - if (field.matchGeometry(geometry)) { - fieldsArr.push(UIField(field, entity, true)); - } - }); - - if (entity.isHighwayIntersection(context.graph()) && presets.field('restrictions')) { - fieldsArr.push(UIField(presets.field('restrictions'), entity, true)); - } - - presets.universal().forEach(function(field) { - if (preset.fields.indexOf(field) < 0) { - fieldsArr.push(UIField(field, entity)); - } - }); - } - - var shown = fieldsArr.filter(function(field) { return field.shown(); }), - notShown = fieldsArr.filter(function(field) { return !field.shown(); }); - - - var form = selection.selectAll('.preset-form') - .data([0]); - - form = form.enter() - .append('div') - .attr('class', 'preset-form inspector-inner fillL3') - .merge(form); - - - var fields = form.selectAll('.form-field') - .data(shown, fieldKey); - - fields.exit() - .remove(); - - // Enter - var enter = fields.enter() - .append('div') - .attr('class', function(field) { - return 'form-field form-field-' + field.id; - }); - - var label = enter - .append('label') - .attr('class', 'form-label') - .attr('for', function(field) { return 'preset-input-' + field.id; }) - .text(function(field) { return field.label(); }); - - var wrap = label - .append('div') - .attr('class', 'form-label-button-wrap'); - - wrap.append('button') - .attr('class', 'remove-icon') - .attr('tabindex', -1) - .call(svgIcon('#operation-delete')); - - wrap.append('button') - .attr('class', 'modified-icon') - .attr('tabindex', -1) - .call( - (textDirection === 'rtl') ? svgIcon('#icon-redo') : svgIcon('#icon-undo') - ); - - - // Update - fields = fields - .merge(enter); - - fields.selectAll('.form-label-button-wrap .remove-icon') - .on('click', remove); - - fields.selectAll('.modified-icon') - .on('click', revert); - - fields - .order() - .classed('modified', function(field) { return field.modified(); }) - .classed('present', function(field) { return field.present(); }) - .each(function(field) { - var referenceKey = field.key; - if (field.type === 'multiCombo') { // lookup key without the trailing ':' - referenceKey = referenceKey.replace(/:$/, ''); - } - var reference = uiTagReference(field.reference || { key: referenceKey }, context); - - if (state === 'hover') { - reference.showing(false); - } - - d3.select(this) - .call(field.input) - .selectAll('input') - .on('keydown', function() { - // if user presses enter, and combobox is not active, accept edits.. - if (d3.event.keyCode === 13 && d3.select('.combobox').empty()) { - context.enter(modeBrowse(context)); - } - }); - - d3.select(this) - .call(reference.body) - .select('.form-label-button-wrap') - .call(reference.button); - - field.input.tags(tags); - }); - - notShown = notShown.map(function(field) { - return { - title: field.label(), - value: field.label(), - field: field - }; - }); - - - var more = selection.selectAll('.more-fields') - .data((notShown.length > 0) ? [0] : []); - - more.exit() - .remove(); - - more = more.enter() - .append('div') - .attr('class', 'more-fields') - .append('label') - .text(t('inspector.add_fields')) - .merge(more); - - - var input = more.selectAll('.value') - .data([0]); - - input.exit() - .remove(); - - input = input.enter() - .append('input') - .attr('class', 'value') - .attr('type', 'text') - .call(utilNoAuto) - .merge(input); - - input - .call(utilGetSetValue, '') - .attr('placeholder', function() { - var placeholder = []; - for (var field in notShown) { - placeholder.push(notShown[field].title); - } - return placeholder.slice(0,3).join(', ') + ((placeholder.length > 3) ? '…' : ''); - }) - .call(d3combobox() - .container(context.container()) - .data(notShown) - .minItems(1) - .on('accept', show) - ); - - - function show(field) { - field = field.field; - field.show = true; - content(selection); - field.input.focus(); - } - - - function revert(field) { - d3.event.stopPropagation(); - d3.event.preventDefault(); - dispatch.call('change', field, field.revert()); - } - - - function remove(field) { - d3.event.stopPropagation(); - d3.event.preventDefault(); - dispatch.call('change', field, field.remove()); - } - } - - - presets.preset = function(_) { - if (!arguments.length) return preset; - if (preset && preset.id === _.id) return presets; - preset = _; - fieldsArr = null; - return presets; - }; - - - presets.state = function(_) { - if (!arguments.length) return state; - state = _; - return presets; - }; - - - presets.tags = function(_) { - if (!arguments.length) return tags; - tags = _; - // Don't reset fieldsArr here. - return presets; - }; - - - presets.entityID = function(_) { - if (!arguments.length) return id; - if (id === _) return presets; - id = _; - fieldsArr = null; - return presets; - }; - - - return utilRebind(presets, dispatch, 'on'); -} diff --git a/modules/ui/preset_editor.js b/modules/ui/preset_editor.js new file mode 100644 index 000000000..beeb67cc5 --- /dev/null +++ b/modules/ui/preset_editor.js @@ -0,0 +1,220 @@ +import * as d3 from 'd3'; +import { d3combobox } from '../lib/d3.combobox.js'; +import { t } from '../util/locale'; +import { modeBrowse } from '../modes'; +import { uiDisclosure } from './disclosure'; +import { uiField } from './field'; +import { + utilGetSetValue, + utilNoAuto, + utilRebind +} from '../util'; + + +export function uiPresetEditor(context) { + var dispatch = d3.dispatch('change'), + expandedPreference = (context.storage('preset_fields.expanded') !== 'false'), + state, + fieldsArr, + preset, + tags, + entityId; + + + function presetEditor(selection) { + selection.call(uiDisclosure() + .title(t('inspector.all_fields')) + .expanded(expandedPreference) + .on('toggled', toggled) + .content(render) + ); + + function toggled(expanded) { + expandedPreference = expanded; + context.storage('preset_fields.expanded', expanded); + } + } + + + function render(selection) { + if (!fieldsArr) { + var entity = context.entity(entityId), + geometry = context.geometry(entityId), + presets = context.presets(); + + fieldsArr = []; + + preset.fields.forEach(function(field) { + if (field.matchGeometry(geometry)) { + fieldsArr.push( + uiField(context, field, entity) + ); + } + }); + + if (entity.isHighwayIntersection(context.graph()) && presets.field('restrictions')) { + fieldsArr.push( + uiField(context, presets.field('restrictions'), entity) + ); + } + + presets.universal().forEach(function(field) { + if (preset.fields.indexOf(field) === -1) { + fieldsArr.push( + uiField(context, field, entity, { show: false }) + ); + } + }); + + fieldsArr.forEach(function(field) { + field + .on('change', function(t, onInput) { + dispatch.call('change', field, t, onInput); + }); + }); + } + + fieldsArr.forEach(function(field) { + field + .state(state) + .tags(tags); + }); + + var shown = fieldsArr.filter(function(field) { return field.isShown(); }), + notShown = fieldsArr.filter(function(field) { return !field.isShown(); }); + + + var form = selection.selectAll('.preset-form') + .data([0]); + + form = form.enter() + .append('div') + .attr('class', 'preset-form inspector-inner fillL3') + .merge(form); + + + var fields = form.selectAll('.wrap-form-field') + .data(shown, function(d) { return d.id; }); + + fields.exit() + .remove(); + + // Enter + var enter = fields.enter() + .append('div') + .attr('class', function(d) { return 'wrap-form-field wrap-form-field-' + d.id; }); + + // Update + fields = fields + .merge(enter); + + fields + .order() + .each(function(d) { + d3.select(this) + .call(d.render) + .selectAll('input') + .on('keydown', function() { + // if user presses enter, and combobox is not active, accept edits.. + if (d3.event.keyCode === 13 && d3.select('.combobox').empty()) { + context.enter(modeBrowse(context)); + } + }); + }); + + + notShown = notShown.map(function(field) { + return { + title: field.label(), + value: field.label(), + field: field + }; + }); + + + var more = selection.selectAll('.more-fields') + .data((notShown.length > 0) ? [0] : []); + + more.exit() + .remove(); + + more = more.enter() + .append('div') + .attr('class', 'more-fields') + .append('label') + .text(t('inspector.add_fields')) + .merge(more); + + + var input = more.selectAll('.value') + .data([0]); + + input.exit() + .remove(); + + input = input.enter() + .append('input') + .attr('class', 'value') + .attr('type', 'text') + .call(utilNoAuto) + .merge(input); + + input + .call(utilGetSetValue, '') + .attr('placeholder', function() { + var placeholder = []; + for (var field in notShown) { + placeholder.push(notShown[field].title); + } + return placeholder.slice(0,3).join(', ') + ((placeholder.length > 3) ? '…' : ''); + }) + .call(d3combobox() + .container(context.container()) + .data(notShown) + .minItems(1) + .on('accept', function (d) { + var field = d.field; + field.show = true; + render(selection); + field.focus(); + }) + ); + + } + + + presetEditor.preset = function(_) { + if (!arguments.length) return preset; + if (preset && preset.id === _.id) return presetEditor; + preset = _; + fieldsArr = null; + return presetEditor; + }; + + + presetEditor.state = function(_) { + if (!arguments.length) return state; + state = _; + return presetEditor; + }; + + + presetEditor.tags = function(_) { + if (!arguments.length) return tags; + tags = _; + // Don't reset fieldsArr here. + return presetEditor; + }; + + + presetEditor.entityID = function(_) { + if (!arguments.length) return entityId; + if (entityId === _) return presetEditor; + entityId = _; + fieldsArr = null; + return presetEditor; + }; + + + return utilRebind(presetEditor, dispatch, 'on'); +}