From 5b10acc5c37fa68ca26449fe1f2ae6f4cda9edff Mon Sep 17 00:00:00 2001 From: Kushan Joshi <0o3ko0@gmail.com> Date: Fri, 17 Jun 2016 16:15:58 +0530 Subject: [PATCH] add iD.ui.preset --- Makefile | 20 +- index.html | 14 +- js/lib/id/ui/intro.js | 37 + js/lib/id/ui/preset.js | 1862 ++++++++++++++++++ {js/id => modules}/ui/preset/access.js | 4 +- {js/id => modules}/ui/preset/address.js | 4 +- {js/id => modules}/ui/preset/check.js | 6 +- {js/id => modules}/ui/preset/combo.js | 7 +- {js/id => modules}/ui/preset/cycleway.js | 4 +- modules/ui/preset/index.js | 12 + {js/id => modules}/ui/preset/input.js | 14 +- {js/id => modules}/ui/preset/localized.js | 4 +- {js/id => modules}/ui/preset/maxspeed.js | 4 +- {js/id => modules}/ui/preset/radio.js | 4 +- {js/id => modules}/ui/preset/restrictions.js | 4 +- {js/id => modules}/ui/preset/textarea.js | 4 +- {js/id => modules}/ui/preset/wikipedia.js | 4 +- test/index.html | 8 +- 18 files changed, 1952 insertions(+), 64 deletions(-) create mode 100644 js/lib/id/ui/preset.js rename {js/id => modules}/ui/preset/access.js (99%) rename {js/id => modules}/ui/preset/address.js (99%) rename {js/id => modules}/ui/preset/check.js (97%) rename {js/id => modules}/ui/preset/combo.js (98%) rename {js/id => modules}/ui/preset/cycleway.js (98%) create mode 100644 modules/ui/preset/index.js rename {js/id => modules}/ui/preset/input.js (94%) rename {js/id => modules}/ui/preset/localized.js (99%) rename {js/id => modules}/ui/preset/maxspeed.js (98%) rename {js/id => modules}/ui/preset/radio.js (98%) rename {js/id => modules}/ui/preset/restrictions.js (98%) rename {js/id => modules}/ui/preset/textarea.js (95%) rename {js/id => modules}/ui/preset/wikipedia.js (99%) diff --git a/Makefile b/Makefile index 1fc7dfbaf..90fd89932 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,8 @@ MODULE_TARGETS = \ js/lib/id/ui/intro.js \ js/lib/id/svg.js \ js/lib/id/ui.js \ + js/lib/id/ui/intro.js \ + js/lib/id/ui/preset.js \ js/lib/id/util.js \ js/lib/id/validations.js @@ -106,6 +108,10 @@ js/lib/id/ui.js: $(shell find modules/ui -type f) @rm -f $@ node_modules/.bin/rollup -f umd -n iD.ui modules/ui/index.js --no-strict -o $@ +js/lib/id/ui/preset.js: $(shell find modules/ui/preset -type f) + @rm -f $@ + node_modules/.bin/rollup -f umd -n iD.ui.preset modules/ui/preset/index.js --no-strict -o $@ + js/lib/id/util.js: $(shell find modules/util -type f) @rm -f $@ node_modules/.bin/rollup -f umd -n iD.util modules/util/index.js --no-strict -o $@ @@ -136,20 +142,8 @@ dist/iD.js: \ js/lib/marked.js \ js/id/start.js \ js/id/id.js \ - $(MODULE_TARGETS) \ js/id/ui.js \ - js/id/ui/preset/access.js \ - js/id/ui/preset/address.js \ - js/id/ui/preset/check.js \ - js/id/ui/preset/combo.js \ - js/id/ui/preset/cycleway.js \ - js/id/ui/preset/input.js \ - js/id/ui/preset/localized.js \ - js/id/ui/preset/maxspeed.js \ - js/id/ui/preset/radio.js \ - js/id/ui/preset/restrictions.js \ - js/id/ui/preset/textarea.js \ - js/id/ui/preset/wikipedia.js \ + $(MODULE_TARGETS) \ js/id/end.js \ js/lib/locale.js \ data/introGraph.js diff --git a/index.html b/index.html index 112df6491..18c48b90b 100644 --- a/index.html +++ b/index.html @@ -49,20 +49,8 @@ - - - - - - - - - - - - - + diff --git a/js/lib/id/ui/intro.js b/js/lib/id/ui/intro.js index 512396d5e..465216d98 100644 --- a/js/lib/id/ui/intro.js +++ b/js/lib/id/ui/intro.js @@ -247,6 +247,40 @@ return d3.rebind(step, event, 'on'); } + + function pointBox(point, context) { + var rect = context.surfaceRect(); + point = context.projection(point); + return { + left: point[0] + rect.left - 30, + top: point[1] + rect.top - 50, + width: 60, + height: 70 + }; + } + + function pad(box, padding, context) { + if (box instanceof Array) { + var rect = context.surfaceRect(); + box = context.projection(box); + box = { + left: box[0] + rect.left, + top: box[1] + rect.top + }; + } + return { + left: box.left - padding, + top: box.top - padding, + width: (box.width || 0) + 2 * padding, + height: (box.width || 0) + 2 * padding + }; + } + + function icon(name, svgklass) { + return '' + + ''; + } + function navigation(context, reveal) { var event = d3.dispatch('done'), timeouts = []; @@ -573,6 +607,9 @@ exports.area = area; exports.line = line; + exports.pad = pad; + exports.pointBox = pointBox; + exports.icon = icon; exports.navigation = navigation; exports.point = point; exports.startEditing = startEditing; diff --git a/js/lib/id/ui/preset.js b/js/lib/id/ui/preset.js new file mode 100644 index 000000000..5d3ff168b --- /dev/null +++ b/js/lib/id/ui/preset.js @@ -0,0 +1,1862 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.iD = global.iD || {}, global.iD.ui = global.iD.ui || {}, global.iD.ui.preset = global.iD.ui.preset || {}))); +}(this, function (exports) { 'use strict'; + + function access(field) { + var dispatch = d3.dispatch('change'), + items; + + function access(selection) { + var wrap = selection.selectAll('.preset-input-wrap') + .data([0]); + + wrap.enter().append('div') + .attr('class', 'cf preset-input-wrap') + .append('ul'); + + items = wrap.select('ul').selectAll('li') + .data(field.keys); + + // Enter + + var enter = items.enter().append('li') + .attr('class', function(d) { return 'cf preset-access-' + d; }); + + enter.append('span') + .attr('class', 'col6 label preset-label-access') + .attr('for', function(d) { return 'preset-input-access-' + d; }) + .text(function(d) { return field.t('types.' + d); }); + + enter.append('div') + .attr('class', 'col6 preset-input-access-wrap') + .append('input') + .attr('type', 'text') + .attr('class', 'preset-input-access') + .attr('id', function(d) { return 'preset-input-access-' + d; }) + .each(function(d) { + d3.select(this) + .call(d3.combobox() + .data(access.options(d))); + }); + + // Update + + wrap.selectAll('.preset-input-access') + .on('change', change) + .on('blur', change); + } + + function change(d) { + var tag = {}; + tag[d] = d3.select(this).value() || undefined; + dispatch.change(tag); + } + + access.options = function(type) { + var options = ['no', 'permissive', 'private', 'destination']; + + if (type !== 'access') { + options.unshift('yes'); + options.push('designated'); + + if (type === 'bicycle') { + options.push('dismount'); + } + } + + return options.map(function(option) { + return { + title: field.t('options.' + option + '.description'), + value: option + }; + }); + }; + + var placeholders = { + footway: { + foot: 'designated', + motor_vehicle: 'no' + }, + steps: { + foot: 'yes', + motor_vehicle: 'no', + bicycle: 'no', + horse: 'no' + }, + pedestrian: { + foot: 'yes', + motor_vehicle: 'no' + }, + cycleway: { + motor_vehicle: 'no', + bicycle: 'designated' + }, + bridleway: { + motor_vehicle: 'no', + horse: 'designated' + }, + path: { + foot: 'yes', + motor_vehicle: 'no', + bicycle: 'yes', + horse: 'yes' + }, + motorway: { + foot: 'no', + motor_vehicle: 'yes', + bicycle: 'no', + horse: 'no' + }, + trunk: { + motor_vehicle: 'yes' + }, + primary: { + foot: 'yes', + motor_vehicle: 'yes', + bicycle: 'yes', + horse: 'yes' + }, + secondary: { + foot: 'yes', + motor_vehicle: 'yes', + bicycle: 'yes', + horse: 'yes' + }, + tertiary: { + foot: 'yes', + motor_vehicle: 'yes', + bicycle: 'yes', + horse: 'yes' + }, + residential: { + foot: 'yes', + motor_vehicle: 'yes', + bicycle: 'yes', + horse: 'yes' + }, + unclassified: { + foot: 'yes', + motor_vehicle: 'yes', + bicycle: 'yes', + horse: 'yes' + }, + service: { + foot: 'yes', + motor_vehicle: 'yes', + bicycle: 'yes', + horse: 'yes' + }, + motorway_link: { + foot: 'no', + motor_vehicle: 'yes', + bicycle: 'no', + horse: 'no' + }, + trunk_link: { + motor_vehicle: 'yes' + }, + primary_link: { + foot: 'yes', + motor_vehicle: 'yes', + bicycle: 'yes', + horse: 'yes' + }, + secondary_link: { + foot: 'yes', + motor_vehicle: 'yes', + bicycle: 'yes', + horse: 'yes' + }, + tertiary_link: { + foot: 'yes', + motor_vehicle: 'yes', + bicycle: 'yes', + horse: 'yes' + } + }; + + access.tags = function(tags) { + items.selectAll('.preset-input-access') + .value(function(d) { return tags[d] || ''; }) + .attr('placeholder', function() { + return tags.access ? tags.access : field.placeholder(); + }); + + // items.selectAll('#preset-input-access-access') + // .attr('placeholder', 'yes'); + + _.forEach(placeholders[tags.highway], function(v, k) { + items.selectAll('#preset-input-access-' + k) + .attr('placeholder', function() { return (tags.access || v); }); + }); + }; + + access.focus = function() { + items.selectAll('.preset-input-access') + .node().focus(); + }; + + return d3.rebind(access, dispatch, 'on'); + } + + function address(field, context) { + var dispatch = d3.dispatch('init', 'change'), + wrap, + entity, + isInitialized; + + var widths = { + housenumber: 1/3, + street: 2/3, + city: 2/3, + state: 1/4, + postcode: 1/3 + }; + + function getStreets() { + var extent = entity.extent(context.graph()), + l = extent.center(), + box = iD.geo.Extent(l).padByMeters(200); + + return context.intersects(box) + .filter(isAddressable) + .map(function(d) { + var loc = context.projection([ + (extent[0][0] + extent[1][0]) / 2, + (extent[0][1] + extent[1][1]) / 2]), + choice = iD.geo.chooseEdge(context.childNodes(d), loc, context.projection); + return { + title: d.tags.name, + value: d.tags.name, + dist: choice.distance + }; + }).sort(function(a, b) { + return a.dist - b.dist; + }); + + function isAddressable(d) { + return d.tags.highway && d.tags.name && d.type === 'way'; + } + } + + function getCities() { + var extent = entity.extent(context.graph()), + l = extent.center(), + box = iD.geo.Extent(l).padByMeters(200); + + return context.intersects(box) + .filter(isAddressable) + .map(function(d) { + return { + title: d.tags['addr:city'] || d.tags.name, + value: d.tags['addr:city'] || d.tags.name, + dist: iD.geo.sphericalDistance(d.extent(context.graph()).center(), l) + }; + }).sort(function(a, b) { + return a.dist - b.dist; + }); + + function isAddressable(d) { + if (d.tags.name && + (d.tags.admin_level === '8' || d.tags.border_type === 'city')) + return true; + + if (d.tags.place && d.tags.name && ( + d.tags.place === 'city' || + d.tags.place === 'town' || + d.tags.place === 'village')) + return true; + + if (d.tags['addr:city']) return true; + + return false; + } + } + + function getPostCodes() { + var extent = entity.extent(context.graph()), + l = extent.center(), + box = iD.geo.Extent(l).padByMeters(200); + + return context.intersects(box) + .filter(isAddressable) + .map(function(d) { + return { + title: d.tags['addr:postcode'], + value: d.tags['addr:postcode'], + dist: iD.geo.sphericalDistance(d.extent(context.graph()).center(), l) + }; + }).sort(function(a, b) { + return a.dist - b.dist; + }); + + function isAddressable(d) { + return d.tags['addr:postcode']; + } + } + + function address(selection) { + isInitialized = false; + + wrap = selection.selectAll('.preset-input-wrap') + .data([0]); + + // Enter + + wrap.enter() + .append('div') + .attr('class', 'preset-input-wrap'); + + var center = entity.extent(context.graph()).center(), + addressFormat; + + iD.services.nominatim().countryCode(center, function (err, countryCode) { + addressFormat = _.find(iD.data.addressFormats, function (a) { + return a && a.countryCodes && _.includes(a.countryCodes, countryCode); + }) || _.first(iD.data.addressFormats); + + function row(r) { + // Normalize widths. + var total = _.reduce(r, function(sum, field) { + return sum + (widths[field] || 0.5); + }, 0); + + return r.map(function (field) { + return { + id: field, + width: (widths[field] || 0.5) / total + }; + }); + } + + wrap.selectAll('div') + .data(addressFormat.format) + .enter() + .append('div') + .attr('class', 'addr-row') + .selectAll('input') + .data(row) + .enter() + .append('input') + .property('type', 'text') + .attr('placeholder', function (d) { return field.t('placeholders.' + d.id); }) + .attr('class', function (d) { return 'addr-' + d.id; }) + .style('width', function (d) { return d.width * 100 + '%'; }); + + // Update + + wrap.selectAll('.addr-street') + .call(d3.combobox() + .fetcher(function(value, callback) { + callback(getStreets()); + })); + + wrap.selectAll('.addr-city') + .call(d3.combobox() + .fetcher(function(value, callback) { + callback(getCities()); + })); + + wrap.selectAll('.addr-postcode') + .call(d3.combobox() + .fetcher(function(value, callback) { + callback(getPostCodes()); + })); + + wrap.selectAll('input') + .on('blur', change()) + .on('change', change()); + + wrap.selectAll('input:not(.combobox-input)') + .on('input', change(true)); + + dispatch.init(); + isInitialized = true; + }); + } + + function change(onInput) { + return function() { + var tags = {}; + + wrap.selectAll('input') + .each(function (field) { + tags['addr:' + field.id] = this.value || undefined; + }); + + dispatch.change(tags, onInput); + }; + } + + function updateTags(tags) { + wrap.selectAll('input') + .value(function (field) { + return tags['addr:' + field.id] || ''; + }); + } + + address.entity = function(_) { + if (!arguments.length) return entity; + entity = _; + return address; + }; + + address.tags = function(tags) { + if (isInitialized) { + updateTags(tags); + } else { + dispatch.on('init', function () { + updateTags(tags); + }); + } + }; + + address.focus = function() { + var node = wrap.selectAll('input').node(); + if (node) node.focus(); + }; + + return d3.rebind(address, dispatch, 'on'); + } + + function check(field) { + var dispatch = d3.dispatch('change'), + options = field.strings && field.strings.options, + values = [], + texts = [], + entity, value, box, text, label; + + if (options) { + for (var k in options) { + values.push(k === 'undefined' ? undefined : k); + texts.push(field.t('options.' + k, { 'default': options[k] })); + } + } else { + values = [undefined, 'yes']; + texts = [t('inspector.unknown'), t('inspector.check.yes')]; + if (field.type === 'check') { + values.push('no'); + texts.push(t('inspector.check.no')); + } + } + + var check = function(selection) { + // hack: pretend oneway field is a oneway_yes field + // where implied oneway tag exists (e.g. `junction=roundabout`) #2220, #1841 + if (field.id === 'oneway') { + for (var key in entity.tags) { + if (key in iD.oneWayTags && (entity.tags[key] in iD.oneWayTags[key])) { + texts[0] = t('presets.fields.oneway_yes.options.undefined'); + break; + } + } + } + + selection.classed('checkselect', 'true'); + + label = selection.selectAll('.preset-input-wrap') + .data([0]); + + var enter = label.enter().append('label') + .attr('class', 'preset-input-wrap'); + + enter.append('input') + .property('indeterminate', field.type === 'check') + .attr('type', 'checkbox') + .attr('id', 'preset-input-' + field.id); + + enter.append('span') + .text(texts[0]) + .attr('class', 'value'); + + box = label.select('input') + .on('click', function() { + var t = {}; + t[field.key] = values[(values.indexOf(value) + 1) % values.length]; + dispatch.change(t); + d3.event.stopPropagation(); + }); + + text = label.select('span.value'); + }; + + check.entity = function(_) { + if (!arguments.length) return entity; + entity = _; + return check; + }; + + check.tags = function(tags) { + value = tags[field.key]; + box.property('indeterminate', field.type === 'check' && !value); + box.property('checked', value === 'yes'); + text.text(texts[values.indexOf(value)]); + label.classed('set', !!value); + }; + + check.focus = function() { + box.node().focus(); + }; + + return d3.rebind(check, dispatch, 'on'); + } + + function combo(field, context) { + var dispatch = d3.dispatch('change'), + isMulti = (field.type === 'multiCombo'), + optstrings = field.strings && field.strings.options, + optarray = field.options, + snake_case = (field.snake_case || (field.snake_case === undefined)), + combobox = d3.combobox().minItems(isMulti ? 1 : 2), + comboData = [], + multiData = [], + container, + input, + entity; + + // ensure multiCombo field.key ends with a ':' + if (isMulti && field.key.match(/:$/) === null) { + field.key += ':'; + } + + + 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(';'); + } + + + // returns the tag value for a display value + // (for multiCombo, dval should be the key suffix, not the entire key) + function tagValue(dval) { + dval = clean(dval || ''); + + if (optstrings) { + var match = _.find(comboData, function(o) { + return o.key && clean(o.value) === dval; + }); + if (match) { + return match.key; + } + } + + if (field.type === 'typeCombo' && !dval) { + return 'yes'; + } + + return (snake_case ? snake(dval) : dval) || undefined; + } + + + // returns the display value for a tag value + // (for multiCombo, tval should be the key suffix, not the entire key) + function displayValue(tval) { + tval = tval || ''; + + if (optstrings) { + var match = _.find(comboData, function(o) { return o.key === tval && o.value; }); + if (match) { + return match.value; + } + } + + if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') { + return ''; + } + + return snake_case ? unsnake(tval) : tval; + } + + + function objectDifference(a, b) { + return _.reject(a, function(d1) { + return _.some(b, function(d2) { return d1.value === d2.value; }); + }); + } + + + function initCombo(selection, attachTo) { + if (optstrings) { + selection.attr('readonly', 'readonly'); + selection.call(combobox, attachTo); + setStaticValues(setPlaceholder); + + } else if (optarray) { + selection.call(combobox, attachTo); + setStaticValues(setPlaceholder); + + } else if (context.taginfo()) { + selection.call(combobox.fetcher(setTaginfoValues), attachTo); + setTaginfoValues('', setPlaceholder); + } + } + + + function setStaticValues(callback) { + if (!(optstrings || optarray)) return; + + if (optstrings) { + comboData = Object.keys(optstrings).map(function(k) { + var v = field.t('options.' + k, { 'default': optstrings[k] }); + return { + key: k, + value: v, + title: v + }; + }); + + } else if (optarray) { + comboData = optarray.map(function(k) { + var v = snake_case ? unsnake(k) : k; + return { + key: k, + value: v, + title: v + }; + }); + } + + combobox.data(objectDifference(comboData, multiData)); + if (callback) callback(comboData); + } + + + function setTaginfoValues(q, callback) { + var fn = isMulti ? 'multikeys' : 'values'; + context.taginfo()[fn]({ + debounce: true, + key: field.key, + geometry: context.geometry(entity.id), + query: (isMulti ? field.key : '') + q + }, function(err, data) { + if (err) return; + comboData = _.map(data, 'value').map(function(k) { + if (isMulti) k = k.replace(field.key, ''); + var v = snake_case ? unsnake(k) : k; + return { + key: k, + value: v, + title: v + }; + }); + comboData = objectDifference(comboData, multiData); + if (callback) callback(comboData); + }); + } + + + function setPlaceholder(d) { + var ph; + if (isMulti) { + ph = field.placeholder() || t('inspector.add'); + } else { + var vals = _.map(d, 'value').filter(function(s) { return s.length < 20; }), + placeholders = vals.length > 1 ? vals : _.map(d, 'key'); + ph = field.placeholder() || placeholders.slice(0, 3).join(', '); + } + + input.attr('placeholder', ph + '…'); + } + + + function change() { + var val = tagValue(input.value()), + t = {}; + + if (isMulti) { + if (!val) return; + container.classed('active', false); + input.value(''); + field.keys.push(field.key + val); + t[field.key + val] = 'yes'; + window.setTimeout(function() { input.node().focus(); }, 10); + + } else { + t[field.key] = val; + } + + dispatch.change(t); + } + + + function removeMultikey(d) { + d3.event.stopPropagation(); + var t = {}; + t[d.key] = undefined; + dispatch.change(t); + } + + + function combo(selection) { + if (isMulti) { + container = selection.selectAll('ul').data([0]); + + container.enter() + .append('ul') + .attr('class', 'form-field-multicombo') + .on('click', function() { + window.setTimeout(function() { input.node().focus(); }, 10); + }); + + } else { + container = selection; + } + + input = container.selectAll('input') + .data([0]); + + input.enter() + .append('input') + .attr('type', 'text') + .attr('id', 'preset-input-' + field.id) + .call(initCombo, selection); + + input + .on('change', change) + .on('blur', change); + + if (isMulti) { + combobox + .on('accept', function() { + input.node().blur(); + input.node().focus(); + }); + + input + .on('focus', function() { container.classed('active', true); }); + } + } + + + combo.tags = function(tags) { + if (isMulti) { + multiData = []; + + // Build multiData array containing keys already set.. + Object.keys(tags).forEach(function(key) { + if (key.indexOf(field.key) !== 0 || tags[key].toLowerCase() !== 'yes') return; + + var suffix = key.substring(field.key.length); + multiData.push({ + key: key, + value: displayValue(suffix) + }); + }); + + // Set keys for form-field modified (needed for undo and reset buttons).. + field.keys = _.map(multiData, 'key'); + + // Exclude existing multikeys from combo options.. + var available = objectDifference(comboData, multiData); + combobox.data(available); + + // Hide "Add" button if this field uses fixed set of + // translateable optstrings and they're all currently used.. + container.selectAll('.combobox-input, .combobox-caret') + .classed('hide', optstrings && !available.length); + + + // Render chips + var chips = container.selectAll('.chips').data(multiData); + + var enter = chips.enter() + .insert('li', 'input') + .attr('class', 'chips'); + + enter.append('span'); + enter.append('a'); + + chips.select('span') + .text(function(d) { return d.value; }); + + chips.select('a') + .on('click', removeMultikey) + .attr('class', 'remove') + .text('×'); + + chips.exit() + .remove(); + + } else { + input.value(displayValue(tags[field.key])); + } + }; + + + combo.focus = function() { + input.node().focus(); + }; + + + combo.entity = function(_) { + if (!arguments.length) return entity; + entity = _; + return combo; + }; + + + return d3.rebind(combo, dispatch, 'on'); + } + + function cycleway(field) { + var dispatch = d3.dispatch('change'), + items; + + function cycleway(selection) { + var wrap = selection.selectAll('.preset-input-wrap') + .data([0]); + + wrap.enter().append('div') + .attr('class', 'cf preset-input-wrap') + .append('ul'); + + items = wrap.select('ul').selectAll('li') + .data(field.keys); + + // Enter + + var enter = items.enter().append('li') + .attr('class', function(d) { return 'cf preset-cycleway-' + d; }); + + enter.append('span') + .attr('class', 'col6 label preset-label-cycleway') + .attr('for', function(d) { return 'preset-input-cycleway-' + d; }) + .text(function(d) { return field.t('types.' + d); }); + + enter.append('div') + .attr('class', 'col6 preset-input-cycleway-wrap') + .append('input') + .attr('type', 'text') + .attr('class', 'preset-input-cycleway') + .attr('id', function(d) { return 'preset-input-cycleway-' + d; }) + .each(function(d) { + d3.select(this) + .call(d3.combobox() + .data(cycleway.options(d))); + }); + + // Update + + wrap.selectAll('.preset-input-cycleway') + .on('change', change) + .on('blur', change); + } + + function change() { + var inputs = d3.selectAll('.preset-input-cycleway')[0], + left = d3.select(inputs[0]).value(), + right = d3.select(inputs[1]).value(), + tag = {}; + if (left === 'none' || left === '') { left = undefined; } + if (right === 'none' || right === '') { right = undefined; } + + // Always set both left and right as changing one can affect the other + tag = { + cycleway: undefined, + 'cycleway:left': left, + 'cycleway:right': right + }; + + // If the left and right tags match, use the cycleway tag to tag both + // sides the same way + if (left === right) { + tag = { + cycleway: left, + 'cycleway:left': undefined, + 'cycleway:right': undefined + }; + } + + dispatch.change(tag); + } + + cycleway.options = function() { + return d3.keys(field.strings.options).map(function(option) { + return { + title: field.t('options.' + option + '.description'), + value: option + }; + }); + }; + + cycleway.tags = function(tags) { + items.selectAll('.preset-input-cycleway') + .value(function(d) { + // If cycleway is set, always return that + if (tags.cycleway) { + return tags.cycleway; + } + return tags[d] || ''; + }) + .attr('placeholder', field.placeholder()); + }; + + cycleway.focus = function() { + items.selectAll('.preset-input-cycleway') + .node().focus(); + }; + + return d3.rebind(cycleway, dispatch, 'on'); + } + + function url(field, context) { + + var dispatch = d3.dispatch('change'), + input, + entity; + + function i(selection) { + var fieldId = 'preset-input-' + field.id; + + input = selection.selectAll('input') + .data([0]); + + input.enter().append('input') + .attr('type', field.type) + .attr('id', fieldId) + .attr('placeholder', field.placeholder() || t('inspector.unknown')); + + input + .on('input', change(true)) + .on('blur', change()) + .on('change', change()); + + if (field.type === 'tel') { + var center = entity.extent(context.graph()).center(); + iD.services.nominatim().countryCode(center, function (err, countryCode) { + if (err || !iD.data.phoneFormats[countryCode]) return; + selection.selectAll('#' + fieldId) + .attr('placeholder', iD.data.phoneFormats[countryCode]); + }); + + } else if (field.type === 'number') { + input.attr('type', 'text'); + + var spinControl = selection.selectAll('.spin-control') + .data([0]); + + var enter = spinControl.enter().append('div') + .attr('class', 'spin-control'); + + enter.append('button') + .datum(1) + .attr('class', 'increment') + .attr('tabindex', -1); + + enter.append('button') + .datum(-1) + .attr('class', 'decrement') + .attr('tabindex', -1); + + spinControl.selectAll('button') + .on('click', function(d) { + d3.event.preventDefault(); + var num = parseInt(input.node().value || 0, 10); + if (!isNaN(num)) input.node().value = num + d; + change()(); + }); + } + } + + function change(onInput) { + return function() { + var t = {}; + t[field.key] = input.value() || undefined; + dispatch.change(t, onInput); + }; + } + + i.entity = function(_) { + if (!arguments.length) return entity; + entity = _; + return i; + }; + + i.tags = function(tags) { + input.value(tags[field.key] || ''); + }; + + i.focus = function() { + var node = input.node(); + if (node) node.focus(); + }; + + return d3.rebind(i, dispatch, 'on'); + } + + function localized(field, context) { + var dispatch = d3.dispatch('change', 'input'), + wikipedia = iD.services.wikipedia(), + input, localizedInputs, wikiTitles, + entity; + + function localized(selection) { + input = selection.selectAll('.localized-main') + .data([0]); + + input.enter().append('input') + .attr('type', 'text') + .attr('id', 'preset-input-' + field.id) + .attr('class', 'localized-main') + .attr('placeholder', field.placeholder()); + + if (field.id === 'name') { + var preset = context.presets().match(entity, context.graph()); + input.call(d3.combobox().fetcher( + iD.util.SuggestNames(preset, iD.data.suggestions) + )); + } + + input + .on('input', change(true)) + .on('blur', change()) + .on('change', change()); + + var translateButton = selection.selectAll('.localized-add') + .data([0]); + + translateButton.enter() + .append('button') + .attr('class', 'button-input-action localized-add minor') + .attr('tabindex', -1) + .call(iD.svg.Icon('#icon-plus')) + .call(bootstrap.tooltip() + .title(t('translate.translate')) + .placement('left')); + + translateButton + .on('click', addNew); + + localizedInputs = selection.selectAll('.localized-wrap') + .data([0]); + + localizedInputs.enter().append('div') + .attr('class', 'localized-wrap'); + } + + function addNew() { + d3.event.preventDefault(); + var data = localizedInputs.selectAll('div.entry').data(); + var defaultLang = iD.detect().locale.toLowerCase().split('-')[0]; + var langExists = _.find(data, function(datum) { return datum.lang === defaultLang;}); + var isLangEn = defaultLang.indexOf('en') > -1; + if (isLangEn || langExists) { + defaultLang = ''; + } + data.push({ lang: defaultLang, value: '' }); + localizedInputs.call(render, data); + } + + function change(onInput) { + return function() { + var t = {}; + t[field.key] = d3.select(this).value() || undefined; + dispatch.change(t, onInput); + }; + } + + function key(lang) { return field.key + ':' + lang; } + + function changeLang(d) { + var lang = d3.select(this).value(), + t = {}, + language = _.find(iD.data.wikipedia, function(d) { + return d[0].toLowerCase() === lang.toLowerCase() || + d[1].toLowerCase() === lang.toLowerCase(); + }); + + if (language) lang = language[2]; + + if (d.lang && d.lang !== lang) { + t[key(d.lang)] = undefined; + } + + var value = d3.select(this.parentNode) + .selectAll('.localized-value') + .value(); + + if (lang && value) { + t[key(lang)] = value; + } else if (lang && wikiTitles && wikiTitles[d.lang]) { + t[key(lang)] = wikiTitles[d.lang]; + } + + d.lang = lang; + dispatch.change(t); + } + + function changeValue(d) { + if (!d.lang) return; + var t = {}; + t[key(d.lang)] = d3.select(this).value() || undefined; + dispatch.change(t); + } + + function fetcher(value, cb) { + var v = value.toLowerCase(); + + cb(iD.data.wikipedia.filter(function(d) { + return d[0].toLowerCase().indexOf(v) >= 0 || + d[1].toLowerCase().indexOf(v) >= 0 || + d[2].toLowerCase().indexOf(v) >= 0; + }).map(function(d) { + return { value: d[1] }; + })); + } + + function render(selection, data) { + var wraps = selection.selectAll('div.entry'). + data(data, function(d) { return d.lang; }); + + var innerWrap = wraps.enter() + .insert('div', ':first-child'); + + innerWrap.attr('class', 'entry') + .each(function() { + var wrap = d3.select(this); + var langcombo = d3.combobox().fetcher(fetcher).minItems(0); + + var label = wrap.append('label') + .attr('class','form-label') + .text(t('translate.localized_translation_label')) + .attr('for','localized-lang'); + + label.append('button') + .attr('class', 'minor remove') + .on('click', function(d){ + d3.event.preventDefault(); + var t = {}; + t[key(d.lang)] = undefined; + dispatch.change(t); + d3.select(this.parentNode.parentNode) + .style('top','0') + .style('max-height','240px') + .transition() + .style('opacity', '0') + .style('max-height','0px') + .remove(); + }) + .call(iD.svg.Icon('#operation-delete')); + + wrap.append('input') + .attr('class', 'localized-lang') + .attr('type', 'text') + .attr('placeholder',t('translate.localized_translation_language')) + .on('blur', changeLang) + .on('change', changeLang) + .call(langcombo); + + wrap.append('input') + .on('blur', changeValue) + .on('change', changeValue) + .attr('type', 'text') + .attr('placeholder', t('translate.localized_translation_name')) + .attr('class', 'localized-value'); + }); + + innerWrap + .style('margin-top', '0px') + .style('max-height', '0px') + .style('opacity', '0') + .transition() + .duration(200) + .style('margin-top', '10px') + .style('max-height', '240px') + .style('opacity', '1') + .each('end', function() { + d3.select(this) + .style('max-height', '') + .style('overflow', 'visible'); + }); + + wraps.exit() + .transition() + .duration(200) + .style('max-height','0px') + .style('opacity', '0') + .style('top','-10px') + .remove(); + + var entry = selection.selectAll('.entry'); + + entry.select('.localized-lang') + .value(function(d) { + var lang = _.find(iD.data.wikipedia, function(lang) { return lang[2] === d.lang; }); + return lang ? lang[1] : d.lang; + }); + + entry.select('.localized-value') + .value(function(d) { return d.value; }); + } + + localized.tags = function(tags) { + // Fetch translations from wikipedia + if (tags.wikipedia && !wikiTitles) { + wikiTitles = {}; + var wm = tags.wikipedia.match(/([^:]+):(.+)/); + if (wm && wm[0] && wm[1]) { + wikipedia.translations(wm[1], wm[2], function(d) { + wikiTitles = d; + }); + } + } + + input.value(tags[field.key] || ''); + + var postfixed = [], k, m; + for (k in tags) { + m = k.match(/^(.*):([a-zA-Z_-]+)$/); + if (m && m[1] === field.key && m[2]) { + postfixed.push({ lang: m[2], value: tags[k] }); + } + } + + localizedInputs.call(render, postfixed.reverse()); + }; + + localized.focus = function() { + input.node().focus(); + }; + + localized.entity = function(_) { + if (!arguments.length) return entity; + entity = _; + return localized; + }; + + return d3.rebind(localized, dispatch, 'on'); + } + + function maxspeed(field, context) { + var dispatch = d3.dispatch('change'), + entity, + imperial, + unitInput, + combobox, + input; + + var metricValues = [20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120], + imperialValues = [20, 25, 30, 35, 40, 45, 50, 55, 65, 70]; + + function maxspeed(selection) { + combobox = d3.combobox(); + var unitCombobox = d3.combobox().data(['km/h', 'mph'].map(comboValues)); + + input = selection.selectAll('#preset-input-' + field.id) + .data([0]); + + input.enter().append('input') + .attr('type', 'text') + .attr('id', 'preset-input-' + field.id) + .attr('placeholder', field.placeholder()); + + input + .call(combobox) + .on('change', change) + .on('blur', change); + + var childNodes = context.graph().childNodes(context.entity(entity.id)), + loc = childNodes[~~(childNodes.length/2)].loc; + + imperial = _.some(iD.data.imperial.features, function(f) { + return _.some(f.geometry.coordinates, function(d) { + return iD.geo.pointInPolygon(loc, d); + }); + }); + + unitInput = selection.selectAll('input.maxspeed-unit') + .data([0]); + + unitInput.enter().append('input') + .attr('type', 'text') + .attr('class', 'maxspeed-unit'); + + unitInput + .on('blur', changeUnits) + .on('change', changeUnits) + .call(unitCombobox); + + function changeUnits() { + imperial = unitInput.value() === 'mph'; + unitInput.value(imperial ? 'mph' : 'km/h'); + setSuggestions(); + change(); + } + + } + + function setSuggestions() { + combobox.data((imperial ? imperialValues : metricValues).map(comboValues)); + unitInput.value(imperial ? 'mph' : 'km/h'); + } + + function comboValues(d) { + return { + value: d.toString(), + title: d.toString() + }; + } + + function change() { + var tag = {}, + value = input.value(); + + if (!value) { + tag[field.key] = undefined; + } else if (isNaN(value) || !imperial) { + tag[field.key] = value; + } else { + tag[field.key] = value + ' mph'; + } + + dispatch.change(tag); + } + + maxspeed.tags = function(tags) { + var value = tags[field.key]; + + if (value && value.indexOf('mph') >= 0) { + value = parseInt(value, 10); + imperial = true; + } else if (value) { + imperial = false; + } + + setSuggestions(); + + input.value(value || ''); + }; + + maxspeed.focus = function() { + input.node().focus(); + }; + + maxspeed.entity = function(_) { + entity = _; + }; + + return d3.rebind(maxspeed, dispatch, 'on'); + } + + function radio(field) { + var dispatch = d3.dispatch('change'), + labels, radios, placeholder; + + function radio(selection) { + selection.classed('preset-radio', true); + + var wrap = selection.selectAll('.preset-input-wrap') + .data([0]); + + var buttonWrap = wrap.enter().append('div') + .attr('class', 'preset-input-wrap toggle-list'); + + buttonWrap.append('span') + .attr('class', 'placeholder'); + + placeholder = selection.selectAll('.placeholder'); + + labels = wrap.selectAll('label') + .data(field.options || field.keys); + + var enter = labels.enter().append('label'); + + enter.append('input') + .attr('type', 'radio') + .attr('name', field.id) + .attr('value', function(d) { return field.t('options.' + d, { 'default': d }); }) + .attr('checked', false); + + enter.append('span') + .text(function(d) { return field.t('options.' + d, { 'default': d }); }); + + radios = labels.selectAll('input') + .on('change', change); + } + + function change() { + var t = {}; + if (field.key) t[field.key] = undefined; + radios.each(function(d) { + var active = d3.select(this).property('checked'); + if (field.key) { + if (active) t[field.key] = d; + } else { + t[d] = active ? 'yes' : undefined; + } + }); + dispatch.change(t); + } + + radio.tags = function(tags) { + function checked(d) { + if (field.key) { + return tags[field.key] === d; + } else { + return !!(tags[d] && tags[d] !== 'no'); + } + } + + labels.classed('active', checked); + radios.property('checked', checked); + var selection = radios.filter(function() { return this.checked; }); + if (selection.empty()) { + placeholder.text(t('inspector.none')); + } else { + placeholder.text(selection.attr('value')); + } + }; + + radio.focus = function() { + radios.node().focus(); + }; + + return d3.rebind(radio, dispatch, 'on'); + } + + function restrictions(field, context) { + var dispatch = d3.dispatch('change'), + hover = iD.behavior.Hover(context), + vertexID, + fromNodeID; + + + function restrictions(selection) { + // if form field is hidden or has detached from dom, clean up. + if (!d3.select('.inspector-wrap.inspector-hidden').empty() || !selection.node().parentNode) { + selection.call(restrictions.off); + return; + } + + var wrap = selection.selectAll('.preset-input-wrap') + .data([0]); + + var enter = wrap.enter() + .append('div') + .attr('class', 'preset-input-wrap'); + + enter + .append('div') + .attr('class', 'restriction-help'); + + + var intersection = iD.geo.Intersection(context.graph(), vertexID), + graph = intersection.graph, + vertex = graph.entity(vertexID), + filter = d3.functor(true), + extent = iD.geo.Extent(), + projection = iD.geo.RawMercator(); + + var d = wrap.dimensions(), + c = [d[0] / 2, d[1] / 2], + z = 24; + + projection + .scale(256 * Math.pow(2, z) / (2 * Math.PI)); + + var s = projection(vertex.loc); + + projection + .translate([c[0] - s[0], c[1] - s[1]]) + .clipExtent([[0, 0], d]); + + var drawLayers = iD.svg.Layers(projection, context).only('osm').dimensions(d), + drawVertices = iD.svg.Vertices(projection, context), + drawLines = iD.svg.Lines(projection, context), + drawTurns = iD.svg.Turns(projection, context); + + enter + .call(drawLayers) + .selectAll('.surface') + .call(hover); + + + var surface = wrap.selectAll('.surface'); + + surface + .dimensions(d) + .call(drawVertices, graph, [vertex], filter, extent, z) + .call(drawLines, graph, intersection.ways, filter) + .call(drawTurns, graph, intersection.turns(fromNodeID)); + + surface + .on('click.restrictions', click) + .on('mouseover.restrictions', mouseover) + .on('mouseout.restrictions', mouseout); + + surface + .selectAll('.selected') + .classed('selected', false); + + if (fromNodeID) { + surface + .selectAll('.' + intersection.highways[fromNodeID].id) + .classed('selected', true); + } + + mouseout(); + + context.history() + .on('change.restrictions', render); + + d3.select(window) + .on('resize.restrictions', function() { + wrap.dimensions(null); + render(); + }); + + function click() { + var datum = d3.event.target.__data__; + if (datum instanceof iD.Entity) { + fromNodeID = intersection.adjacentNodeId(datum.id); + render(); + } else if (datum instanceof iD.geo.Turn) { + if (datum.restriction) { + context.perform( + iD.actions.UnrestrictTurn(datum, projection), + t('operations.restriction.annotation.delete')); + } else { + context.perform( + iD.actions.RestrictTurn(datum, projection), + t('operations.restriction.annotation.create')); + } + } + } + + function mouseover() { + var datum = d3.event.target.__data__; + if (datum instanceof iD.geo.Turn) { + var graph = context.graph(), + presets = context.presets(), + preset; + + if (datum.restriction) { + preset = presets.match(graph.entity(datum.restriction), graph); + } else { + preset = presets.item('type/restriction/' + + iD.geo.inferRestriction( + graph, + datum.from, + datum.via, + datum.to, + projection)); + } + + wrap.selectAll('.restriction-help') + .text(t('operations.restriction.help.' + + (datum.restriction ? 'toggle_off' : 'toggle_on'), + {restriction: preset.name()})); + } + } + + function mouseout() { + wrap.selectAll('.restriction-help') + .text(t('operations.restriction.help.' + + (fromNodeID ? 'toggle' : 'select'))); + } + + function render() { + if (context.hasEntity(vertexID)) { + restrictions(selection); + } + } + } + + restrictions.entity = function(_) { + if (!vertexID || vertexID !== _.id) { + fromNodeID = null; + vertexID = _.id; + } + }; + + restrictions.tags = function() {}; + restrictions.focus = function() {}; + + restrictions.off = function(selection) { + selection.selectAll('.surface') + .call(hover.off) + .on('click.restrictions', null) + .on('mouseover.restrictions', null) + .on('mouseout.restrictions', null); + + context.history() + .on('change.restrictions', null); + + d3.select(window) + .on('resize.restrictions', null); + }; + + return d3.rebind(restrictions, dispatch, 'on'); + } + + function textarea(field) { + var dispatch = d3.dispatch('change'), + input; + + function textarea(selection) { + input = selection.selectAll('textarea') + .data([0]); + + input.enter().append('textarea') + .attr('id', 'preset-input-' + field.id) + .attr('placeholder', field.placeholder() || t('inspector.unknown')) + .attr('maxlength', 255); + + input + .on('input', change(true)) + .on('blur', change()) + .on('change', change()); + } + + function change(onInput) { + return function() { + var t = {}; + t[field.key] = input.value() || undefined; + dispatch.change(t, onInput); + }; + } + + textarea.tags = function(tags) { + input.value(tags[field.key] || ''); + }; + + textarea.focus = function() { + input.node().focus(); + }; + + return d3.rebind(textarea, dispatch, 'on'); + } + + function wikipedia(field, context) { + var dispatch = d3.dispatch('change'), + wikipedia = iD.services.wikipedia(), + wikidata = iD.services.wikidata(), + link, entity, lang, title; + + function wiki(selection) { + var langcombo = d3.combobox() + .fetcher(function(value, cb) { + var v = value.toLowerCase(); + + cb(iD.data.wikipedia.filter(function(d) { + return d[0].toLowerCase().indexOf(v) >= 0 || + d[1].toLowerCase().indexOf(v) >= 0 || + d[2].toLowerCase().indexOf(v) >= 0; + }).map(function(d) { + return { value: d[1] }; + })); + }); + + var titlecombo = d3.combobox() + .fetcher(function(value, cb) { + + if (!value) value = context.entity(entity.id).tags.name || ''; + var searchfn = value.length > 7 ? wikipedia.search : wikipedia.suggestions; + + searchfn(language()[2], value, function(query, data) { + cb(data.map(function(d) { + return { value: d }; + })); + }); + }); + + lang = selection.selectAll('input.wiki-lang') + .data([0]); + + lang.enter().append('input') + .attr('type', 'text') + .attr('class', 'wiki-lang') + .attr('placeholder', t('translate.localized_translation_language')) + .value('English'); + + lang + .call(langcombo) + .on('blur', changeLang) + .on('change', changeLang); + + title = selection.selectAll('input.wiki-title') + .data([0]); + + title.enter().append('input') + .attr('type', 'text') + .attr('class', 'wiki-title') + .attr('id', 'preset-input-' + field.id); + + title + .call(titlecombo) + .on('blur', blur) + .on('change', change); + + link = selection.selectAll('a.wiki-link') + .data([0]); + + link.enter().append('a') + .attr('class', 'wiki-link button-input-action minor') + .attr('tabindex', -1) + .attr('target', '_blank') + .call(iD.svg.Icon('#icon-out-link', 'inline')); + } + + function language() { + var value = lang.value().toLowerCase(); + var locale = iD.detect().locale.toLowerCase(); + var localeLanguage; + return _.find(iD.data.wikipedia, function(d) { + if (d[2] === locale) localeLanguage = d; + return d[0].toLowerCase() === value || + d[1].toLowerCase() === value || + d[2] === value; + }) || localeLanguage || ['English', 'English', 'en']; + } + + function changeLang() { + lang.value(language()[1]); + change(true); + } + + function blur() { + change(true); + } + + function change(skipWikidata) { + var value = title.value(), + m = value.match(/https?:\/\/([-a-z]+)\.wikipedia\.org\/(?:wiki|\1-[-a-z]+)\/([^#]+)(?:#(.+))?/), + l = m && _.find(iD.data.wikipedia, function(d) { return m[1] === d[2]; }), + anchor, + syncTags = {}; + + if (l) { + // Normalize title http://www.mediawiki.org/wiki/API:Query#Title_normalization + value = decodeURIComponent(m[2]).replace(/_/g, ' '); + if (m[3]) { + try { + // Best-effort `anchordecode:` implementation + anchor = decodeURIComponent(m[3].replace(/\.([0-9A-F]{2})/g, '%$1')); + } catch (e) { + anchor = decodeURIComponent(m[3]); + } + value += '#' + anchor.replace(/_/g, ' '); + } + value = value.slice(0, 1).toUpperCase() + value.slice(1); + lang.value(l[1]); + title.value(value); + } + + syncTags.wikipedia = value ? language()[2] + ':' + value : undefined; + if (!skipWikidata) { + syncTags.wikidata = undefined; + } + + dispatch.change(syncTags); + + + if (skipWikidata || !value || !language()[2]) return; + + // attempt asynchronous update of wikidata tag.. + var initEntityId = entity.id, + initWikipedia = context.entity(initEntityId).tags.wikipedia; + + wikidata.itemsByTitle(language()[2], value, function (title, data) { + // 1. most recent change was a tag change + var annotation = t('operations.change_tags.annotation'), + currAnnotation = context.history().undoAnnotation(); + if (currAnnotation !== annotation) return; + + // 2. same entity exists and still selected + var selectedIds = context.selectedIDs(), + currEntityId = selectedIds.length > 0 && selectedIds[0]; + if (currEntityId !== initEntityId) return; + + // 3. wikipedia value has not changed + var currTags = _.clone(context.entity(currEntityId).tags), + qids = data && Object.keys(data); + if (initWikipedia !== currTags.wikipedia) return; + + // ok to coalesce the update of wikidata tag into the previous tag change + currTags.wikidata = qids && _.find(qids, function (id) { + return id.match(/^Q\d+$/); + }); + + context.overwrite(iD.actions.ChangeTags(currEntityId, currTags), annotation); + dispatch.change(currTags); + }); + } + + wiki.tags = function(tags) { + var value = tags[field.key] || '', + m = value.match(/([^:]+):([^#]+)(?:#(.+))?/), + l = m && _.find(iD.data.wikipedia, function(d) { return m[1] === d[2]; }), + anchor = m && m[3]; + + // value in correct format + if (l) { + lang.value(l[1]); + title.value(m[2] + (anchor ? ('#' + anchor) : '')); + if (anchor) { + try { + // Best-effort `anchorencode:` implementation + anchor = encodeURIComponent(anchor.replace(/ /g, '_')).replace(/%/g, '.'); + } catch (e) { + anchor = anchor.replace(/ /g, '_'); + } + } + link.attr('href', 'https://' + m[1] + '.wikipedia.org/wiki/' + + m[2].replace(/ /g, '_') + (anchor ? ('#' + anchor) : '')); + + // unrecognized value format + } else { + title.value(value); + if (value && value !== '') { + lang.value(''); + } + link.attr('href', 'https://en.wikipedia.org/wiki/Special:Search?search=' + value); + } + }; + + wiki.entity = function(_) { + if (!arguments.length) return entity; + entity = _; + return wiki; + }; + + wiki.focus = function() { + title.node().focus(); + }; + + return d3.rebind(wiki, dispatch, 'on'); + } + + exports.access = access; + exports.address = address; + exports.check = check; + exports.defaultcheck = check; + exports.combo = combo; + exports.typeCombo = combo; + exports.multiCombo = combo; + exports.cycleway = cycleway; + exports.text = url; + exports.url = url; + exports.number = url; + exports.email = url; + exports.tel = url; + exports.localized = localized; + exports.maxspeed = maxspeed; + exports.radio = radio; + exports.restrictions = restrictions; + exports.textarea = textarea; + exports.wikipedia = wikipedia; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); \ No newline at end of file diff --git a/js/id/ui/preset/access.js b/modules/ui/preset/access.js similarity index 99% rename from js/id/ui/preset/access.js rename to modules/ui/preset/access.js index 51354758c..2b9f6525c 100644 --- a/js/id/ui/preset/access.js +++ b/modules/ui/preset/access.js @@ -1,4 +1,4 @@ -iD.ui.preset.access = function(field) { +export function access(field) { var dispatch = d3.dispatch('change'), items; @@ -193,4 +193,4 @@ iD.ui.preset.access = function(field) { }; return d3.rebind(access, dispatch, 'on'); -}; +} diff --git a/js/id/ui/preset/address.js b/modules/ui/preset/address.js similarity index 99% rename from js/id/ui/preset/address.js rename to modules/ui/preset/address.js index e125f6002..56358338a 100644 --- a/js/id/ui/preset/address.js +++ b/modules/ui/preset/address.js @@ -1,4 +1,4 @@ -iD.ui.preset.address = function(field, context) { +export function address(field, context) { var dispatch = d3.dispatch('init', 'change'), wrap, entity, @@ -216,4 +216,4 @@ iD.ui.preset.address = function(field, context) { }; return d3.rebind(address, dispatch, 'on'); -}; +} diff --git a/js/id/ui/preset/check.js b/modules/ui/preset/check.js similarity index 97% rename from js/id/ui/preset/check.js rename to modules/ui/preset/check.js index 7765aba36..9bcf02d1b 100644 --- a/js/id/ui/preset/check.js +++ b/modules/ui/preset/check.js @@ -1,5 +1,5 @@ -iD.ui.preset.check = -iD.ui.preset.defaultcheck = function(field) { +export { check as defaultcheck }; +export function check(field) { var dispatch = d3.dispatch('change'), options = field.strings && field.strings.options, values = [], @@ -79,4 +79,4 @@ iD.ui.preset.defaultcheck = function(field) { }; return d3.rebind(check, dispatch, 'on'); -}; +} diff --git a/js/id/ui/preset/combo.js b/modules/ui/preset/combo.js similarity index 98% rename from js/id/ui/preset/combo.js rename to modules/ui/preset/combo.js index cafd3e70b..cebe8741d 100644 --- a/js/id/ui/preset/combo.js +++ b/modules/ui/preset/combo.js @@ -1,6 +1,5 @@ -iD.ui.preset.combo = -iD.ui.preset.typeCombo = -iD.ui.preset.multiCombo = function(field, context) { +export { combo as typeCombo, combo as multiCombo }; +export function combo(field, context) { var dispatch = d3.dispatch('change'), isMulti = (field.type === 'multiCombo'), optstrings = field.strings && field.strings.options, @@ -304,4 +303,4 @@ iD.ui.preset.multiCombo = function(field, context) { return d3.rebind(combo, dispatch, 'on'); -}; +} diff --git a/js/id/ui/preset/cycleway.js b/modules/ui/preset/cycleway.js similarity index 98% rename from js/id/ui/preset/cycleway.js rename to modules/ui/preset/cycleway.js index bf56c94b7..24af9e893 100644 --- a/js/id/ui/preset/cycleway.js +++ b/modules/ui/preset/cycleway.js @@ -1,4 +1,4 @@ -iD.ui.preset.cycleway = function(field) { +export function cycleway(field) { var dispatch = d3.dispatch('change'), items; @@ -97,4 +97,4 @@ iD.ui.preset.cycleway = function(field) { }; return d3.rebind(cycleway, dispatch, 'on'); -}; +} diff --git a/modules/ui/preset/index.js b/modules/ui/preset/index.js new file mode 100644 index 000000000..264462df9 --- /dev/null +++ b/modules/ui/preset/index.js @@ -0,0 +1,12 @@ +export { access } from './access'; +export { address } from './address'; +export { check, defaultcheck} from './check'; +export { combo, typeCombo, multiCombo } from './combo'; +export { cycleway } from './cycleway'; +export { text, url, number, email, tel } from './input'; +export { localized } from './localized'; +export { maxspeed } from './maxspeed'; +export { radio } from './radio'; +export { restrictions } from './restrictions'; +export { textarea } from './textarea'; +export { wikipedia } from './wikipedia'; diff --git a/js/id/ui/preset/input.js b/modules/ui/preset/input.js similarity index 94% rename from js/id/ui/preset/input.js rename to modules/ui/preset/input.js index 926b9a8a2..8d222ecc4 100644 --- a/js/id/ui/preset/input.js +++ b/modules/ui/preset/input.js @@ -1,8 +1,10 @@ -iD.ui.preset.text = -iD.ui.preset.number = -iD.ui.preset.tel = -iD.ui.preset.email = -iD.ui.preset.url = function(field, context) { +export { + url as text, + url as number, + url as tel, + url as email +}; +export function url(field, context) { var dispatch = d3.dispatch('change'), input, @@ -85,4 +87,4 @@ iD.ui.preset.url = function(field, context) { }; return d3.rebind(i, dispatch, 'on'); -}; +} diff --git a/js/id/ui/preset/localized.js b/modules/ui/preset/localized.js similarity index 99% rename from js/id/ui/preset/localized.js rename to modules/ui/preset/localized.js index f41d2c191..6c7de3bcd 100644 --- a/js/id/ui/preset/localized.js +++ b/modules/ui/preset/localized.js @@ -1,4 +1,4 @@ -iD.ui.preset.localized = function(field, context) { +export function localized(field, context) { var dispatch = d3.dispatch('change', 'input'), wikipedia = iD.services.wikipedia(), input, localizedInputs, wikiTitles, @@ -239,4 +239,4 @@ iD.ui.preset.localized = function(field, context) { }; return d3.rebind(localized, dispatch, 'on'); -}; +} diff --git a/js/id/ui/preset/maxspeed.js b/modules/ui/preset/maxspeed.js similarity index 98% rename from js/id/ui/preset/maxspeed.js rename to modules/ui/preset/maxspeed.js index a11ee6855..7a4848a97 100644 --- a/js/id/ui/preset/maxspeed.js +++ b/modules/ui/preset/maxspeed.js @@ -1,4 +1,4 @@ -iD.ui.preset.maxspeed = function(field, context) { +export function maxspeed(field, context) { var dispatch = d3.dispatch('change'), entity, imperial, @@ -107,4 +107,4 @@ iD.ui.preset.maxspeed = function(field, context) { }; return d3.rebind(maxspeed, dispatch, 'on'); -}; +} diff --git a/js/id/ui/preset/radio.js b/modules/ui/preset/radio.js similarity index 98% rename from js/id/ui/preset/radio.js rename to modules/ui/preset/radio.js index 11241e37c..d1a3374eb 100644 --- a/js/id/ui/preset/radio.js +++ b/modules/ui/preset/radio.js @@ -1,4 +1,4 @@ -iD.ui.preset.radio = function(field) { +export function radio(field) { var dispatch = d3.dispatch('change'), labels, radios, placeholder; @@ -72,4 +72,4 @@ iD.ui.preset.radio = function(field) { }; return d3.rebind(radio, dispatch, 'on'); -}; +} diff --git a/js/id/ui/preset/restrictions.js b/modules/ui/preset/restrictions.js similarity index 98% rename from js/id/ui/preset/restrictions.js rename to modules/ui/preset/restrictions.js index fe95a9138..2874a9787 100644 --- a/js/id/ui/preset/restrictions.js +++ b/modules/ui/preset/restrictions.js @@ -1,4 +1,4 @@ -iD.ui.preset.restrictions = function(field, context) { +export function restrictions(field, context) { var dispatch = d3.dispatch('change'), hover = iD.behavior.Hover(context), vertexID, @@ -171,4 +171,4 @@ iD.ui.preset.restrictions = function(field, context) { }; return d3.rebind(restrictions, dispatch, 'on'); -}; +} diff --git a/js/id/ui/preset/textarea.js b/modules/ui/preset/textarea.js similarity index 95% rename from js/id/ui/preset/textarea.js rename to modules/ui/preset/textarea.js index 92d18aa63..f00f668b3 100644 --- a/js/id/ui/preset/textarea.js +++ b/modules/ui/preset/textarea.js @@ -1,4 +1,4 @@ -iD.ui.preset.textarea = function(field) { +export function textarea(field) { var dispatch = d3.dispatch('change'), input; @@ -34,4 +34,4 @@ iD.ui.preset.textarea = function(field) { }; return d3.rebind(textarea, dispatch, 'on'); -}; +} diff --git a/js/id/ui/preset/wikipedia.js b/modules/ui/preset/wikipedia.js similarity index 99% rename from js/id/ui/preset/wikipedia.js rename to modules/ui/preset/wikipedia.js index b129eb9ba..6f7cf4f3b 100644 --- a/js/id/ui/preset/wikipedia.js +++ b/modules/ui/preset/wikipedia.js @@ -1,4 +1,4 @@ -iD.ui.preset.wikipedia = function(field, context) { +export function wikipedia(field, context) { var dispatch = d3.dispatch('change'), wikipedia = iD.services.wikipedia(), wikidata = iD.services.wikidata(), @@ -195,4 +195,4 @@ iD.ui.preset.wikipedia = function(field, context) { }; return d3.rebind(wiki, dispatch, 'on'); -}; +} diff --git a/test/index.html b/test/index.html index 63babcfe5..d305c0218 100644 --- a/test/index.html +++ b/test/index.html @@ -57,14 +57,8 @@ + - - - - - - -