modules/util/popover.js (220 lines of code) (raw):

import { event as d3_event, select as d3_select } from 'd3-selection'; import { utilFunctor } from './util'; var _popoverID = 0; export function popover(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); 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 = d3_select('.popover-' + _id); return !popoverSelection.empty() && popoverSelection.classed('in'); }; popover.show = function() { _anchorSelection.each(show); }; 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('mouseenter.popover', null) .on('mouseleave.popover', null) .on('mouseup.popover', null) .on('mousedown.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'); }; var isTouchEvent = false; 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 place = _placement.apply(this, arguments); popoverSelection.classed(place, true); var display = _displayType.apply(this, arguments); if (display === 'hover') { anchor.on('touchstart.popover', function() { // hack to avoid showing popovers upon touch input isTouchEvent = true; }); anchor.on('mouseenter.popover', show); anchor.on('mouseleave.popover', hide); } else if (display === 'clickFocus') { anchor .on('mousedown.popover', function() { d3_event.preventDefault(); d3_event.stopPropagation(); }) .on('mouseup.popover', function() { d3_event.preventDefault(); d3_event.stopPropagation(); }) .on('click.popover', toggle); popoverSelection .attr('tabindex', 0) .on('blur.popover', function() { anchor.each(function() { hide.apply(this, arguments); }); }); } } function show() { if (isTouchEvent) { isTouchEvent = false; return; } 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); if (_displayType.apply(this, arguments) === 'clickFocus') { anchor.classed('active', true); popoverSelection.node().focus(); } if (_content) popoverSelection.selectAll('.popover-inner').call(_content.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 place = _placement.apply(this, arguments); var alignment = _alignment.apply(this, arguments); var alignFactor = 0.5; if (alignment === 'leading') { alignFactor = 0; } else if (alignment === 'trailing') { alignFactor = 1; } var outer = getPosition(anchor.node()); var inner = getPosition(popoverSelection.node()); var pos; switch (place) { case 'top': pos = { x: outer.x + (outer.w - inner.w) * alignFactor, y: outer.y - inner.h }; break; case 'bottom': pos = { x: outer.x + (outer.w - inner.w) * alignFactor, y: outer.y + outer.h }; break; case 'left': pos = { x: outer.x - inner.w, y: outer.y + (outer.h - inner.h) * alignFactor }; break; case 'right': pos = { x: outer.x + outer.w, y: outer.y + (outer.h - inner.h) * alignFactor }; break; } if (pos) { if (scrollNode && (place === 'top' || place === 'bottom')) { var initialPosX = pos.x; if (pos.x + inner.w > scrollNode.offsetWidth - 10) { pos.x = scrollNode.offsetWidth - 10 - inner.w; } else if (pos.x < 10) { pos.x = 10; } var arrow = popoverSelection.selectAll('.popover-arrow'); // keep the arrow centered on the button, or as close as possible var arrowPosX = Math.min(Math.max(inner.w / 2 - (pos.x - initialPosX), 10), inner.w - 10); arrow.style('left', ~~arrowPosX + 'px'); } popoverSelection.style('left', ~~pos.x + 'px').style('top', ~~pos.y + 'px'); } else { popoverSelection.style('left', null).style('top', null); } function getPosition(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; }