mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 17:23:02 +00:00
323 lines
11 KiB
JavaScript
323 lines
11 KiB
JavaScript
import { select as d3_select } from 'd3-selection';
|
|
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
|
|
|
import { geoVecAdd } from '../geo';
|
|
import { localizer } from '../core/localizer';
|
|
import { uiTooltip } from './tooltip';
|
|
import { utilRebind } from '../util/rebind';
|
|
import { utilHighlightEntities } from '../util/util';
|
|
import { utilGetDimensions } from '../util/dimensions';
|
|
import { svgIcon } from '../svg/icon';
|
|
|
|
|
|
export function uiEditMenu(context) {
|
|
var dispatch = d3_dispatch('toggled');
|
|
|
|
var _menu = d3_select(null);
|
|
var _operations = [];
|
|
// the position the menu should be displayed relative to
|
|
var _anchorLoc = [0, 0];
|
|
var _anchorLocLonLat = [0, 0];
|
|
// a string indicating how the menu was opened
|
|
var _triggerType = '';
|
|
|
|
var _vpTopMargin = 85; // viewport top margin
|
|
var _vpBottomMargin = 45; // viewport bottom margin
|
|
var _vpSideMargin = 35; // viewport side margin
|
|
|
|
var _menuTop = false;
|
|
var _menuHeight;
|
|
var _menuWidth;
|
|
|
|
// hardcode these values to make menu positioning easier
|
|
var _verticalPadding = 4;
|
|
|
|
// see also `.edit-menu .tooltip` CSS; include margin
|
|
var _tooltipWidth = 210;
|
|
|
|
// offset the menu slightly from the target location
|
|
var _menuSideMargin = 10;
|
|
|
|
var _tooltips = [];
|
|
|
|
var editMenu = function(selection) {
|
|
|
|
var isTouchMenu = _triggerType.includes('touch') || _triggerType.includes('pen');
|
|
|
|
var ops = _operations.filter(function(op) {
|
|
return !isTouchMenu || !op.mouseOnly;
|
|
});
|
|
|
|
if (!ops.length) return;
|
|
|
|
_tooltips = [];
|
|
|
|
// Position the menu above the anchor for stylus and finger input
|
|
// since the mapper's hand likely obscures the screen below the anchor
|
|
_menuTop = isTouchMenu;
|
|
|
|
// Show labels for touch input since there aren't hover tooltips
|
|
var showLabels = isTouchMenu;
|
|
|
|
var buttonHeight = showLabels ? 32 : 34;
|
|
if (showLabels) {
|
|
// Get a general idea of the width based on the length of the label
|
|
_menuWidth = 52 + Math.min(120, 6 * Math.max.apply(Math, ops.map(function(op) {
|
|
return op.title.length;
|
|
})));
|
|
} else {
|
|
_menuWidth = 44;
|
|
}
|
|
|
|
_menuHeight = _verticalPadding * 2 + ops.length * buttonHeight;
|
|
|
|
_menu = selection
|
|
.append('div')
|
|
.attr('class', 'edit-menu')
|
|
.classed('touch-menu', isTouchMenu)
|
|
.style('padding', _verticalPadding + 'px 0');
|
|
|
|
var buttons = _menu.selectAll('.edit-menu-item')
|
|
.data(ops);
|
|
|
|
// enter
|
|
var buttonsEnter = buttons.enter()
|
|
.append('button')
|
|
.attr('class', function (d) { return 'edit-menu-item edit-menu-item-' + d.id; })
|
|
.style('height', buttonHeight + 'px')
|
|
.on('click', click)
|
|
// don't listen for `mouseup` because we only care about non-mouse pointer types
|
|
.on('pointerup', pointerup)
|
|
.on('pointerdown mousedown', function pointerdown(d3_event) {
|
|
// don't let button presses also act as map input - #1869
|
|
d3_event.stopPropagation();
|
|
})
|
|
.on('mouseenter.highlight', function(d3_event, d) {
|
|
if (!d.relatedEntityIds || d3_select(this).classed('disabled')) return;
|
|
|
|
utilHighlightEntities(d.relatedEntityIds(), true, context);
|
|
})
|
|
.on('mouseleave.highlight', function(d3_event, d) {
|
|
if (!d.relatedEntityIds) return;
|
|
|
|
utilHighlightEntities(d.relatedEntityIds(), false, context);
|
|
});
|
|
|
|
buttonsEnter.each(function(d) {
|
|
var tooltip = uiTooltip()
|
|
.heading(() => d.title)
|
|
.title(d.tooltip)
|
|
.keys([d.keys[0]]);
|
|
|
|
_tooltips.push(tooltip);
|
|
|
|
d3_select(this)
|
|
.call(tooltip)
|
|
.append('div')
|
|
.attr('class', 'icon-wrap')
|
|
.call(svgIcon(d.icon && d.icon() || '#iD-operation-' + d.id, 'operation'));
|
|
});
|
|
|
|
if (showLabels) {
|
|
buttonsEnter.append('span')
|
|
.attr('class', 'label')
|
|
.each(function(d) {
|
|
d3_select(this).call(d.title);
|
|
});
|
|
}
|
|
|
|
// update
|
|
buttonsEnter
|
|
.merge(buttons)
|
|
.classed('disabled', function(d) { return d.disabled(); });
|
|
|
|
updatePosition();
|
|
|
|
var initialScale = context.projection.scale();
|
|
context.map()
|
|
.on('move.edit-menu', function() {
|
|
if (initialScale !== context.projection.scale()) {
|
|
editMenu.close();
|
|
}
|
|
})
|
|
.on('drawn.edit-menu', function(info) {
|
|
if (info.full) updatePosition();
|
|
});
|
|
|
|
var lastPointerUpType;
|
|
// `pointerup` is always called before `click`
|
|
function pointerup(d3_event) {
|
|
lastPointerUpType = d3_event.pointerType;
|
|
}
|
|
|
|
function click(d3_event, operation) {
|
|
d3_event.stopPropagation();
|
|
|
|
if (operation.relatedEntityIds) {
|
|
utilHighlightEntities(operation.relatedEntityIds(), false, context);
|
|
}
|
|
|
|
if (operation.disabled()) {
|
|
if (lastPointerUpType === 'touch' ||
|
|
lastPointerUpType === 'pen') {
|
|
// there are no tooltips for touch interactions so flash feedback instead
|
|
context.ui().flash
|
|
.duration(4000)
|
|
.iconName('#iD-operation-' + operation.id)
|
|
.iconClass('operation disabled')
|
|
.label(operation.tooltip())();
|
|
}
|
|
} else {
|
|
if (lastPointerUpType === 'touch' ||
|
|
lastPointerUpType === 'pen') {
|
|
context.ui().flash
|
|
.duration(2000)
|
|
.iconName('#iD-operation-' + operation.id)
|
|
.iconClass('operation')
|
|
.label(operation.annotation() || operation.title)();
|
|
}
|
|
|
|
operation();
|
|
editMenu.close();
|
|
}
|
|
lastPointerUpType = null;
|
|
}
|
|
|
|
dispatch.call('toggled', this, true);
|
|
};
|
|
|
|
function updatePosition() {
|
|
|
|
if (!_menu || _menu.empty()) return;
|
|
|
|
var anchorLoc = context.projection(_anchorLocLonLat);
|
|
|
|
var viewport = context.surfaceRect();
|
|
|
|
if (anchorLoc[0] < 0 ||
|
|
anchorLoc[0] > viewport.width ||
|
|
anchorLoc[1] < 0 ||
|
|
anchorLoc[1] > viewport.height) {
|
|
// close the menu if it's gone offscreen
|
|
|
|
editMenu.close();
|
|
return;
|
|
}
|
|
|
|
var menuLeft = displayOnLeft(viewport);
|
|
|
|
var offset = [0, 0];
|
|
|
|
offset[0] = menuLeft ? -1 * (_menuSideMargin + _menuWidth) : _menuSideMargin;
|
|
|
|
if (_menuTop) {
|
|
if (anchorLoc[1] - _menuHeight < _vpTopMargin) {
|
|
// menu is near top viewport edge, shift downward
|
|
offset[1] = -anchorLoc[1] + _vpTopMargin;
|
|
} else {
|
|
offset[1] = -_menuHeight;
|
|
}
|
|
} else {
|
|
if (anchorLoc[1] + _menuHeight > (viewport.height - _vpBottomMargin)) {
|
|
// menu is near bottom viewport edge, shift upwards
|
|
offset[1] = -anchorLoc[1] - _menuHeight + viewport.height - _vpBottomMargin;
|
|
} else {
|
|
offset[1] = 0;
|
|
}
|
|
}
|
|
|
|
var origin = geoVecAdd(anchorLoc, offset);
|
|
// repositioning the menu to account for the top menu height
|
|
var _verticalOffset = parseFloat(utilGetDimensions(d3_select('.top-toolbar-wrap'))[1]);
|
|
origin[1] -= _verticalOffset;
|
|
|
|
_menu
|
|
.style('left', origin[0] + 'px')
|
|
.style('top', origin[1] + 'px');
|
|
|
|
var tooltipSide = tooltipPosition(viewport, menuLeft);
|
|
_tooltips.forEach(function(tooltip) {
|
|
tooltip.placement(tooltipSide);
|
|
});
|
|
|
|
function displayOnLeft(viewport) {
|
|
if (localizer.textDirection() === 'ltr') {
|
|
if ((anchorLoc[0] + _menuSideMargin + _menuWidth) > (viewport.width - _vpSideMargin)) {
|
|
// right menu would be too close to the right viewport edge, go left
|
|
return true;
|
|
}
|
|
// prefer right menu
|
|
return false;
|
|
|
|
} else { // rtl
|
|
if ((anchorLoc[0] - _menuSideMargin - _menuWidth) < _vpSideMargin) {
|
|
// left menu would be too close to the left viewport edge, go right
|
|
return false;
|
|
}
|
|
// prefer left menu
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function tooltipPosition(viewport, menuLeft) {
|
|
if (localizer.textDirection() === 'ltr') {
|
|
if (menuLeft) {
|
|
// if there's not room for a right-side menu then there definitely
|
|
// isn't room for right-side tooltips
|
|
return 'left';
|
|
}
|
|
if ((anchorLoc[0] + _menuSideMargin + _menuWidth + _tooltipWidth) > (viewport.width - _vpSideMargin)) {
|
|
// right tooltips would be too close to the right viewport edge, go left
|
|
return 'left';
|
|
}
|
|
// prefer right tooltips
|
|
return 'right';
|
|
|
|
} else { // rtl
|
|
if (!menuLeft) {
|
|
return 'right';
|
|
}
|
|
if ((anchorLoc[0] - _menuSideMargin - _menuWidth - _tooltipWidth) < _vpSideMargin) {
|
|
// left tooltips would be too close to the left viewport edge, go right
|
|
return 'right';
|
|
}
|
|
// prefer left tooltips
|
|
return 'left';
|
|
}
|
|
}
|
|
}
|
|
|
|
editMenu.close = function () {
|
|
|
|
context.map()
|
|
.on('move.edit-menu', null)
|
|
.on('drawn.edit-menu', null);
|
|
|
|
_menu.remove();
|
|
_tooltips = [];
|
|
|
|
dispatch.call('toggled', this, false);
|
|
};
|
|
|
|
editMenu.anchorLoc = function(val) {
|
|
if (!arguments.length) return _anchorLoc;
|
|
_anchorLoc = val;
|
|
_anchorLocLonLat = context.projection.invert(_anchorLoc);
|
|
return editMenu;
|
|
};
|
|
|
|
editMenu.triggerType = function(val) {
|
|
if (!arguments.length) return _triggerType;
|
|
_triggerType = val;
|
|
return editMenu;
|
|
};
|
|
|
|
editMenu.operations = function(val) {
|
|
if (!arguments.length) return _operations;
|
|
_operations = val;
|
|
return editMenu;
|
|
};
|
|
|
|
return utilRebind(editMenu, dispatch, 'on');
|
|
}
|