UNPKG

@blueprintjs/core

Version:

Core styles & components

201 lines 10.5 kB
"use strict"; /* * Copyright 2016 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Handle = void 0; const tslib_1 = require("tslib"); const classnames_1 = tslib_1.__importDefault(require("classnames")); const React = tslib_1.__importStar(require("react")); const common_1 = require("../../common"); const props_1 = require("../../common/props"); const sliderUtils_1 = require("./sliderUtils"); // props that require number values, for validation const NUMBER_PROPS = ["max", "min", "stepSize", "tickSize", "value"]; /** Internal component for a Handle with click/drag/keyboard logic to determine a new value. */ class Handle extends common_1.AbstractPureComponent { constructor() { super(...arguments); this.state = { isMoving: false, }; this.handleElement = null; this.refHandlers = { handle: (el) => (this.handleElement = el), }; this.beginHandleMovement = (event) => { document.addEventListener("mousemove", this.handleHandleMovement); document.addEventListener("mouseup", this.endHandleMovement); this.setState({ isMoving: true }); this.changeValue(this.clientToValue(this.mouseEventClientOffset(event))); }; this.beginHandleTouchMovement = (event) => { document.addEventListener("touchmove", this.handleHandleTouchMovement); document.addEventListener("touchend", this.endHandleTouchMovement); document.addEventListener("touchcancel", this.endHandleTouchMovement); this.setState({ isMoving: true }); this.changeValue(this.clientToValue(this.touchEventClientOffset(event))); }; this.getStyleProperties = () => { if (this.handleElement == null) { return {}; } // The handle midpoint of RangeSlider is actually shifted by a margin to // be on the edge of the visible handle element. Because the midpoint // calculation does not take this margin into account, we instead // measure the long side (which is equal to the short side plus the // margin). const { min = 0, tickSizeRatio, value, vertical } = this.props; const { handleMidpoint } = this.getHandleMidpointAndOffset(this.handleElement, true); const offsetRatio = (value - min) * tickSizeRatio; const offsetCalc = `calc(${(0, sliderUtils_1.formatPercentage)(offsetRatio)} - ${handleMidpoint}px)`; return vertical ? { bottom: offsetCalc } : { left: offsetCalc }; }; this.endHandleMovement = (event) => { this.handleMoveEndedAt(this.mouseEventClientOffset(event)); }; this.endHandleTouchMovement = (event) => { this.handleMoveEndedAt(this.touchEventClientOffset(event)); }; this.handleMoveEndedAt = (clientPixel) => { var _a, _b; this.removeDocumentEventListeners(); this.setState({ isMoving: false }); // always invoke onRelease; changeValue may call onChange if value is different const finalValue = this.changeValue(this.clientToValue(clientPixel)); (_b = (_a = this.props).onRelease) === null || _b === void 0 ? void 0 : _b.call(_a, finalValue); }; this.handleHandleMovement = (event) => { this.handleMovedTo(this.mouseEventClientOffset(event)); }; this.handleHandleTouchMovement = (event) => { this.handleMovedTo(this.touchEventClientOffset(event)); }; this.handleMovedTo = (clientPixel) => { if (this.state.isMoving && !this.props.disabled) { this.changeValue(this.clientToValue(clientPixel)); } }; this.handleKeyDown = (event) => { const { stepSize, value } = this.props; const direction = common_1.Utils.getArrowKeyDirection(event, ["ArrowLeft", "ArrowDown"], ["ArrowRight", "ArrowUp"]); if (direction !== undefined) { this.changeValue(value + stepSize * direction); // this key event has been handled! prevent browser scroll on up/down event.preventDefault(); } }; this.handleKeyUp = (event) => { var _a, _b; if (common_1.Utils.isArrowKey(event)) { (_b = (_a = this.props).onRelease) === null || _b === void 0 ? void 0 : _b.call(_a, this.props.value); } }; } componentDidMount() { // The first time this component renders, it has no ref to the handle and thus incorrectly centers the handle. // Therefore, on the first mount, force a re-render to center the handle with the ref'd component. this.forceUpdate(); } render() { const { className, disabled, label, min, max, value, vertical, htmlProps } = this.props; const { isMoving } = this.state; return (React.createElement("span", { role: "slider", tabIndex: 0, ...htmlProps, className: (0, classnames_1.default)(common_1.Classes.SLIDER_HANDLE, { [common_1.Classes.ACTIVE]: isMoving }, className), onKeyDown: disabled ? undefined : this.handleKeyDown, onKeyUp: disabled ? undefined : this.handleKeyUp, onMouseDown: disabled ? undefined : this.beginHandleMovement, onTouchStart: disabled ? undefined : this.beginHandleTouchMovement, ref: this.refHandlers.handle, style: this.getStyleProperties(), "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": value, "aria-disabled": disabled, "aria-orientation": vertical ? "vertical" : "horizontal" }, label == null ? null : React.createElement("span", { className: common_1.Classes.SLIDER_LABEL }, label))); } componentWillUnmount() { this.removeDocumentEventListeners(); } /** Convert client pixel to value between min and max. */ clientToValue(clientPixel) { const { stepSize, tickSize, value, vertical } = this.props; if (this.handleElement == null) { return value; } // #1769: this logic doesn't work perfectly when the tick size is // smaller than the handle size; it may be off by a tick or two. const clientPixelNormalized = vertical ? window.innerHeight - clientPixel : clientPixel; const handleCenterPixel = this.getHandleElementCenterPixel(this.handleElement); const pixelDelta = clientPixelNormalized - handleCenterPixel; if (isNaN(pixelDelta)) { return value; } // convert pixels to range value in increments of `stepSize` return value + Math.round(pixelDelta / (tickSize * stepSize)) * stepSize; } mouseEventClientOffset(event) { return this.props.vertical ? event.clientY : event.clientX; } touchEventClientOffset(event) { const touch = event.changedTouches[0]; return this.props.vertical ? touch.clientY : touch.clientX; } validateProps(props) { for (const prop of NUMBER_PROPS) { if (typeof props[prop] !== "number") { throw new Error(`[Blueprint] <Handle> requires number value for ${prop} prop`); } } } /** Clamp value and invoke callback if it differs from current value */ changeValue(newValue, callback = this.props.onChange) { newValue = this.clamp(newValue); if (!isNaN(newValue) && this.props.value !== newValue) { callback === null || callback === void 0 ? void 0 : callback(newValue); } return newValue; } /** Clamp value between min and max props */ clamp(value) { return common_1.Utils.clamp(value, this.props.min, this.props.max); } getHandleElementCenterPixel(handleElement) { const { handleMidpoint, handleOffset } = this.getHandleMidpointAndOffset(handleElement); return handleOffset + handleMidpoint; } getHandleMidpointAndOffset(handleElement, useOppositeDimension = false) { if (handleElement == null) { return { handleMidpoint: 0, handleOffset: 0 }; } const { vertical } = this.props; // N.B. element.clientHeight does not include border size. // Also, element.getBoundingClientRect() is useful to get the top & left position on the page, but // it fails to accurately measure element width & height inside absolutely-positioned and CSS-transformed // containers like Popovers, so we use element.offsetWidth & offsetHeight instead (see https://github.com/palantir/blueprint/issues/4417). const handleRect = handleElement.getBoundingClientRect(); handleRect.width = handleElement.offsetWidth; handleRect.height = handleElement.offsetHeight; const sizeKey = vertical ? useOppositeDimension ? "width" : "height" : useOppositeDimension ? "height" : "width"; // "bottom" value seems to be consistently incorrect, so explicitly // calculate it using the window offset instead. const handleOffset = vertical ? window.innerHeight - (handleRect.top + handleRect[sizeKey]) : handleRect.left; return { handleMidpoint: handleRect[sizeKey] / 2, handleOffset }; } removeDocumentEventListeners() { document.removeEventListener("mousemove", this.handleHandleMovement); document.removeEventListener("mouseup", this.endHandleMovement); document.removeEventListener("touchmove", this.handleHandleTouchMovement); document.removeEventListener("touchend", this.endHandleTouchMovement); document.removeEventListener("touchcancel", this.endHandleTouchMovement); } } exports.Handle = Handle; Handle.displayName = `${props_1.DISPLAYNAME_PREFIX}.SliderHandle`; //# sourceMappingURL=handle.js.map