├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── README.md ├── demo.gif ├── index.html ├── logo.png ├── package.json ├── src └── index.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const restrictedGlobals = [ 2 | "addEventListener", 3 | "blur", 4 | "close", 5 | "closed", 6 | "confirm", 7 | "defaultStatus", 8 | "defaultstatus", 9 | "event", 10 | "external", 11 | "find", 12 | "focus", 13 | "frameElement", 14 | "frames", 15 | "history", 16 | "innerHeight", 17 | "innerWidth", 18 | "length", 19 | "location", 20 | "locationbar", 21 | "menubar", 22 | "moveBy", 23 | "moveTo", 24 | "name", 25 | "onblur", 26 | "onerror", 27 | "onfocus", 28 | "onload", 29 | "onresize", 30 | "onunload", 31 | "open", 32 | "opener", 33 | "opera", 34 | "outerHeight", 35 | "outerWidth", 36 | "pageXOffset", 37 | "pageYOffset", 38 | "parent", 39 | "print", 40 | "removeEventListener", 41 | "resizeBy", 42 | "resizeTo", 43 | "screen", 44 | "screenLeft", 45 | "screenTop", 46 | "screenX", 47 | "screenY", 48 | "scroll", 49 | "scrollbars", 50 | "scrollBy", 51 | "scrollTo", 52 | "scrollX", 53 | "scrollY", 54 | "self", 55 | "status", 56 | "statusbar", 57 | "stop", 58 | "toolbar", 59 | "top", 60 | ]; 61 | 62 | module.exports = { 63 | extends: ["eslint:recommended"], 64 | root: true, 65 | // parser: "babel-eslint", 66 | plugins: ["import"], 67 | env: { 68 | browser: true, 69 | commonjs: true, 70 | es6: true, 71 | jest: true, 72 | node: true, 73 | }, 74 | parserOptions: { 75 | ecmaVersion: 2018, 76 | sourceType: "module", 77 | }, 78 | overrides: [ 79 | { 80 | files: ["**/*.ts?(x)"], 81 | parser: "@typescript-eslint/parser", 82 | parserOptions: { 83 | ecmaVersion: 2018, 84 | sourceType: "module", 85 | 86 | // typescript-eslint specific options 87 | warnOnUnsupportedTypeScriptVersion: true, 88 | }, 89 | plugins: ["@typescript-eslint"], 90 | // If adding a typescript-eslint version of an existing ESLint rule, 91 | // make sure to disable the ESLint rule here. 92 | rules: { 93 | // 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/291) 94 | "no-dupe-class-members": 0, 95 | // 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477) 96 | "no-undef": 0, 97 | 98 | // Add TypeScript specific rules (and turn off ESLint equivalents) 99 | "@typescript-eslint/consistent-type-assertions": 1, 100 | "no-array-constructor": 0, 101 | "@typescript-eslint/no-array-constructor": 1, 102 | "no-use-before-define": 0, 103 | "@typescript-eslint/no-use-before-define": [ 104 | 1, 105 | { 106 | functions: false, 107 | classes: false, 108 | variables: false, 109 | typedefs: false, 110 | }, 111 | ], 112 | "no-unused-expressions": 0, 113 | "@typescript-eslint/no-unused-expressions": [ 114 | 2, 115 | { 116 | allowShortCircuit: true, 117 | allowTernary: true, 118 | allowTaggedTemplates: true, 119 | }, 120 | ], 121 | "no-unused-vars": 0, 122 | "@typescript-eslint/no-unused-vars": [ 123 | 1, 124 | { 125 | args: "none", 126 | ignoreRestSiblings: true, 127 | }, 128 | ], 129 | "no-useless-constructor": 0, 130 | "@typescript-eslint/no-useless-constructor": 1, 131 | }, 132 | }, 133 | ], 134 | 135 | // NOTE: When adding rules here, you need to make sure they are compatible with 136 | // `typescript-eslint`, as some rules such as `no-array-constructor` aren't compatible. 137 | rules: { 138 | // http://eslint.org/docs/rules/ 139 | "array-callback-return": 1, 140 | "default-case": 0, 141 | "dot-location": [1, "property"], 142 | eqeqeq: [1, "smart"], 143 | "new-parens": 1, 144 | "no-array-constructor": 0, 145 | "no-caller": 1, 146 | "no-cond-assign": [1, "except-parens"], 147 | "no-const-assign": 1, 148 | "no-control-regex": 1, 149 | "no-delete-var": 1, 150 | "no-dupe-args": 1, 151 | "no-dupe-class-members": 1, 152 | "no-dupe-keys": 1, 153 | "no-duplicate-case": 1, 154 | "no-empty-character-class": 1, 155 | "no-empty-pattern": 1, 156 | "no-eval": 1, 157 | "no-ex-assign": 1, 158 | "no-extend-native": 1, 159 | "no-extra-bind": 1, 160 | "no-extra-label": 1, 161 | "no-fallthrough": 1, 162 | "no-func-assign": 1, 163 | "no-implied-eval": 1, 164 | "no-invalid-regexp": 1, 165 | "no-iterator": 1, 166 | "no-label-var": 1, 167 | "no-labels": [1, { allowLoop: true, allowSwitch: false }], 168 | "no-lone-blocks": 1, 169 | "no-loop-func": 1, 170 | "no-mixed-operators": [ 171 | 1, 172 | { 173 | groups: [ 174 | ["&", "|", "^", "~", "<<", ">>", ">>>"], 175 | ["==", "!=", "===", "!==", ">", ">=", "<", "<="], 176 | ["&&", "||"], 177 | ["in", "instanceof"], 178 | ], 179 | allowSamePrecedence: false, 180 | }, 181 | ], 182 | "no-multi-str": 1, 183 | "no-native-reassign": 1, 184 | "no-negated-in-lhs": 1, 185 | "no-new-func": 1, 186 | "no-new-object": 1, 187 | "no-new-symbol": 1, 188 | "no-new-wrappers": 1, 189 | "no-obj-calls": 1, 190 | "no-octal": 1, 191 | "no-octal-escape": 1, 192 | "no-regex-spaces": 1, 193 | "no-restricted-syntax": [1, "WithStatement"], 194 | "no-script-url": 1, 195 | "no-self-assign": 1, 196 | "no-self-compare": 1, 197 | "no-sequences": 1, 198 | "no-shadow-restricted-names": 1, 199 | "no-sparse-arrays": 1, 200 | "no-template-curly-in-string": 1, 201 | "no-this-before-super": 1, 202 | "no-throw-literal": 1, 203 | "no-undef": 2, 204 | "no-restricted-globals": [2].concat(restrictedGlobals), 205 | "no-unreachable": 1, 206 | "no-unused-expressions": [ 207 | 2, 208 | { 209 | allowShortCircuit: true, 210 | allowTernary: true, 211 | allowTaggedTemplates: true, 212 | }, 213 | ], 214 | "no-unused-labels": 1, 215 | "no-unused-vars": [ 216 | 1, 217 | { 218 | args: "none", 219 | ignoreRestSiblings: true, 220 | }, 221 | ], 222 | "no-use-before-define": [ 223 | 1, 224 | { 225 | functions: false, 226 | classes: false, 227 | variables: false, 228 | }, 229 | ], 230 | "no-useless-computed-key": 1, 231 | "no-useless-concat": 1, 232 | "no-useless-constructor": 1, 233 | "no-useless-escape": 1, 234 | "no-useless-rename": [ 235 | 1, 236 | { 237 | ignoreDestructuring: false, 238 | ignoreImport: false, 239 | ignoreExport: false, 240 | }, 241 | ], 242 | "no-with": 1, 243 | "no-whitespace-before-property": 1, 244 | "require-yield": 1, 245 | "rest-spread-spacing": [1, "never"], 246 | strict: [1, "never"], 247 | "unicode-bom": [1, "never"], 248 | "use-isnan": 1, 249 | "valid-typeof": 1, 250 | "getter-return": 1, 251 | 252 | // // https://github.com/benmosher/eslint-plugin-import/tree/master/docs/rules 253 | "import/first": 2, 254 | "import/no-amd": 2, 255 | "import/no-anonymous-default-export": 1, 256 | "import/no-webpack-loader-syntax": 2, 257 | }, 258 | }; 259 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | npm-debug.log* 4 | yarn-error.log* 5 | .vscode 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Reach observeRect 4 | 5 |

6 | 7 |

8 | Observe the rect of a DOM element. 9 |

10 | 11 |

12 | 13 | 14 |

15 | 16 |

17 | Demo 18 |

19 | 20 | ## Installation 21 | 22 | ``` 23 | npm install @reach/observe-rect 24 | # or 25 | yarn add @reach/observe-rect 26 | ``` 27 | 28 | ## Usage 29 | 30 | ```js 31 | import observeRect from "@reach/observe-rect"; 32 | 33 | let node = document.getElementById("some-node"); 34 | 35 | let rectObserver = observeRect(node, rect => { 36 | console.log("left", rect.left); 37 | console.log("top", rect.top); 38 | console.log("height", rect.height); 39 | console.log("width", rect.width); 40 | }); 41 | 42 | // start observing 43 | rectObserver.observe(); 44 | 45 | // stop observing 46 | rectObserver.unobserve(); 47 | ``` 48 | 49 | ## About 50 | 51 | A lot of things can change the position or size of an element, like scrolling, content reflows and user input. This utility observes and notifies you when your element's rect changes. 52 | 53 | ## Legal 54 | 55 | MIT License 56 | Copyright (c) 2018-present, Ryan Florence 57 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reach/observe-rect/4da69010382bf9b9a4cb2078afe613babab9d764/demo.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Reach observeRect 7 | 8 | 12 |
21 | You need to run this from a file server.
22 | 
23 | From the root of this project run
24 | 
25 |   $ serve
26 | 
27 | then open up http://localhost:5000/example.html.
28 |     
29 | 30 | 31 |

32 | 33 | 34 | 35 | I am being measured, you can edit me! 45 | 46 | 47 | 68 | 69 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reach/observe-rect/4da69010382bf9b9a4cb2078afe613babab9d764/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reach/observe-rect", 3 | "version": "1.2.0", 4 | "description": "Observe the Rect of a DOM element.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/reach/observe-rect", 8 | "directory": "packages/auto-id" 9 | }, 10 | "scripts": { 11 | "build": "cross-env NODE_ENV=production tsdx build --format=cjs,esm,umd", 12 | "start": "cross-env NODE_ENV=development tsdx watch", 13 | "test": "cross-env NODE_ENV=test tsdx test", 14 | "serve": "serve" 15 | }, 16 | "files": [ 17 | "dist", 18 | "README.md" 19 | ], 20 | "main": "dist/index.js", 21 | "umd:main": "dist/observe-rect.umd.production.js", 22 | "module": "dist/observe-rect.esm.js", 23 | "typings": "dist/index.d.ts", 24 | "author": "React Training ", 25 | "license": "MIT", 26 | "dependencies": {}, 27 | "devDependencies": { 28 | "@typescript-eslint/eslint-plugin": "^3.3.0", 29 | "@typescript-eslint/parser": "^3.3.0", 30 | "babel-eslint": "^10.1.0", 31 | "cross-env": "^7.0.2", 32 | "eslint": "^7.3.0", 33 | "eslint-plugin-import": "^2.21.2", 34 | "eslint-plugin-jest": "^23.16.0", 35 | "husky": "^4.2.5", 36 | "prettier": "^2.0.5", 37 | "pretty-quick": "^2.0.1", 38 | "serve": "^11.3.2", 39 | "tsdx": "^0.12.1", 40 | "tslib": "^2.0.0", 41 | "typescript": "^3.9.5" 42 | }, 43 | "husky": { 44 | "hooks": { 45 | "pre-commit": "pretty-quick --staged" 46 | } 47 | }, 48 | "prettier": { 49 | "semi": true, 50 | "trailingComma": "es5", 51 | "useTabs": true 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | let props: (keyof DOMRect)[] = [ 2 | "bottom", 3 | "height", 4 | "left", 5 | "right", 6 | "top", 7 | "width", 8 | ]; 9 | 10 | let rectChanged = (a: DOMRect = {} as DOMRect, b: DOMRect = {} as DOMRect) => 11 | props.some((prop) => a[prop] !== b[prop]); 12 | 13 | let observedNodes = new Map(); 14 | let rafId: number; 15 | 16 | let run = () => { 17 | const changedStates: RectProps[] = []; 18 | observedNodes.forEach((state, node) => { 19 | let newRect = node.getBoundingClientRect(); 20 | if (rectChanged(newRect, state.rect)) { 21 | state.rect = newRect; 22 | changedStates.push(state); 23 | } 24 | }); 25 | 26 | changedStates.forEach((state) => { 27 | state.callbacks.forEach((cb) => cb(state.rect)); 28 | }); 29 | 30 | rafId = window.requestAnimationFrame(run); 31 | }; 32 | 33 | export default function observeRect( 34 | node: Element, 35 | cb: (rect: DOMRect) => void 36 | ) { 37 | return { 38 | observe() { 39 | let wasEmpty = observedNodes.size === 0; 40 | if (observedNodes.has(node)) { 41 | observedNodes.get(node)!.callbacks.push(cb); 42 | } else { 43 | observedNodes.set(node, { 44 | rect: undefined, 45 | hasRectChanged: false, 46 | callbacks: [cb], 47 | }); 48 | } 49 | if (wasEmpty) run(); 50 | }, 51 | 52 | unobserve() { 53 | let state = observedNodes.get(node); 54 | if (state) { 55 | // Remove the callback 56 | const index = state.callbacks.indexOf(cb); 57 | if (index >= 0) state.callbacks.splice(index, 1); 58 | 59 | // Remove the node reference 60 | if (!state.callbacks.length) observedNodes.delete(node); 61 | 62 | // Stop the loop 63 | if (!observedNodes.size) cancelAnimationFrame(rafId); 64 | } 65 | }, 66 | }; 67 | } 68 | 69 | export type PartialRect = Partial; 70 | 71 | export type RectProps = { 72 | rect: DOMRect | undefined; 73 | hasRectChanged: boolean; 74 | callbacks: Function[]; 75 | }; 76 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types", "test"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "node", 23 | "baseUrl": "./", 24 | "paths": { 25 | "@": ["./"], 26 | "*": ["src/*", "node_modules/*"] 27 | }, 28 | "esModuleInterop": true 29 | } 30 | } 31 | --------------------------------------------------------------------------------