├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── dist ├── dist.js ├── dist.min.js ├── index.js └── src │ ├── HighlightElement.js │ ├── InfoElement.js │ ├── css.js │ └── utils.js ├── index.html ├── index.ts ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── HighlightElement.ts ├── InfoElement.ts ├── css.ts └── utils.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "overrides": [ 8 | ], 9 | "parserOptions": { 10 | "parser": 'babel-eslint', 11 | "ecmaVersion": 12, 12 | "sourceType": "module", 13 | "allowImportExportEverywhere": true 14 | }, 15 | "rules": { 16 | } 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | */.DS_Store 2 | dist 3 | node_modules 4 | *.json 5 | *.md 6 | .eslintrc.js 7 | .prettierignore 8 | .prettierrc -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | printWidth: 80 4 | trailingComma: 'none' 5 | arrowParens: 'avoid' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple novice guide 2 | 3 | 一个简单的新手引导库。 4 | 5 | # 安装 6 | 7 | ```bash 8 | npm i simple-novice-guide 9 | ``` 10 | 11 | # 使用 12 | 13 | ```js 14 | import SimpleNoviceGuide from 'simple-novice-guide' 15 | 16 | new SimpleNoviceGuide({ 17 | steps: [ 18 | { 19 | element: '#id', 20 | title: '我是标题', 21 | text: '我是信息', 22 | img: '我是图片' 23 | } 24 | ] 25 | }).start() 26 | ``` 27 | 28 | 如果要使用`umd`格式的文件,可以安装完后在`node_modules/simple-novice-guide/dist/`目录里选择使用`dist.js`或`dist.min.js`文件。 29 | 30 | # 本地开发 31 | 32 | 1.开启`ts`编译 33 | 34 | ```bash 35 | npm run tsc 36 | ``` 37 | 38 | 2.开启打包编译 39 | 40 | ```bash 41 | npm run build 42 | ``` 43 | 44 | 3.开启页面服务 45 | 46 | ```bash 47 | npx http-server -e js -c-1 48 | ``` 49 | 50 | 访问`[ip][port]/index.html`。 51 | 52 | 然后就可以愉快的修改代码了,不过没有热更新功能哦,所以记得修改后要刷新页面。 53 | 54 | # 文档 55 | 56 | ## 创建实例 57 | 58 | ```js 59 | const noviceGuide = new SimpleNoviceGuide(options) 60 | ``` 61 | 62 | ### 参数options 63 | 64 | 对象类型,可以传递以下选项: 65 | 66 | | 属性 | 类型 | 默认值 | 描述 | 67 | | ---------------- | -------- | ------------------ | ------------------------------------------------------------ | 68 | | steps | array | | 步骤数据,必填,信息数据见下表 | 69 | | padding | number | 10 | 高亮元素和信息框元素的内边距,单位`px` | 70 | | margin | number | 10 | 高亮元素和信息框元素之间的间距,单位`px` | 71 | | boxShadowColor | string | rgba(0, 0, 0, 0.5) | 高亮元素的box-shadow颜色 | 72 | | transition | string | all 0.3s ease-out | 高亮元素过渡效果 | 73 | | borderRadius | string | 5px | 高亮元素和信息框元素的圆角 | 74 | | highlightElClass | string | | 要添加到高亮元素上的css类名 | 75 | | backgroundColor | string | \#fff | 信息框元素的背景颜色 | 76 | | infoElClass | string | | 要添加到信息框元素上的css类名 | 77 | | prevText | string | 上一步 | 上一步按钮的文字 | 78 | | nextText | string | 下一步 | 下一步按钮的文字 | 79 | | completeText | string | 完成 | 完成按钮的文字 | 80 | | showIndicator | boolean | true | 是否显示信息框内的指示器 | 81 | | zIndex | number | 9999 | 高亮元素和信息框的z-index | 82 | | useCustomInfo | boolean | false | 是否使用自定义的信息框,如果开启,需要传递getCustomInfoEl选项 | 83 | | getCustomInfoEl | function | null | 返回自定义信息框元素 | 84 | 85 | ### options.steps属性 86 | 87 | `options.steps`属性值需为一个对象数组,对象的结构如下: 88 | 89 | | 属性 | 类型 | 默认值 | 描述 | 90 | | ------- | --------------------- | ------ | ------------------------------------------------------------ | 91 | | element | HTMLElement \| string | | 该步骤需要高亮的`html`元素,可以是一个选择器,也可以是`dom`节点对象,如果当前步骤不需要高亮元素,也可以不传 | 92 | | title | string \| number | | 当前步骤的标题 | 93 | | text | string \| number | | 当前步骤的信息 | 94 | | img | string | | 当前步骤的图片 | 95 | 96 | 97 | 98 | ## 实例属性 99 | 100 | ### noviceGuide.options 101 | 102 | 选项对象。 103 | 104 | 105 | 106 | ### noviceGuide.steps 107 | 108 | 步骤列表数据。 109 | 110 | 111 | 112 | ### noviceGuide.currentStepIndex 113 | 114 | 当前所在步骤的索引。 115 | 116 | 117 | 118 | ## 实例方法 119 | 120 | ### noviceGuide.start() 121 | 122 | 开始。 123 | 124 | 125 | 126 | ### noviceGuide.next() 127 | 128 | 下一步。 129 | 130 | 131 | 132 | ### noviceGuide.prev() 133 | 134 | 上一步。 135 | 136 | 137 | 138 | ### noviceGuide.jump(stepIndex: number) 139 | 140 | 跳转到指定步骤。 141 | 142 | 143 | 144 | ### noviceGuide.done() 145 | 146 | 结束。 147 | 148 | 149 | 150 | ### noviceGuide.isFirstStep() 151 | 152 | 是否是第一步。 153 | 154 | 155 | 156 | ### noviceGuide.isLastStep() 157 | 158 | 是否是最后一步。 159 | 160 | 161 | 162 | ### noviceGuide.on(eventName, (...args) => {}) 163 | 164 | 监听事件。 165 | 166 | 事件发送继承的是[eventemitter3](https://github.com/primus/eventemitter3),详细文档可以参考它的文档。 167 | 168 | 实例会发出的事件如下: 169 | 170 | | 事件名 | 回调参数 | 描述 | 171 | | ------------------ | ------------------------- | ------------ | 172 | | before-step-change | stepIndex(当前步骤索引) | 即将切换步骤 | 173 | | after-step-change | stepIndex(当前步骤索引) | 步骤切换完毕 | 174 | | done | | 新手引导结束 | 175 | 176 | 177 | 178 | ### noviceGuide.emit(eventName, ...args) 179 | 180 | 发送事件。 181 | 182 | 183 | 184 | ### noviceGuide.off(eventName, fn?) 185 | 186 | 解除监听事件。 187 | 188 | 189 | 190 | ## 自定义信息框 191 | 192 | 如果内置的信息框无法满足你的需求,也可以自定义信息框,首先实例化时需要传递以下两个参数: 193 | 194 | ```js 195 | const noviceGuide = new SimpleNoviceGuide({ 196 | useCustomInfo: true, 197 | getCustomInfoEl: async (step) => { 198 | return document.querySelector('.customInfoBox') 199 | } 200 | }) 201 | ``` 202 | 203 | `getCustomInfoEl`方法需要返回你自定义的信息框的节点,考虑到可能有异步的操作,所以统一返回一个`Promise`。 204 | 205 | 注意你自定义的信息框元素需要设置绝对定位,`z-index`也是必不可少的: 206 | 207 | ```css 208 | .customInfoBox { 209 | position: absolute; 210 | z-index: 99999; 211 | } 212 | ``` 213 | 214 | 然后需要在你的信息框中创建相应的上一步、下一步、完成的按钮,然后手动调用下列方法: 215 | 216 | ```js 217 | noviceGuide.prev() 218 | 219 | noviceGuide.next() 220 | 221 | noviceGuide.done() 222 | ``` 223 | 224 | 通常还需要监听`done`事件来删除或隐藏你的自定义信息框: 225 | 226 | ```js 227 | noviceGuide.on('done', () => { 228 | customInfoBoxEl.style.display = 'none' 229 | }) 230 | ``` 231 | 232 | -------------------------------------------------------------------------------- /dist/dist.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.SimpleNoviceGuide = factory()); 5 | })(this, (function () { 'use strict'; 6 | 7 | var prefix = 'simple-novice-guide-'; 8 | var getScrollAncestor = function (el) { 9 | var style = window.getComputedStyle(el); 10 | var isAbsolute = style.position === 'absolute'; 11 | var isFixed = style.position === 'fixed'; 12 | var reg = /(auto|scroll)/; 13 | if (isFixed) 14 | return document.body; 15 | var parent = el.parentElement; 16 | while (parent) { 17 | style = window.getComputedStyle(parent); 18 | if (!(isAbsolute && style.position === 'static')) { 19 | if (reg.test(style.overflow + style.overflowX + style.overflowY)) { 20 | return parent; 21 | } 22 | } 23 | parent = parent.parentElement; 24 | } 25 | return document.body; 26 | }; 27 | var scrollAncestorToElement = function (el) { 28 | var parent = getScrollAncestor(el); 29 | if (parent === document.body) 30 | return; 31 | var parentRect = parent.getBoundingClientRect(); 32 | var rect = el.getBoundingClientRect(); 33 | parent.scrollTop = parent.scrollTop + rect.top - parentRect.top; 34 | scrollAncestorToElement(parent); 35 | }; 36 | var elementIsInView = function (el) { 37 | var rect = el.getBoundingClientRect(); 38 | return (rect.top >= 0 && 39 | rect.left >= 0 && 40 | rect.bottom <= window.innerHeight && 41 | rect.right <= window.innerWidth); 42 | }; 43 | var loadImage = function (img) { 44 | return new Promise(function (resolve, reject) { 45 | var image = new Image(); 46 | image.onload = resolve; 47 | image.onerror = reject; 48 | image.src = img; 49 | }); 50 | }; 51 | 52 | var HighlightElement$1 = (function () { 53 | function HighlightElement(app) { 54 | this.app = app; 55 | this.app = app; 56 | this.el = null; 57 | } 58 | HighlightElement.prototype.show = function (step) { 59 | if (!this.el) { 60 | this.createEl(); 61 | } 62 | var left = 0, top = 0, width = 0, height = 0; 63 | if (step.element) { 64 | var rect = step.element.getBoundingClientRect(); 65 | var padding = this.app.options.padding; 66 | left = rect.left + window.pageXOffset - padding; 67 | top = rect.top + window.pageYOffset - padding; 68 | width = rect.width + padding * 2; 69 | height = rect.height + padding * 2; 70 | } 71 | else { 72 | left = window.innerWidth / 2 + window.pageXOffset; 73 | top = window.innerHeight / 2 + window.pageYOffset; 74 | width = 0; 75 | height = 0; 76 | } 77 | this.el.style.left = left + 'px'; 78 | this.el.style.top = top + 'px'; 79 | this.el.style.width = width + 'px'; 80 | this.el.style.height = height + 'px'; 81 | }; 82 | HighlightElement.prototype.createEl = function () { 83 | var _a = this.app.options, boxShadowColor = _a.boxShadowColor, transition = _a.transition, borderRadius = _a.borderRadius, highlightElClass = _a.highlightElClass, zIndex = _a.zIndex; 84 | this.el = document.createElement('div'); 85 | this.el.className = prefix + 'highlight-el'; 86 | this.el.style.cssText = "\n box-shadow: 0 0 0 5000px ".concat(boxShadowColor, ";\n border-radius: ").concat(borderRadius, ";\n transition: ").concat(transition, ";\n z-index: ").concat(zIndex, ";\n "); 87 | if (highlightElClass) { 88 | this.el.classList.add(highlightElClass); 89 | } 90 | document.body.appendChild(this.el); 91 | }; 92 | HighlightElement.prototype.removeEl = function () { 93 | if (this.el) { 94 | document.body.removeChild(this.el); 95 | this.el = null; 96 | } 97 | }; 98 | return HighlightElement; 99 | }()); 100 | 101 | var __awaiter$1 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { 102 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 103 | return new (P || (P = Promise))(function (resolve, reject) { 104 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 105 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 106 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 107 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 108 | }); 109 | }; 110 | var __generator$1 = (undefined && undefined.__generator) || function (thisArg, body) { 111 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 112 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 113 | function verb(n) { return function (v) { return step([n, v]); }; } 114 | function step(op) { 115 | if (f) throw new TypeError("Generator is already executing."); 116 | while (g && (g = 0, op[0] && (_ = 0)), _) try { 117 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 118 | if (y = 0, t) op = [op[0] & 2, t.value]; 119 | switch (op[0]) { 120 | case 0: case 1: t = op; break; 121 | case 4: _.label++; return { value: op[1], done: false }; 122 | case 5: _.label++; y = op[1]; op = [0]; continue; 123 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 124 | default: 125 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 126 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 127 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 128 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 129 | if (t[2]) _.ops.pop(); 130 | _.trys.pop(); continue; 131 | } 132 | op = body.call(thisArg, _); 133 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 134 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 135 | } 136 | }; 137 | var HighlightElement = (function () { 138 | function HighlightElement(app) { 139 | this.app = app; 140 | this.app = app; 141 | this.el = null; 142 | this.app.on('after-step-change', this.onStepChange.bind(this)); 143 | } 144 | HighlightElement.prototype.show = function (step) { 145 | return __awaiter$1(this, void 0, void 0, function () { 146 | var el, res; 147 | return __generator$1(this, function (_a) { 148 | switch (_a.label) { 149 | case 0: 150 | if (!(this.app.options.useCustomInfo && this.app.options.getCustomInfoEl)) return [3, 2]; 151 | return [4, this.app.options.getCustomInfoEl(step)]; 152 | case 1: 153 | el = _a.sent(); 154 | res = this.getInfoRect(step, el); 155 | el.style.left = res.left + 'px'; 156 | el.style.top = res.top + 'px'; 157 | return [3, 4]; 158 | case 2: return [4, this.showInnerInfo(step)]; 159 | case 3: 160 | _a.sent(); 161 | _a.label = 4; 162 | case 4: return [2]; 163 | } 164 | }); 165 | }); 166 | }; 167 | HighlightElement.prototype.showInnerInfo = function (step) { 168 | return __awaiter$1(this, void 0, void 0, function () { 169 | var error_1, res; 170 | return __generator$1(this, function (_a) { 171 | switch (_a.label) { 172 | case 0: 173 | if (!this.el) { 174 | this.createEl(); 175 | } 176 | if (!step.img) return [3, 4]; 177 | _a.label = 1; 178 | case 1: 179 | _a.trys.push([1, 3, , 4]); 180 | return [4, loadImage(step.img)]; 181 | case 2: 182 | _a.sent(); 183 | return [3, 4]; 184 | case 3: 185 | error_1 = _a.sent(); 186 | console.error(error_1); 187 | return [3, 4]; 188 | case 4: 189 | this.el.innerHTML = this.createHTML(step); 190 | res = this.getInfoRect(step, this.el); 191 | this.el.style.left = res.left + 'px'; 192 | this.el.style.top = res.top + 'px'; 193 | return [2]; 194 | } 195 | }); 196 | }); 197 | }; 198 | HighlightElement.prototype.getInfoRect = function (step, el) { 199 | if (step.element) { 200 | return this.computeInfoPosition(step, el); 201 | } 202 | else { 203 | var rect = el.getBoundingClientRect(); 204 | return { 205 | left: (window.innerWidth - rect.width) / 2 + window.pageXOffset, 206 | top: (window.innerHeight - rect.height) / 2 + window.pageYOffset 207 | }; 208 | } 209 | }; 210 | HighlightElement.prototype.createHTML = function (step) { 211 | var _this = this; 212 | var _a = this.app.options, prevText = _a.prevText, nextText = _a.nextText, showIndicator = _a.showIndicator; 213 | return "\n
\n
").concat(step.title || '', "
\n
\u00D7
\n
\n
\n ").concat(step.img 214 | ? "") 215 | : '', "\n
").concat(step.text || '', "
\n
\n
\n ").concat(showIndicator 216 | ? this.app.steps 217 | .map(function (_, index) { 218 | return "
"); 219 | }) 220 | .join('\n') 221 | : '', "\n
\n
\n
").concat(prevText, "
\n
").concat(nextText, "
\n
\n "); 222 | }; 223 | HighlightElement.prototype.createEl = function () { 224 | var _a = this.app.options, padding = _a.padding, borderRadius = _a.borderRadius, backgroundColor = _a.backgroundColor, infoElClass = _a.infoElClass, zIndex = _a.zIndex; 225 | this.el = document.createElement('div'); 226 | this.el.className = prefix + 'info-el'; 227 | this.el.style.cssText = "\n background-color: ".concat(backgroundColor, "; \n padding: ").concat(padding, "px;\n border-radius: ").concat(borderRadius, ";\n z-index: ").concat(zIndex, ";\n "); 228 | if (infoElClass) { 229 | this.el.classList.add(infoElClass); 230 | } 231 | document.body.appendChild(this.el); 232 | this.el.addEventListener('click', this.onClick.bind(this)); 233 | }; 234 | HighlightElement.prototype.onClick = function (e) { 235 | var type = e.target.getAttribute('data-type'); 236 | switch (type) { 237 | case 'close': 238 | this.app.done(); 239 | break; 240 | case 'prev': 241 | this.app.prev(); 242 | break; 243 | case 'next': 244 | this.app.next(); 245 | break; 246 | case 'indicator': 247 | var index = e.target.getAttribute('data-index'); 248 | if (!Number.isNaN(Number(index))) { 249 | this.app.jump(Number(index)); 250 | } 251 | break; 252 | } 253 | }; 254 | HighlightElement.prototype.removeEl = function () { 255 | if (this.el) { 256 | document.body.removeChild(this.el); 257 | this.el = null; 258 | } 259 | }; 260 | HighlightElement.prototype.onStepChange = function (stepIndex) { 261 | var _a = this.app.options, nextText = _a.nextText, completeText = _a.completeText, useCustomInfo = _a.useCustomInfo; 262 | if (useCustomInfo) 263 | return; 264 | var prevEl = document.querySelector(".".concat(prefix, "info-el-btn-prev")); 265 | var nextEl = document.querySelector(".".concat(prefix, "info-el-btn-next")); 266 | prevEl.classList.remove('disabled'); 267 | nextEl.textContent = nextText; 268 | if (this.app.isFirstStep()) { 269 | prevEl.classList.add('disabled'); 270 | } 271 | if (this.app.isLastStep()) { 272 | nextEl.textContent = completeText; 273 | } 274 | var indicatorEls = Array.from(document.querySelectorAll(".".concat(prefix, "info-el-indicator-item"))); 275 | indicatorEls.forEach(function (item) { 276 | if (item.classList.contains('active')) { 277 | item.classList.remove('active'); 278 | } 279 | }); 280 | if (indicatorEls[stepIndex]) { 281 | indicatorEls[stepIndex].classList.add('active'); 282 | } 283 | }; 284 | HighlightElement.prototype.computeInfoPosition = function (step, el) { 285 | var _a = this.app.options, padding = _a.padding, margin = _a.margin; 286 | var windowWidth = window.innerWidth; 287 | var windowHeight = window.innerHeight; 288 | var windowPageXOffset = window.pageXOffset; 289 | var windowPageYOffset = window.pageYOffset; 290 | var rect = step.element.getBoundingClientRect(); 291 | var infoRect = el.getBoundingClientRect(); 292 | var left = 0; 293 | var top = 0; 294 | var adjustLeft = function () { 295 | if (windowWidth - rect.left - padding >= infoRect.width) { 296 | return rect.left - padding + windowPageXOffset; 297 | } 298 | else if (rect.right + padding >= infoRect.width) { 299 | return rect.right + padding - infoRect.width + windowPageXOffset; 300 | } 301 | else { 302 | return (windowWidth - infoRect.width) / 2 + windowPageXOffset; 303 | } 304 | }; 305 | var adjustTop = function () { 306 | if (windowHeight - rect.top - padding >= infoRect.height) { 307 | return rect.top - padding + windowPageYOffset; 308 | } 309 | else if (rect.bottom + padding >= infoRect.height) { 310 | return rect.bottom + padding - infoRect.height + windowPageYOffset; 311 | } 312 | else { 313 | return (windowHeight - infoRect.height) / 2 + windowPageYOffset; 314 | } 315 | }; 316 | if (rect.bottom + padding + margin + infoRect.height <= windowHeight && 317 | infoRect.width <= windowWidth) { 318 | left = adjustLeft(); 319 | top = rect.bottom + padding + margin + windowPageYOffset; 320 | } 321 | else if (rect.top - padding - margin >= infoRect.height && 322 | infoRect.width <= windowWidth) { 323 | left = adjustLeft(); 324 | top = rect.top - padding - margin - infoRect.height + windowPageYOffset; 325 | } 326 | else if (rect.left - padding - margin >= infoRect.width && 327 | infoRect.height <= windowHeight) { 328 | left = rect.left - padding - margin - infoRect.width + windowPageXOffset; 329 | top = adjustTop(); 330 | } 331 | else if (rect.right + padding + margin + infoRect.width <= windowWidth && 332 | infoRect.height <= windowHeight) { 333 | left = rect.right + padding + margin + windowPageXOffset; 334 | top = adjustTop(); 335 | } 336 | else { 337 | var totalHeightLessThenWindow = rect.height + padding * 2 + margin + infoRect.height <= windowHeight; 338 | if (totalHeightLessThenWindow && 339 | Math.max(rect.width + padding * 2, infoRect.width) <= windowWidth) { 340 | var newTop = (windowHeight - 341 | (rect.height + padding * 2 + margin + infoRect.height)) / 342 | 2; 343 | window.scrollBy(0, rect.top - newTop); 344 | } 345 | left = adjustLeft(); 346 | top = rect.bottom + padding + margin + windowPageYOffset; 347 | } 348 | return { 349 | left: left, 350 | top: top 351 | }; 352 | }; 353 | return HighlightElement; 354 | }()); 355 | 356 | var styleEl = null; 357 | var addCss = function () { 358 | var cssText = ''; 359 | cssText += "\n .".concat(prefix, "highlight-el {\n position: absolute;\n }\n "); 360 | cssText += "\n .".concat(prefix, "info-el {\n position: absolute;\n min-width: 250px;\n max-width: 300px;\n }\n\n .").concat(prefix, "info-el-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n\n .").concat(prefix, "info-el-title {\n font-size: 18px;\n margin: 0;\n padding: 0;\n font-weight: 700;\n }\n\n .").concat(prefix, "info-el-close {\n cursor: pointer;\n color: #616161;\n font-size: 22px;\n font-weight: 700;\n }\n\n .").concat(prefix, "info-el-info {\n padding: 15px 0;\n }\n\n .").concat(prefix, "info-el-info-img {\n width: 100%;\n }\n\n .").concat(prefix, "info-el-info-text {\n\n }\n\n .").concat(prefix, "info-el-indicator {\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 10px;\n }\n\n .").concat(prefix, "info-el-indicator-item {\n width: 6px;\n height: 6px;\n background: #ccc;\n transition: width .1s ease-in;\n border-radius: 10px;\n cursor: pointer;\n margin: 0 2px;\n }\n\n .").concat(prefix, "info-el-indicator-item.active, .").concat(prefix, "info-el-indicator-item:hover {\n width: 15px;\n background: #999;\n }\n\n .").concat(prefix, "info-el-btn-group {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-top: 1px solid #e0e0e0;\n padding-top: 10px;\n }\n\n .").concat(prefix, "info-el-btn {\n width: 60px;\n height: 35px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid #bdbdbd;\n text-shadow: 1px 1px 0 #fff;\n font-size: 14px;\n color: #424242;\n white-space: nowrap;\n cursor: pointer;\n background-color: #f4f4f4;\n border-radius: 3px;\n }\n\n .").concat(prefix, "info-el-btn.disabled {\n color: #9e9e9e;\n border-color: #bdbdbd;\n cursor: default;\n background-color: #f4f4f4;\n }\n\n .").concat(prefix, "info-el-btn:hover {\n border-color: #9e9e9e;\n background-color: #e0e0e0;\n color: #212121;\n }\n\n .").concat(prefix, "info-el-btn.disabled:hover {\n color: #9e9e9e;\n border-color: #bdbdbd;\n cursor: default;\n background-color: #f4f4f4;\n }\n "); 361 | styleEl = document.createElement('style'); 362 | styleEl.innerHTML = cssText; 363 | document.head.appendChild(styleEl); 364 | }; 365 | var removeCss = function () { 366 | if (styleEl) { 367 | document.head.removeChild(styleEl); 368 | } 369 | }; 370 | 371 | function createCommonjsModule(fn, module) { 372 | return module = { exports: {} }, fn(module, module.exports), module.exports; 373 | } 374 | 375 | var eventemitter3 = createCommonjsModule(function (module) { 376 | 377 | var has = Object.prototype.hasOwnProperty 378 | , prefix = '~'; 379 | 380 | /** 381 | * Constructor to create a storage for our `EE` objects. 382 | * An `Events` instance is a plain object whose properties are event names. 383 | * 384 | * @constructor 385 | * @private 386 | */ 387 | function Events() {} 388 | 389 | // 390 | // We try to not inherit from `Object.prototype`. In some engines creating an 391 | // instance in this way is faster than calling `Object.create(null)` directly. 392 | // If `Object.create(null)` is not supported we prefix the event names with a 393 | // character to make sure that the built-in object properties are not 394 | // overridden or used as an attack vector. 395 | // 396 | if (Object.create) { 397 | Events.prototype = Object.create(null); 398 | 399 | // 400 | // This hack is needed because the `__proto__` property is still inherited in 401 | // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5. 402 | // 403 | if (!new Events().__proto__) prefix = false; 404 | } 405 | 406 | /** 407 | * Representation of a single event listener. 408 | * 409 | * @param {Function} fn The listener function. 410 | * @param {*} context The context to invoke the listener with. 411 | * @param {Boolean} [once=false] Specify if the listener is a one-time listener. 412 | * @constructor 413 | * @private 414 | */ 415 | function EE(fn, context, once) { 416 | this.fn = fn; 417 | this.context = context; 418 | this.once = once || false; 419 | } 420 | 421 | /** 422 | * Add a listener for a given event. 423 | * 424 | * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. 425 | * @param {(String|Symbol)} event The event name. 426 | * @param {Function} fn The listener function. 427 | * @param {*} context The context to invoke the listener with. 428 | * @param {Boolean} once Specify if the listener is a one-time listener. 429 | * @returns {EventEmitter} 430 | * @private 431 | */ 432 | function addListener(emitter, event, fn, context, once) { 433 | if (typeof fn !== 'function') { 434 | throw new TypeError('The listener must be a function'); 435 | } 436 | 437 | var listener = new EE(fn, context || emitter, once) 438 | , evt = prefix ? prefix + event : event; 439 | 440 | if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; 441 | else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); 442 | else emitter._events[evt] = [emitter._events[evt], listener]; 443 | 444 | return emitter; 445 | } 446 | 447 | /** 448 | * Clear event by name. 449 | * 450 | * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. 451 | * @param {(String|Symbol)} evt The Event name. 452 | * @private 453 | */ 454 | function clearEvent(emitter, evt) { 455 | if (--emitter._eventsCount === 0) emitter._events = new Events(); 456 | else delete emitter._events[evt]; 457 | } 458 | 459 | /** 460 | * Minimal `EventEmitter` interface that is molded against the Node.js 461 | * `EventEmitter` interface. 462 | * 463 | * @constructor 464 | * @public 465 | */ 466 | function EventEmitter() { 467 | this._events = new Events(); 468 | this._eventsCount = 0; 469 | } 470 | 471 | /** 472 | * Return an array listing the events for which the emitter has registered 473 | * listeners. 474 | * 475 | * @returns {Array} 476 | * @public 477 | */ 478 | EventEmitter.prototype.eventNames = function eventNames() { 479 | var names = [] 480 | , events 481 | , name; 482 | 483 | if (this._eventsCount === 0) return names; 484 | 485 | for (name in (events = this._events)) { 486 | if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); 487 | } 488 | 489 | if (Object.getOwnPropertySymbols) { 490 | return names.concat(Object.getOwnPropertySymbols(events)); 491 | } 492 | 493 | return names; 494 | }; 495 | 496 | /** 497 | * Return the listeners registered for a given event. 498 | * 499 | * @param {(String|Symbol)} event The event name. 500 | * @returns {Array} The registered listeners. 501 | * @public 502 | */ 503 | EventEmitter.prototype.listeners = function listeners(event) { 504 | var evt = prefix ? prefix + event : event 505 | , handlers = this._events[evt]; 506 | 507 | if (!handlers) return []; 508 | if (handlers.fn) return [handlers.fn]; 509 | 510 | for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { 511 | ee[i] = handlers[i].fn; 512 | } 513 | 514 | return ee; 515 | }; 516 | 517 | /** 518 | * Return the number of listeners listening to a given event. 519 | * 520 | * @param {(String|Symbol)} event The event name. 521 | * @returns {Number} The number of listeners. 522 | * @public 523 | */ 524 | EventEmitter.prototype.listenerCount = function listenerCount(event) { 525 | var evt = prefix ? prefix + event : event 526 | , listeners = this._events[evt]; 527 | 528 | if (!listeners) return 0; 529 | if (listeners.fn) return 1; 530 | return listeners.length; 531 | }; 532 | 533 | /** 534 | * Calls each of the listeners registered for a given event. 535 | * 536 | * @param {(String|Symbol)} event The event name. 537 | * @returns {Boolean} `true` if the event had listeners, else `false`. 538 | * @public 539 | */ 540 | EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { 541 | var evt = prefix ? prefix + event : event; 542 | 543 | if (!this._events[evt]) return false; 544 | 545 | var listeners = this._events[evt] 546 | , len = arguments.length 547 | , args 548 | , i; 549 | 550 | if (listeners.fn) { 551 | if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); 552 | 553 | switch (len) { 554 | case 1: return listeners.fn.call(listeners.context), true; 555 | case 2: return listeners.fn.call(listeners.context, a1), true; 556 | case 3: return listeners.fn.call(listeners.context, a1, a2), true; 557 | case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; 558 | case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; 559 | case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; 560 | } 561 | 562 | for (i = 1, args = new Array(len -1); i < len; i++) { 563 | args[i - 1] = arguments[i]; 564 | } 565 | 566 | listeners.fn.apply(listeners.context, args); 567 | } else { 568 | var length = listeners.length 569 | , j; 570 | 571 | for (i = 0; i < length; i++) { 572 | if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); 573 | 574 | switch (len) { 575 | case 1: listeners[i].fn.call(listeners[i].context); break; 576 | case 2: listeners[i].fn.call(listeners[i].context, a1); break; 577 | case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; 578 | case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; 579 | default: 580 | if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { 581 | args[j - 1] = arguments[j]; 582 | } 583 | 584 | listeners[i].fn.apply(listeners[i].context, args); 585 | } 586 | } 587 | } 588 | 589 | return true; 590 | }; 591 | 592 | /** 593 | * Add a listener for a given event. 594 | * 595 | * @param {(String|Symbol)} event The event name. 596 | * @param {Function} fn The listener function. 597 | * @param {*} [context=this] The context to invoke the listener with. 598 | * @returns {EventEmitter} `this`. 599 | * @public 600 | */ 601 | EventEmitter.prototype.on = function on(event, fn, context) { 602 | return addListener(this, event, fn, context, false); 603 | }; 604 | 605 | /** 606 | * Add a one-time listener for a given event. 607 | * 608 | * @param {(String|Symbol)} event The event name. 609 | * @param {Function} fn The listener function. 610 | * @param {*} [context=this] The context to invoke the listener with. 611 | * @returns {EventEmitter} `this`. 612 | * @public 613 | */ 614 | EventEmitter.prototype.once = function once(event, fn, context) { 615 | return addListener(this, event, fn, context, true); 616 | }; 617 | 618 | /** 619 | * Remove the listeners of a given event. 620 | * 621 | * @param {(String|Symbol)} event The event name. 622 | * @param {Function} fn Only remove the listeners that match this function. 623 | * @param {*} context Only remove the listeners that have this context. 624 | * @param {Boolean} once Only remove one-time listeners. 625 | * @returns {EventEmitter} `this`. 626 | * @public 627 | */ 628 | EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { 629 | var evt = prefix ? prefix + event : event; 630 | 631 | if (!this._events[evt]) return this; 632 | if (!fn) { 633 | clearEvent(this, evt); 634 | return this; 635 | } 636 | 637 | var listeners = this._events[evt]; 638 | 639 | if (listeners.fn) { 640 | if ( 641 | listeners.fn === fn && 642 | (!once || listeners.once) && 643 | (!context || listeners.context === context) 644 | ) { 645 | clearEvent(this, evt); 646 | } 647 | } else { 648 | for (var i = 0, events = [], length = listeners.length; i < length; i++) { 649 | if ( 650 | listeners[i].fn !== fn || 651 | (once && !listeners[i].once) || 652 | (context && listeners[i].context !== context) 653 | ) { 654 | events.push(listeners[i]); 655 | } 656 | } 657 | 658 | // 659 | // Reset the array, or remove it completely if we have no more listeners. 660 | // 661 | if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; 662 | else clearEvent(this, evt); 663 | } 664 | 665 | return this; 666 | }; 667 | 668 | /** 669 | * Remove all listeners, or those of the specified event. 670 | * 671 | * @param {(String|Symbol)} [event] The event name. 672 | * @returns {EventEmitter} `this`. 673 | * @public 674 | */ 675 | EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { 676 | var evt; 677 | 678 | if (event) { 679 | evt = prefix ? prefix + event : event; 680 | if (this._events[evt]) clearEvent(this, evt); 681 | } else { 682 | this._events = new Events(); 683 | this._eventsCount = 0; 684 | } 685 | 686 | return this; 687 | }; 688 | 689 | // 690 | // Alias methods names because people roll like that. 691 | // 692 | EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 693 | EventEmitter.prototype.addListener = EventEmitter.prototype.on; 694 | 695 | // 696 | // Expose the prefix. 697 | // 698 | EventEmitter.prefixed = prefix; 699 | 700 | // 701 | // Allow `EventEmitter` to be imported as module namespace. 702 | // 703 | EventEmitter.EventEmitter = EventEmitter; 704 | 705 | // 706 | // Expose the module. 707 | // 708 | { 709 | module.exports = EventEmitter; 710 | } 711 | }); 712 | 713 | var __extends = (undefined && undefined.__extends) || (function () { 714 | var extendStatics = function (d, b) { 715 | extendStatics = Object.setPrototypeOf || 716 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 717 | function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; 718 | return extendStatics(d, b); 719 | }; 720 | return function (d, b) { 721 | if (typeof b !== "function" && b !== null) 722 | throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); 723 | extendStatics(d, b); 724 | function __() { this.constructor = d; } 725 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 726 | }; 727 | })(); 728 | var __assign = (undefined && undefined.__assign) || function () { 729 | __assign = Object.assign || function(t) { 730 | for (var s, i = 1, n = arguments.length; i < n; i++) { 731 | s = arguments[i]; 732 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 733 | t[p] = s[p]; 734 | } 735 | return t; 736 | }; 737 | return __assign.apply(this, arguments); 738 | }; 739 | var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { 740 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 741 | return new (P || (P = Promise))(function (resolve, reject) { 742 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 743 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 744 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 745 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 746 | }); 747 | }; 748 | var __generator = (undefined && undefined.__generator) || function (thisArg, body) { 749 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 750 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 751 | function verb(n) { return function (v) { return step([n, v]); }; } 752 | function step(op) { 753 | if (f) throw new TypeError("Generator is already executing."); 754 | while (g && (g = 0, op[0] && (_ = 0)), _) try { 755 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 756 | if (y = 0, t) op = [op[0] & 2, t.value]; 757 | switch (op[0]) { 758 | case 0: case 1: t = op; break; 759 | case 4: _.label++; return { value: op[1], done: false }; 760 | case 5: _.label++; y = op[1]; op = [0]; continue; 761 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 762 | default: 763 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 764 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 765 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 766 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 767 | if (t[2]) _.ops.pop(); 768 | _.trys.pop(); continue; 769 | } 770 | op = body.call(thisArg, _); 771 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 772 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 773 | } 774 | }; 775 | var defaultOptions = { 776 | padding: 10, 777 | margin: 10, 778 | boxShadowColor: 'rgba(0, 0, 0, 0.5)', 779 | transition: 'all 0.3s ease-out', 780 | borderRadius: '5px', 781 | highlightElClass: '', 782 | backgroundColor: '#fff', 783 | infoElClass: '', 784 | prevText: '上一步', 785 | nextText: '下一步', 786 | completeText: '完成', 787 | showIndicator: true, 788 | zIndex: 9999, 789 | useCustomInfo: false, 790 | getCustomInfoEl: null, 791 | steps: [] 792 | }; 793 | var NoviceGuide = (function (_super) { 794 | __extends(NoviceGuide, _super); 795 | function NoviceGuide(options) { 796 | var _this = _super.call(this) || this; 797 | _this.options = options; 798 | _this.options = Object.assign(defaultOptions, options); 799 | _this.steps = []; 800 | _this.currentStepIndex = -1; 801 | _this.highlightElement = new HighlightElement$1(_this); 802 | _this.infoElement = new HighlightElement(_this); 803 | _this.initSteps(); 804 | return _this; 805 | } 806 | NoviceGuide.prototype.initSteps = function () { 807 | var _this = this; 808 | this.options.steps.forEach(function (step) { 809 | _this.steps.push(__assign({}, step)); 810 | }); 811 | }; 812 | NoviceGuide.prototype.start = function () { 813 | if (this.steps.length <= 0) 814 | return; 815 | if (!this.addedCss) { 816 | addCss(); 817 | this.addedCss = true; 818 | } 819 | this.next(); 820 | }; 821 | NoviceGuide.prototype.next = function () { 822 | this.emit('before-step-change', this.currentStepIndex); 823 | if (this.currentStepIndex + 1 >= this.steps.length) { 824 | return this.done(); 825 | } 826 | this.currentStepIndex++; 827 | this.to(this.currentStepIndex); 828 | }; 829 | NoviceGuide.prototype.prev = function () { 830 | this.emit('before-step-change', this.currentStepIndex); 831 | if (this.currentStepIndex - 1 < 0) { 832 | return; 833 | } 834 | this.currentStepIndex--; 835 | this.to(this.currentStepIndex); 836 | }; 837 | NoviceGuide.prototype.jump = function (stepIndex) { 838 | this.currentStepIndex = stepIndex; 839 | this.to(stepIndex); 840 | }; 841 | NoviceGuide.prototype.to = function (stepIndex) { 842 | return __awaiter(this, void 0, void 0, function () { 843 | var currentStep, rect, windowHeight; 844 | return __generator(this, function (_a) { 845 | switch (_a.label) { 846 | case 0: 847 | currentStep = this.steps[stepIndex]; 848 | currentStep.element = 849 | typeof currentStep.element === 'string' 850 | ? document.querySelector(currentStep.element) 851 | : currentStep.element; 852 | if (currentStep.element) { 853 | scrollAncestorToElement(currentStep.element); 854 | rect = currentStep.element.getBoundingClientRect(); 855 | windowHeight = window.innerHeight; 856 | if (!elementIsInView(currentStep.element)) { 857 | window.scrollBy(0, rect.top - (windowHeight - rect.height) / 2); 858 | } 859 | } 860 | this.highlightElement.show(currentStep); 861 | return [4, this.infoElement.show(currentStep)]; 862 | case 1: 863 | _a.sent(); 864 | this.emit('after-step-change', stepIndex); 865 | return [2]; 866 | } 867 | }); 868 | }); 869 | }; 870 | NoviceGuide.prototype.done = function () { 871 | this.highlightElement.removeEl(); 872 | this.infoElement.removeEl(); 873 | removeCss(); 874 | this.addedCss = false; 875 | this.currentStepIndex = -1; 876 | this.emit('done'); 877 | }; 878 | NoviceGuide.prototype.isFirstStep = function () { 879 | return this.currentStepIndex <= 0; 880 | }; 881 | NoviceGuide.prototype.isLastStep = function () { 882 | return this.currentStepIndex >= this.steps.length - 1; 883 | }; 884 | return NoviceGuide; 885 | }(eventemitter3)); 886 | 887 | return NoviceGuide; 888 | 889 | })); 890 | -------------------------------------------------------------------------------- /dist/dist.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).SimpleNoviceGuide=e()}(this,(function(){"use strict";var t="simple-novice-guide-",e=function(t){var n=function(t){var e=window.getComputedStyle(t),n="absolute"===e.position,o=/(auto|scroll)/;if("fixed"===e.position)return document.body;for(var i=t.parentElement;i;){if(e=window.getComputedStyle(i),(!n||"static"!==e.position)&&o.test(e.overflow+e.overflowX+e.overflowY))return i;i=i.parentElement}return document.body}(t);if(n!==document.body){var o=n.getBoundingClientRect(),i=t.getBoundingClientRect();n.scrollTop=n.scrollTop+i.top-o.top,e(n)}},n=function(){function e(t){this.app=t,this.app=t,this.el=null}return e.prototype.show=function(t){this.el||this.createEl();var e=0,n=0,o=0,i=0;if(t.element){var r=t.element.getBoundingClientRect(),s=this.app.options.padding;e=r.left+window.pageXOffset-s,n=r.top+window.pageYOffset-s,o=r.width+2*s,i=r.height+2*s}else e=window.innerWidth/2+window.pageXOffset,n=window.innerHeight/2+window.pageYOffset,o=0,i=0;this.el.style.left=e+"px",this.el.style.top=n+"px",this.el.style.width=o+"px",this.el.style.height=i+"px"},e.prototype.createEl=function(){var e=this.app.options,n=e.boxShadowColor,o=e.transition,i=e.borderRadius,r=e.highlightElClass,s=e.zIndex;this.el=document.createElement("div"),this.el.className=t+"highlight-el",this.el.style.cssText="\n box-shadow: 0 0 0 5000px ".concat(n,";\n border-radius: ").concat(i,";\n transition: ").concat(o,";\n z-index: ").concat(s,";\n "),r&&this.el.classList.add(r),document.body.appendChild(this.el)},e.prototype.removeEl=function(){this.el&&(document.body.removeChild(this.el),this.el=null)},e}(),o=function(t,e,n,o){return new(n||(n=Promise))((function(i,r){function s(t){try{a(o.next(t))}catch(t){r(t)}}function c(t){try{a(o.throw(t))}catch(t){r(t)}}function a(t){var e;t.done?i(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(s,c)}a((o=o.apply(t,e||[])).next())}))},i=function(t,e){var n,o,i,r,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return r={next:c(0),throw:c(1),return:c(2)},"function"==typeof Symbol&&(r[Symbol.iterator]=function(){return this}),r;function c(c){return function(a){return function(c){if(n)throw new TypeError("Generator is already executing.");for(;r&&(r=0,c[0]&&(s=0)),s;)try{if(n=1,o&&(i=2&c[0]?o.return:c[0]?o.throw||((i=o.return)&&i.call(o),0):o.next)&&!(i=i.call(o,c[1])).done)return i;switch(o=0,i&&(c=[2&c[0],i.value]),c[0]){case 0:case 1:i=c;break;case 4:return s.label++,{value:c[1],done:!1};case 5:s.label++,o=c[1],c=[0];continue;case 7:c=s.ops.pop(),s.trys.pop();continue;default:if(!(i=s.trys,(i=i.length>0&&i[i.length-1])||6!==c[0]&&2!==c[0])){s=0;continue}if(3===c[0]&&(!i||c[1]>i[0]&&c[1]\n
').concat(e.title||"",'
\n
×
\n \n
\n ').concat(e.img?''):"",'\n
').concat(e.text||"",'
\n
\n
\n ').concat(s?this.app.steps.map((function(e,o){return'
')})).join("\n"):"",'\n
\n
\n
').concat(i,'
\n
').concat(r,"
\n
\n ")},e.prototype.createEl=function(){var e=this.app.options,n=e.padding,o=e.borderRadius,i=e.backgroundColor,r=e.infoElClass,s=e.zIndex;this.el=document.createElement("div"),this.el.className=t+"info-el",this.el.style.cssText="\n background-color: ".concat(i,"; \n padding: ").concat(n,"px;\n border-radius: ").concat(o,";\n z-index: ").concat(s,";\n "),r&&this.el.classList.add(r),document.body.appendChild(this.el),this.el.addEventListener("click",this.onClick.bind(this))},e.prototype.onClick=function(t){switch(t.target.getAttribute("data-type")){case"close":this.app.done();break;case"prev":this.app.prev();break;case"next":this.app.next();break;case"indicator":var e=t.target.getAttribute("data-index");Number.isNaN(Number(e))||this.app.jump(Number(e))}},e.prototype.removeEl=function(){this.el&&(document.body.removeChild(this.el),this.el=null)},e.prototype.onStepChange=function(e){var n=this.app.options,o=n.nextText,i=n.completeText;if(!n.useCustomInfo){var r=document.querySelector(".".concat(t,"info-el-btn-prev")),s=document.querySelector(".".concat(t,"info-el-btn-next"));r.classList.remove("disabled"),s.textContent=o,this.app.isFirstStep()&&r.classList.add("disabled"),this.app.isLastStep()&&(s.textContent=i);var c=Array.from(document.querySelectorAll(".".concat(t,"info-el-indicator-item")));c.forEach((function(t){t.classList.contains("active")&&t.classList.remove("active")})),c[e]&&c[e].classList.add("active")}},e.prototype.computeInfoPosition=function(t,e){var n=this.app.options,o=n.padding,i=n.margin,r=window.innerWidth,s=window.innerHeight,c=window.pageXOffset,a=window.pageYOffset,l=t.element.getBoundingClientRect(),p=e.getBoundingClientRect(),u=0,h=0,f=function(){return r-l.left-o>=p.width?l.left-o+c:l.right+o>=p.width?l.right+o-p.width+c:(r-p.width)/2+c},d=function(){return s-l.top-o>=p.height?l.top-o+a:l.bottom+o>=p.height?l.bottom+o-p.height+a:(s-p.height)/2+a};if(l.bottom+o+i+p.height<=s&&p.width<=r)u=f(),h=l.bottom+o+i+a;else if(l.top-o-i>=p.height&&p.width<=r)u=f(),h=l.top-o-i-p.height+a;else if(l.left-o-i>=p.width&&p.height<=s)u=l.left-o-i-p.width+c,h=d();else if(l.right+o+i+p.width<=r&&p.height<=s)u=l.right+o+i+c,h=d();else{if(l.height+2*o+i+p.height<=s&&Math.max(l.width+2*o,p.width)<=r){var v=(s-(l.height+2*o+i+p.height))/2;window.scrollBy(0,l.top-v)}u=f(),h=l.bottom+o+i+a}return{left:u,top:h}},e}(),s=null;var c,a=function(t,e){return t(e={exports:{}},e.exports),e.exports}((function(t){var e=Object.prototype.hasOwnProperty,n="~";function o(){}function i(t,e,n){this.fn=t,this.context=e,this.once=n||!1}function r(t,e,o,r,s){if("function"!=typeof o)throw new TypeError("The listener must be a function");var c=new i(o,r||t,s),a=n?n+e:e;return t._events[a]?t._events[a].fn?t._events[a]=[t._events[a],c]:t._events[a].push(c):(t._events[a]=c,t._eventsCount++),t}function s(t,e){0==--t._eventsCount?t._events=new o:delete t._events[e]}function c(){this._events=new o,this._eventsCount=0}Object.create&&(o.prototype=Object.create(null),(new o).__proto__||(n=!1)),c.prototype.eventNames=function(){var t,o,i=[];if(0===this._eventsCount)return i;for(o in t=this._events)e.call(t,o)&&i.push(n?o.slice(1):o);return Object.getOwnPropertySymbols?i.concat(Object.getOwnPropertySymbols(t)):i},c.prototype.listeners=function(t){var e=n?n+t:t,o=this._events[e];if(!o)return[];if(o.fn)return[o.fn];for(var i=0,r=o.length,s=new Array(r);i0&&i[i.length-1])||6!==c[0]&&2!==c[0])){s=0;continue}if(3===c[0]&&(!i||c[1]>i[0]&&c[1]=this.steps.length)return this.done();this.currentStepIndex++,this.to(this.currentStepIndex)},i.prototype.prev=function(){this.emit("before-step-change",this.currentStepIndex),this.currentStepIndex-1<0||(this.currentStepIndex--,this.to(this.currentStepIndex))},i.prototype.jump=function(t){this.currentStepIndex=t,this.to(t)},i.prototype.to=function(t){return u(this,void 0,void 0,(function(){var n,o,i;return h(this,(function(r){switch(r.label){case 0:return(n=this.steps[t]).element="string"==typeof n.element?document.querySelector(n.element):n.element,n.element&&(e(n.element),o=n.element.getBoundingClientRect(),i=window.innerHeight,function(t){var e=t.getBoundingClientRect();return e.top>=0&&e.left>=0&&e.bottom<=window.innerHeight&&e.right<=window.innerWidth}(n.element)||window.scrollBy(0,o.top-(i-o.height)/2)),this.highlightElement.show(n),[4,this.infoElement.show(n)];case 1:return r.sent(),this.emit("after-step-change",t),[2]}}))}))},i.prototype.done=function(){this.highlightElement.removeEl(),this.infoElement.removeEl(),s&&document.head.removeChild(s),this.addedCss=!1,this.currentStepIndex=-1,this.emit("done")},i.prototype.isFirstStep=function(){return this.currentStepIndex<=0},i.prototype.isLastStep=function(){return this.currentStepIndex>=this.steps.length-1},i}(a)})); 2 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | var __extends = (this && this.__extends) || (function () { 2 | var extendStatics = function (d, b) { 3 | extendStatics = Object.setPrototypeOf || 4 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 5 | function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; 6 | return extendStatics(d, b); 7 | }; 8 | return function (d, b) { 9 | if (typeof b !== "function" && b !== null) 10 | throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); 11 | extendStatics(d, b); 12 | function __() { this.constructor = d; } 13 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 14 | }; 15 | })(); 16 | var __assign = (this && this.__assign) || function () { 17 | __assign = Object.assign || function(t) { 18 | for (var s, i = 1, n = arguments.length; i < n; i++) { 19 | s = arguments[i]; 20 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 21 | t[p] = s[p]; 22 | } 23 | return t; 24 | }; 25 | return __assign.apply(this, arguments); 26 | }; 27 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 28 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 29 | return new (P || (P = Promise))(function (resolve, reject) { 30 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 31 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 32 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 33 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 34 | }); 35 | }; 36 | var __generator = (this && this.__generator) || function (thisArg, body) { 37 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 38 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 39 | function verb(n) { return function (v) { return step([n, v]); }; } 40 | function step(op) { 41 | if (f) throw new TypeError("Generator is already executing."); 42 | while (g && (g = 0, op[0] && (_ = 0)), _) try { 43 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 44 | if (y = 0, t) op = [op[0] & 2, t.value]; 45 | switch (op[0]) { 46 | case 0: case 1: t = op; break; 47 | case 4: _.label++; return { value: op[1], done: false }; 48 | case 5: _.label++; y = op[1]; op = [0]; continue; 49 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 50 | default: 51 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 52 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 53 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 54 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 55 | if (t[2]) _.ops.pop(); 56 | _.trys.pop(); continue; 57 | } 58 | op = body.call(thisArg, _); 59 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 60 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 61 | } 62 | }; 63 | import HighlightElement from './src/HighlightElement'; 64 | import InfoElement from './src/InfoElement'; 65 | import { scrollAncestorToElement, elementIsInView } from './src/utils'; 66 | import { addCss, removeCss } from './src/css'; 67 | import EventEmitter from 'eventemitter3'; 68 | var defaultOptions = { 69 | padding: 10, 70 | margin: 10, 71 | boxShadowColor: 'rgba(0, 0, 0, 0.5)', 72 | transition: 'all 0.3s ease-out', 73 | borderRadius: '5px', 74 | highlightElClass: '', 75 | backgroundColor: '#fff', 76 | infoElClass: '', 77 | prevText: '上一步', 78 | nextText: '下一步', 79 | completeText: '完成', 80 | showIndicator: true, 81 | zIndex: 9999, 82 | useCustomInfo: false, 83 | getCustomInfoEl: null, 84 | steps: [] 85 | }; 86 | var NoviceGuide = (function (_super) { 87 | __extends(NoviceGuide, _super); 88 | function NoviceGuide(options) { 89 | var _this = _super.call(this) || this; 90 | _this.options = options; 91 | _this.options = Object.assign(defaultOptions, options); 92 | _this.steps = []; 93 | _this.currentStepIndex = -1; 94 | _this.highlightElement = new HighlightElement(_this); 95 | _this.infoElement = new InfoElement(_this); 96 | _this.initSteps(); 97 | return _this; 98 | } 99 | NoviceGuide.prototype.initSteps = function () { 100 | var _this = this; 101 | this.options.steps.forEach(function (step) { 102 | _this.steps.push(__assign({}, step)); 103 | }); 104 | }; 105 | NoviceGuide.prototype.start = function () { 106 | if (this.steps.length <= 0) 107 | return; 108 | if (!this.addedCss) { 109 | addCss(); 110 | this.addedCss = true; 111 | } 112 | this.next(); 113 | }; 114 | NoviceGuide.prototype.next = function () { 115 | this.emit('before-step-change', this.currentStepIndex); 116 | if (this.currentStepIndex + 1 >= this.steps.length) { 117 | return this.done(); 118 | } 119 | this.currentStepIndex++; 120 | this.to(this.currentStepIndex); 121 | }; 122 | NoviceGuide.prototype.prev = function () { 123 | this.emit('before-step-change', this.currentStepIndex); 124 | if (this.currentStepIndex - 1 < 0) { 125 | return; 126 | } 127 | this.currentStepIndex--; 128 | this.to(this.currentStepIndex); 129 | }; 130 | NoviceGuide.prototype.jump = function (stepIndex) { 131 | this.currentStepIndex = stepIndex; 132 | this.to(stepIndex); 133 | }; 134 | NoviceGuide.prototype.to = function (stepIndex) { 135 | return __awaiter(this, void 0, void 0, function () { 136 | var currentStep, rect, windowHeight; 137 | return __generator(this, function (_a) { 138 | switch (_a.label) { 139 | case 0: 140 | currentStep = this.steps[stepIndex]; 141 | currentStep.element = 142 | typeof currentStep.element === 'string' 143 | ? document.querySelector(currentStep.element) 144 | : currentStep.element; 145 | if (currentStep.element) { 146 | scrollAncestorToElement(currentStep.element); 147 | rect = currentStep.element.getBoundingClientRect(); 148 | windowHeight = window.innerHeight; 149 | if (!elementIsInView(currentStep.element)) { 150 | window.scrollBy(0, rect.top - (windowHeight - rect.height) / 2); 151 | } 152 | } 153 | this.highlightElement.show(currentStep); 154 | return [4, this.infoElement.show(currentStep)]; 155 | case 1: 156 | _a.sent(); 157 | this.emit('after-step-change', stepIndex); 158 | return [2]; 159 | } 160 | }); 161 | }); 162 | }; 163 | NoviceGuide.prototype.done = function () { 164 | this.highlightElement.removeEl(); 165 | this.infoElement.removeEl(); 166 | removeCss(); 167 | this.addedCss = false; 168 | this.currentStepIndex = -1; 169 | this.emit('done'); 170 | }; 171 | NoviceGuide.prototype.isFirstStep = function () { 172 | return this.currentStepIndex <= 0; 173 | }; 174 | NoviceGuide.prototype.isLastStep = function () { 175 | return this.currentStepIndex >= this.steps.length - 1; 176 | }; 177 | return NoviceGuide; 178 | }(EventEmitter)); 179 | export default NoviceGuide; 180 | -------------------------------------------------------------------------------- /dist/src/HighlightElement.js: -------------------------------------------------------------------------------- 1 | import { prefix } from './utils'; 2 | var HighlightElement = (function () { 3 | function HighlightElement(app) { 4 | this.app = app; 5 | this.app = app; 6 | this.el = null; 7 | } 8 | HighlightElement.prototype.show = function (step) { 9 | if (!this.el) { 10 | this.createEl(); 11 | } 12 | var left = 0, top = 0, width = 0, height = 0; 13 | if (step.element) { 14 | var rect = step.element.getBoundingClientRect(); 15 | var padding = this.app.options.padding; 16 | left = rect.left + window.pageXOffset - padding; 17 | top = rect.top + window.pageYOffset - padding; 18 | width = rect.width + padding * 2; 19 | height = rect.height + padding * 2; 20 | } 21 | else { 22 | left = window.innerWidth / 2 + window.pageXOffset; 23 | top = window.innerHeight / 2 + window.pageYOffset; 24 | width = 0; 25 | height = 0; 26 | } 27 | this.el.style.left = left + 'px'; 28 | this.el.style.top = top + 'px'; 29 | this.el.style.width = width + 'px'; 30 | this.el.style.height = height + 'px'; 31 | }; 32 | HighlightElement.prototype.createEl = function () { 33 | var _a = this.app.options, boxShadowColor = _a.boxShadowColor, transition = _a.transition, borderRadius = _a.borderRadius, highlightElClass = _a.highlightElClass, zIndex = _a.zIndex; 34 | this.el = document.createElement('div'); 35 | this.el.className = prefix + 'highlight-el'; 36 | this.el.style.cssText = "\n box-shadow: 0 0 0 5000px ".concat(boxShadowColor, ";\n border-radius: ").concat(borderRadius, ";\n transition: ").concat(transition, ";\n z-index: ").concat(zIndex, ";\n "); 37 | if (highlightElClass) { 38 | this.el.classList.add(highlightElClass); 39 | } 40 | document.body.appendChild(this.el); 41 | }; 42 | HighlightElement.prototype.removeEl = function () { 43 | if (this.el) { 44 | document.body.removeChild(this.el); 45 | this.el = null; 46 | } 47 | }; 48 | return HighlightElement; 49 | }()); 50 | export default HighlightElement; 51 | -------------------------------------------------------------------------------- /dist/src/InfoElement.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | var __generator = (this && this.__generator) || function (thisArg, body) { 11 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 12 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 13 | function verb(n) { return function (v) { return step([n, v]); }; } 14 | function step(op) { 15 | if (f) throw new TypeError("Generator is already executing."); 16 | while (g && (g = 0, op[0] && (_ = 0)), _) try { 17 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 18 | if (y = 0, t) op = [op[0] & 2, t.value]; 19 | switch (op[0]) { 20 | case 0: case 1: t = op; break; 21 | case 4: _.label++; return { value: op[1], done: false }; 22 | case 5: _.label++; y = op[1]; op = [0]; continue; 23 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 24 | default: 25 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 26 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 27 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 28 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 29 | if (t[2]) _.ops.pop(); 30 | _.trys.pop(); continue; 31 | } 32 | op = body.call(thisArg, _); 33 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 34 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 35 | } 36 | }; 37 | import { loadImage, prefix } from './utils'; 38 | var HighlightElement = (function () { 39 | function HighlightElement(app) { 40 | this.app = app; 41 | this.app = app; 42 | this.el = null; 43 | this.app.on('after-step-change', this.onStepChange.bind(this)); 44 | } 45 | HighlightElement.prototype.show = function (step) { 46 | return __awaiter(this, void 0, void 0, function () { 47 | var el, res; 48 | return __generator(this, function (_a) { 49 | switch (_a.label) { 50 | case 0: 51 | if (!(this.app.options.useCustomInfo && this.app.options.getCustomInfoEl)) return [3, 2]; 52 | return [4, this.app.options.getCustomInfoEl(step)]; 53 | case 1: 54 | el = _a.sent(); 55 | res = this.getInfoRect(step, el); 56 | el.style.left = res.left + 'px'; 57 | el.style.top = res.top + 'px'; 58 | return [3, 4]; 59 | case 2: return [4, this.showInnerInfo(step)]; 60 | case 3: 61 | _a.sent(); 62 | _a.label = 4; 63 | case 4: return [2]; 64 | } 65 | }); 66 | }); 67 | }; 68 | HighlightElement.prototype.showInnerInfo = function (step) { 69 | return __awaiter(this, void 0, void 0, function () { 70 | var error_1, res; 71 | return __generator(this, function (_a) { 72 | switch (_a.label) { 73 | case 0: 74 | if (!this.el) { 75 | this.createEl(); 76 | } 77 | if (!step.img) return [3, 4]; 78 | _a.label = 1; 79 | case 1: 80 | _a.trys.push([1, 3, , 4]); 81 | return [4, loadImage(step.img)]; 82 | case 2: 83 | _a.sent(); 84 | return [3, 4]; 85 | case 3: 86 | error_1 = _a.sent(); 87 | console.error(error_1); 88 | return [3, 4]; 89 | case 4: 90 | this.el.innerHTML = this.createHTML(step); 91 | res = this.getInfoRect(step, this.el); 92 | this.el.style.left = res.left + 'px'; 93 | this.el.style.top = res.top + 'px'; 94 | return [2]; 95 | } 96 | }); 97 | }); 98 | }; 99 | HighlightElement.prototype.getInfoRect = function (step, el) { 100 | if (step.element) { 101 | return this.computeInfoPosition(step, el); 102 | } 103 | else { 104 | var rect = el.getBoundingClientRect(); 105 | return { 106 | left: (window.innerWidth - rect.width) / 2 + window.pageXOffset, 107 | top: (window.innerHeight - rect.height) / 2 + window.pageYOffset 108 | }; 109 | } 110 | }; 111 | HighlightElement.prototype.createHTML = function (step) { 112 | var _this = this; 113 | var _a = this.app.options, prevText = _a.prevText, nextText = _a.nextText, showIndicator = _a.showIndicator; 114 | return "\n
\n
").concat(step.title || '', "
\n
\u00D7
\n
\n
\n ").concat(step.img 115 | ? "") 116 | : '', "\n
").concat(step.text || '', "
\n
\n
\n ").concat(showIndicator 117 | ? this.app.steps 118 | .map(function (_, index) { 119 | return "
"); 120 | }) 121 | .join('\n') 122 | : '', "\n
\n
\n
").concat(prevText, "
\n
").concat(nextText, "
\n
\n "); 123 | }; 124 | HighlightElement.prototype.createEl = function () { 125 | var _a = this.app.options, padding = _a.padding, borderRadius = _a.borderRadius, backgroundColor = _a.backgroundColor, infoElClass = _a.infoElClass, zIndex = _a.zIndex; 126 | this.el = document.createElement('div'); 127 | this.el.className = prefix + 'info-el'; 128 | this.el.style.cssText = "\n background-color: ".concat(backgroundColor, "; \n padding: ").concat(padding, "px;\n border-radius: ").concat(borderRadius, ";\n z-index: ").concat(zIndex, ";\n "); 129 | if (infoElClass) { 130 | this.el.classList.add(infoElClass); 131 | } 132 | document.body.appendChild(this.el); 133 | this.el.addEventListener('click', this.onClick.bind(this)); 134 | }; 135 | HighlightElement.prototype.onClick = function (e) { 136 | var type = e.target.getAttribute('data-type'); 137 | switch (type) { 138 | case 'close': 139 | this.app.done(); 140 | break; 141 | case 'prev': 142 | this.app.prev(); 143 | break; 144 | case 'next': 145 | this.app.next(); 146 | break; 147 | case 'indicator': 148 | var index = e.target.getAttribute('data-index'); 149 | if (!Number.isNaN(Number(index))) { 150 | this.app.jump(Number(index)); 151 | } 152 | break; 153 | default: 154 | break; 155 | } 156 | }; 157 | HighlightElement.prototype.removeEl = function () { 158 | if (this.el) { 159 | document.body.removeChild(this.el); 160 | this.el = null; 161 | } 162 | }; 163 | HighlightElement.prototype.onStepChange = function (stepIndex) { 164 | var _a = this.app.options, nextText = _a.nextText, completeText = _a.completeText, useCustomInfo = _a.useCustomInfo; 165 | if (useCustomInfo) 166 | return; 167 | var prevEl = document.querySelector(".".concat(prefix, "info-el-btn-prev")); 168 | var nextEl = document.querySelector(".".concat(prefix, "info-el-btn-next")); 169 | prevEl.classList.remove('disabled'); 170 | nextEl.textContent = nextText; 171 | if (this.app.isFirstStep()) { 172 | prevEl.classList.add('disabled'); 173 | } 174 | if (this.app.isLastStep()) { 175 | nextEl.textContent = completeText; 176 | } 177 | var indicatorEls = Array.from(document.querySelectorAll(".".concat(prefix, "info-el-indicator-item"))); 178 | indicatorEls.forEach(function (item) { 179 | if (item.classList.contains('active')) { 180 | item.classList.remove('active'); 181 | } 182 | }); 183 | if (indicatorEls[stepIndex]) { 184 | indicatorEls[stepIndex].classList.add('active'); 185 | } 186 | }; 187 | HighlightElement.prototype.computeInfoPosition = function (step, el) { 188 | var _a = this.app.options, padding = _a.padding, margin = _a.margin; 189 | var windowWidth = window.innerWidth; 190 | var windowHeight = window.innerHeight; 191 | var windowPageXOffset = window.pageXOffset; 192 | var windowPageYOffset = window.pageYOffset; 193 | var rect = step.element.getBoundingClientRect(); 194 | var infoRect = el.getBoundingClientRect(); 195 | var left = 0; 196 | var top = 0; 197 | var adjustLeft = function () { 198 | if (windowWidth - rect.left - padding >= infoRect.width) { 199 | return rect.left - padding + windowPageXOffset; 200 | } 201 | else if (rect.right + padding >= infoRect.width) { 202 | return rect.right + padding - infoRect.width + windowPageXOffset; 203 | } 204 | else { 205 | return (windowWidth - infoRect.width) / 2 + windowPageXOffset; 206 | } 207 | }; 208 | var adjustTop = function () { 209 | if (windowHeight - rect.top - padding >= infoRect.height) { 210 | return rect.top - padding + windowPageYOffset; 211 | } 212 | else if (rect.bottom + padding >= infoRect.height) { 213 | return rect.bottom + padding - infoRect.height + windowPageYOffset; 214 | } 215 | else { 216 | return (windowHeight - infoRect.height) / 2 + windowPageYOffset; 217 | } 218 | }; 219 | if (rect.bottom + padding + margin + infoRect.height <= windowHeight && 220 | infoRect.width <= windowWidth) { 221 | left = adjustLeft(); 222 | top = rect.bottom + padding + margin + windowPageYOffset; 223 | } 224 | else if (rect.top - padding - margin >= infoRect.height && 225 | infoRect.width <= windowWidth) { 226 | left = adjustLeft(); 227 | top = rect.top - padding - margin - infoRect.height + windowPageYOffset; 228 | } 229 | else if (rect.left - padding - margin >= infoRect.width && 230 | infoRect.height <= windowHeight) { 231 | left = rect.left - padding - margin - infoRect.width + windowPageXOffset; 232 | top = adjustTop(); 233 | } 234 | else if (rect.right + padding + margin + infoRect.width <= windowWidth && 235 | infoRect.height <= windowHeight) { 236 | left = rect.right + padding + margin + windowPageXOffset; 237 | top = adjustTop(); 238 | } 239 | else { 240 | var totalHeightLessThenWindow = rect.height + padding * 2 + margin + infoRect.height <= windowHeight; 241 | if (totalHeightLessThenWindow && 242 | Math.max(rect.width + padding * 2, infoRect.width) <= windowWidth) { 243 | var newTop = (windowHeight - 244 | (rect.height + padding * 2 + margin + infoRect.height)) / 245 | 2; 246 | window.scrollBy(0, rect.top - newTop); 247 | } 248 | else { 249 | } 250 | left = adjustLeft(); 251 | top = rect.bottom + padding + margin + windowPageYOffset; 252 | } 253 | return { 254 | left: left, 255 | top: top 256 | }; 257 | }; 258 | return HighlightElement; 259 | }()); 260 | export default HighlightElement; 261 | -------------------------------------------------------------------------------- /dist/src/css.js: -------------------------------------------------------------------------------- 1 | import { prefix } from './utils'; 2 | var styleEl = null; 3 | export var addCss = function () { 4 | var cssText = ''; 5 | cssText += "\n .".concat(prefix, "highlight-el {\n position: absolute;\n }\n "); 6 | cssText += "\n .".concat(prefix, "info-el {\n position: absolute;\n min-width: 250px;\n max-width: 300px;\n }\n\n .").concat(prefix, "info-el-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n\n .").concat(prefix, "info-el-title {\n font-size: 18px;\n margin: 0;\n padding: 0;\n font-weight: 700;\n }\n\n .").concat(prefix, "info-el-close {\n cursor: pointer;\n color: #616161;\n font-size: 22px;\n font-weight: 700;\n }\n\n .").concat(prefix, "info-el-info {\n padding: 15px 0;\n }\n\n .").concat(prefix, "info-el-info-img {\n width: 100%;\n }\n\n .").concat(prefix, "info-el-info-text {\n\n }\n\n .").concat(prefix, "info-el-indicator {\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 10px;\n }\n\n .").concat(prefix, "info-el-indicator-item {\n width: 6px;\n height: 6px;\n background: #ccc;\n transition: width .1s ease-in;\n border-radius: 10px;\n cursor: pointer;\n margin: 0 2px;\n }\n\n .").concat(prefix, "info-el-indicator-item.active, .").concat(prefix, "info-el-indicator-item:hover {\n width: 15px;\n background: #999;\n }\n\n .").concat(prefix, "info-el-btn-group {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-top: 1px solid #e0e0e0;\n padding-top: 10px;\n }\n\n .").concat(prefix, "info-el-btn {\n width: 60px;\n height: 35px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid #bdbdbd;\n text-shadow: 1px 1px 0 #fff;\n font-size: 14px;\n color: #424242;\n white-space: nowrap;\n cursor: pointer;\n background-color: #f4f4f4;\n border-radius: 3px;\n }\n\n .").concat(prefix, "info-el-btn.disabled {\n color: #9e9e9e;\n border-color: #bdbdbd;\n cursor: default;\n background-color: #f4f4f4;\n }\n\n .").concat(prefix, "info-el-btn:hover {\n border-color: #9e9e9e;\n background-color: #e0e0e0;\n color: #212121;\n }\n\n .").concat(prefix, "info-el-btn.disabled:hover {\n color: #9e9e9e;\n border-color: #bdbdbd;\n cursor: default;\n background-color: #f4f4f4;\n }\n "); 7 | styleEl = document.createElement('style'); 8 | styleEl.innerHTML = cssText; 9 | document.head.appendChild(styleEl); 10 | }; 11 | export var removeCss = function () { 12 | if (styleEl) { 13 | document.head.removeChild(styleEl); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /dist/src/utils.js: -------------------------------------------------------------------------------- 1 | export var prefix = 'simple-novice-guide-'; 2 | export var getScrollAncestor = function (el) { 3 | var style = window.getComputedStyle(el); 4 | var isAbsolute = style.position === 'absolute'; 5 | var isFixed = style.position === 'fixed'; 6 | var reg = /(auto|scroll)/; 7 | if (isFixed) 8 | return document.body; 9 | var parent = el.parentElement; 10 | while (parent) { 11 | style = window.getComputedStyle(parent); 12 | if (!(isAbsolute && style.position === 'static')) { 13 | if (reg.test(style.overflow + style.overflowX + style.overflowY)) { 14 | return parent; 15 | } 16 | } 17 | parent = parent.parentElement; 18 | } 19 | return document.body; 20 | }; 21 | export var scrollAncestorToElement = function (el) { 22 | var parent = getScrollAncestor(el); 23 | if (parent === document.body) 24 | return; 25 | var parentRect = parent.getBoundingClientRect(); 26 | var rect = el.getBoundingClientRect(); 27 | parent.scrollTop = parent.scrollTop + rect.top - parentRect.top; 28 | scrollAncestorToElement(parent); 29 | }; 30 | export var elementIsInView = function (el) { 31 | var rect = el.getBoundingClientRect(); 32 | return (rect.top >= 0 && 33 | rect.left >= 0 && 34 | rect.bottom <= window.innerHeight && 35 | rect.right <= window.innerWidth); 36 | }; 37 | export var loadImage = function (img) { 38 | return new Promise(function (resolve, reject) { 39 | var image = new Image(); 40 | image.onload = resolve; 41 | image.onerror = reject; 42 | image.src = img; 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | simple-novice-guide 9 | 126 | 127 | 128 | 129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | 143 |
144 |
145 |
146 | 147 | 148 | 149 |
150 | 151 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import HighlightElement from './src/HighlightElement' 2 | import InfoElement from './src/InfoElement' 3 | import { scrollAncestorToElement, elementIsInView } from './src/utils' 4 | import { addCss, removeCss } from './src/css' 5 | import EventEmitter from 'eventemitter3' 6 | 7 | export interface Step { 8 | element: HTMLElement 9 | title: string | number 10 | text: string | number 11 | img: string 12 | } 13 | 14 | type GetCustomInfoEl = (step: Step) => Promise 15 | 16 | interface Options { 17 | padding?: number 18 | margin?: number 19 | boxShadowColor?: string 20 | transition?: string 21 | borderRadius?: string 22 | highlightElClass?: string 23 | backgroundColor?: string 24 | infoElClass?: string 25 | prevText?: string 26 | nextText?: string 27 | completeText?: string 28 | showIndicator?: boolean 29 | zIndex?: number 30 | useCustomInfo?: boolean 31 | getCustomInfoEl?: GetCustomInfoEl 32 | steps: Array< 33 | Step & { 34 | element: HTMLElement | string 35 | } 36 | > 37 | } 38 | 39 | type Steps = Array 40 | 41 | interface DefaultOptions { 42 | // 高亮元素和信息框元素的内边距 43 | padding: number 44 | // 高亮元素和信息框元素之间的间距 45 | margin: number 46 | // 高亮元素的box-shadow颜色 47 | boxShadowColor: string 48 | // 高亮元素过渡效果 49 | transition: string 50 | // 高亮元素和信息框元素的圆角 51 | borderRadius: string 52 | // 要添加到高亮元素上的css类名 53 | highlightElClass: string 54 | // 信息框元素的背景颜色 55 | backgroundColor: string 56 | // 要添加到信息框元素上的css类名 57 | infoElClass: string 58 | // 上一步按钮的文字 59 | prevText: string 60 | // 下一步按钮的文字 61 | nextText: string 62 | // 完成按钮的文字 63 | completeText: string 64 | // 是否显示信息框内的指示器 65 | showIndicator: boolean 66 | // 高亮元素和信息框的z-index 67 | zIndex: number 68 | // 是否使用自定义的信息框,如果开启,需要传递getCustomInfoEl选项 69 | useCustomInfo: boolean 70 | // 返回自定义信息框元素 71 | getCustomInfoEl: GetCustomInfoEl 72 | // 步骤数据 73 | steps: Array 74 | } 75 | 76 | // 默认配置 77 | const defaultOptions: DefaultOptions = { 78 | padding: 10, 79 | margin: 10, 80 | boxShadowColor: 'rgba(0, 0, 0, 0.5)', 81 | transition: 'all 0.3s ease-out', 82 | borderRadius: '5px', 83 | highlightElClass: '', 84 | backgroundColor: '#fff', 85 | infoElClass: '', 86 | prevText: '上一步', 87 | nextText: '下一步', 88 | completeText: '完成', 89 | showIndicator: true, 90 | zIndex: 9999, 91 | useCustomInfo: false, 92 | getCustomInfoEl: null, 93 | steps: [] 94 | } 95 | 96 | // 入口类 97 | class NoviceGuide extends EventEmitter { 98 | public steps: Steps 99 | public currentStepIndex: number 100 | public infoElement: InfoElement 101 | public highlightElement: HighlightElement 102 | public addedCss: boolean 103 | constructor(public options: Options) { 104 | super() 105 | // 选项 106 | this.options = Object.assign(defaultOptions, options) 107 | // 步骤数据 108 | this.steps = [] 109 | // 当前所在步骤 110 | this.currentStepIndex = -1 111 | // 实例化辅助类 112 | this.highlightElement = new HighlightElement(this) 113 | this.infoElement = new InfoElement(this) 114 | // 初始化步骤数据 115 | this.initSteps() 116 | } 117 | 118 | // 初始化步骤数据 119 | initSteps() { 120 | this.options.steps.forEach(step => { 121 | this.steps.push({ 122 | ...step 123 | }) 124 | }) 125 | } 126 | 127 | // 开始 128 | start() { 129 | if (this.steps.length <= 0) return 130 | // 添加元素的样式到页面 131 | if (!this.addedCss) { 132 | addCss() 133 | this.addedCss = true 134 | } 135 | this.next() 136 | } 137 | 138 | // 下一步 139 | next() { 140 | this.emit('before-step-change', this.currentStepIndex) 141 | if (this.currentStepIndex + 1 >= this.steps.length) { 142 | return this.done() 143 | } 144 | this.currentStepIndex++ 145 | this.to(this.currentStepIndex) 146 | } 147 | 148 | // 上一步 149 | prev() { 150 | this.emit('before-step-change', this.currentStepIndex) 151 | if (this.currentStepIndex - 1 < 0) { 152 | return 153 | } 154 | this.currentStepIndex-- 155 | this.to(this.currentStepIndex) 156 | } 157 | 158 | // 跳转到指定步骤 159 | jump(stepIndex: number) { 160 | this.currentStepIndex = stepIndex 161 | this.to(stepIndex) 162 | } 163 | 164 | // 达到某一步 165 | async to(stepIndex: number) { 166 | const currentStep = this.steps[stepIndex] 167 | // 当前步骤没有元素就不用处理滚动 168 | currentStep.element = 169 | typeof currentStep.element === 'string' 170 | ? document.querySelector(currentStep.element) 171 | : currentStep.element 172 | if (currentStep.element) { 173 | scrollAncestorToElement(currentStep.element) 174 | const rect = currentStep.element.getBoundingClientRect() 175 | const windowHeight = window.innerHeight 176 | if (!elementIsInView(currentStep.element)) { 177 | window.scrollBy(0, rect.top - (windowHeight - rect.height) / 2) 178 | } 179 | } 180 | this.highlightElement.show(currentStep) 181 | await this.infoElement.show(currentStep) 182 | this.emit('after-step-change', stepIndex) 183 | } 184 | 185 | // 结束 186 | done() { 187 | this.highlightElement.removeEl() 188 | this.infoElement.removeEl() 189 | removeCss() 190 | this.addedCss = false 191 | this.currentStepIndex = -1 192 | this.emit('done') 193 | } 194 | 195 | // 是否是第一步 196 | isFirstStep() { 197 | return this.currentStepIndex <= 0 198 | } 199 | 200 | // 是否是最后一步 201 | isLastStep() { 202 | return this.currentStepIndex >= this.steps.length - 1 203 | } 204 | } 205 | 206 | export default NoviceGuide 207 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-novice-guide", 3 | "version": "0.0.2", 4 | "description": "一个简单的新手引导库", 5 | "authors": [ 6 | { 7 | "name": "街角小林", 8 | "email": "1013335014@qq.com" 9 | }, 10 | { 11 | "name": "理想青年实验室", 12 | "url": "http://lxqnsys.com/" 13 | } 14 | ], 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/wanglin2/simple-novice-guide" 19 | }, 20 | "scripts": { 21 | "tsc": "tsc --watch", 22 | "build": "rollup -c -w", 23 | "lint": "eslint src/", 24 | "format": "prettier --write ." 25 | }, 26 | "module": "dist/index.js", 27 | "main": "dist/dist.min.js", 28 | "dependencies": { 29 | "eventemitter3": "^4.0.7" 30 | }, 31 | "keywords": [ 32 | "javascript", 33 | "novice-guide" 34 | ], 35 | "devDependencies": { 36 | "eslint": "^8.25.0", 37 | "prettier": "^2.7.1", 38 | "rollup": "^3.18.0", 39 | "rollup-plugin-commonjs": "^10.1.0", 40 | "rollup-plugin-node-resolve": "^5.2.0", 41 | "rollup-plugin-terser": "^7.0.2", 42 | "typescript": "^4.9.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import { terser } from 'rollup-plugin-terser' 4 | 5 | export default { 6 | input: './dist/index.js', 7 | output: [ 8 | { 9 | file: './dist/dist.js', 10 | format: 'umd', 11 | name: 'SimpleNoviceGuide' 12 | }, 13 | { 14 | file: './dist/dist.min.js', 15 | format: 'umd', 16 | name: 'SimpleNoviceGuide', 17 | plugins: [terser()] 18 | } 19 | ], 20 | plugins: [ 21 | resolve({ 22 | jsnext: true, 23 | main: true, 24 | browser: true 25 | }), 26 | commonjs() 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/HighlightElement.ts: -------------------------------------------------------------------------------- 1 | import NoviceGuide, { Step } from '../index' 2 | import { prefix } from './utils'; 3 | 4 | // 高亮元素类 5 | export default class HighlightElement { 6 | public el: HTMLElement 7 | constructor(public app: NoviceGuide) { 8 | this.app = app 9 | this.el = null 10 | } 11 | 12 | // 显示高亮元素 13 | show(step: Step) { 14 | if (!this.el) { 15 | this.createEl() 16 | } 17 | let left = 0, 18 | top = 0, 19 | width = 0, 20 | height = 0 21 | if (step.element) { 22 | const rect = step.element.getBoundingClientRect() 23 | let { padding } = this.app.options 24 | left = rect.left + window.pageXOffset - padding 25 | top = rect.top + window.pageYOffset - padding 26 | width = rect.width + padding * 2 27 | height = rect.height + padding * 2 28 | } else { 29 | // 当前步骤没有元素则宽高设为0,然后窗口居中显示 30 | left = window.innerWidth / 2 + window.pageXOffset 31 | top = window.innerHeight / 2 + window.pageYOffset 32 | width = 0 33 | height = 0 34 | } 35 | this.el.style.left = left + 'px' 36 | this.el.style.top = top + 'px' 37 | this.el.style.width = width + 'px' 38 | this.el.style.height = height + 'px' 39 | } 40 | 41 | // 创建高亮元素 42 | createEl() { 43 | let { boxShadowColor, transition, borderRadius, highlightElClass, zIndex } = 44 | this.app.options 45 | this.el = document.createElement('div') 46 | this.el.className = prefix + 'highlight-el' 47 | this.el.style.cssText = ` 48 | box-shadow: 0 0 0 5000px ${boxShadowColor}; 49 | border-radius: ${borderRadius}; 50 | transition: ${transition}; 51 | z-index: ${zIndex}; 52 | ` 53 | if (highlightElClass) { 54 | this.el.classList.add(highlightElClass) 55 | } 56 | document.body.appendChild(this.el) 57 | } 58 | 59 | // 移除高亮元素 60 | removeEl() { 61 | if (this.el) { 62 | document.body.removeChild(this.el) 63 | this.el = null 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/InfoElement.ts: -------------------------------------------------------------------------------- 1 | import NoviceGuide, { Step } from '../index' 2 | import { loadImage, prefix } from './utils' 3 | 4 | // 信息元素类 5 | export default class HighlightElement { 6 | public el: HTMLElement 7 | constructor(public app: NoviceGuide) { 8 | this.app = app 9 | this.el = null 10 | this.app.on('after-step-change', this.onStepChange.bind(this)) 11 | } 12 | 13 | // 显示信息框 14 | async show(step: Step) { 15 | if (this.app.options.useCustomInfo && this.app.options.getCustomInfoEl) { 16 | // 自定义信息框 17 | let el = await this.app.options.getCustomInfoEl(step) 18 | let res = this.getInfoRect(step, el) 19 | el.style.left = res.left + 'px' 20 | el.style.top = res.top + 'px' 21 | } else { 22 | // 内置信息框 23 | await this.showInnerInfo(step) 24 | } 25 | } 26 | 27 | // 显示内置信息框 28 | async showInnerInfo(step: Step) { 29 | if (!this.el) { 30 | this.createEl() 31 | } 32 | if (step.img) { 33 | try { 34 | await loadImage(step.img) 35 | } catch (error) { 36 | console.error(error) 37 | } 38 | } 39 | this.el.innerHTML = this.createHTML(step) 40 | let res = this.getInfoRect(step, this.el) 41 | this.el.style.left = res.left + 'px' 42 | this.el.style.top = res.top + 'px' 43 | } 44 | 45 | // 计算信息框的位置 46 | getInfoRect(step: Step, el: HTMLElement) { 47 | if (step.element) { 48 | return this.computeInfoPosition(step, el) 49 | } else { 50 | // 当前没有元素,则信息元素直接窗口居中显示 51 | const rect = el.getBoundingClientRect() 52 | return { 53 | left: (window.innerWidth - rect.width) / 2 + window.pageXOffset, 54 | top: (window.innerHeight - rect.height) / 2 + window.pageYOffset 55 | } 56 | } 57 | } 58 | 59 | // 创建内置信息框的内容 60 | createHTML(step: Step) { 61 | let { prevText, nextText, showIndicator } = this.app.options 62 | return ` 63 |
64 |
${step.title || ''}
65 |
×
66 |
67 |
68 | ${ 69 | step.img 70 | ? `` 71 | : '' 72 | } 73 |
${step.text || ''}
74 |
75 |
76 | ${ 77 | showIndicator 78 | ? this.app.steps 79 | .map((_, index) => { 80 | return `
` 83 | }) 84 | .join('\n') 85 | : '' 86 | } 87 |
88 |
89 |
${prevText}
92 |
${nextText}
93 |
94 | ` 95 | } 96 | 97 | // 创建内置信息框元素 98 | createEl() { 99 | let { padding, borderRadius, backgroundColor, infoElClass, zIndex } = 100 | this.app.options 101 | this.el = document.createElement('div') 102 | this.el.className = prefix + 'info-el' 103 | this.el.style.cssText = ` 104 | background-color: ${backgroundColor}; 105 | padding: ${padding}px; 106 | border-radius: ${borderRadius}; 107 | z-index: ${zIndex}; 108 | ` 109 | if (infoElClass) { 110 | this.el.classList.add(infoElClass) 111 | } 112 | document.body.appendChild(this.el) 113 | this.el.addEventListener('click', this.onClick.bind(this)) 114 | } 115 | 116 | // 内置信息框的点击事件 117 | onClick(e: MouseEvent) { 118 | let type = (e.target as HTMLElement).getAttribute('data-type') 119 | switch (type) { 120 | case 'close': 121 | this.app.done() 122 | break 123 | case 'prev': 124 | this.app.prev() 125 | break 126 | case 'next': 127 | this.app.next() 128 | break 129 | case 'indicator': 130 | let index = (e.target as HTMLElement).getAttribute('data-index') 131 | if (!Number.isNaN(Number(index))) { 132 | this.app.jump(Number(index)) 133 | } 134 | break 135 | default: 136 | break 137 | } 138 | } 139 | 140 | // 移除内置信息框 141 | removeEl() { 142 | if (this.el) { 143 | document.body.removeChild(this.el) 144 | this.el = null 145 | } 146 | } 147 | 148 | // 更新内置信息框的状态 149 | onStepChange(stepIndex: number) { 150 | let { nextText, completeText, useCustomInfo } = this.app.options 151 | if (useCustomInfo) return 152 | 153 | // 更新按钮样式和文字 154 | let prevEl = document.querySelector(`.${prefix}info-el-btn-prev`) 155 | let nextEl = document.querySelector(`.${prefix}info-el-btn-next`) 156 | prevEl.classList.remove('disabled') 157 | nextEl.textContent = nextText 158 | if (this.app.isFirstStep()) { 159 | prevEl.classList.add('disabled') 160 | } 161 | if (this.app.isLastStep()) { 162 | nextEl.textContent = completeText 163 | } 164 | 165 | // 更新指示器 166 | let indicatorEls = Array.from( 167 | document.querySelectorAll(`.${prefix}info-el-indicator-item`) 168 | ) 169 | indicatorEls.forEach(item => { 170 | if (item.classList.contains('active')) { 171 | item.classList.remove('active') 172 | } 173 | }) 174 | if (indicatorEls[stepIndex]) { 175 | indicatorEls[stepIndex].classList.add('active') 176 | } 177 | } 178 | 179 | // 动态计算信息框的位置 180 | computeInfoPosition(step: Step, el: HTMLElement) { 181 | const { padding, margin } = this.app.options 182 | const windowWidth = window.innerWidth 183 | const windowHeight = window.innerHeight 184 | const windowPageXOffset = window.pageXOffset 185 | const windowPageYOffset = window.pageYOffset 186 | const rect = step.element.getBoundingClientRect() 187 | const infoRect = el.getBoundingClientRect() 188 | let left = 0 189 | let top = 0 190 | const adjustLeft = () => { 191 | // 优先和高亮框左对齐 192 | if (windowWidth - rect.left - padding >= infoRect.width) { 193 | return rect.left - padding + windowPageXOffset 194 | } else if (rect.right + padding >= infoRect.width) { 195 | // 次优先和高亮框右对齐 196 | return rect.right + padding - infoRect.width + windowPageXOffset 197 | } else { 198 | // 否则水平居中显示 199 | return (windowWidth - infoRect.width) / 2 + windowPageXOffset 200 | } 201 | } 202 | const adjustTop = () => { 203 | // 优先和高亮框上对齐 204 | if (windowHeight - rect.top - padding >= infoRect.height) { 205 | return rect.top - padding + windowPageYOffset 206 | } else if (rect.bottom + padding >= infoRect.height) { 207 | // 次优先和高亮框下对齐 208 | return rect.bottom + padding - infoRect.height + windowPageYOffset 209 | } else { 210 | // 否则水平居中显示 211 | return (windowHeight - infoRect.height) / 2 + windowPageYOffset 212 | } 213 | } 214 | if ( 215 | rect.bottom + padding + margin + infoRect.height <= windowHeight && // 下方宽度可以容纳 216 | infoRect.width <= windowWidth // 信息框宽度比浏览器窗口小 217 | ) { 218 | // 可以在下方显示 219 | left = adjustLeft() 220 | top = rect.bottom + padding + margin + windowPageYOffset 221 | } else if ( 222 | rect.top - padding - margin >= infoRect.height && 223 | infoRect.width <= windowWidth 224 | ) { 225 | // 可以在上方显示 226 | left = adjustLeft() 227 | top = rect.top - padding - margin - infoRect.height + windowPageYOffset 228 | } else if ( 229 | rect.left - padding - margin >= infoRect.width && 230 | infoRect.height <= windowHeight 231 | ) { 232 | // 可以在左方显示 233 | left = rect.left - padding - margin - infoRect.width + windowPageXOffset 234 | top = adjustTop() 235 | } else if ( 236 | rect.right + padding + margin + infoRect.width <= windowWidth && 237 | infoRect.height <= windowHeight 238 | ) { 239 | // 可以在右方显示 240 | left = rect.right + padding + margin + windowPageXOffset 241 | top = adjustTop() 242 | } else { 243 | // 否则检查高亮框高度+信息框高度是否小于窗口高度 244 | let totalHeightLessThenWindow = 245 | rect.height + padding * 2 + margin + infoRect.height <= windowHeight 246 | if ( 247 | totalHeightLessThenWindow && 248 | Math.max(rect.width + padding * 2, infoRect.width) <= windowWidth 249 | ) { 250 | // 上下排列可以放置 251 | // 滚动页面,居中显示两者整体 252 | let newTop = 253 | (windowHeight - 254 | (rect.height + padding * 2 + margin + infoRect.height)) / 255 | 2 256 | window.scrollBy(0, rect.top - newTop) 257 | } else { 258 | // 恕我无能为力 259 | // 回到默认位置 260 | } 261 | left = adjustLeft() 262 | top = rect.bottom + padding + margin + windowPageYOffset 263 | } 264 | return { 265 | left, 266 | top 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/css.ts: -------------------------------------------------------------------------------- 1 | import { prefix } from './utils' 2 | 3 | let styleEl: HTMLStyleElement = null 4 | 5 | export const addCss = () => { 6 | let cssText = '' 7 | // 高亮元素样式 8 | cssText += ` 9 | .${prefix}highlight-el { 10 | position: absolute; 11 | } 12 | ` 13 | // 信息元素样式 14 | cssText += ` 15 | .${prefix}info-el { 16 | position: absolute; 17 | min-width: 250px; 18 | max-width: 300px; 19 | } 20 | 21 | .${prefix}info-el-header { 22 | display: flex; 23 | align-items: center; 24 | justify-content: space-between; 25 | } 26 | 27 | .${prefix}info-el-title { 28 | font-size: 18px; 29 | margin: 0; 30 | padding: 0; 31 | font-weight: 700; 32 | } 33 | 34 | .${prefix}info-el-close { 35 | cursor: pointer; 36 | color: #616161; 37 | font-size: 22px; 38 | font-weight: 700; 39 | } 40 | 41 | .${prefix}info-el-info { 42 | padding: 15px 0; 43 | } 44 | 45 | .${prefix}info-el-info-img { 46 | width: 100%; 47 | } 48 | 49 | .${prefix}info-el-info-text { 50 | 51 | } 52 | 53 | .${prefix}info-el-indicator { 54 | display: flex; 55 | align-items: center; 56 | justify-content: center; 57 | margin-bottom: 10px; 58 | } 59 | 60 | .${prefix}info-el-indicator-item { 61 | width: 6px; 62 | height: 6px; 63 | background: #ccc; 64 | transition: width .1s ease-in; 65 | border-radius: 10px; 66 | cursor: pointer; 67 | margin: 0 2px; 68 | } 69 | 70 | .${prefix}info-el-indicator-item.active, .${prefix}info-el-indicator-item:hover { 71 | width: 15px; 72 | background: #999; 73 | } 74 | 75 | .${prefix}info-el-btn-group { 76 | display: flex; 77 | align-items: center; 78 | justify-content: space-between; 79 | border-top: 1px solid #e0e0e0; 80 | padding-top: 10px; 81 | } 82 | 83 | .${prefix}info-el-btn { 84 | width: 60px; 85 | height: 35px; 86 | display: flex; 87 | align-items: center; 88 | justify-content: center; 89 | border: 1px solid #bdbdbd; 90 | text-shadow: 1px 1px 0 #fff; 91 | font-size: 14px; 92 | color: #424242; 93 | white-space: nowrap; 94 | cursor: pointer; 95 | background-color: #f4f4f4; 96 | border-radius: 3px; 97 | } 98 | 99 | .${prefix}info-el-btn.disabled { 100 | color: #9e9e9e; 101 | border-color: #bdbdbd; 102 | cursor: default; 103 | background-color: #f4f4f4; 104 | } 105 | 106 | .${prefix}info-el-btn:hover { 107 | border-color: #9e9e9e; 108 | background-color: #e0e0e0; 109 | color: #212121; 110 | } 111 | 112 | .${prefix}info-el-btn.disabled:hover { 113 | color: #9e9e9e; 114 | border-color: #bdbdbd; 115 | cursor: default; 116 | background-color: #f4f4f4; 117 | } 118 | ` 119 | // 添加到页面 120 | styleEl = document.createElement('style') 121 | styleEl.innerHTML = cssText 122 | document.head.appendChild(styleEl) 123 | } 124 | 125 | export const removeCss = () => { 126 | if (styleEl) { 127 | document.head.removeChild(styleEl) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const prefix = 'simple-novice-guide-' 2 | 3 | // 获取一个html节点最近的可滚动的祖先节点 4 | export const getScrollAncestor = (el: HTMLElement) => { 5 | let style = window.getComputedStyle(el) 6 | const isAbsolute = style.position === 'absolute' 7 | const isFixed = style.position === 'fixed' 8 | const reg = /(auto|scroll)/ 9 | // 如果元素是固定定位,那么可滚动祖先元素为body 10 | if (isFixed) return document.body 11 | let parent = el.parentElement 12 | while (parent) { 13 | style = window.getComputedStyle(parent) 14 | // 如果是绝对定位,那么可滚动的祖先元素必须是有定位的才行 15 | if (!(isAbsolute && style.position === 'static')) { 16 | // 如果某个祖先元素的overflow属性为auto或scroll则代表是可滚动的 17 | if (reg.test(style.overflow + style.overflowX + style.overflowY)) { 18 | return parent 19 | } 20 | } 21 | parent = parent.parentElement 22 | } 23 | return document.body 24 | } 25 | 26 | // 滚动一个节点的最近一个可滚动的祖先节点,让节点出现在祖先节点的可视区域内 27 | export const scrollAncestorToElement = (el: HTMLElement) => { 28 | const parent = getScrollAncestor(el) 29 | if (parent === document.body) return 30 | let parentRect = parent.getBoundingClientRect() 31 | let rect = el.getBoundingClientRect() 32 | parent.scrollTop = parent.scrollTop + rect.top - parentRect.top 33 | scrollAncestorToElement(parent) 34 | } 35 | 36 | // 判断一个节点是否在屏幕可视区域 37 | export const elementIsInView = (el: HTMLElement) => { 38 | const rect = el.getBoundingClientRect() 39 | return ( 40 | rect.top >= 0 && 41 | rect.left >= 0 && 42 | rect.bottom <= window.innerHeight && 43 | rect.right <= window.innerWidth 44 | ) 45 | } 46 | 47 | // 加载图片 48 | export const loadImage = (img: string) => { 49 | return new Promise((resolve, reject) => { 50 | let image = new Image() 51 | image.onload = resolve 52 | image.onerror = reject 53 | image.src = img 54 | }) 55 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "ES2015", 5 | "outDir": "dist", 6 | "target": "es5", 7 | "noImplicitAny": true, 8 | "removeComments": true, 9 | "moduleResolution": "node", 10 | "skipLibCheck": true, 11 | "allowSyntheticDefaultImports": true 12 | }, 13 | "include": [ 14 | "index.ts", 15 | "src/**/*" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } --------------------------------------------------------------------------------