├── src ├── assets │ └── .keep ├── polyfills.ts ├── favicon.png ├── app │ ├── rubberband-options.ts │ ├── point.ts │ ├── rectangle.ts │ └── rubberband.ts ├── styles │ └── _rubberband.scss ├── main.ts ├── style.scss └── index.html ├── demo.gif ├── tsconfig.json ├── .babelrc ├── webpack.dev.js ├── README.md ├── webpack.prod.js ├── package.json ├── LICENSE ├── .gitignore └── webpack.common.js /src/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // import 'core-js/es/array/flat-map'; 2 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hungtcs/tcs-rubberband/HEAD/demo.gif -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hungtcs/tcs-rubberband/HEAD/src/favicon.png -------------------------------------------------------------------------------- /src/app/rubberband-options.ts: -------------------------------------------------------------------------------- 1 | 2 | export class RubberbandOptions { 3 | public element?: HTMLElement; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/point.ts: -------------------------------------------------------------------------------- 1 | 2 | export class Point { 3 | public x: number; 4 | public y: number; 5 | 6 | constructor(that: Partial = {}) { 7 | Object.assign(this, that); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "esModuleInterop": true, 7 | "lib": [ 8 | "DOM", 9 | "ES2015" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/rectangle.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "./point" 2 | 3 | export class Rectangle extends Point { 4 | public width: number; 5 | public height: number; 6 | 7 | constructor(that: Partial = {}) { 8 | super(); 9 | Object.assign(this, that); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "last 2 versions", 9 | "safari >= 7", 10 | "ie >= 10" 11 | ] 12 | } 13 | } 14 | ] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const common = require('./webpack.common'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'development', 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: '[name].js', 10 | }, 11 | devtool: 'source-map' 12 | }); 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TCS Rubberban 2 | ==== 3 | 4 | JS的实现框选效果,**这不是一个library,这只是一个实现demo** 5 | 6 | ![](./demo.gif) 7 | 8 | ### 使用方式 9 | 1. 克隆项目到本地 10 | ```shell 11 | git clone https://github.com/hungtcs-lab/tcs-rubberband.git 12 | ``` 13 | 2. 切换到项目目录 14 | ```shell 15 | cd tcs-rubberband 16 | ``` 17 | 3. 安装npm依赖,接着启动项目 18 | ```shell 19 | npm install 20 | npm start 21 | ``` 22 | 4. 使用浏览器访问,默认端口为`3100` 23 | -------------------------------------------------------------------------------- /src/styles/_rubberband.scss: -------------------------------------------------------------------------------- 1 | 2 | .tcs-rubberband-container { 3 | display: block; 4 | position: relative; 5 | &.tcs-rubberband--active { 6 | user-select: none; 7 | } 8 | > .tcs-rubberband { 9 | border: 1px solid rgba($color: #000000, $alpha: 0.32); 10 | position: absolute; 11 | box-sizing: border-box; 12 | border-radius: 3px; 13 | background-color: rgba($color: #000000, $alpha: 0.25); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Rubberband } from './app/rubberband'; 2 | 3 | document.addEventListener('DOMContentLoaded', _ => { 4 | const countElement = document.querySelector('.selected-count'); 5 | new Rubberband({ 6 | element: document.querySelector('.rubberband-container'), 7 | }) 8 | .onSelectedCellsChange((selectedCells) => { 9 | countElement.innerHTML = `${ selectedCells.size }`; 10 | console.log(selectedCells); 11 | }); 12 | }); 13 | 14 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const common = require('./webpack.common'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'production', 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: '[name].[hash].js', 10 | }, 11 | optimization: { 12 | splitChunks: { 13 | cacheGroups: { 14 | commons: { 15 | test: /[\\/]node_modules[\\/]/, 16 | name: (module, chunks, cacheGroupKey) => { 17 | const moduleFileName = module.identifier().split('/').reduceRight(item => item); 18 | // const allChunksNames = chunks.map((item) => item.name).join('~'); 19 | return `${ cacheGroupKey }-${ moduleFileName }`; 20 | }, 21 | chunks: 'all' 22 | }, 23 | }, 24 | }, 25 | }, 26 | performance: { 27 | hints: false, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-typescript-starter", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "webpack-dev-server --config webpack.dev.js --port=3100", 7 | "build": "rimraf dist && webpack --config webpack.dev.js", 8 | "build:prod": "rimraf dist && webpack --config webpack.prod.js" 9 | }, 10 | "devDependencies": { 11 | "@babel/core": "^7.6.4", 12 | "@babel/preset-env": "^7.6.3", 13 | "babel-loader": "^8.0.6", 14 | "babel-preset-env": "^1.7.0", 15 | "clean-webpack-plugin": "^3.0.0", 16 | "copy-webpack-plugin": "^5.0.4", 17 | "css-loader": "^3.2.0", 18 | "html-webpack-plugin": "^3.2.0", 19 | "mini-css-extract-plugin": "^0.8.0", 20 | "node-sass": "^4.13.0", 21 | "rimraf": "^3.0.2", 22 | "sass-loader": "^8.0.0", 23 | "style-loader": "^1.0.0", 24 | "ts-loader": "^6.2.1", 25 | "typescript": "^3.6.4", 26 | "webpack": "^4.41.2", 27 | "webpack-cli": "^3.3.9", 28 | "webpack-merge": "^4.2.2" 29 | }, 30 | "dependencies": { 31 | "core-js": "^3.6.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 鸿则 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | dist/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 6 | 7 | module.exports = { 8 | entry: { 9 | main: './src/main.ts', 10 | style: './src/style.scss', 11 | polyfills: './src/polyfills.ts', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.ts$/, 17 | use: [ 18 | { 19 | loader: 'babel-loader', 20 | options: { 21 | presets: ['@babel/preset-env'] 22 | }, 23 | }, 24 | { 25 | loader: 'ts-loader', 26 | }, 27 | ], 28 | exclude: /node_modules/, 29 | }, 30 | { 31 | test: /\.s[ac]ss$/i, 32 | use: [ 33 | { 34 | loader: MiniCssExtractPlugin.loader, 35 | }, 36 | { 37 | loader: 'css-loader', 38 | }, 39 | { 40 | loader: 'sass-loader', 41 | }, 42 | ], 43 | include: [ 44 | path.join(__dirname, './src/styles'), 45 | path.join(__dirname, './src/style.scss'), 46 | ], 47 | }, 48 | ], 49 | }, 50 | plugins: [ 51 | new CleanWebpackPlugin({ 52 | verbose: true, 53 | }), 54 | new HtmlWebpackPlugin({ 55 | template: './src/index.html', 56 | }), 57 | new CopyWebpackPlugin([ 58 | { from: './src/assets', to: 'assets' }, 59 | { from: './src/favicon.png', to: 'favicon.png' }, 60 | ]), 61 | new MiniCssExtractPlugin({ 62 | filename: '[name].[hash].css', 63 | chunkFilename: '[id].[hash].css', 64 | }), 65 | ], 66 | resolve: { 67 | extensions: [ '.tsx', '.ts', '.js' ], 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | @import "./styles/rubberband"; 2 | 3 | html, body { 4 | width: 100vw; 5 | margin: 0; 6 | height: 100vh; 7 | padding: 0; 8 | overflow: hidden; 9 | } 10 | 11 | body { 12 | padding: 2rem; 13 | overflow: auto; 14 | flex-flow: column nowrap; 15 | box-sizing: border-box; 16 | background-color: #EEE; 17 | > h2 { 18 | margin: 0px 0px 1.5rem 0px; 19 | font-weight: normal; 20 | font-family: arial,sans-serif; 21 | } 22 | } 23 | 24 | // 自定义滚动条样式 25 | ::-webkit-scrollbar { 26 | width: 8px; 27 | } 28 | 29 | ::-webkit-scrollbar-thumb { 30 | background: rgba($color: #000000, $alpha: 0.25); 31 | border-radius: 4px; 32 | } 33 | 34 | // 实现一个简单的栅格系统 35 | .row { 36 | margin: 0px -0.5rem; 37 | display: flex; 38 | flex-flow: row wrap; 39 | > .col { 40 | @media (max-width: 500px) { 41 | flex: 0 1 (100%/1); 42 | } 43 | @media (min-width: 500px) and (max-width: 720px) { 44 | flex: 0 1 (100%/2); 45 | } 46 | @media (min-width: 720px) and (max-width: 920px) { 47 | flex: 0 1 (100%/3); 48 | } 49 | @media (min-width: 920px) and (max-width: 1080px) { 50 | flex: 0 1 (100%/4); 51 | } 52 | @media (min-width: 1080px) and (max-width: 1500px) { 53 | flex: 0 1 (100%/5); 54 | } 55 | @media (min-width: 1500px) and (max-width: 1920px) { 56 | flex: 0 1 (100%/6); 57 | } 58 | flex: 0 1 25%; 59 | padding: 0.5rem 0.5rem; 60 | box-sizing: border-box; 61 | } 62 | } 63 | 64 | 65 | // 设置划选框样式 66 | .rubberband-container { 67 | padding: 0.5rem 1rem; 68 | overflow: hidden; 69 | box-shadow: 0px 0px 4px 0px rgba($color: #000000, $alpha: 0.5); 70 | border-radius: 6px; 71 | background-color: #FFFFFF; 72 | .tcs-rubberband-cell { 73 | height: 128px; 74 | border-radius: 4px; 75 | background-color: lightblue; 76 | transition: background-color 500ms; 77 | &.tcs-rubberband-cell--selected { 78 | background-color: lightpink; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Webpack Typescript Starter 9 | 10 | 11 |

已选0

12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
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 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | 143 | 144 | -------------------------------------------------------------------------------- /src/app/rubberband.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "./point"; 2 | import { Rectangle } from "./rectangle"; 3 | import { RubberbandOptions } from "./rubberband-options"; 4 | 5 | export class Rubberband { 6 | 7 | /** 8 | * Rubberband配置对象 9 | */ 10 | private options: RubberbandOptions; 11 | 12 | /** 13 | * 开始拖动的坐标 14 | */ 15 | private mouseDownPoint: Point; 16 | 17 | /** 18 | * 框选器元素 19 | */ 20 | private rubberbandElement: HTMLDivElement; 21 | 22 | /** 23 | * 选中的项目 24 | */ 25 | private selectedCells: Set = new Set(); 26 | private selectedCellsChange: (selectedCells: Set) => void; 27 | 28 | get container() { 29 | return this.options.element; 30 | } 31 | 32 | constructor(options: RubberbandOptions) { 33 | this.options = options; 34 | this.container.classList.add('tcs-rubberband-container'); 35 | this.initRubberband(); 36 | } 37 | 38 | public onSelectedCellsChange(callback: (selectedCells: Set) => void) { 39 | this.selectedCellsChange = callback; 40 | return this; 41 | } 42 | 43 | private initRubberband() { 44 | this.container.addEventListener('mousedown', event => this.onMouseDown(event)); 45 | document.addEventListener('mousemove', event => this.onDocumentMouseMove(event)); 46 | document.addEventListener('mouseup', event => this.onDocumentMouseUp(event)); 47 | } 48 | 49 | private onMouseDown(event: MouseEvent) { 50 | // 如果点击到RubberbandCell则不启用选择 51 | if(this.isEventOnRubberbandCellElement(event)) { 52 | return; 53 | } 54 | 55 | const containerDOMRect = this.container.getBoundingClientRect(); 56 | this.mouseDownPoint = new Point({ 57 | x: event.x - containerDOMRect.left, 58 | y: event.y - containerDOMRect.top, 59 | }); 60 | 61 | // 清除上次选中的文件 62 | this.clearSelectedCells(); 63 | 64 | // 删除浏览器文字选择,避免文字拖动问题 65 | window.getSelection().removeAllRanges(); 66 | 67 | if(this.rubberbandElement) { 68 | this.rubberbandElement.remove(); 69 | } 70 | this.rubberbandElement = document.createElement('div'); 71 | this.rubberbandElement.classList.add('tcs-rubberband'); 72 | this.container.appendChild(this.rubberbandElement); 73 | } 74 | 75 | private onDocumentMouseMove(event: MouseEvent) { 76 | if(!this.mouseDownPoint) { 77 | return; 78 | } 79 | const bounds = new Rectangle(); 80 | const containerDOMRect = this.container.getBoundingClientRect(); 81 | const mousePoint = new Point({ 82 | x: event.x - containerDOMRect.left, 83 | y: event.y - containerDOMRect.top, 84 | }); 85 | 86 | if(mousePoint.x < 0) { 87 | mousePoint.x = 0; 88 | } else if(mousePoint.x > this.container.scrollWidth) { 89 | mousePoint.x = this.container.scrollWidth; 90 | } 91 | if(mousePoint.y < 0) { 92 | mousePoint.y = 0; 93 | } else if(mousePoint.y > this.container.scrollHeight) { 94 | mousePoint.y = this.container.scrollHeight; 95 | } 96 | 97 | if(mousePoint.x < this.mouseDownPoint.x) { 98 | bounds.x = mousePoint.x; 99 | bounds.width = this.mouseDownPoint.x - mousePoint.x; 100 | } else { 101 | bounds.x = this.mouseDownPoint.x; 102 | bounds.width = mousePoint.x - this.mouseDownPoint.x; 103 | } 104 | if(mousePoint.y < this.mouseDownPoint.y) { 105 | bounds.y = mousePoint.y; 106 | bounds.height = this.mouseDownPoint.y - mousePoint.y; 107 | } else { 108 | bounds.y = this.mouseDownPoint.y; 109 | bounds.height = mousePoint.y - this.mouseDownPoint.y; 110 | } 111 | 112 | this.rubberbandElement.style.top = `${ bounds.y }px`; 113 | this.rubberbandElement.style.left = `${ bounds.x }px`; 114 | this.rubberbandElement.style.width = `${ bounds.width }px`; 115 | this.rubberbandElement.style.height = `${ bounds.height }px`; 116 | 117 | let changed = false; 118 | const selectedCells = new Set(Array.from(this.container.querySelectorAll('.tcs-rubberband-cell')) 119 | .filter((rubberbandCell: HTMLElement) => this.isSelectRubberbandCell(rubberbandCell, bounds)) as Array); 120 | 121 | this.selectedCells.forEach(cell => { 122 | if(!selectedCells.has(cell)) { 123 | changed = true; 124 | cell.classList.remove('tcs-rubberband-cell--selected'); 125 | } 126 | }); 127 | 128 | if(changed || selectedCells.size !== this.selectedCells.size) { 129 | this.selectedCells = selectedCells; 130 | this.selectedCells.forEach(cell => { 131 | if(!cell.classList.contains('tcs-rubberband-cell--selected')) { 132 | cell.classList.add('tcs-rubberband-cell--selected'); 133 | } 134 | }); 135 | if(this.selectedCellsChange) { 136 | this.selectedCellsChange(this.selectedCells); 137 | } 138 | } 139 | } 140 | 141 | private onDocumentMouseUp(event: MouseEvent) { 142 | if(this.mouseDownPoint) { 143 | this.mouseDownPoint = null; 144 | this.container.classList.remove('tcs-rubberband--active'); 145 | if(this.rubberbandElement) { 146 | this.rubberbandElement.remove(); 147 | this.rubberbandElement = null; 148 | } 149 | } 150 | } 151 | 152 | private clearSelectedCells() { 153 | this.selectedCells.forEach(cell => { 154 | cell.classList.remove('tcs-rubberband-cell--selected'); 155 | }); 156 | if(this.selectedCellsChange && this.selectedCells.size > 0) { 157 | this.selectedCells.clear(); 158 | this.selectedCellsChange(this.selectedCells); 159 | } 160 | } 161 | 162 | private isSelectRubberbandCell(rubberbandCellElement: HTMLElement, bounds: Rectangle) { 163 | const clientRect = rubberbandCellElement.getBoundingClientRect(); 164 | const hostClientRect = this.container.getBoundingClientRect(); 165 | const rubberbandCellBounds = new Rectangle({ 166 | x: clientRect.x - hostClientRect.x, 167 | y: clientRect.y - hostClientRect.y, 168 | width: clientRect.width, 169 | height: clientRect.height, 170 | }); 171 | const rubberbandCenterPoint = new Point({ 172 | x: bounds.x + bounds.width / 2, 173 | y: bounds.y + bounds.height / 2, 174 | }); 175 | const rubberbandCellCenterPoint = new Point({ 176 | x: rubberbandCellBounds.x + rubberbandCellBounds.width / 2, 177 | y: rubberbandCellBounds.y + rubberbandCellBounds.height / 2, 178 | }); 179 | const xIntersect = Math.abs(rubberbandCenterPoint.x - rubberbandCellCenterPoint.x) < (bounds.width / 2 + rubberbandCellBounds.width / 2); 180 | const yIntersect = Math.abs(rubberbandCenterPoint.y - rubberbandCellCenterPoint.y) < (bounds.height / 2 + rubberbandCellBounds.height / 2); 181 | if(xIntersect && yIntersect) { 182 | return true; 183 | } else { 184 | return false; 185 | } 186 | } 187 | 188 | private isEventOnRubberbandCellElement(event: MouseEvent) { 189 | return Array.from(this.container.querySelectorAll('.tcs-rubberband-cell')).some(element => { 190 | return event.target === element || this.isChildNodeOfParentNode(event.target as Node, element); 191 | }); 192 | } 193 | 194 | private isChildNodeOfParentNode(element: Node, parent: Node) { 195 | while(element.parentNode) { 196 | const parentNode = element.parentNode; 197 | if(parentNode === parent) { 198 | return true; 199 | } else { 200 | element = parentNode; 201 | } 202 | } 203 | return false; 204 | } 205 | 206 | } 207 | --------------------------------------------------------------------------------