modules/ui/popover.js (251 lines of code) (raw):
import { select as d3_select } from 'd3-selection';
import { utilFunctor } from '../util/util';
var _popoverID = 0;
export function uiPopover(klass) {
var _id = _popoverID++;
var _anchorSelection = d3_select(null);
var popover = function(selection) {
_anchorSelection = selection;
selection.each(setup);
};
var _animation = utilFunctor(false);
var _placement = utilFunctor('top'); // top, bottom, left, right
var _alignment = utilFunctor('center'); // leading, center, trailing
var _scrollContainer = utilFunctor(d3_select(null));
var _content;
var _displayType = utilFunctor('');
var _hasArrow = utilFunctor(true);
// use pointer events on supported platforms; fallback to mouse events
var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';
popover.displayType = function(val) {
if (arguments.length) {
_displayType = utilFunctor(val);
return popover;
} else {
return _displayType;
}
};
popover.hasArrow = function(val) {
if (arguments.length) {
_hasArrow = utilFunctor(val);
return popover;
} else {
return _hasArrow;
}
};
popover.placement = function(val) {
if (arguments.length) {
_placement = utilFunctor(val);
return popover;
} else {
return _placement;
}
};
popover.alignment = function(val) {
if (arguments.length) {
_alignment = utilFunctor(val);
return popover;
} else {
return _alignment;
}
};
popover.scrollContainer = function(val) {
if (arguments.length) {
_scrollContainer = utilFunctor(val);
return popover;
} else {
return _scrollContainer;
}
};
popover.content = function(val) {
if (arguments.length) {
_content = val;
return popover;
} else {
return _content;
}
};
popover.isShown = function() {
var popoverSelection = _anchorSelection.select('.popover-' + _id);
return !popoverSelection.empty() && popoverSelection.classed('in');
};
popover.show = function() {
_anchorSelection.each(show);
};
popover.updateContent = function() {
_anchorSelection.each(updateContent);
};
popover.hide = function() {
_anchorSelection.each(hide);
};
popover.toggle = function() {
_anchorSelection.each(toggle);
};
popover.destroy = function(selection, selector) {
// by default, just destroy the current popover
selector = selector || '.popover-' + _id;
selection
.on(_pointerPrefix + 'enter.popover', null)
.on(_pointerPrefix + 'leave.popover', null)
.on(_pointerPrefix + 'up.popover', null)
.on(_pointerPrefix + 'down.popover', null)
.on('click.popover', null)
.attr('title', function() {
return this.getAttribute('data-original-title') || this.getAttribute('title');
})
.attr('data-original-title', null)
.selectAll(selector)
.remove();
};
popover.destroyAny = function(selection) {
selection.call(popover.destroy, '.popover');
};
function setup() {
var anchor = d3_select(this);
var animate = _animation.apply(this, arguments);
var popoverSelection = anchor.selectAll('.popover-' + _id)
.data([0]);
var enter = popoverSelection.enter()
.append('div')
.attr('class', 'popover popover-' + _id + ' ' + (klass ? klass : ''))
.classed('arrowed', _hasArrow.apply(this, arguments));
enter
.append('div')
.attr('class', 'popover-arrow');
enter
.append('div')
.attr('class', 'popover-inner');
popoverSelection = enter
.merge(popoverSelection);
if (animate) {
popoverSelection.classed('fade', true);
}
var display = _displayType.apply(this, arguments);
if (display === 'hover') {
var _lastNonMouseEnterTime;
anchor.on(_pointerPrefix + 'enter.popover', function(d3_event) {
if (d3_event.pointerType) {
if (d3_event.pointerType !== 'mouse') {
_lastNonMouseEnterTime = d3_event.timeStamp;
// only allow hover behavior for mouse input
return;
} else if (_lastNonMouseEnterTime &&
d3_event.timeStamp - _lastNonMouseEnterTime < 1500) {
// HACK: iOS 13.4 sends an erroneous `mouse` type pointerenter
// event for non-mouse interactions right after sending
// the correct type pointerenter event. Workaround by discarding
// any mouse event that occurs immediately after a non-mouse event.
return;
}
}
// don't show if buttons are pressed, e.g. during click and drag of map
if (d3_event.buttons !== 0) return;
show.apply(this, arguments);
})
.on(_pointerPrefix + 'leave.popover', function() {
hide.apply(this, arguments);
})
// show on focus too for better keyboard navigation support
.on('focus.popover', function() {
show.apply(this, arguments);
})
.on('blur.popover', function() {
hide.apply(this, arguments);
});
} else if (display === 'clickFocus') {
anchor
.on(_pointerPrefix + 'down.popover', function(d3_event) {
d3_event.preventDefault();
d3_event.stopPropagation();
})
.on(_pointerPrefix + 'up.popover', function(d3_event) {
d3_event.preventDefault();
d3_event.stopPropagation();
})
.on('click.popover', toggle);
popoverSelection
// This attribute lets the popover take focus
.attr('tabindex', 0)
.on('blur.popover', function() {
anchor.each(function() {
hide.apply(this, arguments);
});
});
}
}
function show() {
var anchor = d3_select(this);
var popoverSelection = anchor.selectAll('.popover-' + _id);
if (popoverSelection.empty()) {
// popover was removed somehow, put it back
anchor.call(popover.destroy);
anchor.each(setup);
popoverSelection = anchor.selectAll('.popover-' + _id);
}
popoverSelection.classed('in', true);
var displayType = _displayType.apply(this, arguments);
if (displayType === 'clickFocus') {
anchor.classed('active', true);
popoverSelection.node().focus();
}
anchor.each(updateContent);
}
function updateContent() {
var anchor = d3_select(this);
if (_content) {
anchor.selectAll('.popover-' + _id + ' > .popover-inner')
.call(_content.apply(this, arguments));
}
updatePosition.apply(this, arguments);
// hack: update multiple times to fix instances where the absolute offset is
// set before the dynamic popover size is calculated by the browser
updatePosition.apply(this, arguments);
updatePosition.apply(this, arguments);
}
function updatePosition() {
var anchor = d3_select(this);
var popoverSelection = anchor.selectAll('.popover-' + _id);
var scrollContainer = _scrollContainer && _scrollContainer.apply(this, arguments);
var scrollNode = scrollContainer && !scrollContainer.empty() && scrollContainer.node();
var scrollLeft = scrollNode ? scrollNode.scrollLeft : 0;
var scrollTop = scrollNode ? scrollNode.scrollTop : 0;
var placement = _placement.apply(this, arguments);
popoverSelection
.classed('left', false)
.classed('right', false)
.classed('top', false)
.classed('bottom', false)
.classed(placement, true);
var alignment = _alignment.apply(this, arguments);
var alignFactor = 0.5;
if (alignment === 'leading') {
alignFactor = 0;
} else if (alignment === 'trailing') {
alignFactor = 1;
}
var anchorFrame = getFrame(anchor.node());
var popoverFrame = getFrame(popoverSelection.node());
var position;
switch (placement) {
case 'top':
position = {
x: anchorFrame.x + (anchorFrame.w - popoverFrame.w) * alignFactor,
y: anchorFrame.y - popoverFrame.h
};
break;
case 'bottom':
position = {
x: anchorFrame.x + (anchorFrame.w - popoverFrame.w) * alignFactor,
y: anchorFrame.y + anchorFrame.h
};
break;
case 'left':
position = {
x: anchorFrame.x - popoverFrame.w,
y: anchorFrame.y + (anchorFrame.h - popoverFrame.h) * alignFactor
};
break;
case 'right':
position = {
x: anchorFrame.x + anchorFrame.w,
y: anchorFrame.y + (anchorFrame.h - popoverFrame.h) * alignFactor
};
break;
}
if (position) {
if (scrollNode && (placement === 'top' || placement === 'bottom')) {
var initialPosX = position.x;
if (position.x + popoverFrame.w > scrollNode.offsetWidth - 10) {
position.x = scrollNode.offsetWidth - 10 - popoverFrame.w;
} else if (position.x < 10) {
position.x = 10;
}
var arrow = anchor.selectAll('.popover-' + _id + ' > .popover-arrow');
// keep the arrow centered on the button, or as close as possible
var arrowPosX = Math.min(Math.max(popoverFrame.w / 2 - (position.x - initialPosX), 10), popoverFrame.w - 10);
arrow.style('left', ~~arrowPosX + 'px');
}
popoverSelection.style('left', ~~position.x + 'px').style('top', ~~position.y + 'px');
} else {
popoverSelection.style('left', null).style('top', null);
}
function getFrame(node) {
var positionStyle = d3_select(node).style('position');
if (positionStyle === 'absolute' || positionStyle === 'static') {
return {
x: node.offsetLeft - scrollLeft,
y: node.offsetTop - scrollTop,
w: node.offsetWidth,
h: node.offsetHeight
};
} else {
return {
x: 0,
y: 0,
w: node.offsetWidth,
h: node.offsetHeight
};
}
}
}
function hide() {
var anchor = d3_select(this);
if (_displayType.apply(this, arguments) === 'clickFocus') {
anchor.classed('active', false);
}
anchor.selectAll('.popover-' + _id).classed('in', false);
}
function toggle() {
if (d3_select(this).select('.popover-' + _id).classed('in')) {
hide.apply(this, arguments);
} else {
show.apply(this, arguments);
}
}
return popover;
}