UNPKG

23.5 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6
7var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
8
9var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
10
11var _react = require("react");
12
13var _react2 = _interopRequireDefault(_react);
14
15var _propTypes = require("prop-types");
16
17var _propTypes2 = _interopRequireDefault(_propTypes);
18
19var _reactDom = require("react-dom");
20
21var _reactDom2 = _interopRequireDefault(_reactDom);
22
23var _debug = require("debug");
24
25var _debug2 = _interopRequireDefault(_debug);
26
27var _lodash = require("lodash.throttle");
28
29var _lodash2 = _interopRequireDefault(_lodash);
30
31var _cssVendor = require("css-vendor");
32
33var cssVendor = _interopRequireWildcard(_cssVendor);
34
35var _onResize = require("./on-resize");
36
37var _onResize2 = _interopRequireDefault(_onResize);
38
39var _layout = require("./layout");
40
41var _layout2 = _interopRequireDefault(_layout);
42
43var _platform = require("./platform");
44
45var _utils = require("./utils");
46
47var _utils2 = _interopRequireDefault(_utils);
48
49var _tip = require("./tip");
50
51var _tip2 = _interopRequireDefault(_tip);
52
53function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
54
55function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
56
57function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
58
59function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
60
61function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
62
63var log = (0, _debug2.default)("react-popover");
64
65var supportedCSSValue = _utils2.default.clientOnly(cssVendor.supportedValue);
66
67var jsprefix = function jsprefix(x) {
68 return "" + cssVendor.prefix.js + x;
69};
70
71var cssprefix = function cssprefix(x) {
72 return "" + cssVendor.prefix.css + x;
73};
74
75var cssvalue = function cssvalue(prop, value) {
76 return supportedCSSValue(prop, value) || cssprefix(value);
77};
78
79var coreStyle = {
80 position: "absolute",
81 top: 0,
82 left: 0,
83 display: cssvalue("display", "flex")
84};
85
86var faces = {
87 above: "down",
88 right: "left",
89 below: "up",
90 left: "right"
91
92 /* Flow mappings. Each map maps the flow domain to another domain. */
93
94};var flowToTipTranslations = {
95 row: "translateY",
96 column: "translateX"
97};
98
99var flowToPopoverTranslations = {
100 row: "translateX",
101 column: "translateY"
102};
103
104var Popover = function (_React$Component) {
105 _inherits(Popover, _React$Component);
106
107 function Popover(props) {
108 _classCallCheck(this, Popover);
109
110 var _this = _possibleConstructorReturn(this, (Popover.__proto__ || Object.getPrototypeOf(Popover)).call(this, props));
111
112 _this.checkTargetReposition = function () {
113 if (_this.measureTargetBounds()) _this.resolvePopoverLayout();
114 };
115
116 _this.checkForOuterAction = function (event) {
117 var isOuterAction = !_this.containerEl.contains(event.target) && !_this.targetEl.contains(event.target);
118 if (isOuterAction) _this.props.onOuterAction(event);
119 };
120
121 _this.onTargetResize = function () {
122 log("Recalculating layout because _target_ resized!");
123 _this.measureTargetBounds();
124 _this.resolvePopoverLayout();
125 };
126
127 _this.onPopoverResize = function () {
128 log("Recalculating layout because _popover_ resized!");
129 _this.measurePopoverSize();
130 _this.resolvePopoverLayout();
131 };
132
133 _this.onFrameScroll = function () {
134 log("Recalculating layout because _frame_ scrolled!");
135 _this.measureTargetBounds();
136 _this.resolvePopoverLayout();
137 };
138
139 _this.onFrameResize = function () {
140 log("Recalculating layout because _frame_ resized!");
141 _this.measureFrameBounds();
142 _this.resolvePopoverLayout();
143 };
144
145 _this.getTargetNodeRef = function (targetWrapperEl) {
146 Object.assign(_this, {
147 targetEl: targetWrapperEl ? targetWrapperEl.firstElementChild : null
148 });
149 };
150
151 _this.getContainerNodeRef = function (containerEl) {
152 Object.assign(_this, { containerEl: containerEl });
153 };
154
155 _this.state = {
156 standing: "above",
157 exited: !_this.props.isOpen, // for animation-dependent rendering, should popover close/open?
158 exiting: false, // for tracking in-progress animations
159 toggle: _this.props.isOpen || false // for business logic tracking, should popover close/open?
160 };
161 return _this;
162 }
163
164 _createClass(Popover, [{
165 key: "componentDidMount",
166 value: function componentDidMount() {
167 if (this.props.isOpen) this.enter();
168 }
169 }, {
170 key: "componentWillReceiveProps",
171 value: function componentWillReceiveProps(propsNext) {
172 //log(`Component received props!`, propsNext)
173 var willOpen = !this.props.isOpen && propsNext.isOpen;
174 var willClose = this.props.isOpen && !propsNext.isOpen;
175
176 if (willOpen) this.open();else if (willClose) this.close();
177 }
178 }, {
179 key: "componentDidUpdate",
180 value: function componentDidUpdate(propsPrev, statePrev) {
181 //log(`Component did update!`)
182 var didOpen = !statePrev.toggle && this.state.toggle;
183 var didClose = statePrev.toggle && !this.state.toggle;
184
185 if (didOpen) this.enter();else if (didClose) this.exit();
186 }
187 }, {
188 key: "componentWillUnmount",
189 value: function componentWillUnmount() {
190 /* If the Popover was never opened then then tracking
191 initialization never took place and so calling untrack
192 would be an error. Also see issue 55. */
193 if (this.hasTracked) this.untrackPopover();
194 }
195 }, {
196 key: "resolvePopoverLayout",
197 value: function resolvePopoverLayout() {
198
199 /* Find the optimal zone to position self. Measure the size of each zone and use the one with
200 the greatest area. */
201
202 var pickerSettings = {
203 preferPlace: this.props.preferPlace,
204 place: this.props.place
205
206 /* This is a kludge that solves a general problem very specifically for Popover.
207 The problem is subtle. When Popover positioning changes such that it resolves at
208 a different orientation, its Size will change because the Tip will toggle between
209 extending Height or Width. The general problem of course is that calculating
210 zone positioning based on current size is non-trivial if the Size can change once
211 resolved to a different zone. Infinite recursion can be triggered as we noted here:
212 https://github.com/littlebits/react-popover/issues/18. As an example of how this
213 could happen in another way: Imagine the user changes the CSS styling of the popover
214 based on whether it was `row` or `column` flow. TODO: Find a solution to generally
215 solve this problem so that the user is free to change the Popover styles in any
216 way at any time for any arbitrary trigger. There may be value in investigating the
217 http://overconstrained.io community for its general layout system via the
218 constraint-solver Cassowary. */
219 };if (this.zone) this.size[this.zone.flow === "row" ? "h" : "w"] += this.props.tipSize;
220 var zone = _layout2.default.pickZone(pickerSettings, this.frameBounds, this.targetBounds, this.size);
221 if (this.zone) this.size[this.zone.flow === "row" ? "h" : "w"] -= this.props.tipSize;
222
223 var tb = this.targetBounds;
224 this.zone = zone;
225 log("zone", zone);
226
227 this.setState({
228 standing: zone.standing
229 });
230
231 var axis = _layout2.default.axes[zone.flow];
232 log("axes", axis);
233
234 var dockingEdgeBufferLength = Math.round(getComputedStyle(this.bodyEl).borderRadius.slice(0, -2)) || 0;
235 var scrollSize = _layout2.default.El.calcScrollSize(this.frameEl);
236 scrollSize.main = scrollSize[axis.main.size];
237 scrollSize.cross = scrollSize[axis.cross.size];
238
239 /* When positioning self on the cross-axis do not exceed frame bounds. The strategy to achieve
240 this is thus: First position cross-axis self to the cross-axis-center of the the target. Then,
241 offset self by the amount that self is past the boundaries of frame. */
242 var pos = _layout2.default.calcRelPos(zone, tb, this.size);
243
244 /* Offset allows users to control the distance betweent the tip and the target. */
245 pos[axis.main.start] += this.props.offset * zone.order;
246
247 /* Constrain containerEl Position within frameEl. Try not to penetrate a visually-pleasing buffer from
248 frameEl. `frameBuffer` length is based on tipSize and its offset. */
249
250 var frameBuffer = this.props.tipSize + this.props.offset;
251 var hangingBufferLength = dockingEdgeBufferLength * 2 + this.props.tipSize * 2 + frameBuffer;
252 var frameCrossStart = this.frameBounds[axis.cross.start];
253 var frameCrossEnd = this.frameBounds[axis.cross.end];
254 var frameCrossLength = this.frameBounds[axis.cross.size];
255 var frameCrossInnerLength = frameCrossLength - frameBuffer * 2;
256 var frameCrossInnerStart = frameCrossStart + frameBuffer;
257 var frameCrossInnerEnd = frameCrossEnd - frameBuffer;
258 var popoverCrossStart = pos[axis.cross.start];
259 var popoverCrossEnd = pos[axis.cross.end];
260
261 /* If the popover dose not fit into frameCrossLength then just position it to the `frameCrossStart`.
262 popoverCrossLength` will now be forced to overflow into the `Frame` */
263 if (pos.crossLength > frameCrossLength) {
264 log("popoverCrossLength does not fit frame.");
265 pos[axis.cross.start] = 0;
266
267 /* If the `popoverCrossStart` is forced beyond some threshold of `targetCrossLength` then bound
268 it (`popoverCrossStart`). */
269 } else if (tb[axis.cross.end] < hangingBufferLength) {
270 log("popoverCrossStart cannot hang any further without losing target.");
271 pos[axis.cross.start] = tb[axis.cross.end] - hangingBufferLength;
272
273 /* checking if the cross start of the target area is within the frame and it makes sense
274 to try fitting popover into the frame. */
275 } else if (tb[axis.cross.start] > frameCrossInnerEnd) {
276 log("popoverCrossStart cannot hang any further without losing target.");
277 pos[axis.cross.start] = tb[axis.cross.start] - this.size[axis.cross.size];
278
279 /* If the `popoverCrossStart` does not fit within the inner frame (honouring buffers) then
280 just center the popover in the remaining `frameCrossLength`. */
281 } else if (pos.crossLength > frameCrossInnerLength) {
282 log("popoverCrossLength does not fit within buffered frame.");
283 pos[axis.cross.start] = (frameCrossLength - pos.crossLength) / 2;
284 } else if (popoverCrossStart < frameCrossInnerStart) {
285 log("popoverCrossStart cannot reverse without exceeding frame.");
286 pos[axis.cross.start] = frameCrossInnerStart;
287 } else if (popoverCrossEnd > frameCrossInnerEnd) {
288 log("popoverCrossEnd cannot travel without exceeding frame.");
289 pos[axis.cross.start] = pos[axis.cross.start] - (pos[axis.cross.end] - frameCrossInnerEnd);
290 }
291
292 /* So far the link position has been calculated relative to the target. To calculate the absolute
293 position we need to factor the `Frame``s scroll position */
294
295 pos[axis.cross.start] += scrollSize.cross;
296 pos[axis.main.start] += scrollSize.main;
297
298 /* Apply `flow` and `order` styles. This can impact subsequent measurements of height and width
299 of the container. When tip changes orientation position due to changes from/to `row`/`column`
300 width`/`height` will be impacted. Our layout monitoring will catch these cases and automatically
301 recalculate layout. */
302
303 this.containerEl.style.flexFlow = zone.flow;
304 this.containerEl.style[jsprefix("FlexFlow")] = this.containerEl.style.flexFlow;
305 this.bodyEl.style.order = zone.order;
306 this.bodyEl.style[jsprefix("Order")] = this.bodyEl.style.order;
307
308 /* Apply Absolute Positioning. */
309
310 log("pos", pos);
311 this.containerEl.style.top = pos.y + "px";
312 this.containerEl.style.left = pos.x + "px";
313
314 /* Calculate Tip Position */
315
316 var tipCrossPos =
317 /* Get the absolute tipCrossCenter. Tip is positioned relative to containerEl
318 but it aims at targetCenter which is positioned relative to frameEl... we
319 need to cancel the containerEl positioning so as to hit our intended position. */
320 _layout2.default.centerOfBoundsFromBounds(zone.flow, "cross", tb, pos)
321
322 /* centerOfBounds does not account for scroll so we need to manually add that
323 here. */
324 + scrollSize.cross
325
326 /* Center tip relative to self. We do not have to calcualte half-of-tip-size since tip-size
327 specifies the length from base to tip which is half of total length already. */
328 - this.props.tipSize;
329
330 if (tipCrossPos < dockingEdgeBufferLength) tipCrossPos = dockingEdgeBufferLength;else if (tipCrossPos > pos.crossLength - dockingEdgeBufferLength - this.props.tipSize * 2) {
331 tipCrossPos = pos.crossLength - dockingEdgeBufferLength - this.props.tipSize * 2;
332 }
333
334 this.tipEl.style.transform = flowToTipTranslations[zone.flow] + "(" + tipCrossPos + "px)";
335 this.tipEl.style[jsprefix("Transform")] = this.tipEl.style.transform;
336 }
337 }, {
338 key: "measurePopoverSize",
339 value: function measurePopoverSize() {
340 this.size = _layout2.default.El.calcSize(this.containerEl);
341 }
342 }, {
343 key: "measureTargetBounds",
344 value: function measureTargetBounds() {
345 var newTargetBounds = _layout2.default.El.calcBounds(this.targetEl);
346
347 if (this.targetBounds && _layout2.default.equalCoords(this.targetBounds, newTargetBounds)) {
348 return false;
349 }
350
351 this.targetBounds = newTargetBounds;
352 return true;
353 }
354 }, {
355 key: "open",
356 value: function open() {
357 if (this.state.exiting) this.animateExitStop();
358 this.setState({ toggle: true, exited: false });
359 }
360 }, {
361 key: "close",
362 value: function close() {
363 this.setState({ toggle: false });
364 }
365 }, {
366 key: "enter",
367 value: function enter() {
368 if (_platform.isServer) return;
369 log("enter!");
370 this.trackPopover();
371 this.animateEnter();
372 }
373 }, {
374 key: "exit",
375 value: function exit() {
376 log("exit!");
377 this.animateExit();
378 this.untrackPopover();
379 }
380 }, {
381 key: "animateExitStop",
382 value: function animateExitStop() {
383 clearTimeout(this.exitingAnimationTimer1);
384 clearTimeout(this.exitingAnimationTimer2);
385 this.setState({ exiting: false });
386 }
387 }, {
388 key: "animateExit",
389 value: function animateExit() {
390 var _this2 = this;
391
392 this.setState({ exiting: true });
393 this.exitingAnimationTimer2 = setTimeout(function () {
394 setTimeout(function () {
395 _this2.containerEl.style.transform = flowToPopoverTranslations[_this2.zone.flow] + "(" + _this2.zone.order * 50 + "px)";
396 _this2.containerEl.style.opacity = "0";
397 }, 0);
398 }, 0);
399
400 this.exitingAnimationTimer1 = setTimeout(function () {
401 _this2.setState({ exited: true, exiting: false });
402 }, this.props.enterExitTransitionDurationMs);
403 }
404 }, {
405 key: "animateEnter",
406 value: function animateEnter() {
407 /* Prepare `entering` style so that we can then animate it toward `entered`. */
408
409 this.containerEl.style.transform = flowToPopoverTranslations[this.zone.flow] + "(" + this.zone.order * 50 + "px)";
410 this.containerEl.style[jsprefix("Transform")] = this.containerEl.style.transform;
411 this.containerEl.style.opacity = "0";
412
413 /* After initial layout apply transition animations. */
414 /* Hack: http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes */
415 this.containerEl.offsetHeight;
416
417 /* If enterExitTransitionDurationMs is falsy, tip animation should be also disabled */
418 if (this.props.enterExitTransitionDurationMs) {
419 this.tipEl.style.transition = "transform 150ms ease-in";
420 this.tipEl.style[jsprefix("Transition")] = cssprefix("transform") + " 150ms ease-in";
421 }
422 this.containerEl.style.transitionProperty = "top, left, opacity, transform";
423 this.containerEl.style.transitionDuration = this.props.enterExitTransitionDurationMs + "ms";
424 this.containerEl.style.transitionTimingFunction = "cubic-bezier(0.230, 1.000, 0.320, 1.000)";
425 this.containerEl.style.opacity = "1";
426 this.containerEl.style.transform = "translateY(0)";
427 this.containerEl.style[jsprefix("Transform")] = this.containerEl.style.transform;
428 }
429 }, {
430 key: "trackPopover",
431 value: function trackPopover() {
432 var minScrollRefreshIntervalMs = 200;
433 var minResizeRefreshIntervalMs = 200;
434
435 /* Get references to DOM elements. */
436
437 this.bodyEl = this.containerEl.querySelector(".Popover-body");
438 this.tipEl = this.containerEl.querySelector(".Popover-tip");
439
440 /* Note: frame is hardcoded to window now but we think it will
441 be a nice feature in the future to allow other frames to be used
442 such as local elements that further constrain the popover`s world. */
443
444 this.frameEl = _platform.window;
445 this.hasTracked = true;
446
447 /* Set a general interval for checking if target position changed. There is no way
448 to know this information without polling. */
449 if (this.props.refreshIntervalMs) {
450 this.checkLayoutInterval = setInterval(this.checkTargetReposition, this.props.refreshIntervalMs);
451 }
452
453 /* Watch for boundary changes in all deps, and when one of them changes, recalculate layout.
454 This layout monitoring must be bound immediately because a layout recalculation can recursively
455 cause a change in boundaries. So if we did a one-time force-layout before watching boundaries
456 our final position calculations could be wrong. See comments in resolver function for details
457 about which parts can trigger recursive recalculation. */
458
459 this.onFrameScroll = (0, _lodash2.default)(this.onFrameScroll, minScrollRefreshIntervalMs);
460 this.onFrameResize = (0, _lodash2.default)(this.onFrameResize, minResizeRefreshIntervalMs);
461 this.onPopoverResize = (0, _lodash2.default)(this.onPopoverResize, minResizeRefreshIntervalMs);
462 this.onTargetResize = (0, _lodash2.default)(this.onTargetResize, minResizeRefreshIntervalMs);
463
464 this.frameEl.addEventListener("scroll", this.onFrameScroll);
465 _onResize2.default.on(this.frameEl, this.onFrameResize);
466 _onResize2.default.on(this.containerEl, this.onPopoverResize);
467 _onResize2.default.on(this.targetEl, this.onTargetResize);
468
469 /* Track user actions on the page. Anything that occurs _outside_ the Popover boundaries
470 should close the Popover. */
471
472 _platform.document.addEventListener("mousedown", this.checkForOuterAction);
473 _platform.document.addEventListener("touchstart", this.checkForOuterAction);
474
475 /* Kickstart layout at first boot. */
476
477 this.measurePopoverSize();
478 this.measureFrameBounds();
479 this.measureTargetBounds();
480 this.resolvePopoverLayout();
481 }
482 }, {
483 key: "untrackPopover",
484 value: function untrackPopover() {
485 clearInterval(this.checkLayoutInterval);
486 this.frameEl.removeEventListener("scroll", this.onFrameScroll);
487 _onResize2.default.off(this.frameEl, this.onFrameResize);
488 _onResize2.default.off(this.containerEl, this.onPopoverResize);
489 _onResize2.default.off(this.targetEl, this.onTargetResize);
490 _platform.document.removeEventListener("mousedown", this.checkForOuterAction);
491 _platform.document.removeEventListener("touchstart", this.checkForOuterAction);
492 }
493 }, {
494 key: "measureFrameBounds",
495 value: function measureFrameBounds() {
496 this.frameBounds = _layout2.default.El.calcBounds(this.frameEl);
497 }
498 }, {
499 key: "render",
500 value: function render() {
501 var _props = this.props,
502 _props$className = _props.className,
503 className = _props$className === undefined ? "" : _props$className,
504 _props$style = _props.style,
505 style = _props$style === undefined ? {} : _props$style,
506 tipSize = _props.tipSize;
507 var standing = this.state.standing;
508
509
510 var popoverProps = {
511 className: "Popover Popover-" + standing + " " + className,
512 style: _extends({}, coreStyle, style)
513 };
514
515 var popover = this.state.exited ? null : _react2.default.createElement(
516 "div",
517 _extends({ ref: this.getContainerNodeRef }, popoverProps),
518 _react2.default.createElement("div", { className: "Popover-body", children: this.props.body }),
519 _react2.default.createElement(_tip2.default, {
520 direction: faces[standing],
521 size: tipSize
522 })
523 );
524 return [_react2.default.createElement(
525 "div",
526 { key: "1", ref: this.getTargetNodeRef },
527 this.props.children
528 ), _reactDom2.default.createPortal(popover, this.props.appendTarget)];
529 }
530 }]);
531
532 return Popover;
533}(_react2.default.Component);
534
535Popover.propTypes = {
536 body: _propTypes2.default.node.isRequired,
537 children: _propTypes2.default.element.isRequired,
538 appendTarget: _propTypes2.default.object,
539 className: _propTypes2.default.string,
540 enterExitTransitionDurationMs: _propTypes2.default.number,
541 isOpen: _propTypes2.default.bool,
542 offset: _propTypes2.default.number,
543 place: _propTypes2.default.oneOf(_layout2.default.validTypeValues),
544 preferPlace: _propTypes2.default.oneOf(_layout2.default.validTypeValues),
545 refreshIntervalMs: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.bool]),
546 style: _propTypes2.default.object,
547 tipSize: _propTypes2.default.number,
548 onOuterAction: _propTypes2.default.func
549};
550Popover.defaultProps = {
551 tipSize: 7,
552 preferPlace: null,
553 place: null,
554 offset: 4,
555 isOpen: false,
556 onOuterAction: _utils2.default.noop,
557 enterExitTransitionDurationMs: 500,
558 children: null,
559 refreshIntervalMs: 200,
560 appendTarget: _platform.isServer ? null : _platform.document.body
561};
562exports.default = Popover;
\No newline at end of file