mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 17:23:02 +00:00
277 lines
9.1 KiB
JavaScript
277 lines
9.1 KiB
JavaScript
import * as d3 from 'd3';
|
|
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', 'tooltip-arrow');
|
|
|
|
tooltip
|
|
.append('div')
|
|
.attr('class', 'tooltip-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 col8">' + options.buttonText + '</button></div>';
|
|
}
|
|
|
|
var classes = 'curtain-tooltip tooltip in ' + (options.tooltipClass || '');
|
|
tooltip
|
|
.classed(classes, true)
|
|
.selectAll('.tooltip-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 tooltip-inner if it is very close to the top or bottom edge
|
|
// (doesn't affect the placement of the tooltip-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('.tooltip-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;
|
|
}
|