diff --git a/Makefile b/Makefile
index 46b51f914..1818e10bf 100644
--- a/Makefile
+++ b/Makefile
@@ -44,6 +44,7 @@ $(BUILDJS_TARGETS): $(BUILDJS_SOURCES) build.js
MODULE_TARGETS = \
js/lib/id/actions.js \
+ js/lib/id/behavior.js \
js/lib/id/core.js \
js/lib/id/geo.js \
js/lib/id/modes.js \
@@ -59,6 +60,10 @@ js/lib/id/actions.js: $(shell find modules/actions -type f)
@rm -f $@
node_modules/.bin/rollup -f umd -n iD.actions modules/actions/index.js --no-strict -o $@
+js/lib/id/behavior.js: $(shell find modules/behavior -type f)
+ @rm -f $@
+ node_modules/.bin/rollup -f umd -n iD.behavior modules/behavior/index.js --no-strict -o $@
+
js/lib/id/core.js: $(shell find modules/core -type f)
@rm -f $@
node_modules/.bin/rollup -f umd -n iD modules/core/index.js --no-strict -o $@
@@ -122,20 +127,6 @@ dist/iD.js: \
js/id/start.js \
js/id/id.js \
$(MODULE_TARGETS) \
- js/id/behavior.js \
- js/id/behavior/add_way.js \
- js/id/behavior/breathe.js \
- js/id/behavior/copy.js \
- js/id/behavior/drag.js \
- js/id/behavior/draw.js \
- js/id/behavior/draw_way.js \
- js/id/behavior/edit.js \
- js/id/behavior/hash.js \
- js/id/behavior/hover.js \
- js/id/behavior/lasso.js \
- js/id/behavior/paste.js \
- js/id/behavior/select.js \
- js/id/behavior/tail.js \
js/id/ui.js \
js/id/ui/account.js \
js/id/ui/attribution.js \
diff --git a/index.html b/index.html
index 0a6b1b8e6..fcb62f626 100644
--- a/index.html
+++ b/index.html
@@ -35,6 +35,7 @@
+
@@ -117,21 +118,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/js/id/behavior.js b/js/id/behavior.js
deleted file mode 100644
index c0801afa3..000000000
--- a/js/id/behavior.js
+++ /dev/null
@@ -1 +0,0 @@
-iD.behavior = {};
diff --git a/js/lib/id/behavior.js b/js/lib/id/behavior.js
new file mode 100644
index 000000000..b81bfc77f
--- /dev/null
+++ b/js/lib/id/behavior.js
@@ -0,0 +1,1394 @@
+(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.behavior = global.iD.behavior || {})));
+}(this, function (exports) { 'use strict';
+
+ function Edit(context) {
+ function edit() {
+ context.map()
+ .minzoom(context.minEditableZoom());
+ }
+
+ edit.off = function() {
+ context.map()
+ .minzoom(0);
+ };
+
+ return edit;
+ }
+
+ /*
+ The hover behavior adds the `.hover` class on mouseover to all elements to which
+ the identical datum is bound, and removes it on mouseout.
+
+ The :hover pseudo-class is insufficient for iD's purposes because a datum's visual
+ representation may consist of several elements scattered throughout the DOM hierarchy.
+ Only one of these elements can have the :hover pseudo-class, but all of them will
+ have the .hover class.
+ */
+ function Hover() {
+ var dispatch = d3.dispatch('hover'),
+ selection,
+ altDisables,
+ target;
+
+ function keydown() {
+ if (altDisables && d3.event.keyCode === d3.keybinding.modifierCodes.alt) {
+ dispatch.hover(null);
+ selection.selectAll('.hover')
+ .classed('hover-suppressed', true)
+ .classed('hover', false);
+ }
+ }
+
+ function keyup() {
+ if (altDisables && d3.event.keyCode === d3.keybinding.modifierCodes.alt) {
+ dispatch.hover(target ? target.id : null);
+ selection.selectAll('.hover-suppressed')
+ .classed('hover-suppressed', false)
+ .classed('hover', true);
+ }
+ }
+
+ var hover = function(__) {
+ selection = __;
+
+ function enter(d) {
+ if (d === target) return;
+
+ target = d;
+
+ selection.selectAll('.hover')
+ .classed('hover', false);
+ selection.selectAll('.hover-suppressed')
+ .classed('hover-suppressed', false);
+
+ if (target instanceof iD.Entity) {
+ var selector = '.' + target.id;
+
+ if (target.type === 'relation') {
+ target.members.forEach(function(member) {
+ selector += ', .' + member.id;
+ });
+ }
+
+ var suppressed = altDisables && d3.event && d3.event.altKey;
+
+ selection.selectAll(selector)
+ .classed(suppressed ? 'hover-suppressed' : 'hover', true);
+
+ dispatch.hover(target.id);
+ } else {
+ dispatch.hover(null);
+ }
+ }
+
+ var down;
+
+ function mouseover() {
+ if (down) return;
+ var target = d3.event.target;
+ enter(target ? target.__data__ : null);
+ }
+
+ function mouseout() {
+ if (down) return;
+ var target = d3.event.relatedTarget;
+ enter(target ? target.__data__ : null);
+ }
+
+ function mousedown() {
+ down = true;
+ d3.select(window)
+ .on('mouseup.hover', mouseup);
+ }
+
+ function mouseup() {
+ down = false;
+ }
+
+ selection
+ .on('mouseover.hover', mouseover)
+ .on('mouseout.hover', mouseout)
+ .on('mousedown.hover', mousedown)
+ .on('mouseup.hover', mouseup);
+
+ d3.select(window)
+ .on('keydown.hover', keydown)
+ .on('keyup.hover', keyup);
+ };
+
+ hover.off = function(selection) {
+ selection.selectAll('.hover')
+ .classed('hover', false);
+ selection.selectAll('.hover-suppressed')
+ .classed('hover-suppressed', false);
+
+ selection
+ .on('mouseover.hover', null)
+ .on('mouseout.hover', null)
+ .on('mousedown.hover', null)
+ .on('mouseup.hover', null);
+
+ d3.select(window)
+ .on('keydown.hover', null)
+ .on('keyup.hover', null)
+ .on('mouseup.hover', null);
+ };
+
+ hover.altDisables = function(_) {
+ if (!arguments.length) return altDisables;
+ altDisables = _;
+ return hover;
+ };
+
+ return d3.rebind(hover, dispatch, 'on');
+ }
+
+ function Tail() {
+ var text,
+ container,
+ xmargin = 25,
+ tooltipSize = [0, 0],
+ selectionSize = [0, 0];
+
+ function tail(selection) {
+ if (!text) return;
+
+ d3.select(window)
+ .on('resize.tail', function() { selectionSize = selection.dimensions(); });
+
+ function show() {
+ container.style('display', 'block');
+ tooltipSize = container.dimensions();
+ }
+
+ function mousemove() {
+ if (container.style('display') === 'none') show();
+ var xoffset = ((d3.event.clientX + tooltipSize[0] + xmargin) > selectionSize[0]) ?
+ -tooltipSize[0] - xmargin : xmargin;
+ container.classed('left', xoffset > 0);
+ iD.util.setTransform(container, d3.event.clientX + xoffset, d3.event.clientY);
+ }
+
+ function mouseleave() {
+ if (d3.event.relatedTarget !== container.node()) {
+ container.style('display', 'none');
+ }
+ }
+
+ function mouseenter() {
+ if (d3.event.relatedTarget !== container.node()) {
+ show();
+ }
+ }
+
+ container = d3.select(document.body)
+ .append('div')
+ .style('display', 'none')
+ .attr('class', 'tail tooltip-inner');
+
+ container.append('div')
+ .text(text);
+
+ selection
+ .on('mousemove.tail', mousemove)
+ .on('mouseenter.tail', mouseenter)
+ .on('mouseleave.tail', mouseleave);
+
+ container
+ .on('mousemove.tail', mousemove);
+
+ tooltipSize = container.dimensions();
+ selectionSize = selection.dimensions();
+ }
+
+ tail.off = function(selection) {
+ if (!text) return;
+
+ container
+ .on('mousemove.tail', null)
+ .remove();
+
+ selection
+ .on('mousemove.tail', null)
+ .on('mouseenter.tail', null)
+ .on('mouseleave.tail', null);
+
+ d3.select(window)
+ .on('resize.tail', null);
+ };
+
+ tail.text = function(_) {
+ if (!arguments.length) return text;
+ text = _;
+ return tail;
+ };
+
+ return tail;
+ }
+
+ function Draw(context) {
+ var event = d3.dispatch('move', 'click', 'clickWay',
+ 'clickNode', 'undo', 'cancel', 'finish'),
+ keybinding = d3.keybinding('draw'),
+ hover = Hover(context)
+ .altDisables(true)
+ .on('hover', context.ui().sidebar.hover),
+ tail = Tail(),
+ edit = Edit(context),
+ closeTolerance = 4,
+ tolerance = 12,
+ mouseLeave = false,
+ lastMouse = null,
+ cached = Draw;
+
+ function datum() {
+ if (d3.event.altKey) return {};
+
+ if (d3.event.type === 'keydown') {
+ return (lastMouse && lastMouse.target.__data__) || {};
+ } else {
+ return d3.event.target.__data__ || {};
+ }
+ }
+
+ function mousedown() {
+
+ function point() {
+ var p = context.container().node();
+ return touchId !== null ? d3.touches(p).filter(function(p) {
+ return p.identifier === touchId;
+ })[0] : d3.mouse(p);
+ }
+
+ var element = d3.select(this),
+ touchId = d3.event.touches ? d3.event.changedTouches[0].identifier : null,
+ t1 = +new Date(),
+ p1 = point();
+
+ element.on('mousemove.draw', null);
+
+ d3.select(window).on('mouseup.draw', function() {
+ var t2 = +new Date(),
+ p2 = point(),
+ dist = iD.geo.euclideanDistance(p1, p2);
+
+ element.on('mousemove.draw', mousemove);
+ d3.select(window).on('mouseup.draw', null);
+
+ if (dist < closeTolerance || (dist < tolerance && (t2 - t1) < 500)) {
+ // Prevent a quick second click
+ d3.select(window).on('click.draw-block', function() {
+ d3.event.stopPropagation();
+ }, true);
+
+ context.map().dblclickEnable(false);
+
+ window.setTimeout(function() {
+ context.map().dblclickEnable(true);
+ d3.select(window).on('click.draw-block', null);
+ }, 500);
+
+ click();
+ }
+ });
+ }
+
+ function mousemove() {
+ lastMouse = d3.event;
+ event.move(datum());
+ }
+
+ function mouseenter() {
+ mouseLeave = false;
+ }
+
+ function mouseleave() {
+ mouseLeave = true;
+ }
+
+ function click() {
+ var d = datum();
+ if (d.type === 'way') {
+ var dims = context.map().dimensions(),
+ mouse = context.mouse(),
+ pad = 5,
+ trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad &&
+ mouse[1] > pad && mouse[1] < dims[1] - pad;
+
+ if (trySnap) {
+ var choice = iD.geo.chooseEdge(context.childNodes(d), context.mouse(), context.projection),
+ edge = [d.nodes[choice.index - 1], d.nodes[choice.index]];
+ event.clickWay(choice.loc, edge);
+ } else {
+ event.click(context.map().mouseCoordinates());
+ }
+
+ } else if (d.type === 'node') {
+ event.clickNode(d);
+
+ } else {
+ event.click(context.map().mouseCoordinates());
+ }
+ }
+
+ function space() {
+ var currSpace = context.mouse();
+ if (cached.disableSpace && cached.lastSpace) {
+ var dist = iD.geo.euclideanDistance(cached.lastSpace, currSpace);
+ if (dist > tolerance) {
+ cached.disableSpace = false;
+ }
+ }
+
+ if (cached.disableSpace || mouseLeave || !lastMouse) return;
+
+ // user must move mouse or release space bar to allow another click
+ cached.lastSpace = currSpace;
+ cached.disableSpace = true;
+
+ d3.select(window).on('keyup.space-block', function() {
+ cached.disableSpace = false;
+ d3.select(window).on('keyup.space-block', null);
+ });
+
+ d3.event.preventDefault();
+ click();
+ }
+
+ function backspace() {
+ d3.event.preventDefault();
+ event.undo();
+ }
+
+ function del() {
+ d3.event.preventDefault();
+ event.cancel();
+ }
+
+ function ret() {
+ d3.event.preventDefault();
+ event.finish();
+ }
+
+ function draw(selection) {
+ context.install(hover);
+ context.install(edit);
+
+ if (!context.inIntro() && !cached.usedTails[tail.text()]) {
+ context.install(tail);
+ }
+
+ keybinding
+ .on('⌫', backspace)
+ .on('⌦', del)
+ .on('⎋', ret)
+ .on('↩', ret)
+ .on('space', space)
+ .on('⌥space', space);
+
+ selection
+ .on('mouseenter.draw', mouseenter)
+ .on('mouseleave.draw', mouseleave)
+ .on('mousedown.draw', mousedown)
+ .on('mousemove.draw', mousemove);
+
+ d3.select(document)
+ .call(keybinding);
+
+ return draw;
+ }
+
+ draw.off = function(selection) {
+ context.ui().sidebar.hover.cancel();
+ context.uninstall(hover);
+ context.uninstall(edit);
+
+ if (!context.inIntro() && !cached.usedTails[tail.text()]) {
+ context.uninstall(tail);
+ cached.usedTails[tail.text()] = true;
+ }
+
+ selection
+ .on('mouseenter.draw', null)
+ .on('mouseleave.draw', null)
+ .on('mousedown.draw', null)
+ .on('mousemove.draw', null);
+
+ d3.select(window)
+ .on('mouseup.draw', null);
+ // note: keyup.space-block, click.draw-block should remain
+
+ d3.select(document)
+ .call(keybinding.off);
+ };
+
+ draw.tail = function(_) {
+ tail.text(_);
+ return draw;
+ };
+
+ return d3.rebind(draw, event, 'on');
+ }
+
+ Draw.usedTails = {};
+ Draw.disableSpace = false;
+ Draw.lastSpace = null;
+
+ function AddWay(context) {
+ var event = d3.dispatch('start', 'startFromWay', 'startFromNode'),
+ draw = Draw(context);
+
+ var addWay = function(surface) {
+ draw.on('click', event.start)
+ .on('clickWay', event.startFromWay)
+ .on('clickNode', event.startFromNode)
+ .on('cancel', addWay.cancel)
+ .on('finish', addWay.cancel);
+
+ context.map()
+ .dblclickEnable(false);
+
+ surface.call(draw);
+ };
+
+ addWay.off = function(surface) {
+ surface.call(draw.off);
+ };
+
+ addWay.cancel = function() {
+ window.setTimeout(function() {
+ context.map().dblclickEnable(true);
+ }, 1000);
+
+ context.enter(iD.modes.Browse(context));
+ };
+
+ addWay.tail = function(text) {
+ draw.tail(text);
+ return addWay;
+ };
+
+ return d3.rebind(addWay, event, 'on');
+ }
+
+ function Breathe(){
+ var duration = 800,
+ selector = '.selected.shadow, .selected .shadow',
+ selected = d3.select(null),
+ classed = '',
+ params = {},
+ done;
+
+ function reset(selection) {
+ selection
+ .style('stroke-opacity', null)
+ .style('stroke-width', null)
+ .style('fill-opacity', null)
+ .style('r', null);
+ }
+
+ function setAnimationParams(transition, fromTo) {
+ transition
+ .style('stroke-opacity', function(d) { return params[d.id][fromTo].opacity; })
+ .style('stroke-width', function(d) { return params[d.id][fromTo].width; })
+ .style('fill-opacity', function(d) { return params[d.id][fromTo].opacity; })
+ .style('r', function(d) { return params[d.id][fromTo].width; });
+ }
+
+ function calcAnimationParams(selection) {
+ selection
+ .call(reset)
+ .each(function(d) {
+ var s = d3.select(this),
+ tag = s.node().tagName,
+ p = {'from': {}, 'to': {}},
+ opacity, width;
+
+ // determine base opacity and width
+ if (tag === 'circle') {
+ opacity = parseFloat(s.style('fill-opacity') || 0.5);
+ width = parseFloat(s.style('r') || 15.5);
+ } else {
+ opacity = parseFloat(s.style('stroke-opacity') || 0.7);
+ width = parseFloat(s.style('stroke-width') || 10);
+ }
+
+ // calculate from/to interpolation params..
+ p.tag = tag;
+ p.from.opacity = opacity * 0.6;
+ p.to.opacity = opacity * 1.25;
+ p.from.width = width * 0.9;
+ p.to.width = width * (tag === 'circle' ? 1.5 : 1.25);
+ params[d.id] = p;
+ });
+ }
+
+ function run(surface, fromTo) {
+ var toFrom = (fromTo === 'from' ? 'to': 'from'),
+ currSelected = surface.selectAll(selector),
+ currClassed = surface.attr('class'),
+ n = 0;
+
+ if (done || currSelected.empty()) {
+ selected.call(reset);
+ return;
+ }
+
+ if (!_.isEqual(currSelected, selected) || currClassed !== classed) {
+ selected.call(reset);
+ classed = currClassed;
+ selected = currSelected.call(calcAnimationParams);
+ }
+
+ selected
+ .transition()
+ .call(setAnimationParams, fromTo)
+ .duration(duration)
+ .each(function() { ++n; })
+ .each('end', function() {
+ if (!--n) { // call once
+ surface.call(run, toFrom);
+ }
+ });
+ }
+
+ var breathe = function(surface) {
+ done = false;
+ d3.timer(function() {
+ if (done) return true;
+
+ var currSelected = surface.selectAll(selector);
+ if (currSelected.empty()) return false;
+
+ surface.call(run, 'from');
+ return true;
+ }, 200);
+ };
+
+ breathe.off = function() {
+ done = true;
+ d3.timer.flush();
+ selected
+ .transition()
+ .call(reset)
+ .duration(0);
+ };
+
+ return breathe;
+ }
+
+ function Copy(context) {
+ var keybinding = d3.keybinding('copy');
+
+ function groupEntities(ids, graph) {
+ var entities = ids.map(function (id) { return graph.entity(id); });
+ return _.extend({relation: [], way: [], node: []},
+ _.groupBy(entities, function(entity) { return entity.type; }));
+ }
+
+ function getDescendants(id, graph, descendants) {
+ var entity = graph.entity(id),
+ i, children;
+
+ descendants = descendants || {};
+
+ if (entity.type === 'relation') {
+ children = _.map(entity.members, 'id');
+ } else if (entity.type === 'way') {
+ children = entity.nodes;
+ } else {
+ children = [];
+ }
+
+ for (i = 0; i < children.length; i++) {
+ if (!descendants[children[i]]) {
+ descendants[children[i]] = true;
+ descendants = getDescendants(children[i], graph, descendants);
+ }
+ }
+
+ return descendants;
+ }
+
+ function doCopy() {
+ d3.event.preventDefault();
+ if (context.inIntro()) return;
+
+ var graph = context.graph(),
+ selected = groupEntities(context.selectedIDs(), graph),
+ canCopy = [],
+ skip = {},
+ i, entity;
+
+ for (i = 0; i < selected.relation.length; i++) {
+ entity = selected.relation[i];
+ if (!skip[entity.id] && entity.isComplete(graph)) {
+ canCopy.push(entity.id);
+ skip = getDescendants(entity.id, graph, skip);
+ }
+ }
+ for (i = 0; i < selected.way.length; i++) {
+ entity = selected.way[i];
+ if (!skip[entity.id]) {
+ canCopy.push(entity.id);
+ skip = getDescendants(entity.id, graph, skip);
+ }
+ }
+ for (i = 0; i < selected.node.length; i++) {
+ entity = selected.node[i];
+ if (!skip[entity.id]) {
+ canCopy.push(entity.id);
+ }
+ }
+
+ context.copyIDs(canCopy);
+ }
+
+ function copy() {
+ keybinding.on(iD.ui.cmd('⌘C'), doCopy);
+ d3.select(document).call(keybinding);
+ return copy;
+ }
+
+ copy.off = function() {
+ d3.select(document).call(keybinding.off);
+ };
+
+ return copy;
+ }
+
+ /*
+ `iD.behavior.drag` is like `d3.behavior.drag`, with the following differences:
+
+ * The `origin` function is expected to return an [x, y] tuple rather than an
+ {x, y} object.
+ * The events are `start`, `move`, and `end`.
+ (https://github.com/mbostock/d3/issues/563)
+ * The `start` event is not dispatched until the first cursor movement occurs.
+ (https://github.com/mbostock/d3/pull/368)
+ * The `move` event has a `point` and `delta` [x, y] tuple properties rather
+ than `x`, `y`, `dx`, and `dy` properties.
+ * The `end` event is not dispatched if no movement occurs.
+ * An `off` function is available that unbinds the drag's internal event handlers.
+ * Delegation is supported via the `delegate` function.
+
+ */
+ function drag() {
+ function d3_eventCancel() {
+ d3.event.stopPropagation();
+ d3.event.preventDefault();
+ }
+
+ var event = d3.dispatch('start', 'move', 'end'),
+ origin = null,
+ selector = '',
+ filter = null,
+ event_, target, surface;
+
+ event.of = function(thiz, argumentz) {
+ return function(e1) {
+ var e0 = e1.sourceEvent = d3.event;
+ e1.target = drag;
+ d3.event = e1;
+ try {
+ event[e1.type].apply(thiz, argumentz);
+ } finally {
+ d3.event = e0;
+ }
+ };
+ };
+
+ var d3_event_userSelectProperty = iD.util.prefixCSSProperty('UserSelect'),
+ d3_event_userSelectSuppress = d3_event_userSelectProperty ?
+ function () {
+ var selection = d3.selection(),
+ select = selection.style(d3_event_userSelectProperty);
+ selection.style(d3_event_userSelectProperty, 'none');
+ return function () {
+ selection.style(d3_event_userSelectProperty, select);
+ };
+ } :
+ function (type) {
+ var w = d3.select(window).on('selectstart.' + type, d3_eventCancel);
+ return function () {
+ w.on('selectstart.' + type, null);
+ };
+ };
+
+ function mousedown() {
+ target = this;
+ event_ = event.of(target, arguments);
+ var eventTarget = d3.event.target,
+ touchId = d3.event.touches ? d3.event.changedTouches[0].identifier : null,
+ offset,
+ origin_ = point(),
+ started = false,
+ selectEnable = d3_event_userSelectSuppress(touchId !== null ? 'drag-' + touchId : 'drag');
+
+ var w = d3.select(window)
+ .on(touchId !== null ? 'touchmove.drag-' + touchId : 'mousemove.drag', dragmove)
+ .on(touchId !== null ? 'touchend.drag-' + touchId : 'mouseup.drag', dragend, true);
+
+ if (origin) {
+ offset = origin.apply(target, arguments);
+ offset = [offset[0] - origin_[0], offset[1] - origin_[1]];
+ } else {
+ offset = [0, 0];
+ }
+
+ if (touchId === null) d3.event.stopPropagation();
+
+ function point() {
+ var p = target.parentNode || surface;
+ return touchId !== null ? d3.touches(p).filter(function(p) {
+ return p.identifier === touchId;
+ })[0] : d3.mouse(p);
+ }
+
+ function dragmove() {
+
+ var p = point(),
+ dx = p[0] - origin_[0],
+ dy = p[1] - origin_[1];
+
+ if (dx === 0 && dy === 0)
+ return;
+
+ if (!started) {
+ started = true;
+ event_({
+ type: 'start'
+ });
+ }
+
+ origin_ = p;
+ d3_eventCancel();
+
+ event_({
+ type: 'move',
+ point: [p[0] + offset[0], p[1] + offset[1]],
+ delta: [dx, dy]
+ });
+ }
+
+ function dragend() {
+ if (started) {
+ event_({
+ type: 'end'
+ });
+
+ d3_eventCancel();
+ if (d3.event.target === eventTarget) w.on('click.drag', click, true);
+ }
+
+ w.on(touchId !== null ? 'touchmove.drag-' + touchId : 'mousemove.drag', null)
+ .on(touchId !== null ? 'touchend.drag-' + touchId : 'mouseup.drag', null);
+ selectEnable();
+ }
+
+ function click() {
+ d3_eventCancel();
+ w.on('click.drag', null);
+ }
+ }
+
+ function drag(selection) {
+ var matchesSelector = iD.util.prefixDOMProperty('matchesSelector'),
+ delegate = mousedown;
+
+ if (selector) {
+ delegate = function() {
+ var root = this,
+ target = d3.event.target;
+ for (; target && target !== root; target = target.parentNode) {
+ if (target[matchesSelector](selector) &&
+ (!filter || filter(target.__data__))) {
+ return mousedown.call(target, target.__data__);
+ }
+ }
+ };
+ }
+
+ selection.on('mousedown.drag' + selector, delegate)
+ .on('touchstart.drag' + selector, delegate);
+ }
+
+ drag.off = function(selection) {
+ selection.on('mousedown.drag' + selector, null)
+ .on('touchstart.drag' + selector, null);
+ };
+
+ drag.delegate = function(_) {
+ if (!arguments.length) return selector;
+ selector = _;
+ return drag;
+ };
+
+ drag.filter = function(_) {
+ if (!arguments.length) return origin;
+ filter = _;
+ return drag;
+ };
+
+ drag.origin = function (_) {
+ if (!arguments.length) return origin;
+ origin = _;
+ return drag;
+ };
+
+ drag.cancel = function() {
+ d3.select(window)
+ .on('mousemove.drag', null)
+ .on('mouseup.drag', null);
+ return drag;
+ };
+
+ drag.target = function() {
+ if (!arguments.length) return target;
+ target = arguments[0];
+ event_ = event.of(target, Array.prototype.slice.call(arguments, 1));
+ return drag;
+ };
+
+ drag.surface = function() {
+ if (!arguments.length) return surface;
+ surface = arguments[0];
+ return drag;
+ };
+
+ return d3.rebind(drag, event, 'on');
+ }
+
+ function DrawWay(context, wayId, index, mode, baseGraph) {
+ var way = context.entity(wayId),
+ isArea = context.geometry(wayId) === 'area',
+ finished = false,
+ annotation = t((way.isDegenerate() ?
+ 'operations.start.annotation.' :
+ 'operations.continue.annotation.') + context.geometry(wayId)),
+ draw = Draw(context);
+
+ var startIndex = typeof index === 'undefined' ? way.nodes.length - 1 : 0,
+ start = iD.Node({loc: context.graph().entity(way.nodes[startIndex]).loc}),
+ end = iD.Node({loc: context.map().mouseCoordinates()}),
+ segment = iD.Way({
+ nodes: typeof index === 'undefined' ? [start.id, end.id] : [end.id, start.id],
+ tags: _.clone(way.tags)
+ });
+
+ var f = context[way.isDegenerate() ? 'replace' : 'perform'];
+ if (isArea) {
+ f(iD.actions.AddEntity(end),
+ iD.actions.AddVertex(wayId, end.id, index));
+ } else {
+ f(iD.actions.AddEntity(start),
+ iD.actions.AddEntity(end),
+ iD.actions.AddEntity(segment));
+ }
+
+ function move(datum) {
+ var loc;
+
+ if (datum.type === 'node' && datum.id !== end.id) {
+ loc = datum.loc;
+
+ } else if (datum.type === 'way' && datum.id !== segment.id) {
+ var dims = context.map().dimensions(),
+ mouse = context.mouse(),
+ pad = 5,
+ trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad &&
+ mouse[1] > pad && mouse[1] < dims[1] - pad;
+
+ if (trySnap) {
+ loc = iD.geo.chooseEdge(context.childNodes(datum), context.mouse(), context.projection).loc;
+ }
+ }
+
+ if (!loc) {
+ loc = context.map().mouseCoordinates();
+ }
+
+ context.replace(iD.actions.MoveNode(end.id, loc));
+ }
+
+ function undone() {
+ finished = true;
+ context.enter(iD.modes.Browse(context));
+ }
+
+ function setActiveElements() {
+ var active = isArea ? [wayId, end.id] : [segment.id, start.id, end.id];
+ context.surface().selectAll(iD.util.entitySelector(active))
+ .classed('active', true);
+ }
+
+ var drawWay = function(surface) {
+ draw.on('move', move)
+ .on('click', drawWay.add)
+ .on('clickWay', drawWay.addWay)
+ .on('clickNode', drawWay.addNode)
+ .on('undo', context.undo)
+ .on('cancel', drawWay.cancel)
+ .on('finish', drawWay.finish);
+
+ context.map()
+ .dblclickEnable(false)
+ .on('drawn.draw', setActiveElements);
+
+ setActiveElements();
+
+ surface.call(draw);
+
+ context.history()
+ .on('undone.draw', undone);
+ };
+
+ drawWay.off = function(surface) {
+ if (!finished)
+ context.pop();
+
+ context.map()
+ .on('drawn.draw', null);
+
+ surface.call(draw.off)
+ .selectAll('.active')
+ .classed('active', false);
+
+ context.history()
+ .on('undone.draw', null);
+ };
+
+ function ReplaceTemporaryNode(newNode) {
+ return function(graph) {
+ if (isArea) {
+ return graph
+ .replace(way.addNode(newNode.id, index))
+ .remove(end);
+
+ } else {
+ return graph
+ .replace(graph.entity(wayId).addNode(newNode.id, index))
+ .remove(end)
+ .remove(segment)
+ .remove(start);
+ }
+ };
+ }
+
+ // Accept the current position of the temporary node and continue drawing.
+ drawWay.add = function(loc) {
+
+ // prevent duplicate nodes
+ var last = context.hasEntity(way.nodes[way.nodes.length - (isArea ? 2 : 1)]);
+ if (last && last.loc[0] === loc[0] && last.loc[1] === loc[1]) return;
+
+ var newNode = iD.Node({loc: loc});
+
+ context.replace(
+ iD.actions.AddEntity(newNode),
+ ReplaceTemporaryNode(newNode),
+ annotation);
+
+ finished = true;
+ context.enter(mode);
+ };
+
+ // Connect the way to an existing way.
+ drawWay.addWay = function(loc, edge) {
+ var previousEdge = startIndex ?
+ [way.nodes[startIndex], way.nodes[startIndex - 1]] :
+ [way.nodes[0], way.nodes[1]];
+
+ // Avoid creating duplicate segments
+ if (!isArea && iD.geo.edgeEqual(edge, previousEdge))
+ return;
+
+ var newNode = iD.Node({ loc: loc });
+
+ context.perform(
+ iD.actions.AddMidpoint({ loc: loc, edge: edge}, newNode),
+ ReplaceTemporaryNode(newNode),
+ annotation);
+
+ finished = true;
+ context.enter(mode);
+ };
+
+ // Connect the way to an existing node and continue drawing.
+ drawWay.addNode = function(node) {
+
+ // Avoid creating duplicate segments
+ if (way.areAdjacent(node.id, way.nodes[way.nodes.length - 1])) return;
+
+ context.perform(
+ ReplaceTemporaryNode(node),
+ annotation);
+
+ finished = true;
+ context.enter(mode);
+ };
+
+ // Finish the draw operation, removing the temporary node. If the way has enough
+ // nodes to be valid, it's selected. Otherwise, return to browse mode.
+ drawWay.finish = function() {
+ context.pop();
+ finished = true;
+
+ window.setTimeout(function() {
+ context.map().dblclickEnable(true);
+ }, 1000);
+
+ if (context.hasEntity(wayId)) {
+ context.enter(
+ iD.modes.Select(context, [wayId])
+ .suppressMenu(true)
+ .newFeature(true));
+ } else {
+ context.enter(iD.modes.Browse(context));
+ }
+ };
+
+ // Cancel the draw operation and return to browse, deleting everything drawn.
+ drawWay.cancel = function() {
+ context.perform(
+ d3.functor(baseGraph),
+ t('operations.cancel_draw.annotation'));
+
+ window.setTimeout(function() {
+ context.map().dblclickEnable(true);
+ }, 1000);
+
+ finished = true;
+ context.enter(iD.modes.Browse(context));
+ };
+
+ drawWay.tail = function(text) {
+ draw.tail(text);
+ return drawWay;
+ };
+
+ return drawWay;
+ }
+
+ function Hash(context) {
+ var s0 = null, // cached location.hash
+ lat = 90 - 1e-8; // allowable latitude range
+
+ var parser = function(map, s) {
+ var q = iD.util.stringQs(s);
+ var args = (q.map || '').split('/').map(Number);
+ if (args.length < 3 || args.some(isNaN)) {
+ return true; // replace bogus hash
+ } else if (s !== formatter(map).slice(1)) {
+ map.centerZoom([args[1],
+ Math.min(lat, Math.max(-lat, args[2]))], args[0]);
+ }
+ };
+
+ var formatter = function(map) {
+ var mode = context.mode(),
+ center = map.center(),
+ zoom = map.zoom(),
+ precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)),
+ q = _.omit(iD.util.stringQs(location.hash.substring(1)), 'comment'),
+ newParams = {};
+
+ if (mode && mode.id === 'browse') {
+ delete q.id;
+ } else {
+ var selected = context.selectedIDs().filter(function(id) {
+ return !context.entity(id).isNew();
+ });
+ if (selected.length) {
+ newParams.id = selected.join(',');
+ }
+ }
+
+ newParams.map = zoom.toFixed(2) +
+ '/' + center[0].toFixed(precision) +
+ '/' + center[1].toFixed(precision);
+
+ return '#' + iD.util.qsString(_.assign(q, newParams), true);
+ };
+
+ function update() {
+ if (context.inIntro()) return;
+ var s1 = formatter(context.map());
+ if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map!
+ }
+
+ var throttledUpdate = _.throttle(update, 500);
+
+ function hashchange() {
+ if (location.hash === s0) return; // ignore spurious hashchange events
+ if (parser(context.map(), (s0 = location.hash).substring(1))) {
+ update(); // replace bogus hash
+ }
+ }
+
+ function hash() {
+ context.map()
+ .on('move.hash', throttledUpdate);
+
+ context
+ .on('enter.hash', throttledUpdate);
+
+ d3.select(window)
+ .on('hashchange.hash', hashchange);
+
+ if (location.hash) {
+ var q = iD.util.stringQs(location.hash.substring(1));
+ if (q.id) context.zoomToEntity(q.id.split(',')[0], !q.map);
+ if (q.comment) context.storage('comment', q.comment);
+ hashchange();
+ if (q.map) hash.hadHash = true;
+ }
+ }
+
+ hash.off = function() {
+ throttledUpdate.cancel();
+
+ context.map()
+ .on('move.hash', null);
+
+ context
+ .on('enter.hash', null);
+
+ d3.select(window)
+ .on('hashchange.hash', null);
+
+ location.hash = '';
+ };
+
+ return hash;
+ }
+
+ function Lasso(context) {
+
+ var behavior = function(selection) {
+ var lasso;
+
+ function mousedown() {
+ var button = 0; // left
+ if (d3.event.button === button && d3.event.shiftKey === true) {
+ lasso = null;
+
+ selection
+ .on('mousemove.lasso', mousemove)
+ .on('mouseup.lasso', mouseup);
+
+ d3.event.stopPropagation();
+ }
+ }
+
+ function mousemove() {
+ if (!lasso) {
+ lasso = iD.ui.Lasso(context);
+ context.surface().call(lasso);
+ }
+
+ lasso.p(context.mouse());
+ }
+
+ function normalize(a, b) {
+ return [
+ [Math.min(a[0], b[0]), Math.min(a[1], b[1])],
+ [Math.max(a[0], b[0]), Math.max(a[1], b[1])]];
+ }
+
+ function lassoed() {
+ if (!lasso) return [];
+
+ var graph = context.graph(),
+ bounds = lasso.extent().map(context.projection.invert),
+ extent = iD.geo.Extent(normalize(bounds[0], bounds[1]));
+
+ return _.map(context.intersects(extent).filter(function(entity) {
+ return entity.type === 'node' &&
+ iD.geo.pointInPolygon(context.projection(entity.loc), lasso.coordinates) &&
+ !context.features().isHidden(entity, graph, entity.geometry(graph));
+ }), 'id');
+ }
+
+ function mouseup() {
+ selection
+ .on('mousemove.lasso', null)
+ .on('mouseup.lasso', null);
+
+ if (!lasso) return;
+
+ var ids = lassoed();
+ lasso.close();
+
+ if (ids.length) {
+ context.enter(iD.modes.Select(context, ids));
+ }
+ }
+
+ selection
+ .on('mousedown.lasso', mousedown);
+ };
+
+ behavior.off = function(selection) {
+ selection.on('mousedown.lasso', null);
+ };
+
+ return behavior;
+ }
+
+ function Paste(context) {
+ var keybinding = d3.keybinding('paste');
+
+ function omitTag(v, k) {
+ return (
+ k === 'phone' ||
+ k === 'fax' ||
+ k === 'email' ||
+ k === 'website' ||
+ k === 'url' ||
+ k === 'note' ||
+ k === 'description' ||
+ k.indexOf('name') !== -1 ||
+ k.indexOf('wiki') === 0 ||
+ k.indexOf('addr:') === 0 ||
+ k.indexOf('contact:') === 0
+ );
+ }
+
+ function doPaste() {
+ d3.event.preventDefault();
+ if (context.inIntro()) return;
+
+ var baseGraph = context.graph(),
+ mouse = context.mouse(),
+ projection = context.projection,
+ viewport = iD.geo.Extent(projection.clipExtent()).polygon();
+
+ if (!iD.geo.pointInPolygon(mouse, viewport)) return;
+
+ var extent = iD.geo.Extent(),
+ oldIDs = context.copyIDs(),
+ oldGraph = context.copyGraph(),
+ newIDs = [];
+
+ if (!oldIDs.length) return;
+
+ var action = iD.actions.CopyEntities(oldIDs, oldGraph);
+ context.perform(action);
+
+ var copies = action.copies();
+ for (var id in copies) {
+ var oldEntity = oldGraph.entity(id),
+ newEntity = copies[id];
+
+ extent._extend(oldEntity.extent(oldGraph));
+ newIDs.push(newEntity.id);
+ context.perform(iD.actions.ChangeTags(newEntity.id, _.omit(newEntity.tags, omitTag)));
+ }
+
+ // Put pasted objects where mouse pointer is..
+ var center = projection(extent.center()),
+ delta = [ mouse[0] - center[0], mouse[1] - center[1] ];
+
+ context.perform(iD.actions.Move(newIDs, delta, projection));
+ context.enter(iD.modes.Move(context, newIDs, baseGraph));
+ }
+
+ function paste() {
+ keybinding.on(iD.ui.cmd('⌘V'), doPaste);
+ d3.select(document).call(keybinding);
+ return paste;
+ }
+
+ paste.off = function() {
+ d3.select(document).call(keybinding.off);
+ };
+
+ return paste;
+ }
+
+ function Select(context) {
+ function keydown() {
+ if (d3.event && d3.event.shiftKey) {
+ context.surface()
+ .classed('behavior-multiselect', true);
+ }
+ }
+
+ function keyup() {
+ if (!d3.event || !d3.event.shiftKey) {
+ context.surface()
+ .classed('behavior-multiselect', false);
+ }
+ }
+
+ function click() {
+ var datum = d3.event.target.__data__,
+ lasso = d3.select('#surface .lasso').node(),
+ mode = context.mode();
+
+ if (!(datum instanceof iD.Entity)) {
+ if (!d3.event.shiftKey && !lasso && mode.id !== 'browse')
+ context.enter(iD.modes.Browse(context));
+
+ } else if (!d3.event.shiftKey && !lasso) {
+ // Avoid re-entering Select mode with same entity.
+ if (context.selectedIDs().length !== 1 || context.selectedIDs()[0] !== datum.id) {
+ context.enter(iD.modes.Select(context, [datum.id]));
+ } else {
+ mode.suppressMenu(false).reselect();
+ }
+ } else if (context.selectedIDs().indexOf(datum.id) >= 0) {
+ var selectedIDs = _.without(context.selectedIDs(), datum.id);
+ context.enter(selectedIDs.length ?
+ iD.modes.Select(context, selectedIDs) :
+ iD.modes.Browse(context));
+
+ } else {
+ context.enter(iD.modes.Select(context, context.selectedIDs().concat([datum.id])));
+ }
+ }
+
+ var behavior = function(selection) {
+ d3.select(window)
+ .on('keydown.select', keydown)
+ .on('keyup.select', keyup);
+
+ selection.on('click.select', click);
+
+ keydown();
+ };
+
+ behavior.off = function(selection) {
+ d3.select(window)
+ .on('keydown.select', null)
+ .on('keyup.select', null);
+
+ selection.on('click.select', null);
+
+ keyup();
+ };
+
+ return behavior;
+ }
+
+ exports.AddWay = AddWay;
+ exports.Breathe = Breathe;
+ exports.Copy = Copy;
+ exports.drag = drag;
+ exports.DrawWay = DrawWay;
+ exports.Draw = Draw;
+ exports.Edit = Edit;
+ exports.Hash = Hash;
+ exports.Hover = Hover;
+ exports.Lasso = Lasso;
+ exports.Paste = Paste;
+ exports.Select = Select;
+ exports.Tail = Tail;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+}));
\ No newline at end of file
diff --git a/js/id/behavior/add_way.js b/modules/behavior/add_way.js
similarity index 89%
rename from js/id/behavior/add_way.js
rename to modules/behavior/add_way.js
index 4a52143ee..e85475746 100644
--- a/js/id/behavior/add_way.js
+++ b/modules/behavior/add_way.js
@@ -1,6 +1,8 @@
-iD.behavior.AddWay = function(context) {
+import { Draw } from './draw';
+
+export function AddWay(context) {
var event = d3.dispatch('start', 'startFromWay', 'startFromNode'),
- draw = iD.behavior.Draw(context);
+ draw = Draw(context);
var addWay = function(surface) {
draw.on('click', event.start)
@@ -33,4 +35,4 @@ iD.behavior.AddWay = function(context) {
};
return d3.rebind(addWay, event, 'on');
-};
+}
diff --git a/js/id/behavior/breathe.js b/modules/behavior/breathe.js
similarity index 98%
rename from js/id/behavior/breathe.js
rename to modules/behavior/breathe.js
index 95744c9d3..2f4b5c15b 100644
--- a/js/id/behavior/breathe.js
+++ b/modules/behavior/breathe.js
@@ -1,4 +1,4 @@
-iD.behavior.Breathe = function() {
+export function Breathe(){
var duration = 800,
selector = '.selected.shadow, .selected .shadow',
selected = d3.select(null),
@@ -102,4 +102,4 @@ iD.behavior.Breathe = function() {
};
return breathe;
-};
+}
diff --git a/js/id/behavior/copy.js b/modules/behavior/copy.js
similarity index 98%
rename from js/id/behavior/copy.js
rename to modules/behavior/copy.js
index 7029289ab..e19c4cbd0 100644
--- a/js/id/behavior/copy.js
+++ b/modules/behavior/copy.js
@@ -1,4 +1,4 @@
-iD.behavior.Copy = function(context) {
+export function Copy(context) {
var keybinding = d3.keybinding('copy');
function groupEntities(ids, graph) {
@@ -76,4 +76,4 @@ iD.behavior.Copy = function(context) {
};
return copy;
-};
+}
diff --git a/js/id/behavior/drag.js b/modules/behavior/drag.js
similarity index 99%
rename from js/id/behavior/drag.js
rename to modules/behavior/drag.js
index 839890bd9..04aa2ddbf 100644
--- a/js/id/behavior/drag.js
+++ b/modules/behavior/drag.js
@@ -14,7 +14,7 @@
* Delegation is supported via the `delegate` function.
*/
-iD.behavior.drag = function() {
+export function drag() {
function d3_eventCancel() {
d3.event.stopPropagation();
d3.event.preventDefault();
@@ -91,7 +91,7 @@ iD.behavior.drag = function() {
var p = point(),
dx = p[0] - origin_[0],
dy = p[1] - origin_[1];
-
+
if (dx === 0 && dy === 0)
return;
@@ -198,4 +198,4 @@ iD.behavior.drag = function() {
};
return d3.rebind(drag, event, 'on');
-};
+}
diff --git a/js/id/behavior/draw.js b/modules/behavior/draw.js
similarity index 94%
rename from js/id/behavior/draw.js
rename to modules/behavior/draw.js
index 95057996f..453473dde 100644
--- a/js/id/behavior/draw.js
+++ b/modules/behavior/draw.js
@@ -1,17 +1,21 @@
-iD.behavior.Draw = function(context) {
+import { Edit } from './edit';
+import { Hover } from './hover';
+import { Tail } from './tail';
+
+export function Draw(context) {
var event = d3.dispatch('move', 'click', 'clickWay',
'clickNode', 'undo', 'cancel', 'finish'),
keybinding = d3.keybinding('draw'),
- hover = iD.behavior.Hover(context)
+ hover = Hover(context)
.altDisables(true)
.on('hover', context.ui().sidebar.hover),
- tail = iD.behavior.Tail(),
- edit = iD.behavior.Edit(context),
+ tail = Tail(),
+ edit = Edit(context),
closeTolerance = 4,
tolerance = 12,
mouseLeave = false,
lastMouse = null,
- cached = iD.behavior.Draw;
+ cached = Draw;
function datum() {
if (d3.event.altKey) return {};
@@ -200,8 +204,8 @@ iD.behavior.Draw = function(context) {
};
return d3.rebind(draw, event, 'on');
-};
+}
-iD.behavior.Draw.usedTails = {};
-iD.behavior.Draw.disableSpace = false;
-iD.behavior.Draw.lastSpace = null;
+Draw.usedTails = {};
+Draw.disableSpace = false;
+Draw.lastSpace = null;
diff --git a/js/id/behavior/draw_way.js b/modules/behavior/draw_way.js
similarity index 97%
rename from js/id/behavior/draw_way.js
rename to modules/behavior/draw_way.js
index 5771c7147..c5191869d 100644
--- a/js/id/behavior/draw_way.js
+++ b/modules/behavior/draw_way.js
@@ -1,11 +1,13 @@
-iD.behavior.DrawWay = function(context, wayId, index, mode, baseGraph) {
+import { Draw } from './draw';
+
+export function DrawWay(context, wayId, index, mode, baseGraph) {
var way = context.entity(wayId),
isArea = context.geometry(wayId) === 'area',
finished = false,
annotation = t((way.isDegenerate() ?
'operations.start.annotation.' :
'operations.continue.annotation.') + context.geometry(wayId)),
- draw = iD.behavior.Draw(context);
+ draw = Draw(context);
var startIndex = typeof index === 'undefined' ? way.nodes.length - 1 : 0,
start = iD.Node({loc: context.graph().entity(way.nodes[startIndex]).loc}),
@@ -207,4 +209,4 @@ iD.behavior.DrawWay = function(context, wayId, index, mode, baseGraph) {
};
return drawWay;
-};
+}
diff --git a/js/id/behavior/edit.js b/modules/behavior/edit.js
similarity index 82%
rename from js/id/behavior/edit.js
rename to modules/behavior/edit.js
index 7e3b6d405..33e61b1aa 100644
--- a/js/id/behavior/edit.js
+++ b/modules/behavior/edit.js
@@ -1,4 +1,4 @@
-iD.behavior.Edit = function(context) {
+export function Edit(context) {
function edit() {
context.map()
.minzoom(context.minEditableZoom());
@@ -10,4 +10,4 @@ iD.behavior.Edit = function(context) {
};
return edit;
-};
+}
diff --git a/js/id/behavior/hash.js b/modules/behavior/hash.js
similarity index 98%
rename from js/id/behavior/hash.js
rename to modules/behavior/hash.js
index c861ca575..16eed59ef 100644
--- a/js/id/behavior/hash.js
+++ b/modules/behavior/hash.js
@@ -1,4 +1,4 @@
-iD.behavior.Hash = function(context) {
+export function Hash(context) {
var s0 = null, // cached location.hash
lat = 90 - 1e-8; // allowable latitude range
@@ -89,4 +89,4 @@ iD.behavior.Hash = function(context) {
};
return hash;
-};
+}
diff --git a/js/id/behavior/hover.js b/modules/behavior/hover.js
similarity index 99%
rename from js/id/behavior/hover.js
rename to modules/behavior/hover.js
index 11df4c8f9..1fa46439d 100644
--- a/js/id/behavior/hover.js
+++ b/modules/behavior/hover.js
@@ -7,7 +7,7 @@
Only one of these elements can have the :hover pseudo-class, but all of them will
have the .hover class.
*/
-iD.behavior.Hover = function() {
+export function Hover() {
var dispatch = d3.dispatch('hover'),
selection,
altDisables,
@@ -124,4 +124,4 @@ iD.behavior.Hover = function() {
};
return d3.rebind(hover, dispatch, 'on');
-};
+}
diff --git a/modules/behavior/index.js b/modules/behavior/index.js
new file mode 100644
index 000000000..6a04e35bc
--- /dev/null
+++ b/modules/behavior/index.js
@@ -0,0 +1,13 @@
+export { AddWay } from './add_way';
+export { Breathe } from './breathe';
+export { Copy } from './copy';
+export { drag } from './drag';
+export { DrawWay } from './draw_way';
+export { Draw } from './draw';
+export { Edit } from './edit';
+export { Hash } from './hash';
+export { Hover } from './hover';
+export { Lasso } from './lasso';
+export { Paste } from './paste';
+export { Select } from './select';
+export { Tail } from './tail';
diff --git a/js/id/behavior/lasso.js b/modules/behavior/lasso.js
similarity index 97%
rename from js/id/behavior/lasso.js
rename to modules/behavior/lasso.js
index 5cf379564..910e64657 100644
--- a/js/id/behavior/lasso.js
+++ b/modules/behavior/lasso.js
@@ -1,4 +1,4 @@
-iD.behavior.Lasso = function(context) {
+export function Lasso(context) {
var behavior = function(selection) {
var lasso;
@@ -69,4 +69,4 @@ iD.behavior.Lasso = function(context) {
};
return behavior;
-};
+}
diff --git a/js/id/behavior/paste.js b/modules/behavior/paste.js
similarity index 97%
rename from js/id/behavior/paste.js
rename to modules/behavior/paste.js
index 82dd028da..47101aab6 100644
--- a/js/id/behavior/paste.js
+++ b/modules/behavior/paste.js
@@ -1,4 +1,4 @@
-iD.behavior.Paste = function(context) {
+export function Paste(context) {
var keybinding = d3.keybinding('paste');
function omitTag(v, k) {
@@ -67,4 +67,4 @@ iD.behavior.Paste = function(context) {
};
return paste;
-};
+}
diff --git a/js/id/behavior/select.js b/modules/behavior/select.js
similarity index 97%
rename from js/id/behavior/select.js
rename to modules/behavior/select.js
index ce4484e24..c971095ef 100644
--- a/js/id/behavior/select.js
+++ b/modules/behavior/select.js
@@ -1,4 +1,4 @@
-iD.behavior.Select = function(context) {
+export function Select(context) {
function keydown() {
if (d3.event && d3.event.shiftKey) {
context.surface()
@@ -61,4 +61,4 @@ iD.behavior.Select = function(context) {
};
return behavior;
-};
+}
diff --git a/js/id/behavior/tail.js b/modules/behavior/tail.js
similarity index 98%
rename from js/id/behavior/tail.js
rename to modules/behavior/tail.js
index 6288948a3..2850a360b 100644
--- a/js/id/behavior/tail.js
+++ b/modules/behavior/tail.js
@@ -1,4 +1,4 @@
-iD.behavior.Tail = function() {
+export function Tail() {
var text,
container,
xmargin = 25,
@@ -79,4 +79,4 @@ iD.behavior.Tail = function() {
};
return tail;
-};
+}
diff --git a/test/index.html b/test/index.html
index 8032f5595..8b2b0f539 100644
--- a/test/index.html
+++ b/test/index.html
@@ -43,6 +43,7 @@
+
@@ -102,21 +103,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-