├── .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 | [](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 | name |
52 | type |
53 | default |
54 | description |
55 |
56 |
57 |
58 |
59 | source |
60 | HTMLElement |
61 | |
62 | node wanted to show |
63 |
64 |
65 | container |
66 | HTMLElement |
67 | |
68 | |
69 |
70 |
71 | config.alignWithLeft |
72 | Boolean |
73 | |
74 | whether align with left edge |
75 |
76 |
77 | config.alignWithTop |
78 | Boolean |
79 | |
80 | whether align with top edge |
81 |
82 |
83 | config.offsetTop |
84 | Number |
85 | |
86 | |
87 |
88 |
89 | config.offsetLeft |
90 | Number |
91 | |
92 | |
93 |
94 |
95 | config.offsetBottom |
96 | Number |
97 | |
98 | |
99 |
100 |
101 | config.offsetRight |
102 | Number |
103 | |
104 | |
105 |
106 |
107 | config.allowHorizontalScroll |
108 | Boolean |
109 | |
110 | whether allow horizontal scroll container |
111 |
112 |
113 | config.onlyScrollIfNeeded |
114 | Boolean |
115 | |
116 | whether scroll container when source is visible |
117 |
118 |
119 |
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 |
--------------------------------------------------------------------------------