├── .babelrc.js ├── .editorconfig ├── .gitignore ├── .prettierrc ├── .storybook └── config.js ├── .travis.yml ├── HISTORY.md ├── README.md ├── package.json ├── src ├── index.js └── util.js └── stories └── demo.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | console.log('Load babel config'); 2 | 3 | module.exports = { 4 | presets: [ 5 | [ 6 | '@babel/preset-env', 7 | { 8 | modules: false, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.log 3 | .idea 4 | .ipr 5 | .iws 6 | *~ 7 | ~* 8 | *.diff 9 | *.patch 10 | *.bak 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn 16 | *.swp 17 | *.swo 18 | *.pyc 19 | *.pyo 20 | node_modules 21 | .cache 22 | build 23 | coverage 24 | *.map 25 | /pkg/ 26 | yarn.lock 27 | /storybook-static/ 28 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { addParameters, configure } from '@storybook/react'; 2 | 3 | addParameters({ 4 | options: { 5 | theme: { 6 | brandTitle: 'dom-scroll-into-view', 7 | brandUrl: 'https://github.com/yiminghe/dom-scroll-into-view/', 8 | }, 9 | }, 10 | }); 11 | 12 | // automatically import all files ending in *.stories.js 13 | const req = require.context('../stories', true, /.js$/); 14 | 15 | function loadStories() { 16 | req.keys().forEach(filename => req(filename)); 17 | } 18 | 19 | configure(loadStories, module); 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | notifications: 6 | email: 7 | - yiminghe@gmail.com 8 | 9 | node_js: 10 | - 4.0.0 11 | 12 | before_install: 13 | - | 14 | if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(\.md$)|(^(docs|examples))/' 15 | then 16 | echo "Only docs were updated, stopping build process." 17 | exit 18 | fi 19 | npm install npm@3.x -g 20 | phantomjs --version 21 | script: 22 | - | 23 | if [ "$TEST_TYPE" = test ]; then 24 | npm test 25 | else 26 | npm run $TEST_TYPE 27 | fi 28 | env: 29 | matrix: 30 | - TEST_TYPE=lint 31 | - TEST_TYPE=test 32 | - TEST_TYPE=coverage 33 | 34 | 35 | matrix: 36 | allow_failures: 37 | - env: "TEST_TYPE=saucelabs" -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | ---- 3 | 4 | ## 2.0.0 / 2019-07-28 5 | 6 | - use pack 7 | 8 | ## 1.2.0 / 2016-03-22 9 | 10 | - support offsetTop/Left/Bottom/Right 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dom-scroll-into-view 2 | --- 3 | 4 | scroll node in contain to make node visible 5 | 6 | [![NPM version][npm-image]][npm-url] 7 | [![npm download][download-image]][download-url] 8 | 9 | [npm-image]: http://img.shields.io/npm/v/dom-scroll-into-view.svg?style=flat-square 10 | [npm-url]: http://npmjs.org/package/dom-scroll-into-view 11 | [travis-image]: https://img.shields.io/travis/react-component/dom-scroll-into-view.svg?style=flat-square 12 | [travis-url]: https://travis-ci.org/react-component/dom-scroll-into-view 13 | [coveralls-image]: https://img.shields.io/coveralls/react-component/dom-scroll-into-view.svg?style=flat-square 14 | [coveralls-url]: https://coveralls.io/r/react-component/dom-scroll-into-view?branch=master 15 | [gemnasium-image]: http://img.shields.io/gemnasium/react-component/dom-scroll-into-view.svg?style=flat-square 16 | [gemnasium-url]: https://gemnasium.com/react-component/dom-scroll-into-view 17 | [node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square 18 | [node-url]: http://nodejs.org/download/ 19 | [download-image]: https://img.shields.io/npm/dm/dom-scroll-into-view.svg?style=flat-square 20 | [download-url]: https://npmjs.org/package/dom-scroll-into-view 21 | 22 | 23 | ## install 24 | 25 | [![dom-scroll-into-view](https://nodei.co/npm/dom-scroll-into-view.png)](https://npmjs.org/package/dom-scroll-into-view) 26 | 27 | ## Usage 28 | 29 | ```js 30 | import scrollIntoView from 'dom-scroll-into-view'; 31 | scrollIntoView(source,container,config); 32 | ``` 33 | ## Development 34 | 35 | ``` 36 | npm install 37 | npm start 38 | ``` 39 | 40 | ## Example 41 | 42 | http://localhost:8000/examples/ 43 | 44 | online example: http://yiminghe.github.io/dom-scroll-into-view/ 45 | 46 | ## function parameter 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 |
nametypedefaultdescription
sourceHTMLElementnode wanted to show
containerHTMLElement
config.alignWithLeftBooleanwhether align with left edge
config.alignWithTopBooleanwhether align with top edge
config.offsetTopNumber
config.offsetLeftNumber
config.offsetBottomNumber
config.offsetRightNumber
config.allowHorizontalScrollBooleanwhether allow horizontal scroll container
config.onlyScrollIfNeededBooleanwhether scroll container when source is visible
120 | 121 | ## License 122 | 123 | dom-scroll-into-view is released under the MIT license. 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom-scroll-into-view", 3 | "version": "2.0.1", 4 | "description": "scroll dom node into view automatically", 5 | "keywords": [ 6 | "dom", 7 | "scrollIntoView" 8 | ], 9 | "homepage": "http://github.com/yiminghe/dom-scroll-into-view", 10 | "author": "yiminghe@gmail.com", 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:yiminghe/dom-scroll-into-view.git" 14 | }, 15 | "bugs": { 16 | "url": "http://github.com/yiminghe/dom-scroll-into-view/issues" 17 | }, 18 | "license": "MIT", 19 | "@pika/pack": { 20 | "pipeline": [ 21 | [ 22 | "@pika/plugin-standard-pkg", 23 | { 24 | "exclude": [ 25 | "__tests__/**/*" 26 | ] 27 | } 28 | ], 29 | [ 30 | "pika-plugin-build-web-babel" 31 | ], 32 | [ 33 | "@pika/plugin-build-node" 34 | ] 35 | ] 36 | }, 37 | "scripts": { 38 | "prettier": "prettier --write \"{src,stories}/**/*.{js,tsx}\"", 39 | "start": "start-storybook -p 6006", 40 | "pub": "npm run build && npm publish pkg", 41 | "build": "pack build", 42 | "deploy": "storybook-to-ghpages", 43 | "lint-staged": "lint-staged" 44 | }, 45 | "devDependencies": { 46 | "@pika/plugin-build-node": "0.6.x", 47 | "@pika/plugin-build-types": "0.6.x", 48 | "pika-plugin-build-web-babel": "^0.6.0", 49 | "@pika/plugin-standard-pkg": "0.6.x", 50 | "@pika/types": "0.6.x", 51 | "@storybook/react": "^5.1.9", 52 | "@storybook/storybook-deployer": "^2.8.1", 53 | "babel-loader": "^8.0.6", 54 | "jquery": "^3.4.1", 55 | "lint-staged": "^9.2.1", 56 | "pre-commit": "1.x", 57 | "prettier": "^1.18.2", 58 | "react": "16.x", 59 | "react-dom": "16.x" 60 | }, 61 | "lint-staged": { 62 | "*.{tsx,js,jsx,ts}": [ 63 | "prettier --write", 64 | "git add" 65 | ] 66 | }, 67 | "pre-commit": [ 68 | "lint-staged" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import util from './util'; 2 | 3 | function scrollIntoView(elem, container, config) { 4 | config = config || {}; 5 | // document 归一化到 window 6 | if (container.nodeType === 9) { 7 | container = util.getWindow(container); 8 | } 9 | 10 | let allowHorizontalScroll = config.allowHorizontalScroll; 11 | const onlyScrollIfNeeded = config.onlyScrollIfNeeded; 12 | let alignWithTop = config.alignWithTop; 13 | let alignWithLeft = config.alignWithLeft; 14 | const offsetTop = config.offsetTop || 0; 15 | const offsetLeft = config.offsetLeft || 0; 16 | const offsetBottom = config.offsetBottom || 0; 17 | const offsetRight = config.offsetRight || 0; 18 | 19 | allowHorizontalScroll = 20 | allowHorizontalScroll === undefined ? true : allowHorizontalScroll; 21 | 22 | const isWin = util.isWindow(container); 23 | const elemOffset = util.offset(elem); 24 | const eh = util.outerHeight(elem); 25 | const ew = util.outerWidth(elem); 26 | let containerOffset; 27 | let ch; 28 | let cw; 29 | let containerScroll; 30 | let diffTop; 31 | let diffBottom; 32 | let win; 33 | let winScroll; 34 | let ww; 35 | let wh; 36 | 37 | if (isWin) { 38 | win = container; 39 | wh = util.height(win); 40 | ww = util.width(win); 41 | winScroll = { 42 | left: util.scrollLeft(win), 43 | top: util.scrollTop(win), 44 | }; 45 | // elem 相对 container 可视视窗的距离 46 | diffTop = { 47 | left: elemOffset.left - winScroll.left - offsetLeft, 48 | top: elemOffset.top - winScroll.top - offsetTop, 49 | }; 50 | diffBottom = { 51 | left: elemOffset.left + ew - (winScroll.left + ww) + offsetRight, 52 | top: elemOffset.top + eh - (winScroll.top + wh) + offsetBottom, 53 | }; 54 | containerScroll = winScroll; 55 | } else { 56 | containerOffset = util.offset(container); 57 | ch = container.clientHeight; 58 | cw = container.clientWidth; 59 | containerScroll = { 60 | left: container.scrollLeft, 61 | top: container.scrollTop, 62 | }; 63 | // elem 相对 container 可视视窗的距离 64 | // 注意边框, offset 是边框到根节点 65 | diffTop = { 66 | left: 67 | elemOffset.left - 68 | (containerOffset.left + 69 | (parseFloat(util.css(container, 'borderLeftWidth')) || 0)) - 70 | offsetLeft, 71 | top: 72 | elemOffset.top - 73 | (containerOffset.top + 74 | (parseFloat(util.css(container, 'borderTopWidth')) || 0)) - 75 | offsetTop, 76 | }; 77 | diffBottom = { 78 | left: 79 | elemOffset.left + 80 | ew - 81 | (containerOffset.left + 82 | cw + 83 | (parseFloat(util.css(container, 'borderRightWidth')) || 0)) + 84 | offsetRight, 85 | top: 86 | elemOffset.top + 87 | eh - 88 | (containerOffset.top + 89 | ch + 90 | (parseFloat(util.css(container, 'borderBottomWidth')) || 0)) + 91 | offsetBottom, 92 | }; 93 | } 94 | 95 | if (diffTop.top < 0 || diffBottom.top > 0) { 96 | // 强制向上 97 | if (alignWithTop === true) { 98 | util.scrollTop(container, containerScroll.top + diffTop.top); 99 | } else if (alignWithTop === false) { 100 | util.scrollTop(container, containerScroll.top + diffBottom.top); 101 | } else { 102 | // 自动调整 103 | if (diffTop.top < 0) { 104 | util.scrollTop(container, containerScroll.top + diffTop.top); 105 | } else { 106 | util.scrollTop(container, containerScroll.top + diffBottom.top); 107 | } 108 | } 109 | } else { 110 | if (!onlyScrollIfNeeded) { 111 | alignWithTop = alignWithTop === undefined ? true : !!alignWithTop; 112 | if (alignWithTop) { 113 | util.scrollTop(container, containerScroll.top + diffTop.top); 114 | } else { 115 | util.scrollTop(container, containerScroll.top + diffBottom.top); 116 | } 117 | } 118 | } 119 | 120 | if (allowHorizontalScroll) { 121 | if (diffTop.left < 0 || diffBottom.left > 0) { 122 | // 强制向上 123 | if (alignWithLeft === true) { 124 | util.scrollLeft(container, containerScroll.left + diffTop.left); 125 | } else if (alignWithLeft === false) { 126 | util.scrollLeft(container, containerScroll.left + diffBottom.left); 127 | } else { 128 | // 自动调整 129 | if (diffTop.left < 0) { 130 | util.scrollLeft(container, containerScroll.left + diffTop.left); 131 | } else { 132 | util.scrollLeft(container, containerScroll.left + diffBottom.left); 133 | } 134 | } 135 | } else { 136 | if (!onlyScrollIfNeeded) { 137 | alignWithLeft = alignWithLeft === undefined ? true : !!alignWithLeft; 138 | if (alignWithLeft) { 139 | util.scrollLeft(container, containerScroll.left + diffTop.left); 140 | } else { 141 | util.scrollLeft(container, containerScroll.left + diffBottom.left); 142 | } 143 | } 144 | } 145 | } 146 | } 147 | 148 | export default scrollIntoView; 149 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | const RE_NUM = /[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source; 2 | 3 | function getClientPosition(elem) { 4 | let box; 5 | let x; 6 | let y; 7 | const doc = elem.ownerDocument; 8 | const body = doc.body; 9 | const docElem = doc && doc.documentElement; 10 | // 根据 GBS 最新数据,A-Grade Browsers 都已支持 getBoundingClientRect 方法,不用再考虑传统的实现方式 11 | box = elem.getBoundingClientRect(); 12 | 13 | // 注:jQuery 还考虑减去 docElem.clientLeft/clientTop 14 | // 但测试发现,这样反而会导致当 html 和 body 有边距/边框样式时,获取的值不正确 15 | // 此外,ie6 会忽略 html 的 margin 值,幸运地是没有谁会去设置 html 的 margin 16 | 17 | x = box.left; 18 | y = box.top; 19 | 20 | // In IE, most of the time, 2 extra pixels are added to the top and left 21 | // due to the implicit 2-pixel inset border. In IE6/7 quirks mode and 22 | // IE6 standards mode, this border can be overridden by setting the 23 | // document element's border to zero -- thus, we cannot rely on the 24 | // offset always being 2 pixels. 25 | 26 | // In quirks mode, the offset can be determined by querying the body's 27 | // clientLeft/clientTop, but in standards mode, it is found by querying 28 | // the document element's clientLeft/clientTop. Since we already called 29 | // getClientBoundingRect we have already forced a reflow, so it is not 30 | // too expensive just to query them all. 31 | 32 | // ie 下应该减去窗口的边框吧,毕竟默认 absolute 都是相对窗口定位的 33 | // 窗口边框标准是设 documentElement ,quirks 时设置 body 34 | // 最好禁止在 body 和 html 上边框 ,但 ie < 9 html 默认有 2px ,减去 35 | // 但是非 ie 不可能设置窗口边框,body html 也不是窗口 ,ie 可以通过 html,body 设置 36 | // 标准 ie 下 docElem.clientTop 就是 border-top 37 | // ie7 html 即窗口边框改变不了。永远为 2 38 | // 但标准 firefox/chrome/ie9 下 docElem.clientTop 是窗口边框,即使设了 border-top 也为 0 39 | 40 | x -= docElem.clientLeft || body.clientLeft || 0; 41 | y -= docElem.clientTop || body.clientTop || 0; 42 | 43 | return { 44 | left: x, 45 | top: y, 46 | }; 47 | } 48 | 49 | function getScroll(w, top) { 50 | let ret = w[`page${top ? 'Y' : 'X'}Offset`]; 51 | const method = `scroll${top ? 'Top' : 'Left'}`; 52 | if (typeof ret !== 'number') { 53 | const d = w.document; 54 | // ie6,7,8 standard mode 55 | ret = d.documentElement[method]; 56 | if (typeof ret !== 'number') { 57 | // quirks mode 58 | ret = d.body[method]; 59 | } 60 | } 61 | return ret; 62 | } 63 | 64 | function getScrollLeft(w) { 65 | return getScroll(w); 66 | } 67 | 68 | function getScrollTop(w) { 69 | return getScroll(w, true); 70 | } 71 | 72 | function getOffset(el) { 73 | const pos = getClientPosition(el); 74 | const doc = el.ownerDocument; 75 | const w = doc.defaultView || doc.parentWindow; 76 | pos.left += getScrollLeft(w); 77 | pos.top += getScrollTop(w); 78 | return pos; 79 | } 80 | function _getComputedStyle(elem, name, computedStyle_) { 81 | let val = ''; 82 | const d = elem.ownerDocument; 83 | const computedStyle = 84 | computedStyle_ || d.defaultView.getComputedStyle(elem, null); 85 | 86 | // https://github.com/kissyteam/kissy/issues/61 87 | if (computedStyle) { 88 | val = computedStyle.getPropertyValue(name) || computedStyle[name]; 89 | } 90 | 91 | return val; 92 | } 93 | 94 | const _RE_NUM_NO_PX = new RegExp(`^(${RE_NUM})(?!px)[a-z%]+$`, 'i'); 95 | const RE_POS = /^(top|right|bottom|left)$/; 96 | const CURRENT_STYLE = 'currentStyle'; 97 | const RUNTIME_STYLE = 'runtimeStyle'; 98 | const LEFT = 'left'; 99 | const PX = 'px'; 100 | 101 | function _getComputedStyleIE(elem, name) { 102 | // currentStyle maybe null 103 | // http://msdn.microsoft.com/en-us/library/ms535231.aspx 104 | let ret = elem[CURRENT_STYLE] && elem[CURRENT_STYLE][name]; 105 | 106 | // 当 width/height 设置为百分比时,通过 pixelLeft 方式转换的 width/height 值 107 | // 一开始就处理了! CUSTOM_STYLE.height,CUSTOM_STYLE.width ,cssHook 解决@2011-08-19 108 | // 在 ie 下不对,需要直接用 offset 方式 109 | // borderWidth 等值也有问题,但考虑到 borderWidth 设为百分比的概率很小,这里就不考虑了 110 | 111 | // From the awesome hack by Dean Edwards 112 | // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 113 | // If we're not dealing with a regular pixel number 114 | // but a number that has a weird ending, we need to convert it to pixels 115 | // exclude left right for relativity 116 | if (_RE_NUM_NO_PX.test(ret) && !RE_POS.test(name)) { 117 | // Remember the original values 118 | const style = elem.style; 119 | const left = style[LEFT]; 120 | const rsLeft = elem[RUNTIME_STYLE][LEFT]; 121 | 122 | // prevent flashing of content 123 | elem[RUNTIME_STYLE][LEFT] = elem[CURRENT_STYLE][LEFT]; 124 | 125 | // Put in the new values to get a computed value out 126 | style[LEFT] = name === 'fontSize' ? '1em' : ret || 0; 127 | ret = style.pixelLeft + PX; 128 | 129 | // Revert the changed values 130 | style[LEFT] = left; 131 | 132 | elem[RUNTIME_STYLE][LEFT] = rsLeft; 133 | } 134 | return ret === '' ? 'auto' : ret; 135 | } 136 | 137 | let getComputedStyleX; 138 | if (typeof window !== 'undefined') { 139 | getComputedStyleX = window.getComputedStyle 140 | ? _getComputedStyle 141 | : _getComputedStyleIE; 142 | } 143 | 144 | function each(arr, fn) { 145 | for (let i = 0; i < arr.length; i++) { 146 | fn(arr[i]); 147 | } 148 | } 149 | 150 | function isBorderBoxFn(elem) { 151 | return getComputedStyleX(elem, 'boxSizing') === 'border-box'; 152 | } 153 | 154 | const BOX_MODELS = ['margin', 'border', 'padding']; 155 | const CONTENT_INDEX = -1; 156 | const PADDING_INDEX = 2; 157 | const BORDER_INDEX = 1; 158 | const MARGIN_INDEX = 0; 159 | 160 | function swap(elem, options, callback) { 161 | const old = {}; 162 | const style = elem.style; 163 | let name; 164 | 165 | // Remember the old values, and insert the new ones 166 | for (name in options) { 167 | if (options.hasOwnProperty(name)) { 168 | old[name] = style[name]; 169 | style[name] = options[name]; 170 | } 171 | } 172 | 173 | callback.call(elem); 174 | 175 | // Revert the old values 176 | for (name in options) { 177 | if (options.hasOwnProperty(name)) { 178 | style[name] = old[name]; 179 | } 180 | } 181 | } 182 | 183 | function getPBMWidth(elem, props, which) { 184 | let value = 0; 185 | let prop; 186 | let j; 187 | let i; 188 | for (j = 0; j < props.length; j++) { 189 | prop = props[j]; 190 | if (prop) { 191 | for (i = 0; i < which.length; i++) { 192 | let cssProp; 193 | if (prop === 'border') { 194 | cssProp = `${prop + which[i]}Width`; 195 | } else { 196 | cssProp = prop + which[i]; 197 | } 198 | value += parseFloat(getComputedStyleX(elem, cssProp)) || 0; 199 | } 200 | } 201 | } 202 | return value; 203 | } 204 | 205 | /** 206 | * A crude way of determining if an object is a window 207 | * @member util 208 | */ 209 | function isWindow(obj) { 210 | // must use == for ie8 211 | /* eslint eqeqeq:0 */ 212 | return obj != null && obj == obj.window; 213 | } 214 | 215 | const domUtils = {}; 216 | 217 | each(['Width', 'Height'], name => { 218 | domUtils[`doc${name}`] = refWin => { 219 | const d = refWin.document; 220 | return Math.max( 221 | // firefox chrome documentElement.scrollHeight< body.scrollHeight 222 | // ie standard mode : documentElement.scrollHeight> body.scrollHeight 223 | d.documentElement[`scroll${name}`], 224 | // quirks : documentElement.scrollHeight 最大等于可视窗口多一点? 225 | d.body[`scroll${name}`], 226 | domUtils[`viewport${name}`](d), 227 | ); 228 | }; 229 | 230 | domUtils[`viewport${name}`] = win => { 231 | // pc browser includes scrollbar in window.innerWidth 232 | const prop = `client${name}`; 233 | const doc = win.document; 234 | const body = doc.body; 235 | const documentElement = doc.documentElement; 236 | const documentElementProp = documentElement[prop]; 237 | // 标准模式取 documentElement 238 | // backcompat 取 body 239 | return ( 240 | (doc.compatMode === 'CSS1Compat' && documentElementProp) || 241 | (body && body[prop]) || 242 | documentElementProp 243 | ); 244 | }; 245 | }); 246 | 247 | /* 248 | 得到元素的大小信息 249 | @param elem 250 | @param name 251 | @param {String} [extra] 'padding' : (css width) + padding 252 | 'border' : (css width) + padding + border 253 | 'margin' : (css width) + padding + border + margin 254 | */ 255 | function getWH(elem, name, extra) { 256 | if (isWindow(elem)) { 257 | return name === 'width' 258 | ? domUtils.viewportWidth(elem) 259 | : domUtils.viewportHeight(elem); 260 | } else if (elem.nodeType === 9) { 261 | return name === 'width' 262 | ? domUtils.docWidth(elem) 263 | : domUtils.docHeight(elem); 264 | } 265 | const which = name === 'width' ? ['Left', 'Right'] : ['Top', 'Bottom']; 266 | let borderBoxValue = name === 'width' ? elem.offsetWidth : elem.offsetHeight; 267 | const computedStyle = getComputedStyleX(elem); 268 | const isBorderBox = isBorderBoxFn(elem, computedStyle); 269 | let cssBoxValue = 0; 270 | if (borderBoxValue == null || borderBoxValue <= 0) { 271 | borderBoxValue = undefined; 272 | // Fall back to computed then un computed css if necessary 273 | cssBoxValue = getComputedStyleX(elem, name); 274 | if (cssBoxValue == null || Number(cssBoxValue) < 0) { 275 | cssBoxValue = elem.style[name] || 0; 276 | } 277 | // Normalize '', auto, and prepare for extra 278 | cssBoxValue = parseFloat(cssBoxValue) || 0; 279 | } 280 | if (extra === undefined) { 281 | extra = isBorderBox ? BORDER_INDEX : CONTENT_INDEX; 282 | } 283 | const borderBoxValueOrIsBorderBox = 284 | borderBoxValue !== undefined || isBorderBox; 285 | const val = borderBoxValue || cssBoxValue; 286 | if (extra === CONTENT_INDEX) { 287 | if (borderBoxValueOrIsBorderBox) { 288 | return ( 289 | val - getPBMWidth(elem, ['border', 'padding'], which, computedStyle) 290 | ); 291 | } 292 | return cssBoxValue; 293 | } 294 | if (borderBoxValueOrIsBorderBox) { 295 | const padding = 296 | extra === PADDING_INDEX 297 | ? -getPBMWidth(elem, ['border'], which, computedStyle) 298 | : getPBMWidth(elem, ['margin'], which, computedStyle); 299 | return val + (extra === BORDER_INDEX ? 0 : padding); 300 | } 301 | return ( 302 | cssBoxValue + 303 | getPBMWidth(elem, BOX_MODELS.slice(extra), which, computedStyle) 304 | ); 305 | } 306 | 307 | const cssShow = { 308 | position: 'absolute', 309 | visibility: 'hidden', 310 | display: 'block', 311 | }; 312 | 313 | // fix #119 : https://github.com/kissyteam/kissy/issues/119 314 | function getWHIgnoreDisplay(elem) { 315 | let val; 316 | const args = arguments; 317 | // in case elem is window 318 | // elem.offsetWidth === undefined 319 | if (elem.offsetWidth !== 0) { 320 | val = getWH.apply(undefined, args); 321 | } else { 322 | swap(elem, cssShow, () => { 323 | val = getWH.apply(undefined, args); 324 | }); 325 | } 326 | return val; 327 | } 328 | 329 | function css(el, name, v) { 330 | let value = v; 331 | if (typeof name === 'object') { 332 | for (const i in name) { 333 | if (name.hasOwnProperty(i)) { 334 | css(el, i, name[i]); 335 | } 336 | } 337 | return undefined; 338 | } 339 | if (typeof value !== 'undefined') { 340 | if (typeof value === 'number') { 341 | value += 'px'; 342 | } 343 | el.style[name] = value; 344 | return undefined; 345 | } 346 | return getComputedStyleX(el, name); 347 | } 348 | 349 | each(['width', 'height'], name => { 350 | const first = name.charAt(0).toUpperCase() + name.slice(1); 351 | domUtils[`outer${first}`] = (el, includeMargin) => { 352 | return ( 353 | el && 354 | getWHIgnoreDisplay(el, name, includeMargin ? MARGIN_INDEX : BORDER_INDEX) 355 | ); 356 | }; 357 | const which = name === 'width' ? ['Left', 'Right'] : ['Top', 'Bottom']; 358 | 359 | domUtils[name] = (elem, val) => { 360 | if (val !== undefined) { 361 | if (elem) { 362 | const computedStyle = getComputedStyleX(elem); 363 | const isBorderBox = isBorderBoxFn(elem); 364 | if (isBorderBox) { 365 | val += getPBMWidth(elem, ['padding', 'border'], which, computedStyle); 366 | } 367 | return css(elem, name, val); 368 | } 369 | return undefined; 370 | } 371 | return elem && getWHIgnoreDisplay(elem, name, CONTENT_INDEX); 372 | }; 373 | }); 374 | 375 | // 设置 elem 相对 elem.ownerDocument 的坐标 376 | function setOffset(elem, offset) { 377 | // set position first, in-case top/left are set even on static elem 378 | if (css(elem, 'position') === 'static') { 379 | elem.style.position = 'relative'; 380 | } 381 | 382 | const old = getOffset(elem); 383 | const ret = {}; 384 | let current; 385 | let key; 386 | 387 | for (key in offset) { 388 | if (offset.hasOwnProperty(key)) { 389 | current = parseFloat(css(elem, key)) || 0; 390 | ret[key] = current + offset[key] - old[key]; 391 | } 392 | } 393 | css(elem, ret); 394 | } 395 | 396 | export default { 397 | getWindow(node) { 398 | const doc = node.ownerDocument || node; 399 | return doc.defaultView || doc.parentWindow; 400 | }, 401 | offset(el, value) { 402 | if (typeof value !== 'undefined') { 403 | setOffset(el, value); 404 | } else { 405 | return getOffset(el); 406 | } 407 | }, 408 | isWindow, 409 | each, 410 | css, 411 | clone(obj) { 412 | const ret = {}; 413 | for (const i in obj) { 414 | if (obj.hasOwnProperty(i)) { 415 | ret[i] = obj[i]; 416 | } 417 | } 418 | const overflow = obj.overflow; 419 | if (overflow) { 420 | for (const i in obj) { 421 | if (obj.hasOwnProperty(i)) { 422 | ret.overflow[i] = obj.overflow[i]; 423 | } 424 | } 425 | } 426 | return ret; 427 | }, 428 | scrollLeft(w, v) { 429 | if (isWindow(w)) { 430 | if (v === undefined) { 431 | return getScrollLeft(w); 432 | } 433 | window.scrollTo(v, getScrollTop(w)); 434 | } else { 435 | if (v === undefined) { 436 | return w.scrollLeft; 437 | } 438 | w.scrollLeft = v; 439 | } 440 | }, 441 | scrollTop(w, v) { 442 | if (isWindow(w)) { 443 | if (v === undefined) { 444 | return getScrollTop(w); 445 | } 446 | window.scrollTo(getScrollLeft(w), v); 447 | } else { 448 | if (v === undefined) { 449 | return w.scrollTop; 450 | } 451 | w.scrollTop = v; 452 | } 453 | }, 454 | viewportWidth: 0, 455 | viewportHeight: 0, 456 | ...domUtils, 457 | }; 458 | -------------------------------------------------------------------------------- /stories/demo.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import React from 'react'; 3 | import scrollIntoView from '../src/'; 4 | import { storiesOf } from '@storybook/react'; 5 | 6 | function run() { 7 | scrollIntoView($('#ex1')[0], $('#container')[0], { 8 | alignWithLeft: transformValue($('#alignWithLeft').val()), 9 | alignWithTop: transformValue($('#alignWithTop').val()), 10 | allowHorizontalScroll: transformValue($('#allowHorizontalScroll').val()), 11 | onlyScrollIfNeeded: transformValue($('#onlyScrollIfNeeded').val()), 12 | offsetTop: parseInt($('#offsetTop').val(), 10) || 0, 13 | offsetLeft: parseInt($('#offsetLeft').val(), 10) || 0, 14 | offsetBottom: parseInt($('#offsetBottom').val(), 10) || 0, 15 | offsetRight: parseInt($('#offsetRight').val(), 10) || 0, 16 | }); 17 | } 18 | 19 | const style = ` 20 | .demo-container { 21 | height: 200px; 22 | width: 200px; 23 | position: relative; 24 | overflow: auto; 25 | border: 1px solid black; 26 | } 27 | 28 | .demo-box { 29 | height: 60px; 30 | width: 60px; 31 | border: 1px solid red; 32 | position: absolute; 33 | left: 100px; 34 | top: 200px; 35 | } 36 | 37 | .demo-placeholder { 38 | height:1000px;width:1000px; 39 | }`; 40 | 41 | const Demo = () => ( 42 |
43 | 44 | 45 |
46 | 54 |
55 | 63 | 72 |
73 | 81 |
82 | 86 |
87 | 91 |
92 | 96 |
97 | 101 |
102 |
103 |
104 | find me! 105 |
106 |
container
107 |
108 |
109 | ); 110 | 111 | function transformValue(v) { 112 | if (v === 'false') { 113 | return false; 114 | } 115 | if (v === 'true') { 116 | return true; 117 | } 118 | } 119 | 120 | storiesOf('demo', module).add('simple', () => ); 121 | --------------------------------------------------------------------------------