React——标尺组件封装

693 阅读2分钟
Util.js

export const Util = {
    //去除首尾空格
    //主要判断 null undefined 返回空字符串
    trim: function (str) {
        if (str == null) {
            return '';
        }

        return (str + '').trim();
    },

    // CSS 前缀
    vendorPropName: (function () {
        var cssPrefixes = ['Webkit', 'Moz', 'O', 'ms'],
            emptyStyle = document.createElement('div').style;

        return function (name) {
            if (name in emptyStyle) {
                return name;
            }

            var capName = name[0].toUpperCase() + name.slice(1),
                length = cssPrefixes.length;

            for (var i = 0; i < length; i++) {
                name = cssPrefixes[i] + capName;
                if (name in emptyStyle) {
                    return name;
                }
            }
        };
    })(),

    // 获取样式
    // isNumber 用于是否返回数字
    getStyle: function (elem, attr, isNumber) {
        if (!elem) {
            return;
        }
        let style;

        if (getComputedStyle) {
            // 标准
            style = getComputedStyle(elem)[attr];
        } else if (document.documentElement.currentStyle) {
            // IE
            style = elem.currentStyle[attr];
        }
        elem = null;

        if (isNumber) {
            return parseFloat(style);
        }

        return style;
    },

    //小数乘法
    //bit = Math.log10(乘以10)
    multip: function (n, bit = 1) {
        if (!this.trim(n) || Number.isNaN(+n)) {
            return '0';
        }
        n += '';
        let nArr = n.split('.'),
            digitN = (n.split('.')[1] || '').length,
            res = '';

        if (digitN > bit) {
            res = nArr[0] + nArr[1].slice(0, bit) + '.' + nArr[1].slice(bit);
        } else {
            res = nArr[0] + (nArr[1] || '') + new Array(bit - digitN).fill(0).join('');
        }

        return res.replace(/^0+(?!\.)/, '');
    },
};
Ruler.js

import './style.less';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Util } from '../../libs/utils';


export default class Ruler extends PureComponent {
    constructor(props) {
        super(props);

        this.state = {
            value: props.value || '',
            liStyle: null,
            rulerMax: 0,
            rulerMin: 0,
            rulerStep: 0,
        };

        this.isFirstClick = true;
        this.lastPosition = 0;
        this.eventLastPosition = 0;
        this.transform = Util.vendorPropName('transform');
    }

    transformOffsetToValue = (offset) => {
        return (((this.rulerMiddle - offset) / this.rulerStepWidth) * this.state.rulerStep + this.state.rulerMin).toFixed(this.valueDecimalDigit);
    };

    transformValueToOffset = (value) => {
        return this.rulerMiddle - ((value - this.state.rulerMin) / this.state.rulerStep) * this.rulerStepWidth;
    };

    move = (offset = 0, setValue = true, triggerChange = true) => {
        if (offset > this.minMove || offset < this.maxMove) {
            return;
        }
        this.lastPosition = offset;
        let state = {
            style: {
                [this.transform]: `translateX(${offset}px)`,
            },
        };

        if (!this.focusing && setValue) {
            state.value = this.transformOffsetToValue(offset);
        }

        this.setState(state, () => {
            const { onChange } = this.props;

            if (triggerChange && typeof onChange === 'function') {
                onChange(this.state.value);
            }
        });
    };

    touchStart = (e) => {
        if (this.noNeedMove) {
            return;
        }
        this.focusing = false;
        e.stopPropagation();
        this.endX = this.startX = e.targetTouches[0].pageX;
        if (this.startX <= 0) {
            return;
        }
        this.eventMove = true;
        this.endY = this.startY = e.targetTouches[0].pageY;
        this.eventLastPosition = this.lastPosition;
    };

    touchMove = (e) => {
        if (!this.eventMove) {
            return;
        }
        this.endX = e.targetTouches[0].pageX;
        this.endY = e.targetTouches[0].pageY;
        if (this.touchEndTimer) {
            clearTimeout(this.touchEndTimer);
        }
        this.touchEndTimer = setTimeout(this.touchEnd, 1000);

        if (Math.abs(this.endX - this.startX) - Math.abs(this.endY - this.startY) > 0) {
            this.move(this.eventLastPosition + this.endX - this.startX);
        }
    };

    touchEnd = () => {
        if (this.state.value >= '0') {
            this.move(this.transformValueToOffset(this.state.value));
        }

        if (!this.eventMove) {
            return;
        }
        if (this.touchEndTimer) {
            clearTimeout(this.touchEndTimer);
        }
        if (this.changeTimer) {
            clearTimeout(this.changeTimer);
        }
        this.eventMove = false;
    };

    componentDidMount() {
        const { view, max, min, step, defaultValue, value, digits } = this.props;

        let width = Util.getStyle(this.elem, 'width', true),
            liWidth = Math.round(width / view / 10) * 10,
            rulerStep = Util.multip(step),
            rulerMax = Math.ceil(max / rulerStep) * rulerStep,
            rulerMin = Math.floor(min / rulerStep) * rulerStep;

        this.rulerMiddle = width / 2;
        this.rulerStepWidth = liWidth;
        // this.valueDecimalDigit = ((step + '').split('.')[1] || '').length + 1;
        this.valueDecimalDigit = digits;
        this.minMove = this.rulerMiddle - ((min - rulerMin) / rulerStep) * liWidth;
        this.maxMove = this.rulerMiddle - ((rulerMax - rulerMin) / rulerStep) * liWidth;
        this.noNeedMove = (rulerMax - rulerMin) / rulerStep <= view;
        let firstValue = +(value || defaultValue) || 0;

        if (firstValue < min) {
            firstValue = min;
        } else if (firstValue > max) {
            firstValue = max;
        }

        this.setState(
            {
                rulerMax,
                rulerMin,
                rulerStep,
                liStyle: {
                    minWidth: liWidth + 'px',
                    maxWidth: liWidth + 'px',
                },
            },
            () => {
                this.move(this.transformValueToOffset(firstValue), !!value, false);
            }
        );
    }

    componentDidUpdate(prevProps) {
        const { defaultValue, value } = this.props;
        if (!this.state.value && prevProps.defaultValue !== defaultValue) {
            this.move(this.transformValueToOffset(defaultValue), false, false);
        } else if (value !== prevProps.value) {
            this.move(this.transformValueToOffset(value), true, false);
        }
    }

    componentWillUnmount() {
        if (this.touchEndTimer) {
            clearTimeout(this.touchEndTimer);
        }
        if (this.inputChangeTimer) {
            clearTimeout(this.inputChangeTimer);
        }
    }

    render() {
        const { className, unit, id, unitName, min, disabled, digits, defaultValue } = this.props;
        const { value, style, liStyle, rulerMax, rulerMin, rulerStep } = this.state;

        return (
            <div className={`component-ruler ${className}`} data-ubtid="sect-ruler" id={id}>
                <header>
                    <div className="input-wrap empty">
                        {unitName} <span>{value || defaultValue}</span> {unit}
                    </div>
                </header>

                <section
                    ref={(el) => {
                        this.elem = el;
                    }}
                    onTouchStart={(!disabled && this.touchStart) || null}
                    onTouchMove={(!disabled && this.touchMove) || null}
                    onTouchEnd={(!disabled && this.touchEnd) || null}
                    onTouchCancel={(!disabled && this.touchEnd) || null}
                >
                    <ul className="ruler" style={style}>
                        {new Array((rulerMax - rulerMin) / rulerStep || 0).fill(0).map((item, index, arr) => {
                            let isLast = index === arr.length - 1;

                            return (
                                <li style={liStyle} key={index}>
                                    {isLast && <i></i>}
                                </li>
                            );
                        })}
                    </ul>
                    <ul className="mark" style={style}>
                        {new Array((rulerMax - rulerMin) / rulerStep || 0).fill(0).map((item, index, arr) => {
                            let isLast = index === arr.length - 1;

                            return (
                                <li style={liStyle} key={index}>
                                    <span>{(min + index * rulerStep).toFixed(Math.max(digits - 1, 0))}</span>
                                    {isLast && <span className="last">{(rulerMin + (index + 1) * rulerStep).toFixed(Math.max(digits - 1, 0))}</span>}
                                </li>
                            );
                        })}
                    </ul>
                </section>
            </div>
        );
    }
}
Ruler.defaultProps = {
    disabled: false,
    className: '',
    min: 1,
    max: 35,
    step: 0.1,
    digits: 1,
    low: 4.4,
    high: 10,
    view: 3,
    unitName: '',
    unit: 'ml',
    defaultValue: 6.5,
};
Ruler.propTypes = {
        id: PropTypes.number,
        disabled: PropTypes.bool,
        className: PropTypes.string,
        placeholder: PropTypes.string,
        min: PropTypes.number,
        max: PropTypes.number,
        step: PropTypes.number,
        digits: PropTypes.number,
        low: PropTypes.number,
        high: PropTypes.number,
        view: PropTypes.number,
        unit: PropTypes.string,
        unitName: PropTypes.string,
        defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
        onChange: PropTypes.func,
};
调用方式:
<Modal visible={show} popup animationType={'slide-up'} onClose={onClose}>
    <div className="water-modal-box">
        <DrinkRuler onChange={(val) => getValue(val)} defaultValue={500} min={200} max={500} id={1} key={1} step={1} digits={0} />
        <div className="water-modal-btn">
            <span onClick={onClose}>取消</span>
            <span onClick={waterAdd}>确认</span>
        </div>
    </div>
</Modal>
// onChange获取标尺的value
// defaultValue标尺显示的默认值
// min最小值
// max最大值
ruler.less
.component-ruler {
    text-align: center;

    header {
        display: flex;
        padding: 0 17px;
    }
    .img-wrap {
        padding: 10px 18px;

        img {
            width: 30px;
            height: 30px;
            box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2);
            border-radius: 100%;
        }
    }
    .input-wrap {
        overflow: hidden;
        flex: 1;
        margin: 0 0 12px 0;
        text-align: center;
        font-size: 14px;
        color: #4a4a4a;
        font-weight: 500;
        span {
            // font-family: DINAlternate-Bold;
            font-size: 56px;
            color: #333333;
            // letter-spacing: 0;
            // line-height: 56px;
        }
        input {
            width: 100%;
            height: 100%;
            line-height: 30px;
            text-align: center;
            vertical-align: top;
            color: inherit;

            &::placeholder {
                color: #333;
            }
        }
    }
    p {
        margin-bottom: 1em;
        color: #999;
        font-size: 12px;
    }
    section {
        position: relative;
        overflow: hidden;
        padding-bottom: 30px;

        &:before {
            content: '';
            z-index: 1;
            position: absolute;
            top: 0;
            top: 0;
            left: 50%;
            width: 3px;
            height: 50%;
            margin-left: 0px;
            background: #03c067;
            border-radius: 1px;
        }
    }
    ul {
        display: flex;

        li {
            position: relative;
            flex: 1;
            min-width: 25%;
        }
    }
    .ruler {
        li {
            opacity: 1;
            height: 40px;
            background: linear-gradient(90deg, #e0e0e0, #e0e0e0, transparent 1px) repeat-x;
            background-size: 10% 50%;
            &:before {
                content: '';
                position: absolute;
                top: 0;
                left: 0px;
                width: 2px;
                height: 72%;
                background: #e0e0e0;
            }

            &:after {
                content: '';
                position: absolute;
                top: 0;
                left: 50%;
                width: 1px;
                height: 70%;
                background: #e0e0e0;
            }

            i {
                position: absolute;
                top: 0;
                right: -0.5px;
                width: 2px;
                height: 100%;
                background: #ccc;
            }
        }
    }
    .mark {
        li {
            span {
                position: absolute;
                left: -3em;
                bottom: -20px;
                width: 6em;
                line-height: 1;
                color: #ccc;

                &.last {
                    left: auto;
                    right: -3em;
                }
            }
        }
    }
    &.high,
    &.low,
    &.normal {
        section {
            color: #fff;

            &:before {
                background: #fff !important;
            }
        }
        li span {
            color: #fff;
        }
    }
    &.high {
        header {
            color: #ff2f2f;
        }

        section {
            background-image: linear-gradient(90deg, #ff6d6d, #ff2f2f);
        }
    }
    &.low {
        header {
            color: #ffa100;
        }

        section {
            background-image: linear-gradient(90deg, #ffbe4f, #ffa100);
        }
    }
    &.normal {
        header {
            color: #20ba32;
        }

        section {
            background-image: linear-gradient(90deg, #65de74, #20ba32);
        }
    }
}