├── .gitignore ├── src ├── index.js ├── lib │ ├── fragment.js │ ├── configOptions.js │ ├── syncScroll.js │ ├── scrollControl.js │ └── area.js ├── styles.css └── demo.js ├── .idea ├── misc.xml ├── vcs.xml ├── encodings.xml ├── modules.xml ├── sync-scroll.iml └── workspace.xml ├── babel.config.js ├── index.html ├── LICENSE ├── webpack.prod.config.js ├── webpack.config.js ├── package.json ├── README.md ├── dist ├── index.html ├── md-sync-scroll.js └── styles.css └── template.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './lib/syncScroll'; 2 | export * from './lib/configOptions'; 3 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/fragment.js: -------------------------------------------------------------------------------- 1 | export class Fragment { 2 | 3 | pairId; 4 | el; 5 | offsetTop; 6 | height; 7 | 8 | constructor( 9 | pairId, 10 | el, 11 | offsetTop, 12 | height 13 | ) { 14 | this.pairId = pairId; 15 | this.el = el; 16 | this.offsetTop = offsetTop; 17 | this.height = height; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.idea/sync-scroll.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | [ 3 | "@babel/preset-env", 4 | { 5 | targets: { 6 | edge: "17", 7 | firefox: "60", 8 | chrome: "67", 9 | safari: "11.1", 10 | }, 11 | useBuiltIns: "usage", 12 | corejs: 3 13 | }, 14 | ], 15 | ]; 16 | 17 | const plugins = [ 18 | "@babel/plugin-proposal-class-properties" 19 | ]; 20 | 21 | module.exports = { 22 | presets, 23 | plugins 24 | }; 25 | -------------------------------------------------------------------------------- /src/lib/configOptions.js: -------------------------------------------------------------------------------- 1 | export class ConfigOptions { 2 | 3 | static DEFAULT_OPTIONS = { 4 | syncWithClick: false, 5 | offsetScroll: 0 6 | }; 7 | 8 | constructor( 9 | options 10 | ) { 11 | Object.setPrototypeOf(options, null); 12 | this['syncWithClick'] = options['syncWithClick'] || false; 13 | this['offsetScroll'] = options['offsetScroll'] || 0; 14 | } 15 | 16 | static instance(options) { 17 | return options ? new ConfigOptions(options) : ConfigOptions.DEFAULT_OPTIONS; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sync Scroll Demo 6 | 7 | 8 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/lib/syncScroll.js: -------------------------------------------------------------------------------- 1 | import {ScrollControl} from "./scrollControl"; 2 | import {ConfigOptions} from "./configOptions"; 3 | 4 | export class SyncScroll { 5 | 6 | controller; 7 | areas; 8 | 9 | constructor( 10 | options = ConfigOptions.DEFAULT_OPTIONS 11 | ) { 12 | this.controller = new ScrollControl(options); 13 | this.areas = this.controller._areas; 14 | } 15 | 16 | addAreas(areasInfo) { 17 | for (let i = areasInfo.length; i--;) { 18 | this.addArea(areasInfo[i]); 19 | } 20 | this.update(); 21 | } 22 | 23 | addArea(areaInfo) { 24 | this.controller.addArea(areaInfo.area, areaInfo.queryCriteria, areaInfo.options); 25 | } 26 | 27 | update() { 28 | this.controller.updateAreas(); 29 | } 30 | 31 | destory() { 32 | this.controller.destroyAreas(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import "~github-markdown-css/github-markdown.css"; 2 | @import "~highlight.js/styles/atom-one-dark.css"; 3 | 4 | 5 | /* 设置滚动条样式 */ 6 | ::-webkit-scrollbar { 7 | width: 6px; 8 | height: 6px; 9 | background-color: transparent; 10 | } 11 | 12 | ::-webkit-scrollbar-thumb { 13 | background-color: darkgray; 14 | } 15 | 16 | * { 17 | padding: 0; 18 | margin: 0; 19 | } 20 | body { 21 | height: 100vh; 22 | width: 100vw; 23 | } 24 | button { 25 | border: 0; 26 | padding: 4px; 27 | } 28 | .flex { 29 | display: flex; 30 | } 31 | .flex-col { 32 | flex-direction: column; 33 | } 34 | .heading { 35 | flex: 0 0 48px; 36 | } 37 | .content { 38 | position: relative; 39 | flex: 1 1 auto; 40 | white-space: pre; 41 | } 42 | .area { 43 | position: relative; 44 | overflow: auto; 45 | flex: 1; 46 | box-sizing: border-box; 47 | border: 1px darkgray solid; 48 | height: 100%; 49 | padding: 20px; 50 | } 51 | 52 | .testPerformance div { 53 | margin: 10px; 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 abc1310054026 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | mode: 'production', 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | filename: "md-sync-scroll.js" 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.js$/, 14 | exclude: /(node_modules|bower_components)/, 15 | use: { 16 | loader: 'babel-loader', 17 | options: { 18 | presets: [ 19 | [ 20 | '@babel/preset-env', 21 | { 22 | targets: "> 0.25%, not dead", 23 | corejs: 3, 24 | useBuiltIns: "usage" 25 | } 26 | ] 27 | ], 28 | plugins: [ 29 | require('@babel/plugin-proposal-class-properties'), 30 | ] 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const htmlWebpackPlugin = require('html-webpack-plugin'); 3 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | 5 | module.exports = { 6 | entry: './src/demo.js', 7 | mode: "development", 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: "demo.js" 11 | }, 12 | devServer: { 13 | contentBase: './dist' 14 | }, 15 | // devtool: "eval-source-map", 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /(node_modules|bower_components)/, 21 | use: { 22 | loader: 'babel-loader', 23 | options: { 24 | presets: ['@babel/preset-env'], 25 | plugins: ['@babel/plugin-proposal-class-properties'] 26 | } 27 | } 28 | }, 29 | { 30 | test: /\.css$/, 31 | exclude: /(node_modules|bower_components)/, 32 | use: ExtractTextPlugin.extract({ 33 | fallback: "style-loader", 34 | use: "css-loader" 35 | }) 36 | } 37 | ] 38 | }, 39 | plugins: [ 40 | new htmlWebpackPlugin({ 41 | template: "./template.html", 42 | filename: "index.html" 43 | }), 44 | new ExtractTextPlugin('styles.css') 45 | ] 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "md-sync-scroll", 3 | "version": "1.0.5", 4 | "description": "让两个HTML元素进行同步滚动的插件", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack --config webpack.config.js && webpack-dev-server --open", 9 | "build": "webpack --config webpack.prod.config.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/siaikin/sync-scroll.git" 14 | }, 15 | "keywords": [ 16 | "markdown", 17 | "async scroll" 18 | ], 19 | "author": "siaikin", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/siaikin/sync-scroll/issues" 23 | }, 24 | "homepage": "https://github.com/siaikin/sync-scroll#readme", 25 | "files": [ 26 | "dist/md-sync-scroll.js", 27 | "src/lib", 28 | "src/index.js" 29 | ], 30 | "dependencies": { 31 | "@babel/polyfill": "^7.4.3" 32 | }, 33 | "devDependencies": { 34 | "@babel/cli": "^7.4.3", 35 | "@babel/core": "^7.4.3", 36 | "@babel/plugin-proposal-class-properties": "^7.4.0", 37 | "@babel/preset-env": "^7.4.3", 38 | "babel-loader": "^8.0.0-beta.0", 39 | "babel-plugin-transform-remove-strict-mode": "0.0.2", 40 | "css-loader": "^2.1.1", 41 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 42 | "html-webpack-plugin": "^3.2.0", 43 | "style-loader": "^0.23.1", 44 | "webpack": "^4.30.0", 45 | "webpack-cli": "^3.3.0", 46 | "webpack-dev-server": "^3.3.1", 47 | "core-js": "^3.0.1", 48 | "github-markdown-css": "^3.0.1", 49 | "highlight.js": "^9.15.6", 50 | "markdown-it": "^8.4.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/demo.js: -------------------------------------------------------------------------------- 1 | import MarkdownIt from 'markdown-it'; 2 | import hljs from 'highlight.js/lib/highlight'; 3 | import js from 'highlight.js/lib/languages/javascript'; 4 | import markdown from 'highlight.js/lib/languages/markdown'; 5 | import xml from 'highlight.js/lib/languages/xml'; 6 | hljs.registerLanguage('javascript', js); 7 | hljs.registerLanguage('markdown', markdown); 8 | hljs.registerLanguage('xml', xml); 9 | 10 | 11 | import {SyncScroll, ConfigOptions} from "./index.js"; 12 | 13 | import './styles.css'; 14 | 15 | const md = new MarkdownIt({ 16 | highlight: function (str, lang) { 17 | if (lang && hljs.getLanguage(lang)) { 18 | try { 19 | return `
${hljs.highlight(lang, str, true).value}
`; 20 | } catch (e) { 21 | } 22 | } 23 | 24 | return `
${md.utils.escapeHtml(str)}
`; 25 | } 26 | }); 27 | 28 | const editArea = document.getElementById('edit'); 29 | const previewArea = document.getElementById('preview'); 30 | const editArea2 = document.getElementById('edit2'); 31 | const previewArea2 = document.getElementById('preview2'); 32 | 33 | const options = ConfigOptions.instance({ 34 | syncWithClick: true, 35 | offsetScroll: 100 36 | }); 37 | const syncScroll = new SyncScroll(options); 38 | previewArea.innerHTML = md.render(editArea.innerText); 39 | previewArea2.innerHTML = md.render(editArea2.innerText); 40 | 41 | syncScroll.addAreas([ 42 | { 43 | area: editArea, 44 | queryCriteria: '.h1,.h2,.h3,.h4,.h5,.h6' 45 | }, 46 | { 47 | area: previewArea, 48 | queryCriteria: 'h1,h2,h3,h4,h5,h6' 49 | }, 50 | { 51 | area: editArea2, 52 | queryCriteria: '.h1,.h2,.h3,.h4,.h5,.h6' 53 | }, 54 | { 55 | area: previewArea2, 56 | queryCriteria: 'h1,h2,h3,h4,h5,h6' 57 | } 58 | ]); 59 | -------------------------------------------------------------------------------- /src/lib/scrollControl.js: -------------------------------------------------------------------------------- 1 | import {Area} from "./area"; 2 | 3 | /** 4 | * control类,用于内部对`Area`统一管理 5 | */ 6 | export class ScrollControl { 7 | 8 | /** 9 | * 信号量,避免滚动事件死循环 10 | * @type {number} 11 | * @private 12 | */ 13 | _sign = 1; 14 | /** 15 | * 当前对齐的`Fragment` 16 | */ 17 | _curFrag; 18 | /** 19 | * `Area`集合 20 | */ 21 | _areas; 22 | /** 23 | * 全局配置 24 | */ 25 | _options; 26 | 27 | constructor(options) { 28 | this._options = options; 29 | 30 | this._areas = []; 31 | } 32 | 33 | /** 34 | * 添加`Area` 35 | * @param el - 对应的DOM元素 36 | * @param queryCriteria - 查询字符串 37 | * @param options 38 | */ 39 | addArea(el, queryCriteria, options) { 40 | const area = new Area(this, el, queryCriteria, options || this._options); 41 | this._areas.push(area); 42 | } 43 | 44 | /** 45 | * 以`syncArea`为参照,进行同步滚动。 46 | * @param syncArea - 参照的`Area` 47 | * @param offsetOptions 48 | */ 49 | syncScroll(syncArea, offsetOptions) { 50 | this._curFrag = syncArea.currentFragment(); 51 | 52 | const scrollTop = syncArea.scrollTop, scrollLeft = syncArea.scrollLeft; 53 | for (let i = 0, length = this._areas.length; i < length; i++) { 54 | if (syncArea === this._areas[i]) { continue; } 55 | 56 | this._areas[i].syncWith(this._curFrag, scrollTop ,scrollLeft, offsetOptions); 57 | } 58 | } 59 | 60 | updateAreas() { 61 | let area; 62 | for (let i = 0, length = this._areas.length; i < length; i++) { 63 | area = this._areas[i]; 64 | 65 | area.updateFragments(); 66 | } 67 | } 68 | 69 | destroyAreas() { 70 | this._areas.forEach(function (area) { 71 | area.destory(); 72 | }) 73 | } 74 | 75 | /** 76 | * 判断互斥锁状态 77 | * @returns {boolean} 78 | */ 79 | isLocked() { 80 | return !(this._sign === 1); 81 | } 82 | 83 | /** 84 | * 加锁 85 | */ 86 | lock() { 87 | this._sign = this._sign - this._areas.length + 1; 88 | } 89 | 90 | /** 91 | * 解锁 92 | */ 93 | unlock() { 94 | this._sign = this._sign + 1; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sync Scroll 2 | ![GitHub stars](https://img.shields.io/github/stars/abc1310054026/sync-scroll.svg?style=social) 3 | ![GitHub followers](https://img.shields.io/github/followers/abc1310054026.svg?style=social) 4 | ![GitHub forks](https://img.shields.io/github/forks/abc1310054026/sync-scroll.svg?style=social) 5 | ![GitHub watchers](https://img.shields.io/github/watchers/abc1310054026/sync-scroll.svg?style=social) 6 | ![GitHub last commit](https://img.shields.io/github/last-commit/abc1310054026/sync-scroll.svg) 7 | ![NPM](https://img.shields.io/npm/l/md-sync-scroll.svg) 8 | ![npm](https://img.shields.io/npm/v/md-sync-scroll.svg) 9 | 10 | 这是一个基于**片段**的同步滚动插件。 11 | 12 | 请查看Github上的[Demo页][https://abc1310054026.github.io/sync-scroll/],上面有具体的效果。 13 | 如果你不关心实现的方式,请直接看**Usage** 14 | ## 片断 15 | ```markdown 16 | // 片段start 17 | # 这表示一个片段的开始 18 | 这些是片段内容 19 | 这些也是片段内容 20 | // 片段end 21 | ``` 22 | 如果你想要使用它让你的内容能进行同步滚动,你需要让你的内容符合以上的布局。这种布局很多Markdown格式的文本都是天然符合的,不过就是标题-内容-标题-内容... 23 | 24 | 示例: 25 | 26 | ```markdown 27 | # 文章标题 28 | 标题下的内容... 29 | # 文章标题2 30 | 标题下的内容2... 31 | ... 32 | ``` 33 | 其实这个页面就是一个很好的例子,当你滚动滚轮或滚动条的时候,注意一下以`#`号开头的段落,应该能看到左右两边的滚动距离是不同的。 34 | 35 | # Usage 36 | 如果想让SyncScroll正常运行,需要你用一个块级元素包裹你的内容,你可以按`F12`查看此页的代码。 37 | 38 | 我用`
`包裹了里面的内容,还有设置`overflow: auto`和`position: reactive`。 39 | 请务必设置overflow和position,overflow我想不必多说,它决定了是否能进行滚动。 40 | 41 | 对于position,因为我使用`HTMLElement.offsetTop`来获取片段的偏移高度, 42 | 而`offsetTop`依赖于最近的定位元素。如果你不将包裹的元素设置为定位元素,将无法获取到正确的`offsetTop`。 43 | 44 | 下面我们进入正题,`md-sync-scroll`的用法很简单,你只需要将要进行同步的元素和查询子元素的查询语句 45 | 传入`addArea(el, queryCriteria)`方法中就好了。然后如果你改变了其中的内容,请调用`update()`方法以更新内容。 46 | 47 | 1. 首先引入 48 | ```js 49 | import {SyncScroll} from "md-sync-scroll"; 50 | ``` 51 | 2. 设置要进行同步的DOM元素。你需要指定每个`Area`的子元素查询条件。 52 | 53 | 在内部我使用`querySelectorAll`来查询子元素。所以你需要传入符合css选择器语法的字符串。 54 | ```js 55 | import {SyncScroll, ConfigOptions} from "md-sync-scroll"; 56 | 57 | const editArea = document.getElementById('edit'); 58 | const previewArea = document.getElementById('preview'); 59 | // 通过ConfigOptions可以配置参数,详细信息见下文API->ConfigOptions 60 | const options = ConfigOptions.instance({ 61 | syncWithClick: true, 62 | offsetScroll: 100 63 | }); 64 | const syncScroll = new SyncScroll(options); 65 | 66 | // 对于本页面来说,我用`h1-6`指示片段的开始,那么我就要查询被我指定为`h1-h6`的元素 67 | // 在左边我用class='h1-6'标记,在右边用

-

表示 68 | // syncScroll.addArea(editArea, '.h1,.h2,.h3,.h4,.h5,.h6'); 69 | // syncScroll.addArea(previewArea, 'h1,h2,h3,h4,h5,h6'); 70 | syncScroll.addAreas([ 71 | { 72 | area: editArea, 73 | queryCriteria: '.h1,.h2,.h3,.h4,.h5,.h6' 74 | }, 75 | { 76 | area: previewArea, 77 | queryCriteria: 'h1,h2,h3,h4,h5,h6' 78 | } 79 | ]); 80 | // 可以调用`addArea`单个添加,在`addArea`调用后,需要手动调用`update`更新数据 81 | // syncScroll.addArea({ 82 | // area: editArea, 83 | // queryCriteria: '.h1,.h2,.h3,.h4,.h5,.h6' 84 | // }); 85 | // syncScroll.addArea({ 86 | // area: previewArea, 87 | // queryCriteria: 'h1,h2,h3,h4,h5,h6' 88 | // }); 89 | // syncScroll.update(); 90 | 91 | ``` 92 | # API 93 | ## SyncScroll 94 | ### PROPERTY 95 | | Prop name | Type | Default | Description | 96 | | --- | --- | --- | --- | 97 | | areas | Array | null | Area的数组 | 98 | | controller | ScrollControl | null | 内部的control类,正常使用请不要碰它,主要作用是关联各个`Area`。 | 99 | 100 | ### FUNCTION 101 | | Func name | Params | Description | 102 | | --- | --- | --- | 103 | | SyncScroll | options? | 构造函数,options类型为ConfigOptions | 104 | | addAreas | [{el, queryCriteria}, ...] | 添加多个`Area`,指定Dom元素el,和子元素查询语句,会自动调用更新方法。 | 105 | | addArea | {el, queryCriteria} | 添加一个`Area`,指定一个Dom元素el,和子元素查询语句 | 106 | | update | null | `Area`内容改变时调用,更新所有的`Area` | 107 | | destroy | null | 销毁实例 | 108 | 109 | ## ConfigOptions 110 | ### PROPERTY 111 | | Prop name | Type | Default | Description | 112 | | --- | --- | --- | --- | 113 | | syncWithLick | boolean | false | `click`事件是否触发滚动 | 114 | | offsetScroll | number | 0 | 滚动对齐位置的偏移量,默认在顶部即`0` | 115 | 116 | ### FUNCTION 117 | | Func name | Params | Description | 118 | | --- | --- | --- | 119 | | instance | {syncWithClick, offsetScroll} | 获取一个实例,参数为上面PROPERTY的参数,例子在上文usage第2项 | 120 | | ConfigOptions | {syncWithClick, offsetScroll} | 推荐使用`instance`来获取`ConfigOptions`来获取对象能稍微减小内存开销。构造函数,参数为上面PROPERTY的参数 | 121 | 122 | 有BUG请务必提issue,或者联系我`abc1310054026@163.com` 123 | 124 | [https://abc1310054026.github.io/sync-scroll/]: https://abc1310054026.github.io/sync-scroll/ 125 | -------------------------------------------------------------------------------- /src/lib/area.js: -------------------------------------------------------------------------------- 1 | import {Fragment} from "./fragment"; 2 | 3 | export class Area { 4 | 5 | /** 6 | * control类的引用 7 | */ 8 | _syncControl; 9 | /** 10 | * Dom对象 11 | */ 12 | _el; 13 | /** 14 | * 查询语句 15 | */ 16 | _queryCriteria; 17 | _options; 18 | _fragMap; 19 | _frags; 20 | /** 21 | * 当前对齐的`Fragment` 22 | */ 23 | _curFrag; 24 | /** 25 | * 对齐偏移参数 26 | */ 27 | _ofstOps; 28 | 29 | get scrollTop() { 30 | return this._el.scrollTop; 31 | } 32 | set scrollTop(scrollTop) { 33 | this._el.scrollTop = scrollTop; 34 | } 35 | get scrollLeft() { 36 | return this._el.scrollLeft; 37 | } 38 | set scrollLeft(scrollLeft) { 39 | this._el.scrollLeft = scrollLeft; 40 | } 41 | 42 | currentFragment() { 43 | return this._curFrag; 44 | } 45 | 46 | constructor( 47 | syncControl, 48 | el, 49 | queryCriteria, 50 | options 51 | ) { 52 | this._syncControl = syncControl; 53 | this._el = el; 54 | this._queryCriteria = queryCriteria; 55 | this._options = options; 56 | 57 | this._ofstOps = { 58 | ofstScl: this._options.offsetScroll, 59 | ofstFrag: 0 60 | }; 61 | this._listen(); 62 | 63 | // if (!Area.prototype.hasOwnProperty('scrollTop')) { 64 | // Object.defineProperty(Area.prototype, 'scrollTop', { 65 | // get() { 66 | // return this._el.scrollTop; 67 | // }, 68 | // set(scrollTop) { 69 | // this._el.scrollTop = scrollTop; 70 | // }, 71 | // configurable: true, 72 | // enumerable: true 73 | // }) 74 | // } 75 | } 76 | 77 | /** 78 | * update fragments 79 | * @private 80 | */ 81 | _updateFrags() { 82 | if (!this._el || !this._queryCriteria) { return; } 83 | 84 | let scrollHeight = this._el.scrollHeight; 85 | const els = this._el.querySelectorAll(this._queryCriteria); 86 | 87 | const length = els.length, offsetTopArr = []; 88 | for (let i = 0; i < length; i++) { 89 | offsetTopArr.push(els[i].offsetTop); 90 | } 91 | 92 | this._fragMap = {}; 93 | this._frags = []; 94 | let el, frag, offsetTop; 95 | for (let i = 0; i < length - 1; i++) { 96 | el = els[i]; 97 | offsetTop = offsetTopArr[i]; 98 | frag = new Fragment( 99 | i, 100 | el, 101 | offsetTop, 102 | offsetTopArr[i + 1] - offsetTop 103 | ); 104 | this._fragMap[i] = frag; 105 | this._frags.push(frag); 106 | } 107 | 108 | // handle last item 109 | el = els[length -1]; 110 | frag = new Fragment( 111 | length - 1, 112 | el, 113 | offsetTopArr[length - 1], 114 | scrollHeight - offsetTopArr[length - 1] 115 | ); 116 | this._fragMap[length - 1] = frag; 117 | this._frags.push(frag); 118 | } 119 | 120 | /** 121 | * 通过给定的偏移量确定`片段`位置 122 | * @param offset 123 | * @private 124 | */ 125 | _locateByOffset(offset) { 126 | let curFrag, frags = this._frags; 127 | 128 | for (let start = 0, end = frags.length - 1, mid; start < end - 1;) { 129 | mid = (start + end) >> 1; 130 | if (offset >= frags[mid].offsetTop) { 131 | if (frags[mid + 1].offsetTop > offset) { 132 | curFrag = frags[mid]; 133 | break; 134 | } else { 135 | start = mid; 136 | } 137 | } else if (frags[mid - 1].offsetTop <= offset) { 138 | curFrag = frags[mid - 1]; 139 | break; 140 | } else { 141 | end = mid; 142 | } 143 | } 144 | return curFrag ? curFrag : frags[frags.length - 1]; 145 | } 146 | 147 | _topFrag(ofstOps) { 148 | const scrollTop = this.scrollTop + ofstOps.ofstScl; 149 | return this._locateByOffset(scrollTop); 150 | } 151 | 152 | _listen() { 153 | this._el.addEventListener('scroll', this._onScroll.bind(this)); 154 | 155 | if (this._options.syncWithClick) { 156 | this._el.addEventListener('click', this._onClick.bind(this)); 157 | } 158 | } 159 | 160 | _rmListen() { 161 | this._el.removeEventListener('scroll', this._onScroll); 162 | 163 | if (this._options.syncWithClick) { 164 | this._el.removeEventListener('click', this._onClick); 165 | } 166 | } 167 | 168 | _onClick(event) { 169 | if (this._syncControl.isLocked()) { 170 | this._syncControl.unlock(); 171 | } else { 172 | this._syncControl.lock(); 173 | let tOsTop = event.target.offsetTop; 174 | this._curFrag = this._locateByOffset(tOsTop); 175 | 176 | // 让其他`Area`以这个`Area`为标准进行同步滚动 177 | this._syncControl.syncScroll(this, { 178 | ofstScl: this._curFrag.offsetTop - this.scrollTop, 179 | ofstFrag: tOsTop - this._curFrag.offsetTop 180 | }); 181 | } 182 | } 183 | 184 | _onScroll(event) { 185 | if (this._syncControl.isLocked()) { 186 | this._syncControl.unlock(); 187 | } else { 188 | this._syncControl.lock(); 189 | this._curFrag = this._topFrag(this._ofstOps); 190 | this._ofstOps.ofstFrag = 0; 191 | // 让其他`Area`以这个`Area`为标准进行同步滚动 192 | this._syncControl.syncScroll(this, this._ofstOps); 193 | } 194 | } 195 | 196 | syncWith(pairFrag, scrollTop, scrollLeft, ofstOps) { 197 | const deltaHeight = scrollTop - pairFrag.offsetTop + ofstOps.ofstScl; 198 | 199 | let frag = this._fragMap[pairFrag.pairId], 200 | ratio = frag.height / pairFrag.height; 201 | 202 | const _scrollTop = frag.offsetTop + 203 | ratio * deltaHeight + 204 | (ratio * ofstOps.ofstFrag - ofstOps.ofstFrag); 205 | 206 | this.scrollTop = _scrollTop - ofstOps.ofstScl; 207 | this.scrollLeft = scrollLeft; 208 | } 209 | 210 | updateFragments() { 211 | this._updateFrags(); 212 | } 213 | 214 | destroy() { 215 | this._rmListen(); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 27 | 28 | 29 | 32 | 33 | true 34 | 35 | true 36 | true 37 | 38 | 39 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 370 | 371 | -------------------------------------------------------------------------------- /dist/md-sync-scroll.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=32)}([function(t,e){t.exports="object"==typeof window&&window&&window.Math==Math?window:"object"==typeof self&&self&&self.Math==Math?self:Function("return this")()},function(t,e){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,e){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,e,n){var r=n(5);n(8)({target:"Object",stat:!0,forced:!r,sham:!r},{defineProperty:n(10).f})},function(t,e,n){var r=n(10),o=n(23);t.exports=n(5)?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},function(t,e,n){t.exports=!n(1)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},function(t,e,n){var r=n(0),o=n(9),i=r["__core-js_shared__"]||o("__core-js_shared__",{});(t.exports=function(t,e){return i[t]||(i[t]=void 0!==e?e:{})})("versions",[]).push({version:"3.0.1",mode:n(40)?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},function(t,e,n){var r=n(0),o=n(25).f,i=n(4),c=n(44),u=n(9),s=n(48),a=n(54);t.exports=function(t,e){var n,f,l,p,h,v=t.target,y=t.global,_=t.stat;if(n=y?r:_?r[v]||u(v,{}):(r[v]||{}).prototype)for(f in e){if(p=e[f],l=t.noTargetGet?(h=o(n,f))&&h.value:n[f],!a(y?f:v+(_?".":"#")+f,t.forced)&&void 0!==l){if(typeof p==typeof l)continue;s(p,l)}(t.sham||l&&l.sham)&&i(p,"sham",!0),c(n,f,p,t)}}},function(t,e,n){var r=n(0),o=n(4);t.exports=function(t,e){try{o(r,t,e)}catch(n){r[t]=e}return e}},function(t,e,n){var r=n(5),o=n(21),i=n(11),c=n(22),u=Object.defineProperty;e.f=r?u:function(t,e,n){if(i(t),e=c(e,!0),i(n),o)try{return u(t,e,n)}catch(t){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&&(t[e]=n.value),t}},function(t,e,n){var r=n(2);t.exports=function(t){if(!r(t))throw TypeError(String(t)+" is not an object");return t}},function(t,e,n){var r=n(15),o=n(18);t.exports=function(t){return r(o(t))}},function(t,e,n){"use strict";var r=n(14);n(8)({target:"Array",proto:!0,forced:[].forEach!=r},{forEach:r})},function(t,e,n){"use strict";var r=[].forEach,o=n(33)(0),i=n(42)("forEach");t.exports=i?function(t){return o(this,t,arguments[1])}:r},function(t,e,n){var r=n(1),o=n(16),i="".split;t.exports=r(function(){return!Object("z").propertyIsEnumerable(0)})?function(t){return"String"==o(t)?i.call(t,""):Object(t)}:Object},function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},function(t,e,n){var r=n(18);t.exports=function(t){return Object(r(t))}},function(t,e){t.exports=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t}},function(t,e,n){var r=n(20),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},function(t,e,n){t.exports=!n(5)&&!n(1)(function(){return 7!=Object.defineProperty(n(39)("div"),"a",{get:function(){return 7}}).a})},function(t,e,n){var r=n(2);t.exports=function(t,e){if(!r(t))return t;var n,o;if(e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!r(o=n.call(t)))return o;if(!e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+r).toString(36))}},function(t,e,n){var r=n(5),o=n(43),i=n(23),c=n(12),u=n(22),s=n(6),a=n(21),f=Object.getOwnPropertyDescriptor;e.f=r?f:function(t,e){if(t=c(t),e=u(e,!0),a)try{return f(t,e)}catch(t){}if(s(t,e))return i(!o.f.call(t,e),t[e])}},function(t,e,n){t.exports=n(7)("native-function-to-string",Function.toString)},function(t,e){t.exports={}},function(t,e,n){var r=n(6),o=n(12),i=n(51)(!1),c=n(27);t.exports=function(t,e){var n,u=o(t),s=0,a=[];for(n in u)!r(c,n)&&r(u,n)&&a.push(n);for(;e.length>s;)r(u,n=e[s++])&&(~i(a,n)||a.push(n));return a}},function(t,e){t.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},function(t,e,n){var r=n(57),o=n(14),i=n(4),c=n(0);for(var u in r){var s=c[u],a=s&&s.prototype;if(a&&a.forEach!==o)try{i(a,"forEach",o)}catch(t){a.forEach=o}}},function(t,e,n){"use strict";function r(t,e){for(var n=0;nS;S++)if((p||S in g)&&(_=b(y=g[S],S,d),t))if(n)m[S]=_;else if(_)switch(t){case 3:return!0;case 5:return y;case 6:return S;case 2:m.push(y)}else if(f)return!1;return l?-1:a||f?f:m}}},function(t,e,n){var r=n(35);t.exports=function(t,e,n){if(r(t),void 0===e)return t;switch(n){case 0:return function(){return t.call(e)};case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}}},function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(String(t)+" is not a function");return t}},function(t,e,n){var r=n(2),o=n(37),i=n(38)("species");t.exports=function(t,e){var n;return o(t)&&("function"!=typeof(n=t.constructor)||n!==Array&&!o(n.prototype)?r(n)&&null===(n=n[i])&&(n=void 0):n=void 0),new(void 0===n?Array:n)(0===e?0:e)}},function(t,e,n){var r=n(16);t.exports=Array.isArray||function(t){return"Array"==r(t)}},function(t,e,n){var r=n(7)("wks"),o=n(24),i=n(0).Symbol,c=n(41);t.exports=function(t){return r[t]||(r[t]=c&&i[t]||(c?i:o)("Symbol."+t))}},function(t,e,n){var r=n(2),o=n(0).document,i=r(o)&&r(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},function(t,e){t.exports=!1},function(t,e,n){t.exports=!n(1)(function(){return!String(Symbol())})},function(t,e,n){"use strict";var r=n(1);t.exports=function(t,e){var n=[][t];return!n||!r(function(){n.call(null,e||function(){throw 1},1)})}},function(t,e,n){"use strict";var r={}.propertyIsEnumerable,o=Object.getOwnPropertyDescriptor,i=o&&!r.call({1:2},1);e.f=i?function(t){var e=o(this,t);return!!e&&e.enumerable}:r},function(t,e,n){var r=n(0),o=n(4),i=n(6),c=n(9),u=n(26),s=n(45),a=s.get,f=s.enforce,l=String(u).split("toString");n(7)("inspectSource",function(t){return u.call(t)}),(t.exports=function(t,e,n,u){var s=!!u&&!!u.unsafe,a=!!u&&!!u.enumerable,p=!!u&&!!u.noTargetGet;"function"==typeof n&&("string"!=typeof e||i(n,"name")||o(n,"name",e),f(n).source=l.join("string"==typeof e?e:"")),t!==r?(s?!p&&t[e]&&(a=!0):delete t[e],a?t[e]=n:o(t,e,n)):a?t[e]=n:c(e,n)})(Function.prototype,"toString",function(){return"function"==typeof this&&a(this).source||u.call(this)})},function(t,e,n){var r,o,i,c=n(46),u=n(2),s=n(4),a=n(6),f=n(47),l=n(27),p=n(0).WeakMap;if(c){var h=new p,v=h.get,y=h.has,_=h.set;r=function(t,e){return _.call(h,t,e),e},o=function(t){return v.call(h,t)||{}},i=function(t){return y.call(h,t)}}else{var d=f("state");l[d]=!0,r=function(t,e){return s(t,d,e),e},o=function(t){return a(t,d)?t[d]:{}},i=function(t){return a(t,d)}}t.exports={set:r,get:o,has:i,enforce:function(t){return i(t)?o(t):r(t,{})},getterFor:function(t){return function(e){var n;if(!u(e)||(n=o(e)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return n}}}},function(t,e,n){var r=n(26),o=n(0).WeakMap;t.exports="function"==typeof o&&/native code/.test(r.call(o))},function(t,e,n){var r=n(7)("keys"),o=n(24);t.exports=function(t){return r[t]||(r[t]=o(t))}},function(t,e,n){var r=n(6),o=n(49),i=n(25),c=n(10);t.exports=function(t,e){for(var n=o(e),u=c.f,s=i.f,a=0;af;)if((u=s[f++])!=u)return!0}else for(;a>f;f++)if((t||f in s)&&s[f]===n)return t||f||0;return!t&&-1}}},function(t,e,n){var r=n(20),o=Math.max,i=Math.min;t.exports=function(t,e){var n=r(t);return n<0?o(n+e,0):i(n,e)}},function(t,e){e.f=Object.getOwnPropertySymbols},function(t,e,n){var r=n(1),o=/#|\.prototype\./,i=function(t,e){var n=u[c(t)];return n==a||n!=s&&("function"==typeof e?r(e):!!e)},c=i.normalize=function(t){return String(t).replace(o,".").toLowerCase()},u=i.data={},s=i.NATIVE="N",a=i.POLYFILL="P";t.exports=i},function(t,e,n){var r=n(17),o=n(56),i=n(1)(function(){o(1)});n(8)({target:"Object",stat:!0,forced:i},{keys:function(t){return o(r(t))}})},function(t,e,n){var r=n(28),o=n(29);t.exports=Object.keys||function(t){return r(t,o)}},function(t,e){t.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},function(t,e,n){"use strict";n(3),Object.defineProperty(e,"__esModule",{value:!0}),e.SyncScroll=void 0;var r=n(59),o=n(31);function i(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:o.ConfigOptions.DEFAULT_OPTIONS;!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),c(this,"controller",void 0),c(this,"areas",void 0),this.controller=new r.ScrollControl(e),this.areas=this.controller._areas}var e,n,u;return e=t,(n=[{key:"addAreas",value:function(t){for(var e=t.length;e--;)this.addArea(t[e]);this.update()}},{key:"addArea",value:function(t){this.controller.addArea(t.area,t.queryCriteria,t.options)}},{key:"update",value:function(){this.controller.updateAreas()}},{key:"destory",value:function(){this.controller.destroyAreas()}}])&&i(e.prototype,n),u&&i(e,u),t}();e.SyncScroll=u},function(t,e,n){"use strict";n(13),n(3),n(30),Object.defineProperty(e,"__esModule",{value:!0}),e.ScrollControl=void 0;var r=n(60);function o(t,e){for(var n=0;n=r[n=o+i>>1].offsetTop){if(r[n+1].offsetTop>t){e=r[n];break}o=n}else{if(r[n-1].offsetTop<=t){e=r[n-1];break}i=n}return e||r[r.length-1]}},{key:"_topFrag",value:function(t){var e=this.scrollTop+t.ofstScl;return this._locateByOffset(e)}},{key:"_listen",value:function(){this._el.addEventListener("scroll",this._onScroll.bind(this)),this._options.syncWithClick&&this._el.addEventListener("click",this._onClick.bind(this))}},{key:"_rmListen",value:function(){this._el.removeEventListener("scroll",this._onScroll),this._options.syncWithClick&&this._el.removeEventListener("click",this._onClick)}},{key:"_onClick",value:function(t){if(this._syncControl.isLocked())this._syncControl.unlock();else{this._syncControl.lock();var e=t.target.offsetTop;this._curFrag=this._locateByOffset(e),this._syncControl.syncScroll(this,{ofstScl:this._curFrag.offsetTop-this.scrollTop,ofstFrag:e-this._curFrag.offsetTop})}}},{key:"_onScroll",value:function(t){this._syncControl.isLocked()?this._syncControl.unlock():(this._syncControl.lock(),this._curFrag=this._topFrag(this._ofstOps),this._ofstOps.ofstFrag=0,this._syncControl.syncScroll(this,this._ofstOps))}},{key:"syncWith",value:function(t,e,n,r){var o=e-t.offsetTop+r.ofstScl,i=this._fragMap[t.pairId],c=i.height/t.height,u=i.offsetTop+c*o+(c*r.ofstFrag-r.ofstFrag);this.scrollTop=u-r.ofstScl,this.scrollLeft=n}},{key:"updateFragments",value:function(){this._updateFrags()}},{key:"destroy",value:function(){this._rmListen()}}]),t}();e.Area=u},function(t,e,n){"use strict";function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}n(3),Object.defineProperty(e,"__esModule",{value:!0}),e.Fragment=void 0;e.Fragment=function t(e,n,o,i){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),r(this,"pairId",void 0),r(this,"el",void 0),r(this,"offsetTop",void 0),r(this,"height",void 0),this.pairId=e,this.el=n,this.offsetTop=o,this.height=i}},function(t,e,n){n(8)({target:"Object",stat:!0},{setPrototypeOf:n(63)})},function(t,e,n){var r=n(64);t.exports=Object.setPrototypeOf||("__proto__"in{}?function(){var t,e=!1,n={};try{(t=Object.getOwnPropertyDescriptor(Object.prototype,"__proto__").set).call(n,[]),e=n instanceof Array}catch(t){}return function(n,o){return r(n,o),e?t.call(n,o):n.__proto__=o,n}}():void 0)},function(t,e,n){var r=n(2),o=n(11);t.exports=function(t,e){if(o(t),!r(e)&&null!==e)throw TypeError("Can't set "+String(e)+" as a prototype")}}]); -------------------------------------------------------------------------------- /dist/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: octicons-link; 3 | src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff'); 4 | } 5 | 6 | .markdown-body .octicon { 7 | display: inline-block; 8 | fill: currentColor; 9 | vertical-align: text-bottom; 10 | } 11 | 12 | .markdown-body .anchor { 13 | float: left; 14 | line-height: 1; 15 | margin-left: -20px; 16 | padding-right: 4px; 17 | } 18 | 19 | .markdown-body .anchor:focus { 20 | outline: none; 21 | } 22 | 23 | .markdown-body h1 .octicon-link, 24 | .markdown-body h2 .octicon-link, 25 | .markdown-body h3 .octicon-link, 26 | .markdown-body h4 .octicon-link, 27 | .markdown-body h5 .octicon-link, 28 | .markdown-body h6 .octicon-link { 29 | color: #1b1f23; 30 | vertical-align: middle; 31 | visibility: hidden; 32 | } 33 | 34 | .markdown-body h1:hover .anchor, 35 | .markdown-body h2:hover .anchor, 36 | .markdown-body h3:hover .anchor, 37 | .markdown-body h4:hover .anchor, 38 | .markdown-body h5:hover .anchor, 39 | .markdown-body h6:hover .anchor { 40 | text-decoration: none; 41 | } 42 | 43 | .markdown-body h1:hover .anchor .octicon-link, 44 | .markdown-body h2:hover .anchor .octicon-link, 45 | .markdown-body h3:hover .anchor .octicon-link, 46 | .markdown-body h4:hover .anchor .octicon-link, 47 | .markdown-body h5:hover .anchor .octicon-link, 48 | .markdown-body h6:hover .anchor .octicon-link { 49 | visibility: visible; 50 | } 51 | 52 | .markdown-body { 53 | -ms-text-size-adjust: 100%; 54 | -webkit-text-size-adjust: 100%; 55 | color: #24292e; 56 | line-height: 1.5; 57 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol; 58 | font-size: 16px; 59 | line-height: 1.5; 60 | word-wrap: break-word; 61 | } 62 | 63 | .markdown-body .pl-c { 64 | color: #6a737d; 65 | } 66 | 67 | .markdown-body .pl-c1, 68 | .markdown-body .pl-s .pl-v { 69 | color: #005cc5; 70 | } 71 | 72 | .markdown-body .pl-e, 73 | .markdown-body .pl-en { 74 | color: #6f42c1; 75 | } 76 | 77 | .markdown-body .pl-s .pl-s1, 78 | .markdown-body .pl-smi { 79 | color: #24292e; 80 | } 81 | 82 | .markdown-body .pl-ent { 83 | color: #22863a; 84 | } 85 | 86 | .markdown-body .pl-k { 87 | color: #d73a49; 88 | } 89 | 90 | .markdown-body .pl-pds, 91 | .markdown-body .pl-s, 92 | .markdown-body .pl-s .pl-pse .pl-s1, 93 | .markdown-body .pl-sr, 94 | .markdown-body .pl-sr .pl-cce, 95 | .markdown-body .pl-sr .pl-sra, 96 | .markdown-body .pl-sr .pl-sre { 97 | color: #032f62; 98 | } 99 | 100 | .markdown-body .pl-smw, 101 | .markdown-body .pl-v { 102 | color: #e36209; 103 | } 104 | 105 | .markdown-body .pl-bu { 106 | color: #b31d28; 107 | } 108 | 109 | .markdown-body .pl-ii { 110 | background-color: #b31d28; 111 | color: #fafbfc; 112 | } 113 | 114 | .markdown-body .pl-c2 { 115 | background-color: #d73a49; 116 | color: #fafbfc; 117 | } 118 | 119 | .markdown-body .pl-c2:before { 120 | content: "^M"; 121 | } 122 | 123 | .markdown-body .pl-sr .pl-cce { 124 | color: #22863a; 125 | font-weight: 700; 126 | } 127 | 128 | .markdown-body .pl-ml { 129 | color: #735c0f; 130 | } 131 | 132 | .markdown-body .pl-mh, 133 | .markdown-body .pl-mh .pl-en, 134 | .markdown-body .pl-ms { 135 | color: #005cc5; 136 | font-weight: 700; 137 | } 138 | 139 | .markdown-body .pl-mi { 140 | color: #24292e; 141 | font-style: italic; 142 | } 143 | 144 | .markdown-body .pl-mb { 145 | color: #24292e; 146 | font-weight: 700; 147 | } 148 | 149 | .markdown-body .pl-md { 150 | background-color: #ffeef0; 151 | color: #b31d28; 152 | } 153 | 154 | .markdown-body .pl-mi1 { 155 | background-color: #f0fff4; 156 | color: #22863a; 157 | } 158 | 159 | .markdown-body .pl-mc { 160 | background-color: #ffebda; 161 | color: #e36209; 162 | } 163 | 164 | .markdown-body .pl-mi2 { 165 | background-color: #005cc5; 166 | color: #f6f8fa; 167 | } 168 | 169 | .markdown-body .pl-mdr { 170 | color: #6f42c1; 171 | font-weight: 700; 172 | } 173 | 174 | .markdown-body .pl-ba { 175 | color: #586069; 176 | } 177 | 178 | .markdown-body .pl-sg { 179 | color: #959da5; 180 | } 181 | 182 | .markdown-body .pl-corl { 183 | color: #032f62; 184 | text-decoration: underline; 185 | } 186 | 187 | .markdown-body details { 188 | display: block; 189 | } 190 | 191 | .markdown-body summary { 192 | display: list-item; 193 | } 194 | 195 | .markdown-body a { 196 | background-color: transparent; 197 | } 198 | 199 | .markdown-body a:active, 200 | .markdown-body a:hover { 201 | outline-width: 0; 202 | } 203 | 204 | .markdown-body strong { 205 | font-weight: inherit; 206 | font-weight: bolder; 207 | } 208 | 209 | .markdown-body h1 { 210 | font-size: 2em; 211 | margin: .67em 0; 212 | } 213 | 214 | .markdown-body img { 215 | border-style: none; 216 | } 217 | 218 | .markdown-body code, 219 | .markdown-body kbd, 220 | .markdown-body pre { 221 | font-family: monospace,monospace; 222 | font-size: 1em; 223 | } 224 | 225 | .markdown-body hr { 226 | box-sizing: content-box; 227 | height: 0; 228 | overflow: visible; 229 | } 230 | 231 | .markdown-body input { 232 | font: inherit; 233 | margin: 0; 234 | } 235 | 236 | .markdown-body input { 237 | overflow: visible; 238 | } 239 | 240 | .markdown-body [type=checkbox] { 241 | box-sizing: border-box; 242 | padding: 0; 243 | } 244 | 245 | .markdown-body * { 246 | box-sizing: border-box; 247 | } 248 | 249 | .markdown-body input { 250 | font-family: inherit; 251 | font-size: inherit; 252 | line-height: inherit; 253 | } 254 | 255 | .markdown-body a { 256 | color: #0366d6; 257 | text-decoration: none; 258 | } 259 | 260 | .markdown-body a:hover { 261 | text-decoration: underline; 262 | } 263 | 264 | .markdown-body strong { 265 | font-weight: 600; 266 | } 267 | 268 | .markdown-body hr { 269 | background: transparent; 270 | border: 0; 271 | border-bottom: 1px solid #dfe2e5; 272 | height: 0; 273 | margin: 15px 0; 274 | overflow: hidden; 275 | } 276 | 277 | .markdown-body hr:before { 278 | content: ""; 279 | display: table; 280 | } 281 | 282 | .markdown-body hr:after { 283 | clear: both; 284 | content: ""; 285 | display: table; 286 | } 287 | 288 | .markdown-body table { 289 | border-collapse: collapse; 290 | border-spacing: 0; 291 | } 292 | 293 | .markdown-body td, 294 | .markdown-body th { 295 | padding: 0; 296 | } 297 | 298 | .markdown-body details summary { 299 | cursor: pointer; 300 | } 301 | 302 | .markdown-body h1, 303 | .markdown-body h2, 304 | .markdown-body h3, 305 | .markdown-body h4, 306 | .markdown-body h5, 307 | .markdown-body h6 { 308 | margin-bottom: 0; 309 | margin-top: 0; 310 | } 311 | 312 | .markdown-body h1 { 313 | font-size: 32px; 314 | } 315 | 316 | .markdown-body h1, 317 | .markdown-body h2 { 318 | font-weight: 600; 319 | } 320 | 321 | .markdown-body h2 { 322 | font-size: 24px; 323 | } 324 | 325 | .markdown-body h3 { 326 | font-size: 20px; 327 | } 328 | 329 | .markdown-body h3, 330 | .markdown-body h4 { 331 | font-weight: 600; 332 | } 333 | 334 | .markdown-body h4 { 335 | font-size: 16px; 336 | } 337 | 338 | .markdown-body h5 { 339 | font-size: 14px; 340 | } 341 | 342 | .markdown-body h5, 343 | .markdown-body h6 { 344 | font-weight: 600; 345 | } 346 | 347 | .markdown-body h6 { 348 | font-size: 12px; 349 | } 350 | 351 | .markdown-body p { 352 | margin-bottom: 10px; 353 | margin-top: 0; 354 | } 355 | 356 | .markdown-body blockquote { 357 | margin: 0; 358 | } 359 | 360 | .markdown-body ol, 361 | .markdown-body ul { 362 | margin-bottom: 0; 363 | margin-top: 0; 364 | padding-left: 0; 365 | } 366 | 367 | .markdown-body ol ol, 368 | .markdown-body ul ol { 369 | list-style-type: lower-roman; 370 | } 371 | 372 | .markdown-body ol ol ol, 373 | .markdown-body ol ul ol, 374 | .markdown-body ul ol ol, 375 | .markdown-body ul ul ol { 376 | list-style-type: lower-alpha; 377 | } 378 | 379 | .markdown-body dd { 380 | margin-left: 0; 381 | } 382 | 383 | .markdown-body code, 384 | .markdown-body pre { 385 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace; 386 | font-size: 12px; 387 | } 388 | 389 | .markdown-body pre { 390 | margin-bottom: 0; 391 | margin-top: 0; 392 | } 393 | 394 | .markdown-body input::-webkit-inner-spin-button, 395 | .markdown-body input::-webkit-outer-spin-button { 396 | -webkit-appearance: none; 397 | appearance: none; 398 | margin: 0; 399 | } 400 | 401 | .markdown-body .border { 402 | border: 1px solid #e1e4e8!important; 403 | } 404 | 405 | .markdown-body .border-0 { 406 | border: 0!important; 407 | } 408 | 409 | .markdown-body .border-bottom { 410 | border-bottom: 1px solid #e1e4e8!important; 411 | } 412 | 413 | .markdown-body .rounded-1 { 414 | border-radius: 3px!important; 415 | } 416 | 417 | .markdown-body .bg-white { 418 | background-color: #fff!important; 419 | } 420 | 421 | .markdown-body .bg-gray-light { 422 | background-color: #fafbfc!important; 423 | } 424 | 425 | .markdown-body .text-gray-light { 426 | color: #6a737d!important; 427 | } 428 | 429 | .markdown-body .mb-0 { 430 | margin-bottom: 0!important; 431 | } 432 | 433 | .markdown-body .my-2 { 434 | margin-bottom: 8px!important; 435 | margin-top: 8px!important; 436 | } 437 | 438 | .markdown-body .pl-0 { 439 | padding-left: 0!important; 440 | } 441 | 442 | .markdown-body .py-0 { 443 | padding-bottom: 0!important; 444 | padding-top: 0!important; 445 | } 446 | 447 | .markdown-body .pl-1 { 448 | padding-left: 4px!important; 449 | } 450 | 451 | .markdown-body .pl-2 { 452 | padding-left: 8px!important; 453 | } 454 | 455 | .markdown-body .py-2 { 456 | padding-bottom: 8px!important; 457 | padding-top: 8px!important; 458 | } 459 | 460 | .markdown-body .pl-3, 461 | .markdown-body .px-3 { 462 | padding-left: 16px!important; 463 | } 464 | 465 | .markdown-body .px-3 { 466 | padding-right: 16px!important; 467 | } 468 | 469 | .markdown-body .pl-4 { 470 | padding-left: 24px!important; 471 | } 472 | 473 | .markdown-body .pl-5 { 474 | padding-left: 32px!important; 475 | } 476 | 477 | .markdown-body .pl-6 { 478 | padding-left: 40px!important; 479 | } 480 | 481 | .markdown-body .f6 { 482 | font-size: 12px!important; 483 | } 484 | 485 | .markdown-body .lh-condensed { 486 | line-height: 1.25!important; 487 | } 488 | 489 | .markdown-body .text-bold { 490 | font-weight: 600!important; 491 | } 492 | 493 | .markdown-body:before { 494 | content: ""; 495 | display: table; 496 | } 497 | 498 | .markdown-body:after { 499 | clear: both; 500 | content: ""; 501 | display: table; 502 | } 503 | 504 | .markdown-body>:first-child { 505 | margin-top: 0!important; 506 | } 507 | 508 | .markdown-body>:last-child { 509 | margin-bottom: 0!important; 510 | } 511 | 512 | .markdown-body a:not([href]) { 513 | color: inherit; 514 | text-decoration: none; 515 | } 516 | 517 | .markdown-body blockquote, 518 | .markdown-body dl, 519 | .markdown-body ol, 520 | .markdown-body p, 521 | .markdown-body pre, 522 | .markdown-body table, 523 | .markdown-body ul { 524 | margin-bottom: 16px; 525 | margin-top: 0; 526 | } 527 | 528 | .markdown-body hr { 529 | background-color: #e1e4e8; 530 | border: 0; 531 | height: .25em; 532 | margin: 24px 0; 533 | padding: 0; 534 | } 535 | 536 | .markdown-body blockquote { 537 | border-left: .25em solid #dfe2e5; 538 | color: #6a737d; 539 | padding: 0 1em; 540 | } 541 | 542 | .markdown-body blockquote>:first-child { 543 | margin-top: 0; 544 | } 545 | 546 | .markdown-body blockquote>:last-child { 547 | margin-bottom: 0; 548 | } 549 | 550 | .markdown-body kbd { 551 | background-color: #fafbfc; 552 | border: 1px solid #c6cbd1; 553 | border-bottom-color: #959da5; 554 | border-radius: 3px; 555 | box-shadow: inset 0 -1px 0 #959da5; 556 | color: #444d56; 557 | display: inline-block; 558 | font-size: 11px; 559 | line-height: 10px; 560 | padding: 3px 5px; 561 | vertical-align: middle; 562 | } 563 | 564 | .markdown-body h1, 565 | .markdown-body h2, 566 | .markdown-body h3, 567 | .markdown-body h4, 568 | .markdown-body h5, 569 | .markdown-body h6 { 570 | font-weight: 600; 571 | line-height: 1.25; 572 | margin-bottom: 16px; 573 | margin-top: 24px; 574 | } 575 | 576 | .markdown-body h1 { 577 | font-size: 2em; 578 | } 579 | 580 | .markdown-body h1, 581 | .markdown-body h2 { 582 | border-bottom: 1px solid #eaecef; 583 | padding-bottom: .3em; 584 | } 585 | 586 | .markdown-body h2 { 587 | font-size: 1.5em; 588 | } 589 | 590 | .markdown-body h3 { 591 | font-size: 1.25em; 592 | } 593 | 594 | .markdown-body h4 { 595 | font-size: 1em; 596 | } 597 | 598 | .markdown-body h5 { 599 | font-size: .875em; 600 | } 601 | 602 | .markdown-body h6 { 603 | color: #6a737d; 604 | font-size: .85em; 605 | } 606 | 607 | .markdown-body ol, 608 | .markdown-body ul { 609 | padding-left: 2em; 610 | } 611 | 612 | .markdown-body ol ol, 613 | .markdown-body ol ul, 614 | .markdown-body ul ol, 615 | .markdown-body ul ul { 616 | margin-bottom: 0; 617 | margin-top: 0; 618 | } 619 | 620 | .markdown-body li { 621 | word-wrap: break-all; 622 | } 623 | 624 | .markdown-body li>p { 625 | margin-top: 16px; 626 | } 627 | 628 | .markdown-body li+li { 629 | margin-top: .25em; 630 | } 631 | 632 | .markdown-body dl { 633 | padding: 0; 634 | } 635 | 636 | .markdown-body dl dt { 637 | font-size: 1em; 638 | font-style: italic; 639 | font-weight: 600; 640 | margin-top: 16px; 641 | padding: 0; 642 | } 643 | 644 | .markdown-body dl dd { 645 | margin-bottom: 16px; 646 | padding: 0 16px; 647 | } 648 | 649 | .markdown-body table { 650 | display: block; 651 | overflow: auto; 652 | width: 100%; 653 | } 654 | 655 | .markdown-body table th { 656 | font-weight: 600; 657 | } 658 | 659 | .markdown-body table td, 660 | .markdown-body table th { 661 | border: 1px solid #dfe2e5; 662 | padding: 6px 13px; 663 | } 664 | 665 | .markdown-body table tr { 666 | background-color: #fff; 667 | border-top: 1px solid #c6cbd1; 668 | } 669 | 670 | .markdown-body table tr:nth-child(2n) { 671 | background-color: #f6f8fa; 672 | } 673 | 674 | .markdown-body img { 675 | background-color: #fff; 676 | box-sizing: content-box; 677 | max-width: 100%; 678 | } 679 | 680 | .markdown-body img[align=right] { 681 | padding-left: 20px; 682 | } 683 | 684 | .markdown-body img[align=left] { 685 | padding-right: 20px; 686 | } 687 | 688 | .markdown-body code { 689 | background-color: rgba(27,31,35,.05); 690 | border-radius: 3px; 691 | font-size: 85%; 692 | margin: 0; 693 | padding: .2em .4em; 694 | } 695 | 696 | .markdown-body pre { 697 | word-wrap: normal; 698 | } 699 | 700 | .markdown-body pre>code { 701 | background: transparent; 702 | border: 0; 703 | font-size: 100%; 704 | margin: 0; 705 | padding: 0; 706 | white-space: pre; 707 | word-break: normal; 708 | } 709 | 710 | .markdown-body .highlight { 711 | margin-bottom: 16px; 712 | } 713 | 714 | .markdown-body .highlight pre { 715 | margin-bottom: 0; 716 | word-break: normal; 717 | } 718 | 719 | .markdown-body .highlight pre, 720 | .markdown-body pre { 721 | background-color: #f6f8fa; 722 | border-radius: 3px; 723 | font-size: 85%; 724 | line-height: 1.45; 725 | overflow: auto; 726 | padding: 16px; 727 | } 728 | 729 | .markdown-body pre code { 730 | background-color: transparent; 731 | border: 0; 732 | display: inline; 733 | line-height: inherit; 734 | margin: 0; 735 | max-width: auto; 736 | overflow: visible; 737 | padding: 0; 738 | word-wrap: normal; 739 | } 740 | 741 | .markdown-body .commit-tease-sha { 742 | color: #444d56; 743 | display: inline-block; 744 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace; 745 | font-size: 90%; 746 | } 747 | 748 | .markdown-body .blob-wrapper { 749 | border-bottom-left-radius: 3px; 750 | border-bottom-right-radius: 3px; 751 | overflow-x: auto; 752 | overflow-y: hidden; 753 | } 754 | 755 | .markdown-body .blob-wrapper-embedded { 756 | max-height: 240px; 757 | overflow-y: auto; 758 | } 759 | 760 | .markdown-body .blob-num { 761 | -moz-user-select: none; 762 | -ms-user-select: none; 763 | -webkit-user-select: none; 764 | color: rgba(27,31,35,.3); 765 | cursor: pointer; 766 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace; 767 | font-size: 12px; 768 | line-height: 20px; 769 | min-width: 50px; 770 | padding-left: 10px; 771 | padding-right: 10px; 772 | text-align: right; 773 | user-select: none; 774 | vertical-align: top; 775 | white-space: nowrap; 776 | width: 1%; 777 | } 778 | 779 | .markdown-body .blob-num:hover { 780 | color: rgba(27,31,35,.6); 781 | } 782 | 783 | .markdown-body .blob-num:before { 784 | content: attr(data-line-number); 785 | } 786 | 787 | .markdown-body .blob-code { 788 | line-height: 20px; 789 | padding-left: 10px; 790 | padding-right: 10px; 791 | position: relative; 792 | vertical-align: top; 793 | } 794 | 795 | .markdown-body .blob-code-inner { 796 | color: #24292e; 797 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace; 798 | font-size: 12px; 799 | overflow: visible; 800 | white-space: pre; 801 | word-wrap: normal; 802 | } 803 | 804 | .markdown-body .pl-token.active, 805 | .markdown-body .pl-token:hover { 806 | background: #ffea7f; 807 | cursor: pointer; 808 | } 809 | 810 | .markdown-body kbd { 811 | background-color: #fafbfc; 812 | border: 1px solid #d1d5da; 813 | border-bottom-color: #c6cbd1; 814 | border-radius: 3px; 815 | box-shadow: inset 0 -1px 0 #c6cbd1; 816 | color: #444d56; 817 | display: inline-block; 818 | font: 11px SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace; 819 | line-height: 10px; 820 | padding: 3px 5px; 821 | vertical-align: middle; 822 | } 823 | 824 | .markdown-body :checked+.radio-label { 825 | border-color: #0366d6; 826 | position: relative; 827 | z-index: 1; 828 | } 829 | 830 | .markdown-body .tab-size[data-tab-size="1"] { 831 | -moz-tab-size: 1; 832 | tab-size: 1; 833 | } 834 | 835 | .markdown-body .tab-size[data-tab-size="2"] { 836 | -moz-tab-size: 2; 837 | tab-size: 2; 838 | } 839 | 840 | .markdown-body .tab-size[data-tab-size="3"] { 841 | -moz-tab-size: 3; 842 | tab-size: 3; 843 | } 844 | 845 | .markdown-body .tab-size[data-tab-size="4"] { 846 | -moz-tab-size: 4; 847 | tab-size: 4; 848 | } 849 | 850 | .markdown-body .tab-size[data-tab-size="5"] { 851 | -moz-tab-size: 5; 852 | tab-size: 5; 853 | } 854 | 855 | .markdown-body .tab-size[data-tab-size="6"] { 856 | -moz-tab-size: 6; 857 | tab-size: 6; 858 | } 859 | 860 | .markdown-body .tab-size[data-tab-size="7"] { 861 | -moz-tab-size: 7; 862 | tab-size: 7; 863 | } 864 | 865 | .markdown-body .tab-size[data-tab-size="8"] { 866 | -moz-tab-size: 8; 867 | tab-size: 8; 868 | } 869 | 870 | .markdown-body .tab-size[data-tab-size="9"] { 871 | -moz-tab-size: 9; 872 | tab-size: 9; 873 | } 874 | 875 | .markdown-body .tab-size[data-tab-size="10"] { 876 | -moz-tab-size: 10; 877 | tab-size: 10; 878 | } 879 | 880 | .markdown-body .tab-size[data-tab-size="11"] { 881 | -moz-tab-size: 11; 882 | tab-size: 11; 883 | } 884 | 885 | .markdown-body .tab-size[data-tab-size="12"] { 886 | -moz-tab-size: 12; 887 | tab-size: 12; 888 | } 889 | 890 | .markdown-body .task-list-item { 891 | list-style-type: none; 892 | } 893 | 894 | .markdown-body .task-list-item+.task-list-item { 895 | margin-top: 3px; 896 | } 897 | 898 | .markdown-body .task-list-item input { 899 | margin: 0 .2em .25em -1.6em; 900 | vertical-align: middle; 901 | } 902 | 903 | .markdown-body hr { 904 | border-bottom-color: #eee; 905 | } 906 | 907 | .markdown-body .pl-0 { 908 | padding-left: 0!important; 909 | } 910 | 911 | .markdown-body .pl-1 { 912 | padding-left: 4px!important; 913 | } 914 | 915 | .markdown-body .pl-2 { 916 | padding-left: 8px!important; 917 | } 918 | 919 | .markdown-body .pl-3 { 920 | padding-left: 16px!important; 921 | } 922 | 923 | .markdown-body .pl-4 { 924 | padding-left: 24px!important; 925 | } 926 | 927 | .markdown-body .pl-5 { 928 | padding-left: 32px!important; 929 | } 930 | 931 | .markdown-body .pl-6 { 932 | padding-left: 40px!important; 933 | } 934 | 935 | .markdown-body .pl-7 { 936 | padding-left: 48px!important; 937 | } 938 | 939 | .markdown-body .pl-8 { 940 | padding-left: 64px!important; 941 | } 942 | 943 | .markdown-body .pl-9 { 944 | padding-left: 80px!important; 945 | } 946 | 947 | .markdown-body .pl-10 { 948 | padding-left: 96px!important; 949 | } 950 | 951 | .markdown-body .pl-11 { 952 | padding-left: 112px!important; 953 | } 954 | 955 | .markdown-body .pl-12 { 956 | padding-left: 128px!important; 957 | } 958 | /* 959 | 960 | Atom One Dark by Daniel Gamage 961 | Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax 962 | 963 | base: #282c34 964 | mono-1: #abb2bf 965 | mono-2: #818896 966 | mono-3: #5c6370 967 | hue-1: #56b6c2 968 | hue-2: #61aeee 969 | hue-3: #c678dd 970 | hue-4: #98c379 971 | hue-5: #e06c75 972 | hue-5-2: #be5046 973 | hue-6: #d19a66 974 | hue-6-2: #e6c07b 975 | 976 | */ 977 | 978 | .hljs { 979 | display: block; 980 | overflow-x: auto; 981 | padding: 0.5em; 982 | color: #abb2bf; 983 | background: #282c34; 984 | } 985 | 986 | .hljs-comment, 987 | .hljs-quote { 988 | color: #5c6370; 989 | font-style: italic; 990 | } 991 | 992 | .hljs-doctag, 993 | .hljs-keyword, 994 | .hljs-formula { 995 | color: #c678dd; 996 | } 997 | 998 | .hljs-section, 999 | .hljs-name, 1000 | .hljs-selector-tag, 1001 | .hljs-deletion, 1002 | .hljs-subst { 1003 | color: #e06c75; 1004 | } 1005 | 1006 | .hljs-literal { 1007 | color: #56b6c2; 1008 | } 1009 | 1010 | .hljs-string, 1011 | .hljs-regexp, 1012 | .hljs-addition, 1013 | .hljs-attribute, 1014 | .hljs-meta-string { 1015 | color: #98c379; 1016 | } 1017 | 1018 | .hljs-built_in, 1019 | .hljs-class .hljs-title { 1020 | color: #e6c07b; 1021 | } 1022 | 1023 | .hljs-attr, 1024 | .hljs-variable, 1025 | .hljs-template-variable, 1026 | .hljs-type, 1027 | .hljs-selector-class, 1028 | .hljs-selector-attr, 1029 | .hljs-selector-pseudo, 1030 | .hljs-number { 1031 | color: #d19a66; 1032 | } 1033 | 1034 | .hljs-symbol, 1035 | .hljs-bullet, 1036 | .hljs-link, 1037 | .hljs-meta, 1038 | .hljs-selector-id, 1039 | .hljs-title { 1040 | color: #61aeee; 1041 | } 1042 | 1043 | .hljs-emphasis { 1044 | font-style: italic; 1045 | } 1046 | 1047 | .hljs-strong { 1048 | font-weight: bold; 1049 | } 1050 | 1051 | .hljs-link { 1052 | text-decoration: underline; 1053 | } 1054 | /* 设置滚动条样式 */ 1055 | ::-webkit-scrollbar { 1056 | width: 6px; 1057 | height: 6px; 1058 | background-color: transparent; 1059 | } 1060 | 1061 | ::-webkit-scrollbar-thumb { 1062 | background-color: darkgray; 1063 | } 1064 | 1065 | * { 1066 | padding: 0; 1067 | margin: 0; 1068 | } 1069 | body { 1070 | height: 100vh; 1071 | width: 100vw; 1072 | } 1073 | button { 1074 | border: 0; 1075 | padding: 4px; 1076 | } 1077 | .flex { 1078 | display: flex; 1079 | } 1080 | .flex-col { 1081 | flex-direction: column; 1082 | } 1083 | .heading { 1084 | flex: 0 0 48px; 1085 | } 1086 | .content { 1087 | position: relative; 1088 | flex: 1 1 auto; 1089 | } 1090 | .area { 1091 | position: relative; 1092 | overflow: auto; 1093 | flex: 1; 1094 | box-sizing: border-box; 1095 | border: 1px darkgray solid; 1096 | height: 100%; 1097 | padding: 20px; 1098 | } 1099 | 1100 | .testPerformance div { 1101 | margin: 10px; 1102 | } 1103 | -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sync Scroll Demo 6 | 7 | 8 | 9 | Fork me on GitHub 10 | 11 | 12 |
13 |
14 | 下面那根线是用来帮助观察对齐,DEMO中滚动偏移量为100px,偏移线距离顶部也为100px。 15 |
16 | 滚动的话你会发现标题是在偏移线的位置对齐的,这是一个额外的功能,你可以自行调整对齐的位置(注:默认为偏移量为0,即顶部)。 17 |
18 |
19 |
20 |
# Sync Scroll
21 |
![GitHub stars](https://img.shields.io/github/stars/siaikin/sync-scroll.svg?style=social) 22 | ![GitHub followers](https://img.shields.io/github/followers/siaikin.svg?style=social) 23 | ![GitHub forks](https://img.shields.io/github/forks/siaikin/sync-scroll.svg?style=social) 24 | ![GitHub watchers](https://img.shields.io/github/watchers/siaikin/sync-scroll.svg?style=social) 25 | ![GitHub last commit](https://img.shields.io/github/last-commit/siaikin/sync-scroll.svg) 26 | ![NPM](https://img.shields.io/npm/l/md-sync-scroll.svg) 27 | ![npm](https://img.shields.io/npm/v/md-sync-scroll.svg) 28 |
29 |

30 |
这是一个基于**片段**的同步滚动插件
31 |

 32 | ```
 33 | // sync-scroll被占用了,没得办法.jpg
 34 | npm install md-sync-scroll --save
 35 | ```
 36 |             
37 |
## 片断
38 |

 39 | ```markdown
 40 | // 片段start
 41 | # 这表示一个片段的开始
 42 | 这些是片段内容
 43 | 这些也是片段内容
 44 | // 片段end
 45 | ```
 46 |             
47 |
如果你想要使用它让你的内容能进行同步滚动,你需要让你的内容符合以上的布局。这种布局很多Markdown格式的文本都是天然符合的,不过就是标题-内容-标题-内容...
48 |

49 |
示例:
50 |

 51 | ```markdown
 52 | # 文章标题
 53 | 标题下的内容...
 54 | # 文章标题2
 55 | 标题下的内容2...
 56 | ...
 57 | ```
 58 |             
59 |
其实这个页面就是一个很好的例子,当你滚动滚轮或滚动条的时候,注意一下以`#`号开头的段落,就就能看到效果了。
60 |

61 |
# Usage
62 |
如果想让SyncScroll正常运行,需要你用一个块级元素包裹你的内容,你可以按`F12`查看此页的代码。
63 |
我用`<div>`包裹了里面的内容,还有设置`overflow: auto`和`position: reactive`。 64 | 请务必设置overflow和position,overflow我想不必多说,它决定了是否能进行滚动。 65 |
66 |

67 |
68 | 对于position,因为我使用`HTMLElement.offsetTop`来获取片段的偏移高度, 69 | 而`offsetTop`依赖于最近的定位元素。如果你不将包裹的元素设置为定位元素,将无法获取到正确的`offsetTop`。 70 |
71 |

72 |
73 | 下面我们进入正题,`md-sync-scroll`的用法很简单,你只需要将要进行同步的元素和查询子元素的查询语句 74 | 传入`addArea(el, queryCriteria)`方法中就好了。然后如果你改变了其中的内容,请调用`update()`方法以更新内容。 75 |
76 |

77 |
1. 首先引入
78 |

 79 | ```js
 80 | import {SyncScroll, ConfigOptions} from "md-sync-scroll";
 81 | ```
 82 |             
83 |
2. 设置要进行同步的DOM元素。你需要指定每个`Area`的子元素查询条件。
84 |

85 |
在内部我用`querySelectorAll`来查询子元素。所以你需要传入符合css选择器语法的字符串。 86 |
87 |

 88 | ```js
 89 | import {SyncScroll, ConfigOptions} from "md-sync-scroll";
 90 | 
 91 | const editArea = document.getElementById('edit');
 92 | const previewArea = document.getElementById('preview');
 93 | // 通过ConfigOptions可以配置参数,详细信息见下文`API`
 94 | const options = ConfigOptions.instance({
 95 |      syncWithClick: true,
 96 |      offsetScroll: 100
 97 | });
 98 | const syncScroll = new SyncScroll(options);
 99 | 
100 | // 对于本页面来说,我用`h1-6`指示片段的开始,那么我就要查询被我指定为`h1-h6`的元素
101 | // 在左边我用class='h1-6'标记,在右边用<h1>-<h6>表示
102 | // syncScroll.addArea(editArea, '.h1,.h2,.h3,.h4,.h5,.h6');
103 | // syncScroll.addArea(previewArea, 'h1,h2,h3,h4,h5,h6');
104 | syncScroll.addAreas([
105 |     {
106 |         area: editArea,
107 |         queryCriteria: '.h1,.h2,.h3,.h4,.h5,.h6'
108 |     },
109 |     {
110 |         area: previewArea,
111 |         queryCriteria: 'h1,h2,h3,h4,h5,h6'
112 |     }
113 | ]);
114 | // 可以调用`addArea`单个添加,在`addArea`调用后,需要手动调用`update`更新数据
115 | // syncScroll.addArea({
116 | //     area: editArea,
117 | //     queryCriteria: '.h1,.h2,.h3,.h4,.h5,.h6'
118 | // });
119 | // syncScroll.addArea({
120 | //     area: previewArea,
121 | //     queryCriteria: 'h1,h2,h3,h4,h5,h6'
122 | // });
123 | // syncScroll.update();
124 | 
125 | ```
126 |             
127 |
# API
128 |
## SyncScroll
129 |
### PROPERTY
130 |
| Prop name | Type | Default | Description |
131 |
| --- | --- | --- | --- |
132 |
| areas | Array<Area> | null | `Area`的集合 |
133 |
| controller | ScrollControl | null | 内部的control类,正常使用请不要碰它,主要作用是关联各个`Area`。 |
134 |
### FUNCTION
135 |
| Func name | Params | Default | Description |
136 |
| --- | --- | --- | --- |
137 |
| addAreas | [{el, queryCriteria}, ...] | 添加多个`Area`,指定Dom元素el,和子元素查询语句,会自动调用更新方法。 |
138 |
| addArea | el, queryCriteria | null | 添加一个`Area`,指定一个Dom元素el,和子元素查询语句 |
139 |
| update | null | `Area`内容改变时调用,更新所有的`Area` |
140 |
| destroy | null | 销毁实例 |
141 |

142 |
## ConfigOptions
143 |
### PROPERTY
144 |
| Prop name | Type | Default | Description |
145 |
| --- | --- | --- | --- |
146 |
| syncWithLick | boolean | false | `click`事件是否触发滚动 |
147 |
| offsetScroll | number | 0 | 滚动对齐位置的偏移量,默认在顶部即`0` |
148 |
### FUNCTION
149 |
| Func name | Params | Default | Description |
150 |
| --- | --- | --- | --- |
151 |
| instance | {syncWithClick, offsetScroll} | 获取一个实例,参数为上面PROPERTY的参数,例子在上文usage第2项 |
152 |
| ConfigOptions | {syncWithClick, offsetScroll} | 推荐使用`instance`来获取`ConfigOptions`来获取对象能稍微减小内存开销。构造函数,参数为上面PROPERTY的参数 |
153 |

154 |
# 以下仅作为测试数据,用于测试同步滚动的效果。也可以点击上方按钮增大边距,查看效果
155 |

156 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
157 |

158 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
159 |

160 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
161 |

162 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
163 |

164 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
165 |

166 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
167 |

168 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
169 |

170 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
171 |

172 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
173 |

174 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
175 |

176 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
177 |

178 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
179 |

180 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
181 |

182 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
183 |

184 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
185 |

186 |

187 |

188 |

189 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
190 |

191 |

192 |

193 |

194 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
195 |

196 |

197 |

198 |

199 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
200 |

201 |

202 |

203 |

204 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
205 |

206 |

207 |

208 |

209 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
210 |

211 |

212 |

213 |

214 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
215 |

216 |

217 |

218 |

219 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
220 |

221 |

222 |

223 |

224 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
225 |

226 |

227 |

228 |

229 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
230 |

231 |

232 |

233 |

234 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
235 |

236 |

237 |

238 |

239 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
240 |

241 |

242 |

243 |

244 |
# **Look This** 你有看到滚动的效果吗?
245 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
246 |

247 |

248 |

249 |

250 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
251 |

252 |

253 |

254 |

255 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
256 |

257 |

258 |

259 |

260 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
261 |

262 |

263 |

264 |

265 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
266 |

267 |

268 |

269 |

270 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
271 |

272 |

273 |

274 |

275 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
276 |

277 |

278 |

279 |

280 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
281 |

282 |

283 |

284 |

285 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
286 |

287 |

288 |

289 |

290 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
291 |

292 |

293 |

294 |

295 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
296 |

297 |

298 |

299 |

300 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
301 |

302 |

303 |

304 |

305 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
306 |

307 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
308 |

309 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
310 |

311 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
312 |

313 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
314 |

315 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
316 |

317 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
318 |

319 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
320 |

321 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
322 |

323 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
324 |

325 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
326 |

327 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
328 |

329 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
330 |

331 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
332 |

333 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
334 |

335 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
336 |

337 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
338 |

339 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
340 |

341 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
342 |

343 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
344 |

345 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
346 |

347 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
348 |

349 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
350 |

351 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
352 |

353 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
354 |

355 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
356 |

357 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
358 |

359 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
360 |

361 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
362 |

363 |
364 | 365 |
366 | 367 |
368 |
# Sync Scroll
369 |
![GitHub stars](https://img.shields.io/github/stars/siaikin/sync-scroll.svg?style=social) 370 | ![GitHub followers](https://img.shields.io/github/followers/siaikin.svg?style=social) 371 | ![GitHub forks](https://img.shields.io/github/forks/siaikin/sync-scroll.svg?style=social) 372 | ![GitHub watchers](https://img.shields.io/github/watchers/siaikin/sync-scroll.svg?style=social) 373 | ![GitHub last commit](https://img.shields.io/github/last-commit/siaikin/sync-scroll.svg) 374 | ![NPM](https://img.shields.io/npm/l/md-sync-scroll.svg) 375 | ![npm](https://img.shields.io/npm/v/md-sync-scroll.svg) 376 |
377 |

378 |
这是一个基于**片段**的同步滚动插件
379 |

380 |   ```
381 |   // sync-scroll被占用了,没得办法.jpg
382 |   npm install md-sync-scroll --save
383 |   ```
384 |               
385 |
## 片断
386 |

387 |   ```markdown
388 |   // 片段start
389 |   # 这表示一个片段的开始
390 |   这些是片段内容
391 |   这些也是片段内容
392 |   // 片段end
393 |   ```
394 |               
395 |
如果你想要使用它让你的内容能进行同步滚动,你需要让你的内容符合以上的布局。这种布局很多Markdown格式的文本都是天然符合的,不过就是标题-内容-标题-内容...
396 |

397 |
示例:
398 |

399 |   ```markdown
400 |   # 文章标题
401 |   标题下的内容...
402 |   # 文章标题2
403 |   标题下的内容2...
404 |   ...
405 |   ```
406 |               
407 |
其实这个页面就是一个很好的例子,当你滚动滚轮或滚动条的时候,注意一下以`#`号开头的段落,就就能看到效果了。
408 |

409 |
# Usage
410 |
如果想让SyncScroll正常运行,需要你用一个块级元素包裹你的内容,你可以按`F12`查看此页的代码。
411 |
我用`<div>`包裹了里面的内容,还有设置`overflow: auto`和`position: reactive`。 412 | 请务必设置overflow和position,overflow我想不必多说,它决定了是否能进行滚动。 413 |
414 |

415 |
416 | 对于position,因为我使用`HTMLElement.offsetTop`来获取片段的偏移高度, 417 | 而`offsetTop`依赖于最近的定位元素。如果你不将包裹的元素设置为定位元素,将无法获取到正确的`offsetTop`。 418 |
419 |

420 |
421 | 下面我们进入正题,`md-sync-scroll`的用法很简单,你只需要将要进行同步的元素和查询子元素的查询语句 422 | 传入`addArea(el, queryCriteria)`方法中就好了。然后如果你改变了其中的内容,请调用`update()`方法以更新内容。 423 |
424 |

425 |
1. 首先引入
426 |

427 |   ```js
428 |   import {SyncScroll, ConfigOptions} from "md-sync-scroll";
429 |   ```
430 |               
431 |
2. 设置要进行同步的DOM元素。你需要指定每个`Area`的子元素查询条件。
432 |

433 |
在内部我用`querySelectorAll`来查询子元素。所以你需要传入符合css选择器语法的字符串。 434 |
435 |

436 |   ```js
437 |   import {SyncScroll, ConfigOptions} from "md-sync-scroll";
438 | 
439 |   const editArea = document.getElementById('edit');
440 |   const previewArea = document.getElementById('preview');
441 |   // 通过ConfigOptions可以配置参数,详细信息见下文`API`
442 |   const options = ConfigOptions.instance({
443 |        syncWithClick: true,
444 |        offsetScroll: 100
445 |   });
446 |   const syncScroll = new SyncScroll(options);
447 | 
448 |   // 对于本页面来说,我用`h1-6`指示片段的开始,那么我就要查询被我指定为`h1-h6`的元素
449 |   // 在左边我用class='h1-6'标记,在右边用<h1>-<h6>表示
450 |   // syncScroll.addArea(editArea, '.h1,.h2,.h3,.h4,.h5,.h6');
451 |   // syncScroll.addArea(previewArea, 'h1,h2,h3,h4,h5,h6');
452 |   syncScroll.addAreas([
453 |       {
454 |           area: editArea,
455 |           queryCriteria: '.h1,.h2,.h3,.h4,.h5,.h6'
456 |       },
457 |       {
458 |           area: previewArea,
459 |           queryCriteria: 'h1,h2,h3,h4,h5,h6'
460 |       }
461 |   ]);
462 |   // 可以调用`addArea`单个添加,在`addArea`调用后,需要手动调用`update`更新数据
463 |   // syncScroll.addArea({
464 |   //     area: editArea,
465 |   //     queryCriteria: '.h1,.h2,.h3,.h4,.h5,.h6'
466 |   // });
467 |   // syncScroll.addArea({
468 |   //     area: previewArea,
469 |   //     queryCriteria: 'h1,h2,h3,h4,h5,h6'
470 |   // });
471 |   // syncScroll.update();
472 | 
473 |   ```
474 |               
475 |
# API
476 |
## SyncScroll
477 |
### PROPERTY
478 |
| Prop name | Type | Default | Description |
479 |
| --- | --- | --- | --- |
480 |
| areas | Array<Area> | null | `Area`的集合 |
481 |
| controller | ScrollControl | null | 内部的control类,正常使用请不要碰它,主要作用是关联各个`Area`。 |
482 |
### FUNCTION
483 |
| Func name | Params | Default | Description |
484 |
| --- | --- | --- | --- |
485 |
| addAreas | [{el, queryCriteria}, ...] | 添加多个`Area`,指定Dom元素el,和子元素查询语句,会自动调用更新方法。 |
486 |
| addArea | el, queryCriteria | null | 添加一个`Area`,指定一个Dom元素el,和子元素查询语句 |
487 |
| update | null | `Area`内容改变时调用,更新所有的`Area` |
488 |
| destroy | null | 销毁实例 |
489 |

490 |
## ConfigOptions
491 |
### PROPERTY
492 |
| Prop name | Type | Default | Description |
493 |
| --- | --- | --- | --- |
494 |
| syncWithLick | boolean | false | `click`事件是否触发滚动 |
495 |
| offsetScroll | number | 0 | 滚动对齐位置的偏移量,默认在顶部即`0` |
496 |
### FUNCTION
497 |
| Func name | Params | Default | Description |
498 |
| --- | --- | --- | --- |
499 |
| instance | {syncWithClick, offsetScroll} | 获取一个实例,参数为上面PROPERTY的参数,例子在上文usage第2项 |
500 |
| ConfigOptions | {syncWithClick, offsetScroll} | 推荐使用`instance`来获取`ConfigOptions`来获取对象能稍微减小内存开销。构造函数,参数为上面PROPERTY的参数 |
501 |

502 |
# 以下仅作为测试数据,用于测试同步滚动的效果。也可以点击上方按钮增大边距,查看效果
503 |

504 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
505 |

506 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
507 |

508 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
509 |

510 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
511 |

512 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
513 |

514 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
515 |

516 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
517 |

518 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
519 |

520 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
521 |

522 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
523 |

524 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
525 |

526 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
527 |

528 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
529 |

530 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
531 |

532 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
533 |

534 |

535 |

536 |

537 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
538 |

539 |

540 |

541 |

542 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
543 |

544 |

545 |

546 |

547 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
548 |

549 |

550 |

551 |

552 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
553 |

554 |

555 |

556 |

557 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
558 |

559 |

560 |

561 |

562 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
563 |

564 |

565 |

566 |

567 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
568 |

569 |

570 |

571 |

572 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
573 |

574 |

575 |

576 |

577 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
578 |

579 |

580 |

581 |

582 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
583 |

584 |

585 |

586 |

587 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
588 |

589 |

590 |

591 |

592 |
# **Look This** 你有看到滚动的效果吗?
593 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
594 |

595 |

596 |

597 |

598 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
599 |

600 |

601 |

602 |

603 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
604 |

605 |

606 |

607 |

608 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
609 |

610 |

611 |

612 |

613 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
614 |

615 |

616 |

617 |

618 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
619 |

620 |

621 |

622 |

623 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
624 |

625 |

626 |

627 |

628 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
629 |

630 |

631 |

632 |

633 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
634 |

635 |

636 |

637 |

638 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
639 |

640 |

641 |

642 |

643 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
644 |

645 |

646 |

647 |

648 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
649 |

650 |

651 |

652 |

653 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
654 |

655 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
656 |

657 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
658 |

659 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
660 |

661 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
662 |

663 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
664 |

665 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
666 |

667 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
668 |

669 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
670 |

671 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
672 |

673 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
674 |

675 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
676 |

677 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
678 |

679 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
680 |

681 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
682 |

683 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
684 |

685 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
686 |

687 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
688 |

689 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
690 |

691 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
692 |

693 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
694 |

695 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
696 |

697 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
698 |

699 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
700 |

701 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
702 |

703 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
704 |

705 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
706 |

707 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
708 |

709 |
当我们滚动鼠标滚轮时,他们会基于**片断**进行等比例滚动
710 |

711 |
712 | 713 |
714 |
715 |
716 |
717 | 718 | 719 | --------------------------------------------------------------------------------