UNPKG

react-switch

Version:

Draggable toggle-switch component for react

566 lines (499 loc) 19.4 kB
import React, { Component } from 'react'; import PropTypes from 'prop-types'; function _extends() { _extends = Object.assign ? Object.assign.bind() : 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; }; return _extends.apply(this, arguments); } /* The MIT License (MIT) Copyright (c) 2015 instructure-react Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ var uncheckedIcon = React.createElement('svg', { viewBox: "-2 -5 14 20", height: "100%", width: "100%", style: { position: "absolute", top: 0 } }, React.createElement('path', { d: "M9.9 2.12L7.78 0 4.95 2.828 2.12 0 0 2.12l2.83 2.83L0 7.776 2.123 9.9 4.95 7.07 7.78 9.9 9.9 7.776 7.072 4.95 9.9 2.12", fill: "#fff", fillRule: "evenodd" })); var checkedIcon = React.createElement('svg', { height: "100%", width: "100%", viewBox: "-2 -5 17 21", style: { position: "absolute", top: 0 } }, React.createElement('path', { d: "M11.264 0L5.26 6.004 2.103 2.847 0 4.95l5.26 5.26 8.108-8.107L11.264 0", fill: "#fff", fillRule: "evenodd" })); function createBackgroundColor(pos, checkedPos, uncheckedPos, offColor, onColor) { var relativePos = (pos - uncheckedPos) / (checkedPos - uncheckedPos); if (relativePos === 0) { return offColor; } if (relativePos === 1) { return onColor; } var newColor = "#"; for (var i = 1; i < 6; i += 2) { var offComponent = parseInt(offColor.substr(i, 2), 16); var onComponent = parseInt(onColor.substr(i, 2), 16); var weightedValue = Math.round((1 - relativePos) * offComponent + relativePos * onComponent); var newComponent = weightedValue.toString(16); if (newComponent.length === 1) { newComponent = "0" + newComponent; } newColor += newComponent; } return newColor; } function convertShorthandColor(color) { if (color.length === 7) { return color; } var sixDigitColor = "#"; for (var i = 1; i < 4; i += 1) { sixDigitColor += color[i] + color[i]; } return sixDigitColor; } function getBackgroundColor(pos, checkedPos, uncheckedPos, offColor, onColor) { var sixDigitOffColor = convertShorthandColor(offColor); var sixDigitOnColor = convertShorthandColor(onColor); return createBackgroundColor(pos, checkedPos, uncheckedPos, sixDigitOffColor, sixDigitOnColor); } // Make sure color props are strings that start with "#" since other ways to write colors are not supported. var hexColorPropType = function (props, propName, componentName) { var prop = props[propName]; if (typeof prop !== "string" || prop[0] !== "#" || prop.length !== 4 && prop.length !== 7) { return new Error("Invalid prop '" + propName + "' supplied to '" + componentName + "'. '" + propName + "' has to be either a 3-digit or 6-digit hex-color string. Valid examples: '#abc', '#123456'"); } return null; }; function objectWithoutProperties(obj, exclude) { var target = {}; for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k) && exclude.indexOf(k) === -1) target[k] = obj[k]; return target; } var ReactSwitch = /*@__PURE__*/function (Component) { function ReactSwitch(props) { Component.call(this, props); var height = props.height; var width = props.width; var handleDiameter = props.handleDiameter; var checked = props.checked; this.$handleDiameter = handleDiameter || height - 2; this.$checkedPos = Math.max(width - height, width - (height + this.$handleDiameter) / 2); this.$uncheckedPos = Math.max(0, (height - this.$handleDiameter) / 2); this.state = { $pos: checked ? this.$checkedPos : this.$uncheckedPos }; this.$lastDragAt = 0; this.$lastKeyUpAt = 0; this.$onMouseDown = this.$onMouseDown.bind(this); this.$onMouseMove = this.$onMouseMove.bind(this); this.$onMouseUp = this.$onMouseUp.bind(this); this.$onTouchStart = this.$onTouchStart.bind(this); this.$onTouchMove = this.$onTouchMove.bind(this); this.$onTouchEnd = this.$onTouchEnd.bind(this); this.$onClick = this.$onClick.bind(this); this.$onInputChange = this.$onInputChange.bind(this); this.$onKeyUp = this.$onKeyUp.bind(this); this.$setHasOutline = this.$setHasOutline.bind(this); this.$unsetHasOutline = this.$unsetHasOutline.bind(this); this.$getInputRef = this.$getInputRef.bind(this); } if (Component) ReactSwitch.__proto__ = Component; ReactSwitch.prototype = Object.create(Component && Component.prototype); ReactSwitch.prototype.constructor = ReactSwitch; ReactSwitch.prototype.componentDidMount = function componentDidMount() { this.$isMounted = true; }; ReactSwitch.prototype.componentDidUpdate = function componentDidUpdate(prevProps) { if (prevProps.checked === this.props.checked) { return; } var $pos = this.props.checked ? this.$checkedPos : this.$uncheckedPos; this.setState({ $pos: $pos }); }; ReactSwitch.prototype.componentWillUnmount = function componentWillUnmount() { this.$isMounted = false; }; ReactSwitch.prototype.$onDragStart = function $onDragStart(clientX) { this.$inputRef.focus(); this.setState({ $startX: clientX, $hasOutline: true, $dragStartingTime: Date.now() }); }; ReactSwitch.prototype.$onDrag = function $onDrag(clientX) { var ref = this.state; var $startX = ref.$startX; var $isDragging = ref.$isDragging; var $pos = ref.$pos; var ref$1 = this.props; var checked = ref$1.checked; var startPos = checked ? this.$checkedPos : this.$uncheckedPos; var mousePos = startPos + clientX - $startX; // We need this check to fix a windows glitch where onDrag is triggered onMouseDown in some cases if (!$isDragging && clientX !== $startX) { this.setState({ $isDragging: true }); } var newPos = Math.min(this.$checkedPos, Math.max(this.$uncheckedPos, mousePos)); // Prevent unnecessary rerenders if (newPos !== $pos) { this.setState({ $pos: newPos }); } }; ReactSwitch.prototype.$onDragStop = function $onDragStop(event) { var ref = this.state; var $pos = ref.$pos; var $isDragging = ref.$isDragging; var $dragStartingTime = ref.$dragStartingTime; var ref$1 = this.props; var checked = ref$1.checked; var halfwayCheckpoint = (this.$checkedPos + this.$uncheckedPos) / 2; /* Set position state back to the previous position even if user drags the switch with intention to change the state. This is to prevent the switch from getting stuck in the middle if the event isn't handled in the onChange callback. */ var prevPos = this.props.checked ? this.$checkedPos : this.$uncheckedPos; this.setState({ $pos: prevPos }); // Act as if the user clicked the handle if they didn't drag it _or_ the dragged it for less than 250ms var timeSinceStart = Date.now() - $dragStartingTime; var isSimulatedClick = !$isDragging || timeSinceStart < 250; // Handle when the user has dragged the switch more than halfway from either side var isDraggedHalfway = checked && $pos <= halfwayCheckpoint || !checked && $pos >= halfwayCheckpoint; if (isSimulatedClick || isDraggedHalfway) { this.$onChange(event); } if (this.$isMounted) { this.setState({ $isDragging: false, $hasOutline: false }); } this.$lastDragAt = Date.now(); }; ReactSwitch.prototype.$onMouseDown = function $onMouseDown(event) { event.preventDefault(); // Ignore right click and scroll if (typeof event.button === "number" && event.button !== 0) { return; } this.$onDragStart(event.clientX); window.addEventListener("mousemove", this.$onMouseMove); window.addEventListener("mouseup", this.$onMouseUp); }; ReactSwitch.prototype.$onMouseMove = function $onMouseMove(event) { event.preventDefault(); this.$onDrag(event.clientX); }; ReactSwitch.prototype.$onMouseUp = function $onMouseUp(event) { this.$onDragStop(event); window.removeEventListener("mousemove", this.$onMouseMove); window.removeEventListener("mouseup", this.$onMouseUp); }; ReactSwitch.prototype.$onTouchStart = function $onTouchStart(event) { this.$checkedStateFromDragging = null; this.$onDragStart(event.touches[0].clientX); }; ReactSwitch.prototype.$onTouchMove = function $onTouchMove(event) { this.$onDrag(event.touches[0].clientX); }; ReactSwitch.prototype.$onTouchEnd = function $onTouchEnd(event) { event.preventDefault(); this.$onDragStop(event); }; ReactSwitch.prototype.$onInputChange = function $onInputChange(event) { // This condition is unfortunately needed in some browsers where the input's change event might get triggered // right after the dragstop event is triggered (occurs when dropping over a label element) if (Date.now() - this.$lastDragAt > 50) { this.$onChange(event); // Prevent clicking label, but not key activation from setting outline to true - yes, this is absurd if (Date.now() - this.$lastKeyUpAt > 50) { if (this.$isMounted) { this.setState({ $hasOutline: false }); } } } }; ReactSwitch.prototype.$onKeyUp = function $onKeyUp() { this.$lastKeyUpAt = Date.now(); }; ReactSwitch.prototype.$setHasOutline = function $setHasOutline() { this.setState({ $hasOutline: true }); }; ReactSwitch.prototype.$unsetHasOutline = function $unsetHasOutline() { this.setState({ $hasOutline: false }); }; ReactSwitch.prototype.$getInputRef = function $getInputRef(el) { this.$inputRef = el; }; ReactSwitch.prototype.$onClick = function $onClick(event) { event.preventDefault(); this.$inputRef.focus(); this.$onChange(event); if (this.$isMounted) { this.setState({ $hasOutline: false }); } }; ReactSwitch.prototype.$onChange = function $onChange(event) { var ref = this.props; var checked = ref.checked; var onChange = ref.onChange; var id = ref.id; onChange(!checked, event, id); }; ReactSwitch.prototype.render = function render() { var ref = this.props; var checked = ref.checked; var disabled = ref.disabled; var className = ref.className; var offColor = ref.offColor; var onColor = ref.onColor; var offHandleColor = ref.offHandleColor; var onHandleColor = ref.onHandleColor; var checkedIcon = ref.checkedIcon; var uncheckedIcon = ref.uncheckedIcon; var checkedHandleIcon = ref.checkedHandleIcon; var uncheckedHandleIcon = ref.uncheckedHandleIcon; var boxShadow = ref.boxShadow; var activeBoxShadow = ref.activeBoxShadow; var height = ref.height; var width = ref.width; var borderRadius = ref.borderRadius; ref.handleDiameter; var rest$1 = objectWithoutProperties(ref, ["checked", "disabled", "className", "offColor", "onColor", "offHandleColor", "onHandleColor", "checkedIcon", "uncheckedIcon", "checkedHandleIcon", "uncheckedHandleIcon", "boxShadow", "activeBoxShadow", "height", "width", "borderRadius", "handleDiameter"]); var rest = rest$1; var ref$1 = this.state; var $pos = ref$1.$pos; var $isDragging = ref$1.$isDragging; var $hasOutline = ref$1.$hasOutline; var rootStyle = { position: "relative", display: "inline-block", textAlign: "left", opacity: disabled ? 0.5 : 1, direction: "ltr", borderRadius: height / 2, WebkitTransition: "opacity 0.25s", MozTransition: "opacity 0.25s", transition: "opacity 0.25s", touchAction: "none", WebkitTapHighlightColor: "rgba(0, 0, 0, 0)", WebkitUserSelect: "none", MozUserSelect: "none", msUserSelect: "none", userSelect: "none" }; var backgroundStyle = { height: height, width: width, margin: Math.max(0, (this.$handleDiameter - height) / 2), position: "relative", background: getBackgroundColor($pos, this.$checkedPos, this.$uncheckedPos, offColor, onColor), borderRadius: typeof borderRadius === "number" ? borderRadius : height / 2, cursor: disabled ? "default" : "pointer", WebkitTransition: $isDragging ? null : "background 0.25s", MozTransition: $isDragging ? null : "background 0.25s", transition: $isDragging ? null : "background 0.25s" }; var checkedIconStyle = { height: height, width: Math.min(height * 1.5, width - (this.$handleDiameter + height) / 2 + 1), position: "relative", opacity: ($pos - this.$uncheckedPos) / (this.$checkedPos - this.$uncheckedPos), pointerEvents: "none", WebkitTransition: $isDragging ? null : "opacity 0.25s", MozTransition: $isDragging ? null : "opacity 0.25s", transition: $isDragging ? null : "opacity 0.25s" }; var uncheckedIconStyle = { height: height, width: Math.min(height * 1.5, width - (this.$handleDiameter + height) / 2 + 1), position: "absolute", opacity: 1 - ($pos - this.$uncheckedPos) / (this.$checkedPos - this.$uncheckedPos), right: 0, top: 0, pointerEvents: "none", WebkitTransition: $isDragging ? null : "opacity 0.25s", MozTransition: $isDragging ? null : "opacity 0.25s", transition: $isDragging ? null : "opacity 0.25s" }; var handleStyle = { height: this.$handleDiameter, width: this.$handleDiameter, background: getBackgroundColor($pos, this.$checkedPos, this.$uncheckedPos, offHandleColor, onHandleColor), display: "inline-block", cursor: disabled ? "default" : "pointer", borderRadius: typeof borderRadius === "number" ? borderRadius - 1 : "50%", position: "absolute", transform: "translateX(" + $pos + "px)", top: Math.max(0, (height - this.$handleDiameter) / 2), outline: 0, boxShadow: $hasOutline ? activeBoxShadow : boxShadow, border: 0, WebkitTransition: $isDragging ? null : "background-color 0.25s, transform 0.25s, box-shadow 0.15s", MozTransition: $isDragging ? null : "background-color 0.25s, transform 0.25s, box-shadow 0.15s", transition: $isDragging ? null : "background-color 0.25s, transform 0.25s, box-shadow 0.15s" }; var uncheckedHandleIconStyle = { height: this.$handleDiameter, width: this.$handleDiameter, opacity: Math.max((1 - ($pos - this.$uncheckedPos) / (this.$checkedPos - this.$uncheckedPos) - 0.5) * 2, 0), position: "absolute", left: 0, top: 0, pointerEvents: "none", WebkitTransition: $isDragging ? null : "opacity 0.25s", MozTransition: $isDragging ? null : "opacity 0.25s", transition: $isDragging ? null : "opacity 0.25s" }; var checkedHandleIconStyle = { height: this.$handleDiameter, width: this.$handleDiameter, opacity: Math.max((($pos - this.$uncheckedPos) / (this.$checkedPos - this.$uncheckedPos) - 0.5) * 2, 0), position: "absolute", left: 0, top: 0, pointerEvents: "none", WebkitTransition: $isDragging ? null : "opacity 0.25s", MozTransition: $isDragging ? null : "opacity 0.25s", transition: $isDragging ? null : "opacity 0.25s" }; var inputStyle = { border: 0, clip: "rect(0 0 0 0)", height: 1, margin: -1, overflow: "hidden", padding: 0, position: "absolute", width: 1 }; return React.createElement('div', { className: className, style: rootStyle }, React.createElement('div', { className: "react-switch-bg", style: backgroundStyle, onClick: disabled ? null : this.$onClick, onMouseDown: function (e) { return e.preventDefault(); } }, checkedIcon && React.createElement('div', { style: checkedIconStyle }, checkedIcon), uncheckedIcon && React.createElement('div', { style: uncheckedIconStyle }, uncheckedIcon)), React.createElement('div', { className: "react-switch-handle", style: handleStyle, onClick: function (e) { return e.preventDefault(); }, onMouseDown: disabled ? null : this.$onMouseDown, onTouchStart: disabled ? null : this.$onTouchStart, onTouchMove: disabled ? null : this.$onTouchMove, onTouchEnd: disabled ? null : this.$onTouchEnd, onTouchCancel: disabled ? null : this.$unsetHasOutline }, uncheckedHandleIcon && React.createElement('div', { style: uncheckedHandleIconStyle }, uncheckedHandleIcon), checkedHandleIcon && React.createElement('div', { style: checkedHandleIconStyle }, checkedHandleIcon)), React.createElement('input', _extends({}, { type: "checkbox", role: "switch", 'aria-checked': checked, checked: checked, disabled: disabled, style: inputStyle }, rest, { ref: this.$getInputRef, onFocus: this.$setHasOutline, onBlur: this.$unsetHasOutline, onKeyUp: this.$onKeyUp, onChange: this.$onInputChange }))); }; return ReactSwitch; }(Component); ReactSwitch.propTypes = { checked: PropTypes.bool.isRequired, onChange: PropTypes.func.isRequired, disabled: PropTypes.bool, offColor: hexColorPropType, onColor: hexColorPropType, offHandleColor: hexColorPropType, onHandleColor: hexColorPropType, handleDiameter: PropTypes.number, uncheckedIcon: PropTypes.oneOfType([PropTypes.bool, PropTypes.element]), checkedIcon: PropTypes.oneOfType([PropTypes.bool, PropTypes.element]), boxShadow: PropTypes.string, borderRadius: PropTypes.number, activeBoxShadow: PropTypes.string, uncheckedHandleIcon: PropTypes.element, checkedHandleIcon: PropTypes.element, height: PropTypes.number, width: PropTypes.number, id: PropTypes.string, className: PropTypes.string }; ReactSwitch.defaultProps = { disabled: false, offColor: "#888", onColor: "#080", offHandleColor: "#fff", onHandleColor: "#fff", uncheckedIcon: uncheckedIcon, checkedIcon: checkedIcon, boxShadow: null, activeBoxShadow: "0 0 2px 3px #3bf", height: 28, width: 56 }; export { ReactSwitch as default };