Skip to content

kiner-guide是一个基于react的操作指引组件,简单的配置,可以迅速集成至项目之中,显示友好的操作指引。本示例以Ant Design Mobile项目为例作为演示

Notifications You must be signed in to change notification settings

kiner-tang/kiner-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kiner-guide

kiner-guide是一个基于react的操作指引组件,简单的配置,可以迅速集成至项目之中,显示友好的操作指引。本示例以Ant Design Mobile项目为例作为演示

操作指引

主要文件列表

  • App.js 调用示例
  • src/components/KinerGuide.js 操作指引组件主文件
  • src/components/KinerGuide.css 操作指引组件样式文件
  • src/utils/inobounce.js 为防止在ios中页面滚动导致一些问题引入此文件禁止页面回弹

以下为操作指引组件调用示例

App.js

import React from 'react';
import {List} from 'antd-mobile';
import './App.css';

//step1. 引入操作指引组件
import KinerGuide from './components/KinerGuide/KinerGuide';

const Item = List.Item;
const Brief = Item.Brief;


class App extends React.Component {

    //step2. 定义操作指引信息对象
    guideList = [
        {position: 'bottom', text: "第一步提示内容", offset: 15},
        {position: 'top', text: "第二步提示内容", offset: 15},
        {position: 'auto', text: "第三步提示内容", offsetX: -15},
        {position: 'auto', text: "第四步提示内容", offsetX: -15},
        {position: 'auto', text: "第五步提示内容", offset: -15},
        {position: 'auto', text: "第六步提示内容", offsetX: -15},
        {position: 'auto', text: "第七步提示内容", offsetX: -15},
        {position: 'auto', text: "这是头像", offset: 15, type: 'circular'},
        {position: 'auto', text: "最后一步提示内容", offsetX: -15}
    ];

    constructor(props) {
        super(props);
        //step3. 初始化操作变量
        this.state = {
            disabled: false,//忽略,与本组件无关
            isShowGuide: false,//是否显示操作指引,通过控制此开关便可显示和隐藏操作指引
            guideStep: 0//当前操作指引的步骤
        };
    }

    componentDidMount() {
        //step4. 在页面准备完毕时显示操作指引
        this.setState({
            isShowGuide: true//当页面加载成功或者是如果操作指引依赖于某些异步操作,如axios请求等,则可在请求成功之后让操作指引显示
        });
    }


    render() {

        return (
            //step5. 为页面根元素增加ref,以便操作指引组件获取该对象进行操作
            <div className='container' ref='guideBox'>
                <div className="avator-box" style={{width: 'fit-content'}} ref='guideTarget7'>
                    <div className="avator"></div>
                </div>
                <List renderHeader={() => 'Basic Style'} className="my-list">

                    <Item extra={'extra content'}>
                        {/*step6. 为需要添加操作指引的元素添加ref,以便操作指引插件获取该对象进行操作,如果有多个步骤,一个页面可以添加多个,如guideTarget0,guideTarget1,guideTarget2等*/}
                        <div ref='guideTarget0' style={{width: 'fit-content'}}>
                            这是第一步
                        </div>
                    </Item>
                </List>
                <List renderHeader={() => 'Subtitle'} className="my-list">
                    <Item arrow="horizontal" multipleLine onClick={() => {
                    }}>
                        Title <Brief>subtitle</Brief>
                    </Item>

                    <Item
                        arrow="horizontal"
                        multipleLine
                        onClick={() => {
                        }}
                        platform="android"
                    >
                        ListItem (Android)<Brief>There may have water ripple effect of <br/> material if you set the
                        click
                        event.</Brief>
                    </Item>

                    <Item
                        arrow="horizontal"
                        thumb="https://zos.alipayobjects.com/rmsportal/dNuvNrtqUztHCwM.png"
                        multipleLine
                        onClick={() => {
                        }}
                    >

                        <div ref='guideTarget1' style={{width: 'fit-content'}}>
                            这是第二步 <Brief>subtitle</Brief>
                        </div>

                    </Item>
                </List>

                <List renderHeader={() => 'Customized Right Side(Empty Content / Text / Image)'} className="my-list">
                    <Item>Title</Item>
                    <Item arrow="horizontal" onClick={() => {
                    }}>Title</Item>
                    <Item extra="extra content" arrow="horizontal" onClick={() => {
                    }}>Title</Item>
                    <div ref='guideTarget2' style={{width: '100%'}}>
                        <Item extra="10:30" align="top"
                              thumb="https://zos.alipayobjects.com/rmsportal/dNuvNrtqUztHCwM.png"
                              multipleLine>
                            这是第三步 <Brief>的点点滴滴</Brief>
                        </Item>
                    </div>

                </List>
                <div ref='guideTarget6' style={{width: '100%'}}>

                    <List renderHeader={() => 'Align Vertical Center'} className="my-list">
                        <Item multipleLine extra="extra content">
                            第七步 <Brief>subtitle</Brief>
                        </Item>
                    </List>
                </div>
                <List renderHeader={() => 'Icon in the left'}>
                    <Item
                        thumb="https://zos.alipayobjects.com/rmsportal/dNuvNrtqUztHCwM.png"
                        arrow="horizontal"
                        onClick={() => {
                        }}
                    >My wallet</Item>
                    <div ref='guideTarget5' style={{width: '100%'}}>
                        <Item
                            thumb="https://zos.alipayobjects.com/rmsportal/UmbJMbWOejVOpxe.png"
                            onClick={() => {
                            }}
                            arrow="horizontal"
                        >
                            第六步
                        </Item>
                    </div>
                </List>
                <List renderHeader={() => 'Text Wrapping'} className="my-list">
                    <div ref='guideTarget3' style={{width: '100%'}}>
                        <Item data-seed="logId">这是第四步</Item>
                    </div>
                    <div ref='guideTarget8' style={{width: '100%'}}>
                        <Item wrap>第八步:Multiple line,long text will wrap;Long Text Long Text Long Text Long Text Long
                            Text
                            Long
                            Text</Item>

                    </div>
                    <Item extra="extra content" multipleLine align="top" wrap>
                        Multiple line and long text will wrap. Long Text Long Text Long Text
                    </Item>
                    <Item extra="no arrow" arrow="empty" className="spe" wrap>
                        In rare cases, the text of right side will wrap in the single line with long text. long text
                        long
                        text long text
                    </Item>


                </List>
                <List renderHeader={() => 'Other'} className="my-list">
                    <div ref='guideTarget4' style={{width: '100%'}}>
                        <Item disabled={this.state.disabled} extra="" onClick={() => {
                            console.log('click', this.state.disabled);
                            this.setState({disabled: true});
                        }}>第五步</Item>
                        <Item>
                            <select defaultValue="1">
                                <option value="1">Html select element</option>
                                <option value="2" disabled>Unable to select</option>
                                <option value="3">option 3</option>
                            </select>
                        </Item>
                    </div>

                </List>


                {/*step7. 页面中使用KinerGuide组件,传入相关配置进行配置组件*/}

                {/*visible:         是否显示显示操作指引*/}
                {/*guideBox:        页面容器对象,即step5中指定的元素*/}
                {/*target:          操作指引目标元素,即step6中指定的元素*/}
                {/*type:            操作指引弹框的类型,可选择 方形:square,圆形:circular*/}
                {/*offset:          操作指引框偏移量,优先级高于offsetX和offsetY*/}
                {/*offsetX:         水平偏移量*/}
                {/*offsetY:         垂直偏移量*/}
                {/*guidePosition:   操作提示面板的位置,可选择:自动:auto 操作指引框上方:top 操作指引框下方:bottom*/}
                <KinerGuide
                    visible={this.state.isShowGuide}
                    guideBox={this.refs.guideBox}
                    target={this.refs[`guideTarget${this.state.guideStep}`]}
                    type={this.guideList[this.state.guideStep].type}
                    offset={this.guideList[this.state.guideStep].offset}
                    offsetX={this.guideList[this.state.guideStep].offsetX}
                    offsetY={this.guideList[this.state.guideStep].offsetY}
                    guidePosition={this.guideList[this.state.guideStep].position}
                >
                    {/*操作指引提示框框,可在此处自定义添加代码实现业务逻辑*/}
                    <div className="guide-container">
                        <h1 className="title">{this.guideList[this.state.guideStep].text}</h1>
                        <div className="btn" onClick={e => {
                            if (this.state.guideStep !== this.guideList.length - 1) {
                                let {guideStep} = this.state;
                                this.setState({
                                    guideStep: ++guideStep
                                });
                            } else {
                                this.setState({
                                    isShowGuide: false
                                });
                            }

                        }}>{this.state.guideStep !== this.guideList.length - 1 ? '下一步' : '知道了'}</div>
                    </div>
                </KinerGuide>
            </div>
        );
    }
}

export default App;

操作指引组件KinerGuide.js

import React from 'react';
import iNoBounce from '../../utils/inobounce';

import './KinerGuide.css'

class KinerGuide extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            bodyStyle: document.body.style,//用于临时存放body的样式
            visible: this.props.visible,//是否显示操作指引
            offset: this.props.offset === undefined ? 15 : this.props.offset//有些时候,我们的指引框可能需要比目标区块稍微大一点,指定此偏移量,便可以调整指引框大小
        };
    }

    pageScroll(nextProps) {
        let {
            visible,//是否显示操作指引
            target,//需显示操作指引的目标元素
            guideBox,//页面容器,可对当前页面容器进行位移以便完全显示操作指引
        } = nextProps;

        if (visible && target) {

            //当显示操作指引时将body高度设置为100%并超出隐藏,防止页面滚动
            document.body.style.height = '100%';
            document.body.style.overflow = 'hidden';

            if (guideBox) {
                //多步骤操作指引切换时可能会出现断站的位移,用户体验不佳,故在下一次操作指引开始前先将容器透明度置为0,当切换至下一步骤后再显示
                guideBox.style.opacity = 0;
                //重置页面位置,以便重新计算
                guideBox.style.transform = `translate3d(0,0,0)`;
            }

            //获取带指引区块的react信息,用于定位显示操作指引
            let react = target.getBoundingClientRect();
            //浏览器窗口的一半
            let halfHeightOfWindow = window.innerHeight / 2;
            //目标区块中心点y坐标
            let centerPointYOfTarget = react.top + react.height / 2;

            //当目标区块中心点y坐标大于当前浏览器窗口一半时,需对页面进行位移操作,一遍操作指引能够完全展示
            if (centerPointYOfTarget > halfHeightOfWindow) {
                setTimeout(() => {

                    let y = -Math.abs(halfHeightOfWindow - centerPointYOfTarget);


                    //当元素位移超过页面底部时,固定位移量为页面底部的位移量
                    if (Math.abs(y) >= document.documentElement.offsetHeight - window.innerHeight) {
                        y = -Math.abs(window.innerHeight - react.bottom);
                    }
                    //对页面融进进行位移,并使其透明度变为1使其显示
                    if (guideBox) {
                        guideBox.style.transform = `translate3d(0,${y}px,0)`;
                        guideBox.style.opacity = 1;
                    }

                }, 0);

            } else {
                //若带指引元素无需位移,则直接显示
                if (guideBox) {
                    guideBox.style.opacity = 1;
                }

            }

        } else {
            //若操作指引框无需显示,则重置样式
            document.body.style = this.state.bodyStyle;
            if (guideBox) {
                guideBox.style.transform = `translate3d(0,0,0)`;
                guideBox.style.opacity = 1;
            }
        }
    }

    componentWillReceiveProps(nextProps) {


        let {
            visible,//是否显示操作指引
            offset,//有些时候,我们的指引框可能需要比目标区块稍微大一点,指定此偏移量,便可以调整指引框大小,该属性优先级高于offsetX和offsetY,若指定了offset,则offsetX和offsetY失效
        } = nextProps;
        this.pageScroll(nextProps);

        this.setState({
            visible: visible,
            offset: offset === undefined ? 15 : offset
        });

    }

    componentDidMount() {
        this.pageScroll(this.props);
    }

    render() {
        let {
            target, //需显示操作指引的目标元素
            offset,//有些时候,我们的指引框可能需要比目标区块稍微大一点,指定此偏移量,便可以调整指引框大小,该属性优先级高于offsetX和offsetY,若指定了offset,则offsetX和offsetY失效
            offsetX = 0,//水平偏移量
            offsetY = 0,//垂直偏移量
            guidePosition = 'auto', //指引文案位于目标区块上方还是下方,若为上方则传入'top',若为下方则为'bottom',若传入'auto',则自动判断指引区块位置,默认为'auto'
            type = 'square'//指引框类型,可选项包括'square','circular',默认为方形'square',可选择圆形'circular'
        } = this.props;
        let {
            offsetWidth, //页面宽度
            offsetHeight //页面高度
        } = document.documentElement;


        let react;
        if (target && this.state.visible) {
            //在ios中,由于页面回弹和滚动,会导致一些奇怪的显示问题,故采用iNoBounce禁止页面回弹和滚动
            iNoBounce.enable();
            //获取目标元素的react对象
            react = target.getBoundingClientRect();

            //渲染页面指引
            let width = 0, height = 0, borderTop = 0, borderRight = 0, borderBottom = 0, borderLeft = 0, left = 0,
                top = 0;

            if (offset !== undefined) {
                width = react.width + offset;
                height = react.height + offset;

                borderTop = react.top;
                borderLeft = react.left;
                borderRight = offsetWidth - borderLeft - react.width;
                borderBottom = offsetHeight - borderTop - react.height;

                if (offset < 0) {
                    borderLeft += Math.abs(offset);
                    borderRight += Math.abs(offset);
                    borderTop += Math.abs(offset);
                    borderBottom += Math.abs(offset);
                }

                left = react.left - offset / 2 - borderLeft;
                top = react.top - offset / 2 - borderTop;
            } else {


                borderTop = react.top;
                borderLeft = react.left;

                borderRight = offsetWidth - borderLeft - react.width;
                borderBottom = offsetHeight - borderTop - react.height;


                //offsetX
                width = react.width + offsetX;


                if (offsetX < 0) {
                    borderLeft += Math.abs(offsetX);
                    borderRight += Math.abs(offsetX);
                }

                left = react.left - offsetX / 2 - borderLeft;

                //offsetY

                height = react.height + offsetY;

                if (offsetY < 0) {
                    borderTop += Math.abs(offsetY);
                    borderBottom += Math.abs(offsetY);
                }

                top = react.top - offsetY / 2 - borderTop;


            }

            if (guidePosition === 'auto') {

                if (react.top + react.height / 2 > window.innerHeight / 2) {
                    guidePosition = 'top';
                } else {
                    guidePosition = 'bottom';
                }

            }


            return (<div className="guideModal-container" ref='guideContainer'
                         style={{width: document.documentElement.offsetWidth}}>
                <div className="mask" style={{
                    width: width,
                    height: height,
                    left: left,
                    top: top,
                    borderWidth: `${borderTop}px ${borderRight}px ${borderBottom}px ${borderLeft}px`
                }}>
                    <div className={`target ${type}`}>
                    </div>
                </div>
                <div className="guide-content" style={{
                    left: 0,
                    top: `${guidePosition === 'top' ? react.top - (offset === undefined ? offsetY : offset) / 2 : react.bottom + (offset === undefined ? offsetY : offset) / 2}px`,
                    transform: `${guidePosition === 'top' ? 'translate3d(0,-100%,0)' : 'translate3d(0,0,0)'}`
                }}>
                    {this.props.children}
                </div>
            </div>);
        } else {
            //隐藏操作指引框时恢复页面滚动状态
            iNoBounce.disable();
            return <div></div>
        }
    }
}

export default KinerGuide;

操作指引配套样式KinerGuide.css

.guideModal-container {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    transform: translate3d(0, 0, 0);
    overflow: hidden;
    -webkit-overflow-scrolling: touch;
    z-index: 999999999;
}
.guideModal-container .mask {
    position: absolute;
    border: 0 solid rgba(0, 0, 0, 0.7);
    transition: all .3s;
    z-index: 999999;
    pointer-events: none;
    overflow: hidden;
}
.guideModal-container .mask .target {
    position: relative;
    height: 100%;
    width: 100%;
    border-radius: 5px;
    outline: 7px solid rgba(0, 0, 0, 0.7);
    box-shadow: 0 0 0 7px rgba(0, 0, 0, 0.7), inset 0 0 15px #ccc;
    background-color: transparent;
}
.guideModal-container .mask .target.circular{
    border-radius: 50%;
    box-shadow: 0 0 0 15px rgba(0, 0, 0, 0.7), inset 0 0 15px #ccc;
}
.guideModal-container .guide-content {
    position: fixed;
    z-index: 9999999;
    width: 100%;
    color: #FFFFFF;
}

防止ios中页面滚动回弹的工具inobounce.js

/*! iNoBounce - v0.2.0
* https://github.com/lazd/iNoBounce/
* Copyright (c) 2013 Larry Davis <lazdnet@gmail.com>; Licensed BSD */
(function(global) {
    // Stores the Y position where the touch started
    var startY = 0;

    // Store enabled status
    var enabled = false;

    var supportsPassiveOption = false;
    try {
        var opts = Object.defineProperty({}, 'passive', {
            get: function() {
                supportsPassiveOption = true;
            }
        });
        window.addEventListener('test', null, opts);
    } catch (e) {}

    var handleTouchmove = function(evt) {
        // Get the element that was scrolled upon
        var el = evt.target;

        // Allow zooming
        var zoom = window.innerWidth / window.document.documentElement.clientWidth;
        if (evt.touches.length > 1 || zoom !== 1) {
            return;
        }

        // Check all parent elements for scrollability
        while (el !== document.body && el !== document) {
            // Get some style properties
            var style = window.getComputedStyle(el);

            if (!style) {
                // If we've encountered an element we can't compute the style for, get out
                break;
            }

            // Ignore range input element
            if (el.nodeName === 'INPUT' && el.getAttribute('type') === 'range') {
                return;
            }

            var scrolling = style.getPropertyValue('-webkit-overflow-scrolling');
            var overflowY = style.getPropertyValue('overflow-y');
            var height = parseInt(style.getPropertyValue('height'), 10);

            // Determine if the element should scroll
            var isScrollable = scrolling === 'touch' && (overflowY === 'auto' || overflowY === 'scroll');
            var canScroll = el.scrollHeight > el.offsetHeight;

            if (isScrollable && canScroll) {
                // Get the current Y position of the touch
                var curY = evt.touches ? evt.touches[0].screenY : evt.screenY;

                // Determine if the user is trying to scroll past the top or bottom
                // In this case, the window will bounce, so we have to prevent scrolling completely
                var isAtTop = (startY <= curY && el.scrollTop === 0);
                var isAtBottom = (startY >= curY && el.scrollHeight - el.scrollTop === height);

                // Stop a bounce bug when at the bottom or top of the scrollable element
                if (isAtTop || isAtBottom) {
                    evt.preventDefault();
                }

                // No need to continue up the DOM, we've done our job
                return;
            }

            // Test the next parent
            el = el.parentNode;
        }

        // Stop the bouncing -- no parents are scrollable
        evt.preventDefault();
    };

    var handleTouchstart = function(evt) {
        // Store the first Y position of the touch
        startY = evt.touches ? evt.touches[0].screenY : evt.screenY;
    };

    var enable = function() {
        // Listen to a couple key touch events
        window.addEventListener('touchstart', handleTouchstart, supportsPassiveOption ? { passive : false } : false);
        window.addEventListener('touchmove', handleTouchmove, supportsPassiveOption ? { passive : false } : false);
        enabled = true;
    };

    var disable = function() {
        // Stop listening
        window.removeEventListener('touchstart', handleTouchstart, false);
        window.removeEventListener('touchmove', handleTouchmove, false);
        enabled = false;
    };

    var isEnabled = function() {
        return enabled;
    };

    // Enable by default if the browser supports -webkit-overflow-scrolling
    // Test this by setting the property with JavaScript on an element that exists in the DOM
    // Then, see if the property is reflected in the computed style
    var testDiv = document.createElement('div');
    document.documentElement.appendChild(testDiv);
    testDiv.style.WebkitOverflowScrolling = 'touch';
    var scrollSupport = 'getComputedStyle' in window && window.getComputedStyle(testDiv)['-webkit-overflow-scrolling'] === 'touch';
    document.documentElement.removeChild(testDiv);

    if (scrollSupport) {
        enable();
    }

    // A module to support enabling/disabling iNoBounce
    var iNoBounce = {
        enable: enable,
        disable: disable,
        isEnabled: isEnabled
    };

    if (typeof module !== 'undefined' && module.exports) {
        // Node.js Support
        module.exports = iNoBounce;
    }
    if (typeof global.define === 'function') {
        // AMD Support
        (function(define) {
            define('iNoBounce', [], function() { return iNoBounce; });
        }(global.define));
    }
    else {
        // Browser support
        global.iNoBounce = iNoBounce;
    }
}(this));

About

kiner-guide是一个基于react的操作指引组件,简单的配置,可以迅速集成至项目之中,显示友好的操作指引。本示例以Ant Design Mobile项目为例作为演示

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages