mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-14 01:33:03 +00:00
closes #10634 This happens for example when the tooltip are rendered for the map controlls (right or left edge of the screen depending on user locale): If the anchor is close to the bottom of the screen, the tooltip would be placed not ideally in the corner and hard to read. It is solved by falling back to left/right tooltips with a potentially offset arrow in these cases
296 lines
10 KiB
JavaScript
296 lines
10 KiB
JavaScript
import { easeLinear as d3_easeLinear } from 'd3-ease';
|
|
|
|
import {
|
|
select as d3_select
|
|
} from 'd3-selection';
|
|
|
|
import { localizer } from '../core/localizer';
|
|
import { uiToggle } from './toggle';
|
|
|
|
|
|
// Tooltips and svg mask used to highlight certain features
|
|
export function uiCurtain(containerNode) {
|
|
|
|
var surface = d3_select(null),
|
|
tooltip = d3_select(null),
|
|
darkness = d3_select(null);
|
|
|
|
function curtain(selection) {
|
|
surface = selection
|
|
.append('svg')
|
|
.attr('class', 'curtain')
|
|
.style('top', 0)
|
|
.style('left', 0);
|
|
|
|
darkness = surface.append('path')
|
|
.attr('x', 0)
|
|
.attr('y', 0)
|
|
.attr('class', 'curtain-darkness');
|
|
|
|
d3_select(window).on('resize.curtain', resize);
|
|
|
|
tooltip = selection.append('div')
|
|
.attr('class', 'tooltip');
|
|
|
|
tooltip
|
|
.append('div')
|
|
.attr('class', 'popover-arrow');
|
|
|
|
tooltip
|
|
.append('div')
|
|
.attr('class', 'popover-inner');
|
|
|
|
resize();
|
|
|
|
|
|
function resize() {
|
|
surface
|
|
.attr('width', containerNode.clientWidth)
|
|
.attr('height', containerNode.clientHeight);
|
|
curtain.cut(darkness.datum());
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Reveal cuts the curtain to highlight the given box,
|
|
* and shows a tooltip with instructions next to the box.
|
|
*
|
|
* @param {String|ClientRect|HTMLElement} [box] box used to cut the curtain
|
|
* @param {String} [text] text for a tooltip
|
|
* @param {Object} [options]
|
|
* @param {string} [options.tooltipClass] optional class to add to the tooltip
|
|
* @param {integer} [options.duration] transition time in milliseconds
|
|
* @param {string} [options.buttonText] if set, create a button with this text label
|
|
* @param {function} [options.buttonCallback] if set, the callback for the button
|
|
* @param {function} [options.padding] extra margin in px to put around bbox
|
|
* @param {String|ClientRect} [options.tooltipBox] box for tooltip position, if different from box for the curtain
|
|
*/
|
|
curtain.reveal = function(box, html, options) {
|
|
options = options || {};
|
|
|
|
if (typeof box === 'string') {
|
|
box = d3_select(box).node();
|
|
}
|
|
if (box && box.getBoundingClientRect) {
|
|
box = copyBox(box.getBoundingClientRect());
|
|
}
|
|
if (box) {
|
|
var containerRect = containerNode.getBoundingClientRect();
|
|
box.top -= containerRect.top;
|
|
box.left -= containerRect.left;
|
|
}
|
|
if (box && options.padding) {
|
|
box.top -= options.padding;
|
|
box.left -= options.padding;
|
|
box.bottom += options.padding;
|
|
box.right += options.padding;
|
|
box.height += options.padding * 2;
|
|
box.width += options.padding * 2;
|
|
}
|
|
|
|
var tooltipBox;
|
|
if (options.tooltipBox) {
|
|
tooltipBox = options.tooltipBox;
|
|
if (typeof tooltipBox === 'string') {
|
|
tooltipBox = d3_select(tooltipBox).node();
|
|
}
|
|
if (tooltipBox && tooltipBox.getBoundingClientRect) {
|
|
tooltipBox = copyBox(tooltipBox.getBoundingClientRect());
|
|
}
|
|
} else {
|
|
tooltipBox = box;
|
|
}
|
|
|
|
if (tooltipBox && html) {
|
|
|
|
if (html.indexOf('**') !== -1) {
|
|
if (html.indexOf('<span') === 0) {
|
|
html = html.replace(/^(<span.*?>)(.+?)(\*\*)/, '$1<span>$2</span>$3');
|
|
} else {
|
|
html = html.replace(/^(.+?)(\*\*)/, '<span>$1</span>$2');
|
|
}
|
|
// pseudo markdown bold text for the instruction section..
|
|
html = html.replace(/\*\*(.*?)\*\*/g, '<span class="instruction">$1</span>');
|
|
}
|
|
|
|
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>'); // emphasis
|
|
html = html.replace(/\{br\}/g, '<br/><br/>'); // linebreak
|
|
|
|
if (options.buttonText && options.buttonCallback) {
|
|
html += '<div class="button-section">' +
|
|
'<button href="#" class="button action">' + options.buttonText + '</button></div>';
|
|
}
|
|
|
|
var classes = 'curtain-tooltip popover tooltip arrowed in ' + (options.tooltipClass || '');
|
|
tooltip
|
|
.classed(classes, true)
|
|
.selectAll('.popover-inner')
|
|
.html(html);
|
|
|
|
if (options.buttonText && options.buttonCallback) {
|
|
var button = tooltip.selectAll('.button-section .button.action');
|
|
button
|
|
.on('click', function(d3_event) {
|
|
d3_event.preventDefault();
|
|
options.buttonCallback();
|
|
});
|
|
}
|
|
|
|
var tip = copyBox(tooltip.node().getBoundingClientRect()),
|
|
w = containerNode.clientWidth,
|
|
h = containerNode.clientHeight,
|
|
tooltipWidth = 200,
|
|
tooltipArrow = 5,
|
|
side, pos;
|
|
|
|
|
|
// hack: this will have bottom placement,
|
|
// so need to reserve extra space for the tooltip illustration.
|
|
if (options.tooltipClass === 'intro-mouse') {
|
|
tip.height += 80;
|
|
}
|
|
|
|
// trim box dimensions to just the portion that fits in the container..
|
|
if (tooltipBox.top + tooltipBox.height > h) {
|
|
tooltipBox.height -= (tooltipBox.top + tooltipBox.height - h);
|
|
}
|
|
if (tooltipBox.left + tooltipBox.width > w) {
|
|
tooltipBox.width -= (tooltipBox.left + tooltipBox.width - w);
|
|
}
|
|
|
|
// determine tooltip placement..
|
|
const onLeftOrRightEdge = tooltipBox.left + tooltipBox.width / 2 > w - 100 || tooltipBox.left + tooltipBox.width / 2 < 100;
|
|
if (tooltipBox.top + tooltipBox.height < 100 && !onLeftOrRightEdge) {
|
|
// tooltip below box..
|
|
side = 'bottom';
|
|
pos = [
|
|
tooltipBox.left + tooltipBox.width / 2 - tip.width / 2,
|
|
tooltipBox.top + tooltipBox.height
|
|
];
|
|
|
|
} else if (tooltipBox.top > h - 140 && !onLeftOrRightEdge) {
|
|
// tooltip above box..
|
|
side = 'top';
|
|
pos = [
|
|
tooltipBox.left + tooltipBox.width / 2 - tip.width / 2,
|
|
tooltipBox.top - tip.height
|
|
];
|
|
|
|
} else {
|
|
// tooltip to the side of the tooltipBox..
|
|
var tipY = tooltipBox.top + tooltipBox.height / 2 - tip.height / 2;
|
|
|
|
if (localizer.textDirection() === 'rtl') {
|
|
if (tooltipBox.left - tooltipWidth - tooltipArrow < 70) {
|
|
side = 'right';
|
|
pos = [tooltipBox.left + tooltipBox.width + tooltipArrow, tipY];
|
|
|
|
} else {
|
|
side = 'left';
|
|
pos = [tooltipBox.left - tooltipWidth - tooltipArrow, tipY];
|
|
}
|
|
|
|
} else {
|
|
if (tooltipBox.left + tooltipBox.width + tooltipArrow + tooltipWidth > w - 70) {
|
|
side = 'left';
|
|
pos = [tooltipBox.left - tooltipWidth - tooltipArrow, tipY];
|
|
} else {
|
|
side = 'right';
|
|
pos = [tooltipBox.left + tooltipBox.width + tooltipArrow, tipY];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.duration !== 0 || !tooltip.classed(side)) {
|
|
tooltip.call(uiToggle(true));
|
|
}
|
|
|
|
tooltip
|
|
.style('top', pos[1] + 'px')
|
|
.style('left', pos[0] + 'px')
|
|
.attr('class', classes + ' ' + side);
|
|
|
|
|
|
// shift popover-inner if it is very close to the top or bottom edge
|
|
// (doesn't affect the placement of the popover-arrow)
|
|
var shiftY = 0;
|
|
if (side === 'left' || side === 'right') {
|
|
if (pos[1] < 60) {
|
|
shiftY = 60 - pos[1];
|
|
} else if (pos[1] + tip.height > h - 100) {
|
|
shiftY = h - pos[1] - tip.height - 100;
|
|
}
|
|
}
|
|
tooltip.selectAll('.popover-inner')
|
|
.style('top', shiftY + 'px');
|
|
|
|
} else {
|
|
tooltip
|
|
.classed('in', false)
|
|
.call(uiToggle(false));
|
|
}
|
|
|
|
curtain.cut(box, options.duration);
|
|
|
|
return tooltip;
|
|
};
|
|
|
|
|
|
curtain.cut = function(datum, duration) {
|
|
darkness.datum(datum)
|
|
.interrupt();
|
|
|
|
var selection;
|
|
if (duration === 0) {
|
|
selection = darkness;
|
|
} else {
|
|
selection = darkness
|
|
.transition()
|
|
.duration(duration || 600)
|
|
.ease(d3_easeLinear);
|
|
}
|
|
|
|
selection
|
|
.attr('d', function(d) {
|
|
var containerWidth = containerNode.clientWidth;
|
|
var containerHeight = containerNode.clientHeight;
|
|
var string = 'M 0,0 L 0,' + containerHeight + ' L ' +
|
|
containerWidth + ',' + containerHeight + 'L' +
|
|
containerWidth + ',0 Z';
|
|
|
|
if (!d) return string;
|
|
return string + 'M' +
|
|
d.left + ',' + d.top + 'L' +
|
|
d.left + ',' + (d.top + d.height) + 'L' +
|
|
(d.left + d.width) + ',' + (d.top + d.height) + 'L' +
|
|
(d.left + d.width) + ',' + (d.top) + 'Z';
|
|
|
|
});
|
|
};
|
|
|
|
|
|
curtain.remove = function() {
|
|
surface.remove();
|
|
tooltip.remove();
|
|
d3_select(window).on('resize.curtain', null);
|
|
};
|
|
|
|
|
|
// ClientRects are immutable, so copy them to an object,
|
|
// in case we need to trim the height/width.
|
|
function copyBox(src) {
|
|
return {
|
|
top: src.top,
|
|
right: src.right,
|
|
bottom: src.bottom,
|
|
left: src.left,
|
|
width: src.width,
|
|
height: src.height
|
|
};
|
|
}
|
|
|
|
|
|
return curtain;
|
|
}
|