Files
iD/modules/ui/curtain.js
Quincy Morgan 523a467836 2.x: Make toolbar horizontally scrollable when it overflows (re: #6755, re: 7545f67063b5e7007ef2d8367e3181e59c04a487)
Generalize tooltip into popover control
Use the same popover control for tooltip as the preset browser and tools list popovers
Smartly position the preset browser popover and menu bar tooltips to stay fully onscreen
Position most tooltips closer to their controls
Fix small gap that could appear between a tooltip and its arrow
Allow wider toolbar tooltips
2019-12-16 13:30:07 -05:00

283 lines
9.2 KiB
JavaScript

import { easeLinear as d3_easeLinear } from 'd3-ease';
import {
event as d3_event,
select as d3_select
} from 'd3-selection';
import { textDirection } from '../util/locale';
import { uiToggle } from './toggle';
// Tooltips and svg mask used to highlight certain features
export function uiCurtain() {
var surface = d3_select(null),
tooltip = d3_select(null),
darkness = d3_select(null);
function curtain(selection) {
surface = selection
.append('svg')
.attr('id', 'curtain')
.style('z-index', 1000)
.style('pointer-events', 'none')
.style('position', 'absolute')
.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')
.style('z-index', 1002);
tooltip
.append('div')
.attr('class', 'popover-arrow');
tooltip
.append('div')
.attr('class', 'popover-inner');
resize();
function resize() {
surface
.attr('width', window.innerWidth)
.attr('height', window.innerHeight);
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} [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 {String|ClientRect} [options.tooltipBox] box for tooltip position, if different from box for the curtain
*/
curtain.reveal = function(box, text, options) {
if (typeof box === 'string') {
box = d3_select(box).node();
}
if (box && box.getBoundingClientRect) {
box = copyBox(box.getBoundingClientRect());
}
options = options || {};
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 && text) {
// pseudo markdown bold text for the instruction section..
var parts = text.split('**');
var html = parts[0] ? '<span>' + parts[0] + '</span>' : '';
if (parts[1]) {
html += '<span class="instruction">' + parts[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.preventDefault();
options.buttonCallback();
});
}
var tip = copyBox(tooltip.node().getBoundingClientRect()),
w = window.innerWidth,
h = window.innerHeight,
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 window..
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..
if (tooltipBox.top + tooltipBox.height < 100) {
// tooltip below box..
side = 'bottom';
pos = [
tooltipBox.left + tooltipBox.width / 2 - tip.width / 2,
tooltipBox.top + tooltipBox.height
];
} else if (tooltipBox.top > h - 140) {
// 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 (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 string = 'M 0,0 L 0,' + window.innerHeight + ' L ' +
window.innerWidth + ',' + window.innerHeight + 'L' +
window.innerWidth + ',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;
}