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'); }