├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build └── index.js ├── karma.conf.js ├── package.json ├── src └── index.js └── test └── index.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain 2 | # consistent coding styles between different editors and IDEs. 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "valid-jsdoc": 2, 10 | "no-param-reassign": 0, 11 | "comma-dangle": 0, 12 | "one-var": 0, 13 | "no-else-return": 1, 14 | "no-unused-expressions": 0, 15 | "indent": 1 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | node_modules 4 | lib 5 | coverage 6 | logs 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | examples 4 | test 5 | coverage 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | - "4" 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | script: 9 | - npm run lint 10 | - npm test 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.3.8 / 2016-02-17 2 | ================== 3 | fix typo in getComputedStyle 4 | 5 | 0.3.6 / 2016-02-17 6 | ================== 7 | fix requestAnimationFrame on server side error 8 | 9 | 0.3.5 / 2015-12-09 10 | ================== 11 | fix scrollTo bug 12 | 13 | 0.3.3 / 2015-11-27 14 | ================== 15 | - Fix removeClass method issue, while the element has no a class attr. 16 | 17 | 0.3.1, 0.3.2 / 2015-11-27 18 | ================== 19 | - Add toggleClass 20 | 21 | 0.3.0 / 2015-11-23 22 | ================== 23 | - Add hide, show, toggle 24 | 25 | 0.2.0 / 2015-11-22 26 | ================== 27 | - Add tests 28 | 29 | 0.1.0 / 2015-11-16 30 | ================== 31 | - Init the project 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 oneuijs 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oui-dom-utils [](https://travis-ci.org/oneuijs/oui-dom-utils) [](http://badge.fury.io/js/oui-dom-utils) 2 | 3 | 4 | The DOM Utils of OneUI 5 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | /* eslint no-unused-expressions: 0 */ 7 | var reUnit = /width|height|top|left|right|bottom|margin|padding/i; 8 | var _amId = 1; 9 | var _amDisplay = {}; 10 | 11 | var requestAnimationFrame = undefined; 12 | if (typeof window !== 'undefined') { 13 | requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { 14 | window.setTimeout(callback, 1000 / 60); 15 | }; 16 | } else { 17 | requestAnimationFrame = function () { 18 | throw new Error('requestAnimationFrame is not supported, maybe you are running in the server side'); 19 | }; 20 | } 21 | 22 | function getAmId(obj) { 23 | return obj._amId || (obj._amId = _amId++); 24 | } 25 | 26 | function setAmDisplay(elem, display) { 27 | var id = getAmId(elem); 28 | _amDisplay['_am_' + id] = display; 29 | } 30 | 31 | function getAmDisplay(elem) { 32 | var id = getAmId(elem); 33 | return _amDisplay['_am_' + id]; 34 | } 35 | 36 | exports.default = { 37 | // el can be an Element, NodeList or selector 38 | 39 | addClass: function addClass(el, className) { 40 | var _this = this; 41 | 42 | if (typeof el === 'string') el = document.querySelectorAll(el); 43 | var els = el instanceof NodeList ? [].slice.call(el) : [el]; 44 | 45 | els.forEach(function (e) { 46 | if (_this.hasClass(e, className)) { 47 | return; 48 | } 49 | 50 | if (e.classList) { 51 | e.classList.add(className); 52 | } else { 53 | e.className += ' ' + className; 54 | } 55 | }); 56 | }, 57 | 58 | // el can be an Element, NodeList or selector 59 | removeClass: function removeClass(el, className) { 60 | var _this2 = this; 61 | 62 | if (typeof el === 'string') el = document.querySelectorAll(el); 63 | var els = el instanceof NodeList ? [].slice.call(el) : [el]; 64 | 65 | els.forEach(function (e) { 66 | if (_this2.hasClass(e, className)) { 67 | if (e.classList) { 68 | e.classList.remove(className); 69 | } else { 70 | e.className = e.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); 71 | } 72 | } 73 | }); 74 | }, 75 | 76 | // el can be an Element or selector 77 | hasClass: function hasClass(el, className) { 78 | if (typeof el === 'string') el = document.querySelector(el); 79 | if (el.classList) { 80 | return el.classList.contains(className); 81 | } 82 | return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className); 83 | }, 84 | 85 | // el can be an Element or selector 86 | toggleClass: function toggleClass(el, className) { 87 | if (typeof el === 'string') el = document.querySelector(el); 88 | var flag = this.hasClass(el, className); 89 | if (flag) { 90 | this.removeClass(el, className); 91 | } else { 92 | this.addClass(el, className); 93 | } 94 | return flag; 95 | }, 96 | insertAfter: function insertAfter(newEl, targetEl) { 97 | var parent = targetEl.parentNode; 98 | 99 | if (parent.lastChild === targetEl) { 100 | parent.appendChild(newEl); 101 | } else { 102 | parent.insertBefore(newEl, targetEl.nextSibling); 103 | } 104 | }, 105 | 106 | // el can be an Element, NodeList or query string 107 | remove: function remove(el) { 108 | if (typeof el === 'string') { 109 | [].forEach.call(document.querySelectorAll(el), function (node) { 110 | node.parentNode.removeChild(node); 111 | }); 112 | } else if (el.parentNode) { 113 | // it's an Element 114 | el.parentNode.removeChild(el); 115 | } else if (el instanceof NodeList) { 116 | // it's an array of elements 117 | [].forEach.call(el, function (node) { 118 | node.parentNode.removeChild(node); 119 | }); 120 | } else { 121 | throw new Error('you can only pass Element, array of Elements or query string as argument'); 122 | } 123 | }, 124 | forceReflow: function forceReflow(el) { 125 | el.offsetHeight; 126 | }, 127 | getDocumentScrollTop: function getDocumentScrollTop() { 128 | // IE8 used `document.documentElement` 129 | return document.documentElement && document.documentElement.scrollTop || document.body.scrollTop; 130 | }, 131 | 132 | // Set the current vertical position of the scroll bar for document 133 | // Note: do not support fixed position of body 134 | setDocumentScrollTop: function setDocumentScrollTop(value) { 135 | window.scrollTo(0, value); 136 | return value; 137 | }, 138 | outerHeight: function outerHeight(el) { 139 | return el.offsetHeight; 140 | }, 141 | outerHeightWithMargin: function outerHeightWithMargin(el) { 142 | var height = el.offsetHeight; 143 | var style = getComputedStyle(el); 144 | 145 | height += (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0); 146 | return height; 147 | }, 148 | outerWidth: function outerWidth(el) { 149 | return el.offsetWidth; 150 | }, 151 | outerWidthWithMargin: function outerWidthWithMargin(el) { 152 | var width = el.offsetWidth; 153 | var style = getComputedStyle(el); 154 | 155 | width += (parseFloat(style.marginLeft) || 0) + (parseFloat(style.marginRight) || 0); 156 | return width; 157 | }, 158 | getComputedStyles: function getComputedStyles(el) { 159 | return el.ownerDocument.defaultView.getComputedStyle(el, null); 160 | }, 161 | getOffset: function getOffset(el) { 162 | var html = el.ownerDocument.documentElement; 163 | var box = { top: 0, left: 0 }; 164 | 165 | // If we don't have gBCR, just use 0,0 rather than error 166 | // BlackBerry 5, iOS 3 (original iPhone) 167 | if (typeof el.getBoundingClientRect !== 'undefined') { 168 | box = el.getBoundingClientRect(); 169 | } 170 | 171 | return { 172 | top: box.top + window.pageYOffset - html.clientTop, 173 | left: box.left + window.pageXOffset - html.clientLeft 174 | }; 175 | }, 176 | getPosition: function getPosition(el) { 177 | if (!el) { 178 | return { 179 | left: 0, 180 | top: 0 181 | }; 182 | } 183 | 184 | return { 185 | left: el.offsetLeft, 186 | top: el.offsetTop 187 | }; 188 | }, 189 | setStyle: function setStyle(node, att, val, style) { 190 | style = style || node.style; 191 | 192 | if (style) { 193 | if (val === null || val === '') { 194 | // normalize unsetting 195 | val = ''; 196 | } else if (!isNaN(Number(val)) && reUnit.test(att)) { 197 | // number values may need a unit 198 | val += 'px'; 199 | } 200 | 201 | if (att === '') { 202 | att = 'cssText'; 203 | val = ''; 204 | } 205 | 206 | style[att] = val; 207 | } 208 | }, 209 | setStyles: function setStyles(el, hash) { 210 | var _this3 = this; 211 | 212 | var HAS_CSSTEXT_FEATURE = typeof el.style.cssText !== 'undefined'; 213 | function trim(str) { 214 | return str.replace(/^\s+|\s+$/g, ''); 215 | } 216 | var originStyleText = undefined; 217 | var originStyleObj = {}; 218 | if (!!HAS_CSSTEXT_FEATURE) { 219 | originStyleText = el.style.cssText; 220 | } else { 221 | originStyleText = el.getAttribute('style'); 222 | } 223 | originStyleText.split(';').forEach(function (item) { 224 | if (item.indexOf(':') !== -1) { 225 | var obj = item.split(':'); 226 | originStyleObj[trim(obj[0])] = trim(obj[1]); 227 | } 228 | }); 229 | 230 | var styleObj = {}; 231 | Object.keys(hash).forEach(function (item) { 232 | _this3.setStyle(el, item, hash[item], styleObj); 233 | }); 234 | var mergedStyleObj = Object.assign({}, originStyleObj, styleObj); 235 | var styleText = Object.keys(mergedStyleObj).map(function (item) { 236 | return item + ': ' + mergedStyleObj[item] + ';'; 237 | }).join(' '); 238 | 239 | if (!!HAS_CSSTEXT_FEATURE) { 240 | el.style.cssText = styleText; 241 | } else { 242 | el.setAttribute('style', styleText); 243 | } 244 | }, 245 | getStyle: function getStyle(el, att, style) { 246 | style = style || el.style; 247 | 248 | var val = ''; 249 | 250 | if (style) { 251 | val = style[att]; 252 | 253 | if (val === '') { 254 | val = this.getComputedStyle(el, att); 255 | } 256 | } 257 | 258 | return val; 259 | }, 260 | 261 | // NOTE: Known bug, will return 'auto' if style value is 'auto' 262 | getComputedStyle: function getComputedStyle(el, att) { 263 | var win = el.ownerDocument.defaultView; 264 | // null means not return presudo styles 265 | var computed = win.getComputedStyle(el, null); 266 | 267 | return att ? computed[att] : computed; 268 | }, 269 | getPageSize: function getPageSize() { 270 | var xScroll = undefined, 271 | yScroll = undefined; 272 | 273 | if (window.innerHeight && window.scrollMaxY) { 274 | xScroll = window.innerWidth + window.scrollMaxX; 275 | yScroll = window.innerHeight + window.scrollMaxY; 276 | } else { 277 | if (document.body.scrollHeight > document.body.offsetHeight) { 278 | // all but Explorer Mac 279 | xScroll = document.body.scrollWidth; 280 | yScroll = document.body.scrollHeight; 281 | } else { 282 | // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari 283 | xScroll = document.body.offsetWidth; 284 | yScroll = document.body.offsetHeight; 285 | } 286 | } 287 | 288 | var windowWidth = undefined, 289 | windowHeight = undefined; 290 | 291 | if (self.innerHeight) { 292 | // all except Explorer 293 | if (document.documentElement.clientWidth) { 294 | windowWidth = document.documentElement.clientWidth; 295 | } else { 296 | windowWidth = self.innerWidth; 297 | } 298 | windowHeight = self.innerHeight; 299 | } else { 300 | if (document.documentElement && document.documentElement.clientHeight) { 301 | // Explorer 6 Strict Mode 302 | windowWidth = document.documentElement.clientWidth; 303 | windowHeight = document.documentElement.clientHeight; 304 | } else { 305 | if (document.body) { 306 | // other Explorers 307 | windowWidth = document.body.clientWidth; 308 | windowHeight = document.body.clientHeight; 309 | } 310 | } 311 | } 312 | 313 | var pageHeight = undefined, 314 | pageWidth = undefined; 315 | 316 | // for small pages with total height less then height of the viewport 317 | if (yScroll < windowHeight) { 318 | pageHeight = windowHeight; 319 | } else { 320 | pageHeight = yScroll; 321 | } 322 | // for small pages with total width less then width of the viewport 323 | if (xScroll < windowWidth) { 324 | pageWidth = xScroll; 325 | } else { 326 | pageWidth = windowWidth; 327 | } 328 | 329 | return { 330 | pageWidth: pageWidth, 331 | pageHeight: pageHeight, 332 | windowWidth: windowWidth, 333 | windowHeight: windowHeight 334 | }; 335 | }, 336 | get: function get(selector) { 337 | return document.querySelector(selector) || {}; 338 | }, 339 | getAll: function getAll(selector) { 340 | return document.querySelectorAll(selector); 341 | }, 342 | 343 | // selector 可选。字符串值,规定在何处停止对祖先元素进行匹配的选择器表达式。 344 | // filter 可选。字符串值,包含用于匹配元素的选择器表达式。 345 | parentsUntil: function parentsUntil(el, selector, filter) { 346 | var result = []; 347 | var matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; 348 | // match start from parent 349 | el = el.parentElement; 350 | while (el && !matchesSelector.call(el, selector)) { 351 | if (!filter) { 352 | result.push(el); 353 | } else { 354 | if (matchesSelector.call(el, filter)) { 355 | result.push(el); 356 | } 357 | } 358 | el = el.parentElement; 359 | } 360 | return result; 361 | }, 362 | 363 | // 获得匹配选择器的第一个祖先元素,从当前元素开始沿 DOM 树向上 364 | closest: function closest(el, selector) { 365 | var matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; 366 | 367 | while (el) { 368 | if (matchesSelector.call(el, selector)) { 369 | return el; 370 | } 371 | 372 | el = el.parentElement; 373 | } 374 | return null; 375 | }, 376 | 377 | // el can be an Element, NodeList or selector 378 | _showHide: function _showHide(el, show) { 379 | if (typeof el === 'string') el = document.querySelectorAll(el); 380 | var els = el instanceof NodeList ? [].slice.call(el) : [el]; 381 | var display = undefined; 382 | var values = []; 383 | if (els.length === 0) { 384 | return; 385 | } 386 | els.forEach(function (e, index) { 387 | if (e.style) { 388 | display = e.style.display; 389 | if (show) { 390 | if (display === 'none') { 391 | values[index] = getAmDisplay(e) || ''; 392 | } 393 | } else { 394 | if (display !== 'none') { 395 | values[index] = 'none'; 396 | setAmDisplay(e, display); 397 | } 398 | } 399 | } 400 | }); 401 | 402 | els.forEach(function (e, index) { 403 | if (values[index] !== null) { 404 | els[index].style.display = values[index]; 405 | } 406 | }); 407 | }, 408 | show: function show(elements) { 409 | this._showHide(elements, true); 410 | }, 411 | hide: function hide(elements) { 412 | this._showHide(elements, false); 413 | }, 414 | toggle: function toggle(element) { 415 | if (element.style.display === 'none') { 416 | this.show(element); 417 | } else { 418 | this.hide(element); 419 | } 420 | }, 421 | 422 | /** 423 | * scroll to location with animation 424 | * @param {Number} to to assign the scrollTop value 425 | * @param {Number} duration assign the animate duration 426 | * @return {Null} return null 427 | */ 428 | scrollTo: function scrollTo() { 429 | var _this4 = this; 430 | 431 | var to = arguments.length <= 0 || arguments[0] === undefined ? 0 : arguments[0]; 432 | var duration = arguments.length <= 1 || arguments[1] === undefined ? 16 : arguments[1]; 433 | 434 | if (duration < 0) { 435 | return; 436 | } 437 | var diff = to - this.getDocumentScrollTop(); 438 | if (diff === 0) { 439 | return; 440 | } 441 | var perTick = diff / duration * 10; 442 | requestAnimationFrame(function () { 443 | if (Math.abs(perTick) > Math.abs(diff)) { 444 | _this4.setDocumentScrollTop(_this4.getDocumentScrollTop() + diff); 445 | return; 446 | } 447 | _this4.setDocumentScrollTop(_this4.getDocumentScrollTop() + perTick); 448 | if (diff > 0 && _this4.getDocumentScrollTop() >= to || diff < 0 && _this4.getDocumentScrollTop() <= to) { 449 | return; 450 | } 451 | _this4.scrollTo(to, duration - 16); 452 | }); 453 | }, 454 | 455 | // matches(el, '.my-class'); 这里不能使用伪类选择器 456 | is: function is(el, selector) { 457 | return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector); 458 | }, 459 | width: function width(el) { 460 | var styles = this.getComputedStyles(el); 461 | var width = parseFloat(styles.width.indexOf('px') !== -1 ? styles.width : 0); 462 | 463 | var boxSizing = styles.boxSizing || 'content-box'; 464 | if (boxSizing === 'border-box') { 465 | return width; 466 | } 467 | 468 | var borderLeftWidth = parseFloat(styles.borderLeftWidth); 469 | var borderRightWidth = parseFloat(styles.borderRightWidth); 470 | var paddingLeft = parseFloat(styles.paddingLeft); 471 | var paddingRight = parseFloat(styles.paddingRight); 472 | return width - borderRightWidth - borderLeftWidth - paddingLeft - paddingRight; 473 | }, 474 | height: function height(el) { 475 | var styles = this.getComputedStyles(el); 476 | var height = parseFloat(styles.height.indexOf('px') !== -1 ? styles.height : 0); 477 | 478 | var boxSizing = styles.boxSizing || 'content-box'; 479 | if (boxSizing === 'border-box') { 480 | return height; 481 | } 482 | 483 | var borderTopWidth = parseFloat(styles.borderTopWidth); 484 | var borderBottomWidth = parseFloat(styles.borderBottomWidth); 485 | var paddingTop = parseFloat(styles.paddingTop); 486 | var paddingBottom = parseFloat(styles.paddingBottom); 487 | return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; 488 | } 489 | }; -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sun Nov 22 2015 22:10:47 GMT+0800 (CST) 3 | require('babel-core/register'); 4 | 5 | module.exports = function(config) { 6 | config.set({ 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '.', 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['mocha'], 13 | 14 | // list of files / patterns to load in the browser 15 | files: [ 16 | './test/**/*.spec.js' 17 | ], 18 | 19 | // list of files to exclude 20 | exclude: [ 21 | ], 22 | 23 | // preprocess matching files before serving them to the browser 24 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 25 | preprocessors: { 26 | 'test/**/*.spec.js': ['webpack', 'sourcemap'] 27 | }, 28 | 29 | // test results reporter to use 30 | // possible values: 'dots', 'progress' 31 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 32 | reporters: ['progress'], 33 | 34 | coverageReporter: { 35 | reporters: [ 36 | {type: 'text'}, 37 | {type: 'html', dir: 'coverage'}, 38 | ] 39 | }, 40 | 41 | webpackMiddleware: { 42 | stats: 'minimal' 43 | }, 44 | 45 | webpack: { 46 | cache: true, 47 | devtool: 'inline-source-map', 48 | module: { 49 | loaders: [{ 50 | test: /\.jsx?$/, 51 | loader: 'babel-loader', 52 | exclude: /node_modules/ 53 | }], 54 | postLoaders: [{ 55 | test: /\.js/, 56 | exclude: /(test|node_modules)/, 57 | loader: 'istanbul-instrumenter' 58 | }], 59 | }, 60 | resolve: { 61 | extensions: ['', '.js', '.jsx'] 62 | } 63 | }, 64 | 65 | // web server port 66 | port: 9876, 67 | 68 | // enable / disable colors in the output (reporters and logs) 69 | colors: true, 70 | 71 | // level of logging 72 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 73 | logLevel: config.LOG_INFO, 74 | 75 | // enable / disable watching file and executing tests whenever any file changes 76 | autoWatch: true, 77 | 78 | // start these browsers 79 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 80 | browsers: ['Firefox'], 81 | 82 | // Continuous Integration mode 83 | // if true, Karma captures browsers, runs the tests and exits 84 | // singleRun: false, 85 | 86 | // Concurrency level 87 | // how many browser should be started simultanous 88 | // concurrency: Infinity, 89 | 90 | // plugins: ['karma-phantomjs-launcher', 'karma-sourcemap-loader', 'karma-webpack'] 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-dom-utils", 3 | "version": "0.3.8", 4 | "description": "DOM Utils of OneUI", 5 | "main": "build/index.js", 6 | "author": "oneui group", 7 | "keywords": [ 8 | "OneUI", 9 | "DOM" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/oneuijs/oui-dom-utils.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/oneuijs/oui-dom-utils/issues" 17 | }, 18 | "homepage": "https://github.com/oneuijs/oui-dom-utils", 19 | "scripts": { 20 | "test": "karma start --single-run", 21 | "tdd": "karma start --auto-watch --no-single-run", 22 | "test-cov": "karma start --auto-watch --single-run --reporters progress,coverage", 23 | "build": "babel src --out-dir build", 24 | "clean": "rm -rf build", 25 | "lint": "eslint src test" 26 | }, 27 | "dependencies": {}, 28 | "devDependencies": { 29 | "babel-cli": "^6.2.0", 30 | "babel-core": "^6.1.21", 31 | "babel-eslint": "^4.1.5", 32 | "babel-loader": "^6.2.0", 33 | "babel-preset-es2015": "^6.1.18", 34 | "babel-preset-stage-0": "^6.1.18", 35 | "chai": "^3.4.1", 36 | "eslint": "^1.9.0", 37 | "eslint-config-airbnb": "^1.0.0", 38 | "eslint-plugin-react": "^3.10.0", 39 | "isparta": "^4.0.0", 40 | "istanbul-instrumenter-loader": "^0.1.3", 41 | "karma": "^0.13.15", 42 | "karma-coverage": "^0.5.3", 43 | "karma-firefox-launcher": "^0.1.7", 44 | "karma-mocha": "^0.2.1", 45 | "karma-sourcemap-loader": "^0.3.6", 46 | "karma-webpack": "^1.7.0", 47 | "mocha": "^2.3.4", 48 | "webpack": "^1.12.8" 49 | }, 50 | "license": "MIT" 51 | } 52 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | const reUnit = /width|height|top|left|right|bottom|margin|padding/i; 3 | let _amId = 1; 4 | const _amDisplay = {}; 5 | 6 | let requestAnimationFrame; 7 | if (typeof window !== 'undefined') { 8 | requestAnimationFrame = window.requestAnimationFrame || 9 | window.webkitRequestAnimationFrame || 10 | window.mozRequestAnimationFrame || 11 | function(callback) { 12 | window.setTimeout(callback, 1000 / 60); 13 | }; 14 | } else { 15 | requestAnimationFrame = function() { 16 | throw new Error('requestAnimationFrame is not supported, maybe you are running in the server side'); 17 | }; 18 | } 19 | 20 | function getAmId(obj) { 21 | return obj._amId || (obj._amId = _amId++); 22 | } 23 | 24 | function setAmDisplay(elem, display) { 25 | const id = getAmId(elem); 26 | _amDisplay[`_am_${id}`] = display; 27 | } 28 | 29 | function getAmDisplay(elem) { 30 | const id = getAmId(elem); 31 | return _amDisplay[`_am_${id}`]; 32 | } 33 | 34 | export default { 35 | // el can be an Element, NodeList or selector 36 | addClass(el, className) { 37 | if (typeof el === 'string') el = document.querySelectorAll(el); 38 | const els = (el instanceof NodeList) ? [].slice.call(el) : [el]; 39 | 40 | els.forEach(e => { 41 | if (this.hasClass(e, className)) { return; } 42 | 43 | if (e.classList) { 44 | e.classList.add(className); 45 | } else { 46 | e.className += ' ' + className; 47 | } 48 | }); 49 | }, 50 | 51 | // el can be an Element, NodeList or selector 52 | removeClass(el, className) { 53 | if (typeof el === 'string') el = document.querySelectorAll(el); 54 | const els = (el instanceof NodeList) ? [].slice.call(el) : [el]; 55 | 56 | els.forEach(e => { 57 | if (this.hasClass(e, className)) { 58 | if (e.classList) { 59 | e.classList.remove(className); 60 | } else { 61 | e.className = e.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); 62 | } 63 | } 64 | }); 65 | }, 66 | 67 | // el can be an Element or selector 68 | hasClass(el, className) { 69 | if (typeof el === 'string') el = document.querySelector(el); 70 | if (!el) { 71 | return false; 72 | } 73 | if (el.classList) { 74 | return el.classList.contains(className); 75 | } 76 | return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className); 77 | }, 78 | 79 | // el can be an Element or selector 80 | toggleClass(el, className) { 81 | if (typeof el === 'string') el = document.querySelector(el); 82 | const flag = this.hasClass(el, className); 83 | if (flag) { 84 | this.removeClass(el, className); 85 | } else { 86 | this.addClass(el, className); 87 | } 88 | return flag; 89 | }, 90 | 91 | insertAfter(newEl, targetEl) { 92 | const parent = targetEl.parentNode; 93 | 94 | if (parent.lastChild === targetEl) { 95 | parent.appendChild(newEl); 96 | } else { 97 | parent.insertBefore(newEl, targetEl.nextSibling); 98 | } 99 | }, 100 | 101 | // el can be an Element, NodeList or query string 102 | remove(el) { 103 | if (typeof el === 'string') { 104 | [].forEach.call(document.querySelectorAll(el), node => { 105 | node.parentNode.removeChild(node); 106 | }); 107 | } else if (el.parentNode) { 108 | // it's an Element 109 | el.parentNode.removeChild(el); 110 | } else if (el instanceof NodeList) { 111 | // it's an array of elements 112 | [].forEach.call(el, node => { 113 | node.parentNode.removeChild(node); 114 | }); 115 | } else { 116 | throw new Error('you can only pass Element, array of Elements or query string as argument'); 117 | } 118 | }, 119 | 120 | forceReflow(el) { 121 | el.offsetHeight; 122 | }, 123 | 124 | getDocumentScrollTop() { 125 | // IE8 used `document.documentElement` 126 | return (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; 127 | }, 128 | 129 | // Set the current vertical position of the scroll bar for document 130 | // Note: do not support fixed position of body 131 | setDocumentScrollTop(value) { 132 | window.scrollTo(0, value); 133 | return value; 134 | }, 135 | 136 | outerHeight(el) { 137 | return el.offsetHeight; 138 | }, 139 | 140 | outerHeightWithMargin(el) { 141 | let height = el.offsetHeight; 142 | const style = getComputedStyle(el); 143 | 144 | height += (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0); 145 | return height; 146 | }, 147 | 148 | outerWidth(el) { 149 | return el.offsetWidth; 150 | }, 151 | 152 | outerWidthWithMargin(el) { 153 | let width = el.offsetWidth; 154 | const style = getComputedStyle(el); 155 | 156 | width += (parseFloat(style.marginLeft) || 0) + (parseFloat(style.marginRight) || 0); 157 | return width; 158 | }, 159 | 160 | getComputedStyles(el) { 161 | return el.ownerDocument.defaultView.getComputedStyle(el, null); 162 | }, 163 | 164 | getOffset(el) { 165 | const html = el.ownerDocument.documentElement; 166 | let box = { top: 0, left: 0 }; 167 | 168 | // If we don't have gBCR, just use 0,0 rather than error 169 | // BlackBerry 5, iOS 3 (original iPhone) 170 | if ( typeof el.getBoundingClientRect !== 'undefined' ) { 171 | box = el.getBoundingClientRect(); 172 | } 173 | 174 | return { 175 | top: box.top + window.pageYOffset - html.clientTop, 176 | left: box.left + window.pageXOffset - html.clientLeft 177 | }; 178 | }, 179 | 180 | getPosition(el) { 181 | if (!el) { 182 | return { 183 | left: 0, 184 | top: 0 185 | }; 186 | } 187 | 188 | return { 189 | left: el.offsetLeft, 190 | top: el.offsetTop 191 | }; 192 | }, 193 | 194 | setStyle(node, att, val, style) { 195 | style = style || node.style; 196 | 197 | if (style) { 198 | if (val === null || val === '') { // normalize unsetting 199 | val = ''; 200 | } else if (!isNaN(Number(val)) && reUnit.test(att)) { // number values may need a unit 201 | val += 'px'; 202 | } 203 | 204 | if (att === '') { 205 | att = 'cssText'; 206 | val = ''; 207 | } 208 | 209 | style[att] = val; 210 | } 211 | }, 212 | 213 | setStyles(el, hash) { 214 | const HAS_CSSTEXT_FEATURE = typeof(el.style.cssText) !== 'undefined'; 215 | function trim(str) { 216 | return str.replace(/^\s+|\s+$/g, ''); 217 | } 218 | let originStyleText; 219 | const originStyleObj = {}; 220 | if (!!HAS_CSSTEXT_FEATURE) { 221 | originStyleText = el.style.cssText; 222 | } else { 223 | originStyleText = el.getAttribute('style'); 224 | } 225 | originStyleText.split(';').forEach(item => { 226 | if (item.indexOf(':') !== -1) { 227 | const obj = item.split(':'); 228 | originStyleObj[trim(obj[0])] = trim(obj[1]); 229 | } 230 | }); 231 | 232 | const styleObj = {}; 233 | Object.keys(hash).forEach(item => { 234 | this.setStyle(el, item, hash[item], styleObj); 235 | }); 236 | const mergedStyleObj = Object.assign({}, originStyleObj, styleObj); 237 | const styleText = Object.keys(mergedStyleObj) 238 | .map(item => item + ': ' + mergedStyleObj[item] + ';') 239 | .join(' '); 240 | 241 | if (!!HAS_CSSTEXT_FEATURE) { 242 | el.style.cssText = styleText; 243 | } else { 244 | el.setAttribute('style', styleText); 245 | } 246 | }, 247 | 248 | getStyle(el, att, style) { 249 | style = style || el.style; 250 | 251 | let val = ''; 252 | 253 | if (style) { 254 | val = style[att]; 255 | 256 | if (val === '') { 257 | val = this.getComputedStyle(el, att); 258 | } 259 | } 260 | 261 | return val; 262 | }, 263 | 264 | // NOTE: Known bug, will return 'auto' if style value is 'auto' 265 | getComputedStyle(el, att) { 266 | const win = el.ownerDocument.defaultView; 267 | // null means not return presudo styles 268 | const computed = win.getComputedStyle(el, null); 269 | 270 | return att ? computed[att] : computed; 271 | }, 272 | 273 | getPageSize() { 274 | let xScroll, yScroll; 275 | 276 | if (window.innerHeight && window.scrollMaxY) { 277 | xScroll = window.innerWidth + window.scrollMaxX; 278 | yScroll = window.innerHeight + window.scrollMaxY; 279 | } else { 280 | if (document.body.scrollHeight > document.body.offsetHeight) { // all but Explorer Mac 281 | xScroll = document.body.scrollWidth; 282 | yScroll = document.body.scrollHeight; 283 | } else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari 284 | xScroll = document.body.offsetWidth; 285 | yScroll = document.body.offsetHeight; 286 | } 287 | } 288 | 289 | let windowWidth, windowHeight; 290 | 291 | if (self.innerHeight) { // all except Explorer 292 | if (document.documentElement.clientWidth) { 293 | windowWidth = document.documentElement.clientWidth; 294 | } else { 295 | windowWidth = self.innerWidth; 296 | } 297 | windowHeight = self.innerHeight; 298 | } else { 299 | if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode 300 | windowWidth = document.documentElement.clientWidth; 301 | windowHeight = document.documentElement.clientHeight; 302 | } else { 303 | if (document.body) { // other Explorers 304 | windowWidth = document.body.clientWidth; 305 | windowHeight = document.body.clientHeight; 306 | } 307 | } 308 | } 309 | 310 | let pageHeight, pageWidth; 311 | 312 | // for small pages with total height less then height of the viewport 313 | if (yScroll < windowHeight) { 314 | pageHeight = windowHeight; 315 | } else { 316 | pageHeight = yScroll; 317 | } 318 | // for small pages with total width less then width of the viewport 319 | if (xScroll < windowWidth) { 320 | pageWidth = xScroll; 321 | } else { 322 | pageWidth = windowWidth; 323 | } 324 | 325 | return { 326 | pageWidth: pageWidth, 327 | pageHeight: pageHeight, 328 | windowWidth: windowWidth, 329 | windowHeight: windowHeight 330 | }; 331 | }, 332 | 333 | get(selector) { 334 | return document.querySelector(selector) || {}; 335 | }, 336 | 337 | getAll(selector) { 338 | return document.querySelectorAll(selector); 339 | }, 340 | 341 | // selector 可选。字符串值,规定在何处停止对祖先元素进行匹配的选择器表达式。 342 | // filter 可选。字符串值,包含用于匹配元素的选择器表达式。 343 | parentsUntil(el, selector, filter) { 344 | const result = []; 345 | const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; 346 | // match start from parent 347 | el = el.parentElement; 348 | while (el && !matchesSelector.call(el, selector)) { 349 | if (!filter) { 350 | result.push(el); 351 | } else { 352 | if (matchesSelector.call(el, filter)) { 353 | result.push(el); 354 | } 355 | } 356 | el = el.parentElement; 357 | } 358 | return result; 359 | }, 360 | 361 | // 获得匹配选择器的第一个祖先元素,从当前元素开始沿 DOM 树向上 362 | closest(el, selector) { 363 | const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; 364 | 365 | while (el) { 366 | if (matchesSelector.call(el, selector)) { 367 | return el; 368 | } 369 | 370 | el = el.parentElement; 371 | } 372 | return null; 373 | }, 374 | 375 | // el can be an Element, NodeList or selector 376 | _showHide(el, show ) { 377 | if (typeof el === 'string') el = document.querySelectorAll(el); 378 | const els = (el instanceof NodeList) ? [].slice.call(el) : [el]; 379 | let display; 380 | const values = []; 381 | if (els.length === 0) { 382 | return; 383 | } 384 | els.forEach((e, index) => { 385 | if (e.style) { 386 | display = e.style.display; 387 | if (show) { 388 | if (display === 'none') { 389 | values[index] = getAmDisplay(e) || ''; 390 | } 391 | } else { 392 | if (display !== 'none') { 393 | values[index] = 'none'; 394 | setAmDisplay(e, display); 395 | } 396 | } 397 | } 398 | }); 399 | 400 | els.forEach((e, index) => { 401 | if ( values[index] !== null ) { 402 | els[index].style.display = values[index]; 403 | } 404 | }); 405 | }, 406 | 407 | show(elements) { 408 | this._showHide(elements, true); 409 | }, 410 | 411 | hide(elements) { 412 | this._showHide(elements, false); 413 | }, 414 | 415 | toggle(element) { 416 | if (element.style.display === 'none') { 417 | this.show(element); 418 | } else { 419 | this.hide(element); 420 | } 421 | }, 422 | 423 | /** 424 | * scroll to location with animation 425 | * @param {Number} to to assign the scrollTop value 426 | * @param {Number} duration assign the animate duration 427 | * @return {Null} return null 428 | */ 429 | scrollTo(to = 0, duration = 16) { 430 | if (duration < 0) { 431 | return; 432 | } 433 | const diff = to - this.getDocumentScrollTop(); 434 | if (diff === 0) { 435 | return; 436 | } 437 | const perTick = diff / duration * 10; 438 | requestAnimationFrame(() => { 439 | if (Math.abs(perTick) > Math.abs(diff)) { 440 | this.setDocumentScrollTop(this.getDocumentScrollTop() + diff); 441 | return; 442 | } 443 | this.setDocumentScrollTop(this.getDocumentScrollTop() + perTick); 444 | if (diff > 0 && this.getDocumentScrollTop() >= to || diff < 0 && this.getDocumentScrollTop() <= to) { 445 | return; 446 | } 447 | this.scrollTo(to, duration - 16); 448 | }); 449 | }, 450 | 451 | // matches(el, '.my-class'); 这里不能使用伪类选择器 452 | is(el, selector) { 453 | return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector); 454 | }, 455 | 456 | width(el) { 457 | const styles = this.getComputedStyles(el); 458 | const width = parseFloat(styles.width.indexOf('px') !== -1 ? styles.width : 0); 459 | 460 | const boxSizing = styles.boxSizing || 'content-box'; 461 | if (boxSizing === 'border-box') { 462 | return width; 463 | } 464 | 465 | const borderLeftWidth = parseFloat(styles.borderLeftWidth); 466 | const borderRightWidth = parseFloat(styles.borderRightWidth); 467 | const paddingLeft = parseFloat(styles.paddingLeft); 468 | const paddingRight = parseFloat(styles.paddingRight); 469 | return width - borderRightWidth - borderLeftWidth - paddingLeft - paddingRight; 470 | }, 471 | 472 | height(el) { 473 | const styles = this.getComputedStyles(el); 474 | const height = parseFloat(styles.height.indexOf('px') !== -1 ? styles.height : 0); 475 | 476 | const boxSizing = styles.boxSizing || 'content-box'; 477 | if (boxSizing === 'border-box') { 478 | return height; 479 | } 480 | 481 | const borderTopWidth = parseFloat(styles.borderTopWidth); 482 | const borderBottomWidth = parseFloat(styles.borderBottomWidth); 483 | const paddingTop = parseFloat(styles.paddingTop); 484 | const paddingBottom = parseFloat(styles.paddingBottom); 485 | return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; 486 | } 487 | }; 488 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import D from '../src/index.js'; 3 | 4 | describe('oui-dom-utils', () => { 5 | describe('#addClass', () => { 6 | beforeEach(() => { 7 | document.body.innerHTML = ` 8 |