├── .all-contributorsrc ├── .babelrc ├── .browserslistrc ├── .commitlintrc.js ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitauthor.js ├── .gitignore ├── .huskyrc.js ├── .npmignore ├── .npmrc ├── .prettierrc.js ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README-cn.md ├── README.md ├── example ├── .eslintignore ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── App.vue │ ├── assets │ └── logo.png │ └── main.js ├── package.json ├── rollup.config.js ├── src ├── index.css ├── index.js └── util.js └── test └── test.md /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "js-npm-template", 3 | "projectOwner": "Rabbitzzc", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 10, 10 | "commit": true, 11 | "commitConvention": "gitmoji", 12 | "contributors": [], 13 | "contributorsPerLine": 7 14 | } 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" // 根据浏览器和运行环境,自动的确定 babel 插件和 polyfills 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-proposal-class-properties" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | 2 | # Browsers that we support 3 | 4 | last 1 version 5 | > 1% 6 | maintained node versions 7 | not dead -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | // rules 中的内容可自定义,当前与 @commitlint/config-conventional 一致 4 | // 用于查看可自定义项 5 | rules: { 6 | // body 以空行开头 7 | 'body-leading-blank': [1, 'always'], 8 | // footer 以空行开头 9 | 'footer-leading-blank': [1, 'always'], 10 | // header 最大长度 72 个字符 11 | 'header-max-length': [2, 'always', 72], 12 | // 提交影响范围 13 | 'scope-case': [1, 'always', 'lower-case'], 14 | // 提交内容 15 | 'subject-case': [2, 'always', 'lower-case'], 16 | // 提交内容不能为空 17 | 'subject-empty': [2, 'never'], 18 | // 提交内容不能以 . 结尾 19 | 'subject-full-stop': [2, 'never', '.'], 20 | // 提交类型案例 21 | 'type-case': [2, 'always', 'lower-case'], 22 | // 提交类型不能为空 23 | 'type-empty': [2, 'never'], 24 | // 支持的提交类型 25 | 'type-enum': [ 26 | 2, 27 | 'always', 28 | [ 29 | 'build', 30 | 'chore', 31 | 'ci', 32 | 'docs', 33 | 'feat', 34 | 'fix', 35 | 'perf', 36 | 'refactor', 37 | 'revert', 38 | 'style', 39 | 'test' 40 | ] 41 | ] 42 | } 43 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | # 表明这是最顶层的配置文件,这样才会停止继续向上查找 .editorconfig 文件; 4 | # 查找的 .editorconfig 文件是从顶层开始读取的,类似变量作用域的效果,内部 5 | # 的 .editorconfig 文件属性优先级更高 6 | root = true 7 | 8 | # 指定作用文件格式 - 表示任意文件 ,比如js文件 py文件 php 文件等 9 | [*] 10 | 11 | # 缩进的类型 [space | tab] 12 | indent_style = space 13 | 14 | # 缩进的数量,我喜欢4 15 | indent_size = 4 16 | 17 | # 定义换行符 [lf | cr | crlf] 18 | end_of_line = lf 19 | 20 | # 编码格式。支持latin1、utf-8、utf-8-bom、utf-16be和utf-16le,不建议使用uft-8-bom 21 | charset = utf-8 22 | 23 | # 是否去除换行首尾的任意空白字符 24 | trim_trailing_whitespace = true 25 | 26 | # 文件最后是否使用一个空白行结尾 27 | insert_final_newline = true 28 | 29 | [*.md] 30 | trim_trailing_whitespace = false 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | test/* 4 | 5 | dist/* 6 | 7 | build/* 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | extends: [ 4 | 'alloy', 5 | ], 6 | parserOptions: { 7 | ecmaVersion: 2017, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | experimentalObjectRestSpread: true, 11 | jsx: true 12 | } 13 | }, 14 | env: { 15 | browser: true, 16 | node: true, 17 | commonjs: true, 18 | es6: true 19 | }, 20 | root: true, 21 | rules: { 22 | 'for-direction': 'error', 23 | 'getter-return': [ 24 | 'error', 25 | { 26 | allowImplicit: false 27 | } 28 | ], 29 | 'no-await-in-loop': 'off', 30 | 'no-compare-neg-zero': 'error', 31 | 'no-cond-assign': [ 32 | 'error', 33 | 'except-parens' 34 | ], 35 | 'no-console': 'off', 36 | 'no-constant-condition': [ 37 | 'error', 38 | { 39 | checkLoops: false 40 | } 41 | ], 42 | 'no-control-regex': 'error', 43 | 'no-debugger': 'error', 44 | 'no-dupe-args': 'error', 45 | 'no-dupe-keys': 'error', 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.gitauthor.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').exec 2 | 3 | exec('git config --get user.name && git config --get user.email', (error, stdout, stderr) => { 4 | if (stdout) { 5 | console.log('\x1b[36m%s\x1b[0m', 'start checking user!(∩_∩)O~\n') 6 | const arr = stdout.split('\n') 7 | const user = { 8 | name: arr[0], 9 | email: arr[1] 10 | } 11 | console.log('user.name: ', user.name) 12 | console.log('user.email: ', user.email) 13 | 14 | if (compareAuthor(user)) { 15 | console.log('\x1b[32m%s\x1b[0m', 'check pass~') 16 | process.exit(0) 17 | } else { 18 | console.log('\x1b[31m%s\x1b[0m', 'check fail~') 19 | process.exit(1) 20 | } 21 | 22 | } else { 23 | console.log('no git info!') 24 | } 25 | }) 26 | // git author 信息 27 | const authors = [{ 28 | name: 'Rabbitzzc', 29 | email: 'zzclovelcs@gmail.com' 30 | }] 31 | 32 | 33 | function compareAuthor(author) { 34 | return authors.some(v => v.name === author.name && v.email === author.email) 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | .DS_Store 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | 21 | # lock 22 | yarn.lock 23 | package-lock.json 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | *.lcov 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variables file 79 | .env 80 | .env.test 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | 85 | # Next.js build output 86 | .next 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | 92 | # Gatsby files 93 | .cache/ 94 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 95 | # https://nextjs.org/blog/next-9-1#public-directory-support 96 | # public 97 | 98 | # vuepress build output 99 | .vuepress/dist 100 | 101 | # Serverless directories 102 | .serverless/ 103 | 104 | # FuseBox cache 105 | .fusebox/ 106 | 107 | # DynamoDB Local files 108 | .dynamodb/ 109 | 110 | # TernJS port file 111 | .tern-port 112 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | const tasks = arr => arr.join(' && ') 2 | 3 | module.exports = { 4 | 'hooks': { 5 | 'pre-commit': tasks([ 6 | 'yarn commit-name', 7 | 'yarn lint', 8 | 'yarn test', 9 | ]), 10 | 'commit-msg': tasks([ 11 | 'yarn commit-msg' 12 | ]), 13 | 'pre-push': tasks([ 14 | 'yarn pre-push' 15 | ]) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # 推荐使用 pkg.files 来过滤文件,和 npmignore 最大的差异就是使用白名单。npmignore 文件通过配置一个黑名单列表,当 npm publish 时这些文件不被发布。但是当新增配置文件或某些工具生成临时文件时,这个黑名单就会收到影响 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # npm 2 | # registry=https://registry.npmjs.org 3 | # cnpm 🇨🇳 4 | registry=https://registry.npmjs.org 5 | # save-exact=true 6 | # engine-strict=true 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | tabWidth: 4, 4 | useTabs: false, 5 | singleQuote: true, 6 | semi: false, 7 | trailingComma: "none", 8 | bracketSpacing: true, 9 | arrowParens: 'always', 10 | htmlWhitespaceSensitivity: 'ignore' 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js # 使用 nodejs 2 | node_js: # 版本为 8 3 | - "10" 4 | 5 | cache: yarn # 使用 yarn 而不是 npm (按照实际情况调整) 6 | install: yarn 7 | 8 | script: 9 | - yarn test 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rabbitzzc/step-tooltip/4654a7e92c568c30e97de9bb50223e90c1fa7cd5/CHANGELOG.md -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rabbitzzc/step-tooltip/4654a7e92c568c30e97de9bb50223e90c1fa7cd5/CONTRIBUTING.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 step-tooltip 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-cn.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rabbitzzc/step-tooltip/4654a7e92c568c30e97de9bb50223e90c1fa7cd5/README-cn.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Step Tooltip

4 |
step by step tooltip guilder for any website... lightweight & minify✨✨✨.
5 | npm bundle size 6 | npm 7 | Travis (.org) 8 | GitHub contributors 9 | node-current 10 | GitHub 11 | GitHub issues 12 | GitHub top language 13 | 14 |
15 | 16 | 17 | ## ⭐️ Features 18 | 19 | - Lightweight, minify. 20 | - Generate arrows from pseudo-classes(before/after). 21 | 22 | 23 | 🤚🎨🤚🎨demo: https://step-tooltip-example.logiczc.vercel.app/ 24 | 25 | 26 | ## 📦 Getting Started 27 | 28 | ### install 29 | ```sh 30 | # npm 31 | npm install --save step-tooltip 32 | 33 | # yarn 34 | yarn add step-tooltip 35 | ``` 36 | 37 | ### usage 38 | 39 | ```sh 40 | import stepTooltip from 'step-tooltip' 41 | 42 | stepTooltip() 43 | ``` 44 | 45 | ### config 46 | 47 | ```js 48 | { 49 | initialText: '哈喽,准备好了解 step-tooltip 了么?', 50 | steps: [], 51 | backdropColor: 'rgb(0 0 0 / 0.56)', 52 | options: { 53 | backLabel: '上一步', 54 | nextLabel: '下一步', 55 | skipLabel: '我知道了', 56 | doneLabel: '完成', 57 | } 58 | } 59 | ``` 60 | 61 | | config | func | default | 62 | | ------------- | --------------------------------- | ------------------------------------------------------------ | 63 | | initialText | first show text | `哈喽,准备好了解 step-tooltip 了么?` | 64 | | steps | every step | `[]` | 65 | | backdropColor | backdrop element box-shadow color | `'rgb(0 0 0 / 0.56)'` | 66 | | options | other config | 1. backLabel: 上一步
2. nextLabel: 下一步
3. skipLabel: 我知道了
4. doneLabel: 完成 | 67 | 68 | 69 | 70 | ## 🔖 LICENSE 71 | 72 | [MIT](./LICENSE) 73 | 74 | ## ✈️ Thanks 75 | * [cssarrowplease](http://www.cssarrowplease.com/), css arrow. 76 | -------------------------------------------------------------------------------- /example/.eslintignore: -------------------------------------------------------------------------------- 1 | src 2 | dist 3 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | .vercel 26 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "step-tooltip": "1.0.5", 13 | "vue": "^2.6.11" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "~4.5.0", 17 | "@vue/cli-plugin-eslint": "~4.5.0", 18 | "@vue/cli-service": "~4.5.0", 19 | "babel-eslint": "^10.1.0", 20 | "eslint": "^6.7.2", 21 | "eslint-plugin-vue": "^6.2.2", 22 | "vue-template-compiler": "^2.6.11" 23 | }, 24 | "eslintConfig": { 25 | "root": true, 26 | "env": { 27 | "node": true 28 | }, 29 | "extends": [ 30 | "plugin:vue/essential", 31 | "eslint:recommended" 32 | ], 33 | "parserOptions": { 34 | "parser": "babel-eslint" 35 | }, 36 | "rules": {} 37 | }, 38 | "browserslist": [ 39 | "> 1%", 40 | "last 2 versions", 41 | "not dead" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rabbitzzc/step-tooltip/4654a7e92c568c30e97de9bb50223e90c1fa7cd5/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/src/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 73 | 74 | 171 | -------------------------------------------------------------------------------- /example/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rabbitzzc/step-tooltip/4654a7e92c568c30e97de9bb50223e90c1fa7cd5/example/src/assets/logo.png -------------------------------------------------------------------------------- /example/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: h => h(App), 8 | }).$mount('#app') 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "step-tooltip", 3 | "version": "1.0.5", 4 | "description": "step by step tooltip guilder for any website.lightweight, & minify✨✨✨.", 5 | "main": "dist/index.min.js", 6 | "module": "dist/index.min.js", 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "build": "rollup -c", 12 | "prettier": "prettier --write src/*", 13 | "test": "echo \"Error: no test specified\"", 14 | "lint": "eslint --fix", 15 | "commit-msg": "commitlint -e $GIT_PARAMS", 16 | "commit-name": "node .gitauthor.js", 17 | "pre-push": "npx branch-name-lint", 18 | "contributors:add": "all-contributors add", 19 | "contributors:generate": "all-contributors generate" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/npm-template/js-npm-template.git" 24 | }, 25 | "keywords": [ 26 | "js-npm-template", 27 | "npm-template", 28 | "js" 29 | ], 30 | "author": "Rabbitzzc", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/npm-template/js-npm-template/issues" 34 | }, 35 | "homepage": "https://github.com/npm-template/js-npm-template#readme", 36 | "engines": { 37 | "node": ">=10.0.0" 38 | }, 39 | "files": [ 40 | "dist" 41 | ], 42 | "devDependencies": { 43 | "@babel/core": "^7.11.6", 44 | "@babel/plugin-proposal-class-properties": "7.10.4", 45 | "@babel/plugin-transform-runtime": "^7.11.5", 46 | "@babel/preset-env": "^7.11.5", 47 | "@commitlint/cli": "11.0.0", 48 | "@commitlint/config-conventional": "11.0.0", 49 | "@rollup/plugin-babel": "5.2.1", 50 | "all-contributors-cli": "6.17.2", 51 | "autoprefixer": "^9.8.6", 52 | "babel-eslint": "^10.1.0", 53 | "branch-name-lint": "^1.4.0", 54 | "eslint": "7.9.0", 55 | "eslint-config-alloy": "^3.8.0", 56 | "husky": "^4.2.5", 57 | "prettier": "2.1.1", 58 | "rollup": "^2.26.11", 59 | "rollup-plugin-clear": "2.0.7", 60 | "rollup-plugin-postcss": "3.1.8", 61 | "rollup-plugin-terser": "^7.0.2" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import {babel} from "@rollup/plugin-babel" 2 | 3 | import postcss from 'rollup-plugin-postcss' 4 | import autoPreFixer from 'autoprefixer' 5 | 6 | // 压缩 7 | import { 8 | terser 9 | } from "rollup-plugin-terser" 10 | 11 | // 输出文件夹清除 12 | import clear from "rollup-plugin-clear" 13 | 14 | 15 | const path = require('path') 16 | const resolve = function (filePath) { 17 | return path.join(__dirname, filePath) 18 | } 19 | 20 | 21 | export default { 22 | input: resolve('src/index.js'), // 入口文件 23 | output: [{ // 出口文件 24 | file: resolve('dist/index.min.js'), 25 | format: 'umd', 26 | name: 'index', 27 | }], 28 | plugins: [ 29 | clear({ 30 | targets: ["dist"] 31 | }), 32 | postcss({ 33 | extensions: ['.css'], 34 | minimize: true, // 压缩 35 | extract: resolve('dist/index.min.css'), 36 | plugins: [autoPreFixer()] 37 | }), 38 | babel({ 39 | exclude: 'node_modules/**', 40 | // babelHelpers: 'runtime' 41 | }), 42 | terser(), 43 | ], 44 | external: [] 45 | } 46 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .step-tooltip-backdrop { 2 | position: fixed; 3 | top: 0px; 4 | bottom: 0px; 5 | left: 0px; 6 | right: 0px; 7 | box-sizing: content-box; 8 | z-index: 999999; 9 | background: rgba(0, 0, 0, 0.8); 10 | } 11 | 12 | .step-tooltip-active-matte { 13 | position: absolute; 14 | transition-duration: 150ms; 15 | transition-property: transform; 16 | will-change: transform; 17 | transition-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1); 18 | } 19 | 20 | .step-tooltip-active-container { 21 | position: absolute; 22 | z-index: 999998; 23 | box-sizing: border-box; 24 | word-break: break-all; 25 | background: #f9f9f9; 26 | max-width: 500px; 27 | min-width: 100px; 28 | color: #000; 29 | padding: 24px; 30 | border-radius: 5px; 31 | -webkit-box-shadow: 0 5px 30px rgba(0, 0, 0, 0.1); 32 | box-shadow: 0 5px 30px rgba(0, 0, 0, 0.1); 33 | 34 | transition-property: transform; 35 | will-change: transform; 36 | transform: translate(0, 0); 37 | transition: transform 0.15s ease-out; 38 | } 39 | 40 | #step-tooltip-active-container-text { 41 | margin-bottom: 20px; 42 | } 43 | 44 | .step-tooltip-next, 45 | .step-tooltip-back, 46 | .step-tooltip-end { 47 | padding: 4px 10px; 48 | font-size: 12px; 49 | color: #2d2d2d; 50 | background: #fff; 51 | border: 1px solid #dadada; 52 | border-radius: 10%; 53 | cursor: pointer; 54 | outline: none; 55 | box-sizing: border-box; 56 | margin: 0 2px; 57 | } 58 | 59 | .step-tooltip-footer { 60 | display: flex; 61 | justify-content: space-between; 62 | } 63 | 64 | .scroll-disabled { 65 | overflow: hidden; 66 | -webkit-overflow-scrolling: hidden; 67 | } 68 | 69 | .step-tooltip-disabled-btn[disabled] { 70 | cursor: not-allowed; 71 | background: #dadada; 72 | color: #f6f6f6; 73 | } 74 | 75 | /* arrow */ 76 | .step-tooltip-arrow-bottom :after, 77 | .step-tooltip-arrow-top :after, 78 | .step-tooltip-arrow-left :after, 79 | .step-tooltip-arrow-right :after { 80 | border: solid transparent; 81 | content: ' '; 82 | height: 0; 83 | width: 0; 84 | position: absolute; 85 | pointer-events: none; 86 | border-width: 10px; 87 | } 88 | 89 | .step-tooltip-arrow-bottom :after { 90 | bottom: 100%; 91 | border-bottom-color: #fff; 92 | margin-left: -10px; 93 | } 94 | .step-tooltip-arrow-top :after { 95 | top: 100%; 96 | border-top-color: #fff; 97 | margin-left: -10px; 98 | } 99 | .step-tooltip-arrow-left :after { 100 | left: 100%; 101 | border-left-color: #fff; 102 | margin-top: -10px; 103 | } 104 | .step-tooltip-arrow-right :after { 105 | right: 100%; 106 | border-right-color: #fff; 107 | margin-top: -10px; 108 | } 109 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | getElement, 3 | calculatePositions, 4 | calculateArrowPosition, 5 | OFFSET 6 | } from './util.js' 7 | 8 | import './index.css' 9 | class StepTip { 10 | // 默认定位 11 | static defaultPlacement = 'bottom' 12 | // 整体的背景墙 13 | static backdropHTML = '
' 14 | // 底部按钮 Done Back Next 15 | constructor(options) { 16 | // 配置项 17 | this.options = {...{ 18 | initialText: '哈喽,准备好了解 step-tooltip 了么?', 19 | steps: [], // {element, container} 20 | backdropColor: 'rgb(0 0 0 / 0.56)', 21 | options: { 22 | backLabel: '上一步', 23 | nextLabel: '下一步', 24 | skipLabel: '我知道了', 25 | doneLabel: '完成', 26 | } 27 | },...options} 28 | 29 | this.startStep() 30 | } 31 | 32 | setListener() { 33 | getElement('#step-tooltip-back').addEventListener('click', () => this.toggleStep(-1)) 34 | getElement('#step-tooltip-next').addEventListener('click', () => this.toggleStep(1)) 35 | getElement('#step-tooltip-end').addEventListener('click', () => this.endStep()) 36 | } 37 | 38 | removeListener() { 39 | getElement('#step-tooltip-back').removeEventListener('click', () => {}) 40 | getElement('#step-tooltip-next').removeEventListener('click', () => {}) 41 | getElement('#step-tooltip-end').removeEventListener('click', () => {}) 42 | } 43 | 44 | startStep() { 45 | // 当前 stepIndex 46 | this.stepIndex = 0 47 | 48 | getElement("body").innerHTML += StepTip.backdropHTML 49 | this.backdrop = getElement('#step-tooltip-backdrop') 50 | this.selfStyle = document.head.appendChild(document.createElement("style")) 51 | 52 | this.setup() 53 | this.setListener() 54 | } 55 | // 初始化工作 56 | setup() { 57 | const { 58 | steps 59 | } = this.options 60 | 61 | if (!steps || !steps.length) return 62 | 63 | const current = steps[this.stepIndex] 64 | 65 | const { 66 | element, 67 | container, 68 | } = current 69 | 70 | let placement = current.hasOwnProperty('placement') ? current.placement : StepTip.defaultPlacement 71 | 72 | const innerWidth = window.innerWidth 73 | // 对于宽度很小的场景,应该考虑展示在下方或者上面 74 | if (innerWidth <= 400 && (placement === 'left' || placement === 'right')) placement = StepTip.defaultPlacement 75 | 76 | const elm = getElement(element) 77 | 78 | // 错误 79 | if (!elm) throw new Error('Cant\'t find' + element + ', please check for spelling mistakes.') 80 | 81 | // 禁止 body 滚动 82 | getElement('body').classList.add('scroll-disabled') 83 | 84 | // 让元素滚动到可视区域内 scrollIntoViewIfNeeded 兼容性不行 85 | elm.scrollIntoView({ 86 | behavior: 'smooth', 87 | block: 'center' 88 | }) 89 | 90 | const elmRect = elm.getBoundingClientRect() 91 | 92 | // 生成一个DOM元素 93 | const activeElm = this.createActiveMatte(elmRect, getComputedStyle(elm)) 94 | const containerElm = this.createContainer(container) 95 | 96 | // 创建 arrow css 97 | const placements = ['top', 'left', 'bottom', 'right'] 98 | placements.forEach(v => containerElm.classList.remove(`step-tooltip-arrow-${v}`)) 99 | containerElm.removeAttribute('data-top') 100 | containerElm.removeAttribute('data-left') 101 | 102 | containerElm.classList.add(`step-tooltip-arrow-${placement}`) 103 | 104 | let position = calculatePositions(elm, containerElm, placement) 105 | 106 | let { 107 | width 108 | } = containerElm.getBoundingClientRect() 109 | 110 | 111 | // 是否在可视区域 112 | if (position.x + width >= innerWidth) { 113 | // 向左移 114 | position.x = Math.round(elmRect.right - width + OFFSET) 115 | } else if (position.x <= 0) { 116 | // 向右移 117 | position.x = Math.round(elmRect.x - OFFSET) 118 | 119 | // 如果宽度小于浏览器宽度,则需要调整宽度 120 | if (width >= innerWidth) { 121 | containerElm.style.width = (innerWidth - position.x * 2) + 'px' 122 | } 123 | } 124 | 125 | const arrowStyle = calculateArrowPosition(activeElm, position, placement) 126 | document.head.removeChild(this.selfStyle) 127 | this.selfStyle = document.head.appendChild(document.createElement("style")) 128 | if (placement === 'left' || placement === 'right') { 129 | this.selfStyle.innerHTML = `.step-tooltip-arrow-left :after {top:${arrowStyle.top}} .step-tooltip-arrow-right :after {top:${arrowStyle.top}}` 130 | } else { 131 | this.selfStyle.innerHTML = `.step-tooltip-arrow-bottom :after {left:${arrowStyle.left}} .step-tooltip-arrow-top :after {left:${arrowStyle.left}}` 132 | } 133 | 134 | // 有了 DOM ,也有了偏移量 135 | containerElm.style.transform = `translate(${position.x}px,${position.y}px)` 136 | 137 | } 138 | 139 | // 切换前后 140 | toggleStep(move) { 141 | const { 142 | steps 143 | } = this.options 144 | 145 | this.stepIndex += move 146 | 147 | if (this.stepIndex >= 0 && this.stepIndex < steps.length) { 148 | return this.setup() 149 | } else { 150 | this.endStep() 151 | } 152 | } 153 | 154 | // 终止整个流程 155 | endStep() { 156 | getElement('body').classList.remove('scroll-disabled') 157 | document.head.removeChild(this.selfStyle) 158 | 159 | this.removeListener() 160 | 161 | this.backdrop.parentNode.removeChild(this.backdrop) 162 | this.backdrop = null 163 | 164 | this.stepIndex = 0 165 | 166 | // TODO 调用 onComplete 事件 167 | } 168 | 169 | // 创建 active 蒙版 div 170 | createActiveMatte(elmRect,styles) { 171 | const { backdropColor } = this.options 172 | let activeElm = getElement("#step-tooltip-backdrop .step-tooltip-active-matte") 173 | if (!activeElm) { 174 | activeElm = document.createElement("div") 175 | activeElm.setAttribute("id", "step-tooltip-active-matte") 176 | activeElm.classList.add("step-tooltip-active-matte") 177 | this.backdrop.append(activeElm) 178 | } 179 | activeElm.style.top = Math.round(elmRect.top) + "px" 180 | activeElm.style.left = Math.round(elmRect.left) + "px" 181 | activeElm.style.height = elmRect.height + "px" 182 | activeElm.style.width = elmRect.width + "px" 183 | activeElm.style.borderRadius = styles.borderRadius 184 | activeElm.style.boxShadow = "0 0 0 9999px " + backdropColor 185 | return activeElm 186 | } 187 | 188 | // 创建一个容器,添加配置信息 189 | createContainer(container) { 190 | const { 191 | steps 192 | } = this.options 193 | 194 | let containerElm = getElement('#step-tooltip-backdrop .step-tooltip-active-container') 195 | 196 | // 如果不存在则需要重新创建 197 | if (!containerElm) { 198 | containerElm = document.createElement('div') 199 | 200 | // 分层,提高动画性能 201 | containerElm.style.willChange = 'transform' 202 | containerElm.classList.add('step-tooltip-active-container') 203 | // 自定义内容 204 | containerElm.innerHTML += "
" 205 | // 自定义footer 206 | containerElm.innerHTML += `` 207 | 208 | this.backdrop.append(containerElm) 209 | } 210 | 211 | // button 内置样式 212 | const backBtn = getElement('#step-tooltip-back') 213 | const nextBtn = getElement('#step-tooltip-next') 214 | 215 | // 第一次的时候,禁用 pre 216 | if (this.stepIndex === 0) { 217 | backBtn.setAttribute('disabled', true) 218 | backBtn.classList.add('step-tooltip-disabled-btn') 219 | } else { 220 | backBtn.removeAttribute('disabled', true) 221 | backBtn.classList.remove('step-tooltip-disabled-btn') 222 | } 223 | 224 | nextBtn.innerText = this.stepIndex === steps.length - 1 ? this.options.options.doneLabel : this.options.options.nextLabel 225 | 226 | // 内容区域展示配置信息 227 | getElement('#step-tooltip-active-container-text').innerHTML = container 228 | 229 | return containerElm 230 | } 231 | } 232 | 233 | // 返回一个函数,返回实例 234 | export default options => new StepTip(options) 235 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | // 常量 2 | export const OFFSET = 15 3 | 4 | // 简化 selector 5 | export const getElement = (selector) => document.querySelector(selector) 6 | 7 | // 定位 8 | export const calculatePositions = (el, desc, placement = 'bottom') => { 9 | let position 10 | const elRect = el.getBoundingClientRect() 11 | const descRect = desc.getBoundingClientRect() 12 | 13 | const c = placement[0] 14 | if (c === 't' || c === 'b') { 15 | position = { 16 | x: Math.round(elRect.x + (elRect.width - descRect.width)/2), 17 | y: c === 't' ? 18 | Math.round(elRect.y - descRect.height - OFFSET) : Math.round(elRect.y + elRect.height + OFFSET) 19 | } 20 | } else { 21 | position = { 22 | x: c === 'l' ? 23 | Math.round(elRect.x - descRect.width - OFFSET) : Math.round(elRect.x + elRect.width + OFFSET), 24 | y: Math.round(elRect.y + (elRect.height - descRect.height) / 2) 25 | } 26 | } 27 | 28 | return position 29 | } 30 | 31 | 32 | export const calculateArrowPosition = (el, position, placement='bottom') => { 33 | const c = placement[0] 34 | let style = {} 35 | 36 | const elRect = el.getBoundingClientRect() 37 | if(c === 't' || c === 'b') { 38 | style.left = Math.round(elRect.width/2 + elRect.x - position.x) + 'px' 39 | if(c === 't') { 40 | style.top = '100%' 41 | } else { 42 | style.bottom = '100%' 43 | } 44 | 45 | } else { 46 | style.top = Math.round(elRect.height/2 + elRect.y - position.y) + 'px' 47 | if(c === 'l') { 48 | style.left = '100%' 49 | } else { 50 | style.right = '100%' 51 | } 52 | } 53 | return style 54 | } 55 | -------------------------------------------------------------------------------- /test/test.md: -------------------------------------------------------------------------------- 1 | > 该文件夹为测试目录,用存储编写测试文件,主要是单元测试。 2 | 3 | #### 单元测试(`Unit Test`) 4 | 以function 为最小单位,验证在特定情况下的input 和output 是否正确。 5 | 6 | 7 | ##### 为什么需要测试 8 | http://www.hollischuang.com/archives/954 9 | 10 | #### 测试工具 11 | - [mocha](https://mochajs.cn/) 12 | - [chaijs](https://www.chaijs.com/) 13 | - [node-tap](https://node-tap.org/) --------------------------------------------------------------------------------