UNPKG

react-bootstrap

Version:

Bootstrap 3 components build with React

524 lines (400 loc) 15.5 kB
/*eslint-disable react/prop-types */ 'use strict'; var _extends = require('babel-runtime/helpers/extends')['default']; var _objectWithoutProperties = require('babel-runtime/helpers/object-without-properties')['default']; var _Object$isFrozen = require('babel-runtime/core-js/object/is-frozen')['default']; var _Object$keys = require('babel-runtime/core-js/object/keys')['default']; var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; exports.__esModule = true; var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _classnames = require('classnames'); var _classnames2 = _interopRequireDefault(_classnames); var _utilsDomUtils = require('./utils/domUtils'); var _utilsDomUtils2 = _interopRequireDefault(_utilsDomUtils); var _utilsEventListener = require('./utils/EventListener'); var _utilsEventListener2 = _interopRequireDefault(_utilsEventListener); var _utilsCreateChainedFunction = require('./utils/createChainedFunction'); var _utilsCreateChainedFunction2 = _interopRequireDefault(_utilsCreateChainedFunction); var _utilsCustomPropTypes = require('./utils/CustomPropTypes'); var _utilsCustomPropTypes2 = _interopRequireDefault(_utilsCustomPropTypes); var _Portal = require('./Portal'); var _Portal2 = _interopRequireDefault(_Portal); var _Fade = require('./Fade'); var _Fade2 = _interopRequireDefault(_Fade); var _ModalDialog = require('./ModalDialog'); var _ModalDialog2 = _interopRequireDefault(_ModalDialog); var _ModalBody = require('./ModalBody'); var _ModalBody2 = _interopRequireDefault(_ModalBody); var _ModalHeader = require('./ModalHeader'); var _ModalHeader2 = _interopRequireDefault(_ModalHeader); var _ModalTitle = require('./ModalTitle'); var _ModalTitle2 = _interopRequireDefault(_ModalTitle); var _ModalFooter = require('./ModalFooter'); /** * Gets the correct clientHeight of the modal container * when the body/window/document you need to use the docElement clientHeight * @param {HTMLElement} container * @param {ReactElement|HTMLElement} context * @return {Number} */ var _ModalFooter2 = _interopRequireDefault(_ModalFooter); function containerClientHeight(container, context) { var doc = _utilsDomUtils2['default'].ownerDocument(context); return container === doc.body || container === doc.documentElement ? doc.documentElement.clientHeight : container.clientHeight; } function getContainer(context) { return context.props.container && _react2['default'].findDOMNode(context.props.container) || _utilsDomUtils2['default'].ownerDocument(context).body; } var currentFocusListener = undefined; /** * Firefox doesn't have a focusin event so using capture is easiest way to get bubbling * IE8 can't do addEventListener, but does have onfocusin, so we use that in ie8 * * We only allow one Listener at a time to avoid stack overflows * * @param {ReactElement|HTMLElement} context * @param {Function} handler */ function onFocus(context, handler) { var doc = _utilsDomUtils2['default'].ownerDocument(context); var useFocusin = !doc.addEventListener; var remove = undefined; if (currentFocusListener) { currentFocusListener.remove(); } if (useFocusin) { document.attachEvent('onfocusin', handler); remove = function () { return document.detachEvent('onfocusin', handler); }; } else { document.addEventListener('focus', handler, true); remove = function () { return document.removeEventListener('focus', handler, true); }; } currentFocusListener = { remove: remove }; return currentFocusListener; } var scrollbarSize = undefined; function getScrollbarSize() { if (scrollbarSize !== undefined) { return scrollbarSize; } var scrollDiv = document.createElement('div'); scrollDiv.style.position = 'absolute'; scrollDiv.style.top = '-9999px'; scrollDiv.style.width = '50px'; scrollDiv.style.height = '50px'; scrollDiv.style.overflow = 'scroll'; document.body.appendChild(scrollDiv); scrollbarSize = scrollDiv.offsetWidth - scrollDiv.clientWidth; document.body.removeChild(scrollDiv); scrollDiv = null; return scrollbarSize; } var Modal = _react2['default'].createClass({ displayName: 'Modal', propTypes: _extends({}, _Portal2['default'].propTypes, _ModalDialog2['default'].propTypes, { /** * Include a backdrop component. Specify 'static' for a backdrop that doesn't trigger an "onHide" when clicked. */ backdrop: _react2['default'].PropTypes.oneOf(['static', true, false]), /** * Close the modal when escape key is pressed */ keyboard: _react2['default'].PropTypes.bool, /** * Open and close the Modal with a slide and fade animation. */ animation: _react2['default'].PropTypes.bool, /** * A Component type that provides the modal content Markup. This is a useful prop when you want to use your own * styles and markup to create a custom modal component. */ dialogComponent: _utilsCustomPropTypes2['default'].elementType, /** * When `true` The modal will automatically shift focus to itself when it opens, and replace it to the last focused element when it closes. * Generally this should never be set to false as it makes the Modal less accessible to assistive technologies, like screen-readers. */ autoFocus: _react2['default'].PropTypes.bool, /** * When `true` The modal will prevent focus from leaving the Modal while open. * Consider leaving the default value here, as it is necessary to make the Modal work well with assistive technologies, * such as screen readers. */ enforceFocus: _react2['default'].PropTypes.bool }), getDefaultProps: function getDefaultProps() { return { bsClass: 'modal', dialogComponent: _ModalDialog2['default'], show: false, animation: true, backdrop: true, keyboard: true, autoFocus: true, enforceFocus: true }; }, getInitialState: function getInitialState() { return { exited: !this.props.show }; }, render: function render() { var _props = this.props; var children = _props.children; var animation = _props.animation; var backdrop = _props.backdrop; var props = _objectWithoutProperties(_props, ['children', 'animation', 'backdrop']); var onExit = props.onExit; var onExiting = props.onExiting; var onEnter = props.onEnter; var onEntering = props.onEntering; var onEntered = props.onEntered; var show = !!props.show; var Dialog = props.dialogComponent; var mountModal = show || animation && !this.state.exited; if (!mountModal) { return null; } var modal = _react2['default'].createElement( Dialog, _extends({}, props, { ref: this._setDialogRef, className: _classnames2['default']({ 'in': show && !animation }), onClick: backdrop === true ? this.handleBackdropClick : null }), this.renderContent() ); if (animation) { modal = _react2['default'].createElement( _Fade2['default'], { transitionAppear: true, unmountOnExit: true, 'in': show, duration: Modal.TRANSITION_DURATION, onExit: onExit, onExiting: onExiting, onExited: this.handleHidden, onEnter: onEnter, onEntering: onEntering, onEntered: onEntered }, modal ); } if (backdrop) { modal = this.renderBackdrop(modal); } return _react2['default'].createElement( _Portal2['default'], { container: props.container }, modal ); }, renderContent: function renderContent() { var _this = this; return _react2['default'].Children.map(this.props.children, function (child) { // TODO: use context in 0.14 if (child && child.type && child.type.__isModalHeader) { return _react.cloneElement(child, { onHide: _utilsCreateChainedFunction2['default'](_this.props.onHide, child.props.onHide) }); } return child; }); }, renderBackdrop: function renderBackdrop(modal) { var _props2 = this.props; var animation = _props2.animation; var bsClass = _props2.bsClass; var duration = Modal.BACKDROP_TRANSITION_DURATION; // Don't handle clicks for "static" backdrops var onClick = this.props.backdrop === true ? this.handleBackdropClick : null; var backdrop = _react2['default'].createElement('div', { ref: "backdrop", className: _classnames2['default'](bsClass + '-backdrop', { 'in': this.props.show && !animation }), onClick: onClick }); return _react2['default'].createElement( 'div', { ref: 'modal' }, animation ? _react2['default'].createElement( _Fade2['default'], { transitionAppear: true, 'in': this.props.show, duration: duration }, backdrop ) : backdrop, modal ); }, _setDialogRef: function _setDialogRef(ref) { // issue #1074 // due to: https://github.com/facebook/react/blob/v0.13.3/src/core/ReactCompositeComponent.js#L842 // // when backdrop is `false` react hasn't had a chance to reassign the refs to a usable object, b/c there are no other // "classic" refs on the component (or they haven't been processed yet) // TODO: Remove the need for this in next breaking release if (_Object$isFrozen(this.refs) && !_Object$keys(this.refs).length) { this.refs = {}; } this.refs.dialog = ref; //maintains backwards compat with older component breakdown if (!this.props.backdrop) { this.refs.modal = ref; } }, componentWillReceiveProps: function componentWillReceiveProps(nextProps) { if (nextProps.show) { this.setState({ exited: false }); } else if (!nextProps.animation) { // Otherwise let handleHidden take care of marking exited. this.setState({ exited: true }); } }, componentWillUpdate: function componentWillUpdate(nextProps) { if (nextProps.show) { this.checkForFocus(); } }, componentDidMount: function componentDidMount() { if (this.props.show) { this.onShow(); } }, componentDidUpdate: function componentDidUpdate(prevProps) { var animation = this.props.animation; if (prevProps.show && !this.props.show && !animation) { //otherwise handleHidden will call this. this.onHide(); } else if (!prevProps.show && this.props.show) { this.onShow(); } }, componentWillUnmount: function componentWillUnmount() { if (this.props.show) { this.onHide(); } }, onShow: function onShow() { var _this2 = this; var doc = _utilsDomUtils2['default'].ownerDocument(this); var win = _utilsDomUtils2['default'].ownerWindow(this); this._onDocumentKeyupListener = _utilsEventListener2['default'].listen(doc, 'keyup', this.handleDocumentKeyUp); this._onWindowResizeListener = _utilsEventListener2['default'].listen(win, 'resize', this.handleWindowResize); if (this.props.enforceFocus) { this._onFocusinListener = onFocus(this, this.enforceFocus); } var container = getContainer(this); container.className += container.className.length ? ' modal-open' : 'modal-open'; this._containerIsOverflowing = container.scrollHeight > containerClientHeight(container, this); this._originalPadding = container.style.paddingRight; if (this._containerIsOverflowing) { container.style.paddingRight = parseInt(this._originalPadding || 0, 10) + getScrollbarSize() + 'px'; } if (this.props.backdrop) { this.iosClickHack(); } this.setState(this._getStyles(), //eslint-disable-line react/no-did-mount-set-state function () { return _this2.focusModalContent(); }); }, onHide: function onHide() { this._onDocumentKeyupListener.remove(); this._onWindowResizeListener.remove(); if (this._onFocusinListener) { this._onFocusinListener.remove(); } var container = getContainer(this); container.style.paddingRight = this._originalPadding; container.className = container.className.replace(/ ?modal-open/, ''); this.restoreLastFocus(); }, handleHidden: function handleHidden() { this.setState({ exited: true }); this.onHide(); if (this.props.onExited) { var _props3; (_props3 = this.props).onExited.apply(_props3, arguments); } }, handleBackdropClick: function handleBackdropClick(e) { if (e.target !== e.currentTarget) { return; } this.props.onHide(); }, handleDocumentKeyUp: function handleDocumentKeyUp(e) { if (this.props.keyboard && e.keyCode === 27) { this.props.onHide(); } }, handleWindowResize: function handleWindowResize() { this.setState(this._getStyles()); }, checkForFocus: function checkForFocus() { if (_utilsDomUtils2['default'].canUseDom) { try { this.lastFocus = document.activeElement; } catch (e) {} // eslint-disable-line no-empty } }, focusModalContent: function focusModalContent() { var modalContent = _react2['default'].findDOMNode(this.refs.dialog); var current = _utilsDomUtils2['default'].activeElement(this); var focusInModal = current && _utilsDomUtils2['default'].contains(modalContent, current); if (modalContent && this.props.autoFocus && !focusInModal) { this.lastFocus = current; modalContent.focus(); } }, restoreLastFocus: function restoreLastFocus() { if (this.lastFocus && this.lastFocus.focus) { this.lastFocus.focus(); this.lastFocus = null; } }, enforceFocus: function enforceFocus() { if (!this.isMounted()) { return; } var active = _utilsDomUtils2['default'].activeElement(this); var modal = _react2['default'].findDOMNode(this.refs.dialog); if (modal && modal !== active && !_utilsDomUtils2['default'].contains(modal, active)) { modal.focus(); } }, iosClickHack: function iosClickHack() { // IOS only allows click events to be delegated to the document on elements // it considers 'clickable' - anchors, buttons, etc. We fake a click handler on the // DOM nodes themselves. Remove if handled by React: https://github.com/facebook/react/issues/1169 _react2['default'].findDOMNode(this.refs.modal).onclick = function () {}; _react2['default'].findDOMNode(this.refs.backdrop).onclick = function () {}; }, _getStyles: function _getStyles() { if (!_utilsDomUtils2['default'].canUseDom) { return {}; } var node = _react2['default'].findDOMNode(this.refs.modal); var scrollHt = node.scrollHeight; var container = getContainer(this); var containerIsOverflowing = this._containerIsOverflowing; var modalIsOverflowing = scrollHt > containerClientHeight(container, this); return { dialogStyles: { paddingRight: containerIsOverflowing && !modalIsOverflowing ? getScrollbarSize() : void 0, paddingLeft: !containerIsOverflowing && modalIsOverflowing ? getScrollbarSize() : void 0 } }; } }); Modal.Body = _ModalBody2['default']; Modal.Header = _ModalHeader2['default']; Modal.Title = _ModalTitle2['default']; Modal.Footer = _ModalFooter2['default']; Modal.Dialog = _ModalDialog2['default']; Modal.TRANSITION_DURATION = 300; Modal.BACKDROP_TRANSITION_DURATION = 150; exports['default'] = Modal; module.exports = exports['default'];