├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .github ├── FUNDING.yml └── workflows │ ├── codeql.yml │ ├── gh-pages.yml │ ├── nodejs-ci.yml │ └── semantic-release-dry.yml ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── scripts ├── gulpfile.js └── proxyDirectories.js ├── src ├── DOMMouseMoveTracker.ts ├── PointerMoveTracker.ts ├── WheelHandler.ts ├── addClass.ts ├── addStyle.ts ├── canUseDOM.ts ├── cancelAnimationFramePolyfill.ts ├── contains.ts ├── getAnimationEnd.ts ├── getContainer.ts ├── getHeight.ts ├── getOffset.ts ├── getOffsetParent.ts ├── getPosition.ts ├── getScrollbarSize.ts ├── getStyle.ts ├── getTransitionEnd.ts ├── getTransitionProperties.ts ├── getWidth.ts ├── getWindow.ts ├── hasClass.ts ├── index.ts ├── isFocusable.ts ├── isOverflowing.ts ├── nodeName.ts ├── off.ts ├── on.ts ├── ownerDocument.ts ├── ownerWindow.ts ├── removeClass.ts ├── removeStyle.ts ├── requestAnimationFramePolyfill.ts ├── scrollLeft.ts ├── scrollTop.ts ├── toggleClass.ts ├── translateDOMPositionXY.ts └── utils │ ├── BrowserSupportCore.ts │ ├── UserAgent.ts │ ├── camelize.ts │ ├── camelizeStyleName.ts │ ├── debounce.ts │ ├── emptyFunction.ts │ ├── getComputedStyle.ts │ ├── getGlobal.ts │ ├── getVendorPrefixedName.ts │ ├── hyphenateStyleName.ts │ ├── isEventSupported.ts │ ├── normalizeWheel.ts │ ├── stringFormatter.ts │ └── throttle.ts ├── test ├── PointerMoveTrackerSpec.js ├── WheelHandlerSpec.js ├── classSpec.js ├── eventSpec.js ├── html │ ├── PointerMoveTracker.html │ ├── WheelHandler.html │ ├── class.html │ ├── events.html │ ├── query.html │ └── style.html ├── index.js ├── querySpec.js ├── styleSpec.js └── utilsSpec.js └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | bin 2 | build 3 | lib 4 | tools 5 | node_modules 6 | coverage 7 | /.git 8 | karma.conf.js 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const OFF = 0; 2 | const WARNING = 1; 3 | const ERROR = 2; 4 | 5 | module.exports = { 6 | env: { 7 | browser: true, 8 | es6: true 9 | }, 10 | parser: '@typescript-eslint/parser', 11 | extends: [ 12 | 'plugin:@typescript-eslint/recommended', 13 | 'prettier/@typescript-eslint', 14 | 'plugin:prettier/recommended' 15 | ], 16 | parserOptions: {}, 17 | plugins: ['@typescript-eslint'], 18 | rules: { 19 | quotes: [ERROR, 'single'], 20 | semi: [ERROR, 'always'], 21 | 'space-infix-ops': ERROR, 22 | 'prefer-spread': ERROR, 23 | 'no-multi-spaces': ERROR, 24 | 'class-methods-use-this': WARNING, 25 | 'arrow-parens': [ERROR, 'as-needed'], 26 | '@typescript-eslint/no-unused-vars': ERROR, 27 | '@typescript-eslint/no-explicit-any': OFF, 28 | '@typescript-eslint/explicit-function-return-type': OFF, 29 | '@typescript-eslint/explicit-member-accessibility': OFF, 30 | '@typescript-eslint/no-namespace': OFF, 31 | '@typescript-eslint/explicit-module-boundary-types': OFF, 32 | '@typescript-eslint/ban-ts-comment': OFF, 33 | '@typescript-eslint/no-empty-function': OFF 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/build/.* 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | 10 | [options] 11 | 12 | [strict] 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: rsuite 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "5 11 * * 6" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - docs/typedoc 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-20.04 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 'lts/*' 21 | 22 | - name: 📦 Install dependencies 23 | run: npm install 24 | 25 | - name: 📖 Generate TypeDoc docs 26 | run: npm run docs:generate 27 | 28 | - name: 🚀 Deploy to GitHub Pages 29 | uses: peaceiris/actions-gh-pages@v3 30 | with: 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | publish_dir: ./docs 33 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | 14 | jobs: 15 | test: 16 | name: 'Test' 17 | 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | browser: [ChromeCi, Firefox] 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Setup kernel, increase watchers 28 | run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p 29 | 30 | - name: Use Node.js 12.x 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: '12.x' 34 | - name: Cache Node.js modules 35 | uses: actions/cache@v1 36 | with: 37 | path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS 38 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 39 | restore-keys: | 40 | ${{ runner.OS }}-node- 41 | ${{ runner.OS }}- 42 | - name: Install dependencies 43 | run: npm ci 44 | - name: Run headless tests 45 | run: xvfb-run --auto-servernum npm test 46 | env: 47 | CI: true 48 | BROWSER: ${{ matrix.browser }} 49 | 50 | 51 | release: 52 | name: Release 53 | needs: test 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v2 58 | with: 59 | fetch-depth: 0 60 | - name: Setup Node.js 61 | uses: actions/setup-node@v1 62 | with: 63 | node-version: 16 64 | - name: Install dependencies 65 | run: npm ci 66 | - name: 🔨 Build package 67 | run: npm run build 68 | - name: 🚀 Semantic Release 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 72 | run: npx semantic-release -------------------------------------------------------------------------------- /.github/workflows/semantic-release-dry.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs semantic-release in dry mode to see whether changes in 2 | # semantic-release configurations are working as expected 3 | 4 | name: semantic-release (dryRun) 5 | 6 | on: 7 | push: 8 | branches: 9 | - 'ci/semantic-release' 10 | 11 | jobs: 12 | release-dry: 13 | name: Release (dry) 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 16 24 | - name: Install dependencies 25 | run: npm ci 26 | - name: 🔨 Build package 27 | run: npm run build 28 | - name: 🚀 Semantic Release (dry) 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | run: npx semantic-release --dry-run --branches 'ci/semantic-release' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .DS_Store 40 | dist/ 41 | docs/ 42 | lib/ 43 | es/ 44 | yarn.lock 45 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | src/ 3 | test/ 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | singleQuote: true, 5 | arrowParens: 'avoid', 6 | trailingComma: 'none' 7 | }; 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.0.1](https://github.com/rsuite/dom-lib/compare/3.0.0...3.0.1) (2021-12-16) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **getContainer:** container can be null ([#27](https://github.com/rsuite/dom-lib/issues/27)) ([121ac6d](https://github.com/rsuite/dom-lib/commit/121ac6dec305d0c9193e5e65e31274520595fcde)) 7 | 8 | 9 | 10 | # [3.0.0](https://github.com/rsuite/dom-lib/compare/2.1.0...3.0.0) (2021-11-02) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **animation:** remove webkitRequestAnimationFrame ([#22](https://github.com/rsuite/dom-lib/issues/22)) ([69dbbb1](https://github.com/rsuite/dom-lib/commit/69dbbb17a54cf1dfb3a7dee65e6ce4069c578982)) 16 | * **deps:** add @babel/runtime ([#25](https://github.com/rsuite/dom-lib/issues/25)) ([b40f9e5](https://github.com/rsuite/dom-lib/commit/b40f9e5e2d965115c1fa8f30e294cc4665e82826)) 17 | 18 | 19 | ### Features 20 | 21 | * **query:** add support for isFocusable ([#23](https://github.com/rsuite/dom-lib/issues/23)) ([eee920a](https://github.com/rsuite/dom-lib/commit/eee920ac0efe5670762734d16c14c2117c48f053)) 22 | 23 | 24 | 25 | # 2.1.0 26 | 27 | - feat(getPosition): support keep margin (#20) 28 | 29 | # 2.0.2 30 | 31 | - Fix typescript type definition 32 | 33 | # 2.0.1 34 | 35 | - fix: Update type definition 36 | 37 | # 2.0.0 38 | 39 | - refactor: Migrate from flow to typescript 40 | 41 | # 1.3.0 42 | 43 | - Add animation events helper 44 | 45 | # 1.2.1 46 | 47 | - Add parameter enable3DTransform for translateDOMPositionXY 48 | 49 | # 1.2.0 50 | 51 | - Added support for ESM 52 | 53 | # 1.1.0 54 | 55 | - Support Server-side Rendering 56 | - Upgrade to Bebel 7 57 | 58 | # 0.2.3 59 | 60 | > 2017-06-26 61 | 62 | - Added `WheelHandler` 63 | - Added `translateDOMPositionXY` 64 | 65 | # 0.2.1 66 | 67 | - Added `throttle` and `debounce` 68 | - Change `space` to 2 69 | 70 | # 0.2.0 71 | 72 | - All changes to support es2015 73 | - Added test case 74 | 75 | # 0.1.1 76 | 77 | > 2017-03-31 78 | 79 | - Feature: Added support react 15.\* 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) HYPERS, Inc. and its affiliates. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOM helper library 2 | 3 | [![CI](https://github.com/rsuite/dom-lib/workflows/Node.js%20CI/badge.svg)](https://github.com/rsuite/dom-lib/actions) 4 | [![NPM Version](https://img.shields.io/npm/v/dom-lib?color=33cd56&logo=npm)](https://www.npmjs.com/package/dom-lib) 5 | 6 | Click the "Exports" link in the sidebar to see a complete list of everything in the package. 7 | 8 | ## Install 9 | 10 | ``` 11 | npm install dom-lib --save 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```js 17 | import addClass from 'dom-lib/addClass'; 18 | 19 | addClass(element, 'foo'); 20 | // output:
21 | ``` 22 | 23 | ## API 24 | 25 | Class 26 | 27 | ```typescript 28 | hasClass: (node: Element, className: string) => boolean; 29 | addClass: (node: Element, className: string) => Element; 30 | removeClass: (node: Element, className: string) => Element; 31 | toggleClass: (node: Element, className: string) => Element; 32 | ``` 33 | 34 | Style 35 | 36 | ```typescript 37 | getStyle: (node: Element, property: string) => string; 38 | getStyle: (node: Element) => Object; 39 | 40 | removeStyle: (node: Element, property: string) => void; 41 | removeStyle: (node: Element, propertys: Array) => void; 42 | 43 | addStyle: (node: Element, property: string, value: string) => void; 44 | addStyle: (node: Element, style: Object) => void; 45 | ``` 46 | 47 | Events 48 | 49 | ```typescript 50 | on: (target: Element, eventName: string, listener: Function, capture: boolean = false) => { 51 | off: Function; 52 | }; 53 | off: (target: Element, eventName: string, listener: Function, capture: boolean = false) => 54 | void; 55 | ``` 56 | 57 | Query 58 | 59 | ```typescript 60 | activeElement: () => Element; 61 | getHeight: (node: Element, client: Element) => number; 62 | getWidth: (node: Element, client: Element) => number; 63 | getOffset: (node: Element) => Object; 64 | getOffsetParent: (node: Element) => Object; 65 | getPosition: (node: Element, offsetParent) => Object; 66 | getWindow: (node: Element) => String; 67 | nodeName: (node: Element) => String; 68 | ownerDocument: (node: Element) => Object; 69 | ownerWindow: (node: Element) => Object; 70 | contains: (context: Element, node: Element) => boolean; 71 | scrollLeft: (node: Element) => number; 72 | scrollTop: (node: Element) => number; 73 | isFocusable: (node: Element) => boolean; 74 | ``` 75 | 76 | Utils 77 | 78 | ```typescript 79 | scrollLeft: (node: Element)=> number; 80 | scrollLeft: (node: Element, val: number)=> void; 81 | 82 | scrollTop: (node: Element)=> number; 83 | scrollTop: (node: Element, val: number) => void; 84 | ``` 85 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api, options) => { 2 | const { NODE_ENV } = options || process.env; 3 | const modules = NODE_ENV === 'esm' ? false : 'commonjs'; 4 | 5 | if (api) { 6 | api.cache(() => NODE_ENV); 7 | } 8 | 9 | const plugins = [ 10 | ['@babel/plugin-proposal-class-properties', { loose: true }], 11 | '@babel/plugin-proposal-optional-chaining', 12 | '@babel/plugin-proposal-nullish-coalescing-operator', 13 | '@babel/plugin-proposal-export-namespace-from', 14 | '@babel/plugin-proposal-export-default-from', 15 | ['@babel/plugin-transform-runtime', { useESModules: !modules }] 16 | ]; 17 | 18 | if (NODE_ENV !== 'test') { 19 | plugins.push('babel-plugin-add-import-extension'); 20 | } 21 | 22 | if (modules) { 23 | plugins.push('add-module-exports'); 24 | } 25 | 26 | return { 27 | presets: [['@babel/preset-env', { modules, loose: true }], '@babel/preset-typescript'], 28 | plugins 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 执行全部测试用例: npm run tdd 3 | * 执行单个组件的测试用例: M=BreadcrumbItem npm run tdd 4 | */ 5 | 6 | const webpackConfig = { 7 | output: { 8 | pathinfo: true 9 | }, 10 | mode: 'development', 11 | resolve: { 12 | extensions: ['.ts', '.tsx', '.js'] 13 | }, 14 | devtool: 'inline-source-map', 15 | entry: __dirname + '/test/index.js', 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.tsx?$/, 20 | use: ['babel-loader?babelrc'], 21 | exclude: /node_modules/ 22 | } 23 | ] 24 | } 25 | }; 26 | 27 | module.exports = config => { 28 | const { env } = process; 29 | 30 | config.set({ 31 | basePath: '', 32 | frameworks: ['mocha', 'sinon-chai'], 33 | reporters: ['mocha'], 34 | files: ['test/html/*.html', 'test/index.js'], 35 | port: 9876, 36 | colors: true, 37 | autoWatch: true, 38 | logLevel: config.LOG_INFO, 39 | 40 | browsers: env.BROWSER ? env.BROWSER.split(',') : ['Chrome'], 41 | customLaunchers: { 42 | ChromeCi: { 43 | base: 'Chrome', 44 | flags: ['--no-sandbox'] 45 | } 46 | }, 47 | preprocessors: { 48 | 'test/html/*.html': 'html2js', 49 | 'test/index.js': ['webpack', 'sourcemap'] 50 | }, 51 | webpack: webpackConfig, 52 | webpackMiddleware: { 53 | noInfo: true 54 | } 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom-lib", 3 | "version": "0.0.0-development", 4 | "description": "DOM helper library", 5 | "main": "lib/cjs/index.js", 6 | "module": "lib/esm/index.js", 7 | "typings": "lib/esm/index.d.ts", 8 | "directories": { 9 | "lib": "cjs/" 10 | }, 11 | "keywords": [ 12 | "dom-library", 13 | "dom" 14 | ], 15 | "scripts": { 16 | "build": "npm run build:gulp && npm run build:types", 17 | "build:gulp": "gulp build --gulpfile scripts/gulpfile.js", 18 | "build:types": "npx tsc --emitDeclarationOnly --outDir lib/cjs && npx tsc --emitDeclarationOnly --outDir lib/esm", 19 | "tdd": "NODE_ENV=test karma start", 20 | "docs:generate": "typedoc src/index.ts", 21 | "lint": "eslint src/**/*.ts", 22 | "test": "npm run lint && NODE_ENV=test karma start --single-run", 23 | "prepublishOnly": "npm run build", 24 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" 25 | }, 26 | "author": "Simon Guo ", 27 | "license": "MIT", 28 | "repository": { 29 | "type": "git", 30 | "url": "https:git@github.com:rsuite/dom-lib.git" 31 | }, 32 | "files": [ 33 | "CHANGELOG.md", 34 | "lib", 35 | "es" 36 | ], 37 | "devDependencies": { 38 | "@babel/cli": "^7.7.0", 39 | "@babel/core": "^7.7.2", 40 | "@babel/plugin-proposal-class-properties": "^7.0.0", 41 | "@babel/plugin-proposal-export-default-from": "^7.0.0", 42 | "@babel/plugin-proposal-export-namespace-from": "^7.0.0", 43 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", 44 | "@babel/plugin-proposal-optional-chaining": "^7.6.0", 45 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 46 | "@babel/plugin-transform-proto-to-assign": "^7.0.0", 47 | "@babel/plugin-transform-runtime": "^7.1.0", 48 | "@babel/preset-env": "^7.7.1", 49 | "@babel/preset-typescript": "^7.12.7", 50 | "@typescript-eslint/eslint-plugin": "^4.11.1", 51 | "@typescript-eslint/parser": "^4.11.1", 52 | "babel-eslint": "^10.0.3", 53 | "babel-loader": "^8.0.0", 54 | "babel-plugin-add-import-extension": "^1.6.0", 55 | "babel-plugin-add-module-exports": "^1.0.4", 56 | "brfs": "^1.5.0", 57 | "chai": "^3.5.0", 58 | "conventional-changelog-cli": "^2.1.1", 59 | "del": "^6.0.0", 60 | "es5-shim": "^4.1.14", 61 | "eslint": "^6.7.2", 62 | "eslint-config-prettier": "^6.11.0", 63 | "eslint-plugin-babel": "^5.3.0", 64 | "eslint-plugin-import": "^2.19.1", 65 | "eslint-plugin-prettier": "^3.3.1", 66 | "gulp": "^4.0.2", 67 | "gulp-babel": "^8.0.0", 68 | "jquery": "^3.2.1", 69 | "karma": "^6.3.14", 70 | "karma-chrome-launcher": "^2.2.0", 71 | "karma-cli": "^2.0.0", 72 | "karma-coverage": "^1.1.1", 73 | "karma-es5-shim": "^0.0.4", 74 | "karma-firefox-launcher": "^1.0.1", 75 | "karma-html2js-preprocessor": "^1.1.0", 76 | "karma-mocha": "^1.1.1", 77 | "karma-mocha-reporter": "^2.0.4", 78 | "karma-safari-launcher": "^1.0.0", 79 | "karma-sinon-chai": "^1.2.2", 80 | "karma-sourcemap-loader": "^0.3.7", 81 | "karma-webpack": "^4.0.0-beta.0", 82 | "mocha": "^10.1.0", 83 | "prettier": "^2.2.1", 84 | "semantic-release": "^19.0.2", 85 | "simulant": "^0.2.2", 86 | "sinon": "^2.1.0", 87 | "sinon-chai": "^2.9.0", 88 | "style-loader": "^0.13.1", 89 | "typedoc": "^0.22.13", 90 | "typescript": "^4.1.3", 91 | "webpack": "^4.27.1", 92 | "webpack-cli": "^3.1.2" 93 | }, 94 | "dependencies": { 95 | "@babel/runtime": "^7.20.0" 96 | }, 97 | "release": { 98 | "tagFormat": "${version}", 99 | "plugins": [ 100 | "@semantic-release/commit-analyzer", 101 | "@semantic-release/release-notes-generator", 102 | "@semantic-release/github", 103 | [ 104 | "@semantic-release/npm", 105 | { 106 | "pkgRoot": "lib" 107 | } 108 | ] 109 | ] 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /scripts/gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const fs = require('fs'); 3 | const util = require('util'); 4 | const del = require('del'); 5 | const path = require('path'); 6 | const babel = require('gulp-babel'); 7 | const gulp = require('gulp'); 8 | const babelrc = require('../babel.config'); 9 | const { default: proxyDirectories } = require('./proxyDirectories'); 10 | const pkg = require('../package.json'); 11 | 12 | const writeFile = util.promisify(fs.writeFile); 13 | const srcRoot = path.join(__dirname, '../src'); 14 | const libRoot = path.join(__dirname, '../lib'); 15 | 16 | const esmRoot = path.join(libRoot, 'esm'); 17 | const cjsRoot = path.join(libRoot, 'cjs'); 18 | const tsSources = [`${srcRoot}/**/*.ts`]; 19 | 20 | function clean(done) { 21 | del.sync([libRoot], { force: true }); 22 | done(); 23 | } 24 | 25 | function buildCjs() { 26 | return gulp.src(tsSources).pipe(babel(babelrc())).pipe(gulp.dest(cjsRoot)); 27 | } 28 | 29 | function buildEsm() { 30 | return gulp 31 | .src(tsSources) 32 | .pipe( 33 | babel( 34 | babelrc(null, { 35 | NODE_ENV: 'esm' 36 | }) 37 | ) 38 | ) 39 | .pipe(gulp.dest(esmRoot)); 40 | } 41 | 42 | function buildDirectories(done) { 43 | proxyDirectories().then(() => { 44 | done(); 45 | }); 46 | } 47 | 48 | function copyDocs() { 49 | return gulp.src(['../README.md', '../CHANGELOG.md', '../LICENSE']).pipe(gulp.dest(libRoot)); 50 | } 51 | 52 | function createPkgFile(done) { 53 | delete pkg.devDependencies; 54 | delete pkg.files; 55 | 56 | pkg.main = 'cjs/index.js'; 57 | pkg.module = 'esm/index.js'; 58 | pkg.typings = 'esm/index.d.ts'; 59 | pkg.scripts = { 60 | //prepublishOnly: '../node_modules/mocha/bin/mocha ../test/validateBuilds.js' 61 | }; 62 | 63 | writeFile(`${libRoot}/package.json`, JSON.stringify(pkg, null, 2) + '\n') 64 | .then(() => { 65 | done(); 66 | }) 67 | .catch(err => { 68 | if (err) console.error(err.toString()); 69 | }); 70 | } 71 | 72 | exports.build = gulp.series( 73 | clean, 74 | gulp.parallel(buildCjs, buildEsm), 75 | gulp.parallel(copyDocs, createPkgFile), 76 | buildDirectories 77 | ); 78 | -------------------------------------------------------------------------------- /scripts/proxyDirectories.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a package.json for each directory and proxy to CJS and ESM files. 3 | * Can make importing a component easier. 4 | * 5 | * E.g: 6 | * import addStyle from 'dom-lib/addStyle'; 7 | */ 8 | 9 | /* eslint-disable @typescript-eslint/no-var-requires */ 10 | 11 | const path = require('path'); 12 | const fs = require('fs'); 13 | const util = require('util'); 14 | 15 | const mkDir = util.promisify(fs.mkdir); 16 | const writeFile = util.promisify(fs.writeFile); 17 | const srcRoot = path.join(__dirname, '../src'); 18 | const libRoot = path.join(__dirname, '../lib'); 19 | 20 | function findResources(options) { 21 | const { dir = srcRoot, ignores = [], isFile } = options; 22 | const resources = []; 23 | fs.readdirSync(dir).forEach(item => { 24 | const itemPath = path.resolve(dir, item); 25 | const pathname = itemPath.replace(/[a-z0-9\-]*\//gi, '').replace('.ts', ''); 26 | 27 | if (fs.statSync(itemPath).isDirectory()) { 28 | resources.push(pathname); 29 | } 30 | if (isFile && fs.statSync(itemPath).isFile()) { 31 | resources.push(pathname); 32 | } 33 | }); 34 | 35 | //console.log(resources); 36 | 37 | return resources.filter(item => !ignores.includes(item)); 38 | } 39 | 40 | function proxyResource(options) { 41 | const { pkgName = 'rsuite', name, file, filePath = '../' } = options; 42 | const proxyPkg = { 43 | name: `${pkgName}/${name}`, 44 | private: true, 45 | main: `${filePath}/cjs/${file}.js`, 46 | module: `${filePath}/esm/${file}.js`, 47 | types: `${filePath}/esm/${file}.d.ts` 48 | }; 49 | 50 | return JSON.stringify(proxyPkg, null, 2) + '\n'; 51 | } 52 | 53 | async function writePkgFile(options) { 54 | const { resources = [], pkgName = 'dom-lib' } = options; 55 | await Promise.all( 56 | resources.map(async item => { 57 | const name = item; 58 | const file = `${item}`; 59 | const filePath = '..'; 60 | const proxyDir = path.join(libRoot, name); 61 | await mkDir(libRoot).catch(() => {}); 62 | await mkDir(proxyDir).catch(() => {}); 63 | await writeFile( 64 | `${proxyDir}/package.json`, 65 | proxyResource({ pkgName, name, file, filePath }) 66 | ).catch(err => { 67 | if (err) console.error(err.toString()); 68 | }); 69 | }) 70 | ); 71 | } 72 | 73 | /** 74 | * Use package.json file to proxy component directory 75 | * 76 | * outputs: 77 | * lib/addClass/package.json 78 | * lib/addStyle/package.json 79 | * ..... 80 | */ 81 | async function proxyComponent() { 82 | const resources = findResources({ dir: srcRoot, isFile: true, ignores: ['utils'] }); 83 | 84 | await writePkgFile({ resources }); 85 | } 86 | 87 | async function proxy() { 88 | await proxyComponent(); 89 | } 90 | 91 | module.exports.findResources = findResources; 92 | module.exports.default = proxy; 93 | -------------------------------------------------------------------------------- /src/DOMMouseMoveTracker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Source code reference from: 3 | * https://github.com/facebook/fbjs/blob/master/packages/fbjs/src/dom/DOMMouseMoveTracker.js 4 | */ 5 | 6 | import on from './on'; 7 | import cancelAnimationFramePolyfill from './cancelAnimationFramePolyfill'; 8 | import requestAnimationFramePolyfill from './requestAnimationFramePolyfill'; 9 | 10 | /** 11 | * Mouse drag tracker, get the coordinate value where the mouse moves in time. 12 | * 13 | * ```typescript 14 | * const tracker = new DOMMouseMoveTracker( 15 | * onMove:(deltaX: number, deltaY: number, moveEvent: Object) => void, 16 | * onMoveEnd:() => void, 17 | * container: HTMLElement 18 | * ); 19 | * ``` 20 | */ 21 | class DOMMouseMoveTracker { 22 | isDraggingStatus = false; 23 | animationFrameID = null; 24 | domNode: Element; 25 | onMove = null; 26 | onMoveEnd = null; 27 | eventMoveToken = null; 28 | eventUpToken = null; 29 | moveEvent = null; 30 | deltaX = 0; 31 | deltaY = 0; 32 | x = 0; 33 | y = 0; 34 | 35 | /** 36 | * onMove is the callback that will be called on every mouse move. 37 | * onMoveEnd is called on mouse up when movement has ended. 38 | */ 39 | constructor(onMove: (x: number, y: number, e) => void, onMoveEnd: (e) => void, domNode: Element) { 40 | this.domNode = domNode; 41 | this.onMove = onMove; 42 | this.onMoveEnd = onMoveEnd; 43 | } 44 | 45 | /** 46 | * This is to set up the listeners for listening to mouse move 47 | * and mouse up signaling the movement has ended. Please note that these 48 | * listeners are added at the document.body level. It takes in an event 49 | * in order to grab inital state. 50 | */ 51 | captureMouseMoves(event) { 52 | if (!this.eventMoveToken && !this.eventUpToken) { 53 | this.eventMoveToken = on(this.domNode, 'mousemove', this.onMouseMove); 54 | this.eventUpToken = on(this.domNode, 'mouseup', this.onMouseUp); 55 | } 56 | 57 | if (!this.isDraggingStatus) { 58 | this.deltaX = 0; 59 | this.deltaY = 0; 60 | this.isDraggingStatus = true; 61 | this.x = event.clientX; 62 | this.y = event.clientY; 63 | } 64 | 65 | event.preventDefault(); 66 | } 67 | 68 | /** 69 | * These releases all of the listeners on document.body. 70 | */ 71 | releaseMouseMoves() { 72 | if (this.eventMoveToken) { 73 | this.eventMoveToken.off(); 74 | this.eventMoveToken = null; 75 | } 76 | 77 | if (this.eventUpToken) { 78 | this.eventUpToken.off(); 79 | this.eventUpToken = null; 80 | } 81 | 82 | if (this.animationFrameID !== null) { 83 | cancelAnimationFramePolyfill(this.animationFrameID); 84 | this.animationFrameID = null; 85 | } 86 | 87 | if (this.isDraggingStatus) { 88 | this.isDraggingStatus = false; 89 | this.x = 0; 90 | this.y = 0; 91 | } 92 | } 93 | 94 | /** 95 | * Returns whether or not if the mouse movement is being tracked. 96 | */ 97 | isDragging = () => this.isDraggingStatus; 98 | 99 | /** 100 | * Calls onMove passed into constructor and updates internal state. 101 | */ 102 | onMouseMove = event => { 103 | const x = (event as MouseEvent).clientX; 104 | const y = (event as MouseEvent).clientY; 105 | 106 | this.deltaX += x - this.x; 107 | this.deltaY += y - this.y; 108 | 109 | if (this.animationFrameID === null) { 110 | // The mouse may move faster then the animation frame does. 111 | // Use `requestAnimationFramePolyfill` to avoid over-updating. 112 | this.animationFrameID = requestAnimationFramePolyfill(this.didMouseMove); 113 | } 114 | 115 | this.x = x; 116 | this.y = y; 117 | 118 | this.moveEvent = event; 119 | event.preventDefault(); 120 | }; 121 | 122 | didMouseMove = () => { 123 | this.animationFrameID = null; 124 | this.onMove(this.deltaX, this.deltaY, this.moveEvent); 125 | 126 | this.deltaX = 0; 127 | this.deltaY = 0; 128 | }; 129 | /** 130 | * Calls onMoveEnd passed into constructor and updates internal state. 131 | */ 132 | onMouseUp = event => { 133 | if (this.animationFrameID) { 134 | this.didMouseMove(); 135 | } 136 | this.onMoveEnd && this.onMoveEnd(event); 137 | }; 138 | } 139 | 140 | export default DOMMouseMoveTracker; 141 | -------------------------------------------------------------------------------- /src/PointerMoveTracker.ts: -------------------------------------------------------------------------------- 1 | import on from './on'; 2 | import isEventSupported from './utils/isEventSupported'; 3 | 4 | interface PointerMoveTrackerOptions { 5 | useTouchEvent?: boolean; 6 | onMove: (x: number, y: number, event: MouseEvent | TouchEvent) => void; 7 | onMoveEnd: (event: MouseEvent | TouchEvent) => void; 8 | } 9 | 10 | /** 11 | * Track mouse/touch events for a given element. 12 | */ 13 | export default class PointerMoveTracker { 14 | isDragStatus = false; 15 | useTouchEvent = true; 16 | animationFrameID = null; 17 | domNode: Element; 18 | onMove = null; 19 | onMoveEnd = null; 20 | eventMoveToken = null; 21 | eventUpToken = null; 22 | moveEvent = null; 23 | deltaX = 0; 24 | deltaY = 0; 25 | x = 0; 26 | y = 0; 27 | 28 | /** 29 | * onMove is the callback that will be called on every mouse move. 30 | * onMoveEnd is called on mouse up when movement has ended. 31 | */ 32 | constructor( 33 | domNode: Element, 34 | { onMove, onMoveEnd, useTouchEvent = true }: PointerMoveTrackerOptions 35 | ) { 36 | this.domNode = domNode; 37 | this.onMove = onMove; 38 | this.onMoveEnd = onMoveEnd; 39 | this.useTouchEvent = useTouchEvent; 40 | } 41 | 42 | isSupportTouchEvent() { 43 | return this.useTouchEvent && isEventSupported('touchstart'); 44 | } 45 | 46 | getClientX(event: TouchEvent | MouseEvent) { 47 | return this.isSupportTouchEvent() 48 | ? (event as TouchEvent).touches?.[0].clientX 49 | : (event as MouseEvent).clientX; 50 | } 51 | 52 | getClientY(event: TouchEvent | MouseEvent) { 53 | return this.isSupportTouchEvent() 54 | ? (event as TouchEvent).touches?.[0].clientY 55 | : (event as MouseEvent).clientY; 56 | } 57 | 58 | /** 59 | * This is to set up the listeners for listening to mouse move 60 | * and mouse up signaling the movement has ended. Please note that these 61 | * listeners are added at the document.body level. It takes in an event 62 | * in order to grab inital state. 63 | */ 64 | captureMoves(event) { 65 | if (!this.eventMoveToken && !this.eventUpToken) { 66 | if (this.isSupportTouchEvent()) { 67 | this.eventMoveToken = on(this.domNode, 'touchmove', this.onDragMove, { passive: false }); 68 | this.eventUpToken = on(this.domNode, 'touchend', this.onDragUp, { passive: false }); 69 | on(this.domNode, 'touchcancel', this.releaseMoves); 70 | } else { 71 | this.eventMoveToken = on(this.domNode, 'mousemove', this.onDragMove); 72 | this.eventUpToken = on(this.domNode, 'mouseup', this.onDragUp); 73 | } 74 | } 75 | 76 | if (!this.isDragStatus) { 77 | this.deltaX = 0; 78 | this.deltaY = 0; 79 | this.isDragStatus = true; 80 | this.x = this.getClientX(event); 81 | this.y = this.getClientY(event); 82 | } 83 | 84 | if (event.cancelable) { 85 | event.preventDefault(); 86 | } 87 | } 88 | 89 | /** 90 | * These releases all of the listeners on document.body. 91 | */ 92 | releaseMoves() { 93 | if (this.eventMoveToken) { 94 | this.eventMoveToken.off(); 95 | this.eventMoveToken = null; 96 | } 97 | 98 | if (this.eventUpToken) { 99 | this.eventUpToken.off(); 100 | this.eventUpToken = null; 101 | } 102 | 103 | if (this.animationFrameID !== null) { 104 | cancelAnimationFrame(this.animationFrameID); 105 | this.animationFrameID = null; 106 | } 107 | 108 | if (this.isDragStatus) { 109 | this.isDragStatus = false; 110 | this.x = 0; 111 | this.y = 0; 112 | } 113 | } 114 | 115 | /** 116 | * Returns whether or not if the mouse movement is being tracked. 117 | */ 118 | isDragging = () => this.isDragStatus; 119 | 120 | /** 121 | * Calls onMove passed into constructor and updates internal state. 122 | */ 123 | onDragMove = (event: MouseEvent | TouchEvent) => { 124 | const x = this.getClientX(event); 125 | const y = this.getClientY(event); 126 | 127 | this.deltaX += x - this.x; 128 | this.deltaY += x - this.y; 129 | 130 | if (this.animationFrameID === null) { 131 | // The mouse may move faster then the animation frame does. 132 | // Use `requestAnimationFrame` to avoid over-updating. 133 | this.animationFrameID = requestAnimationFrame(this.didDragMove); 134 | } 135 | 136 | this.x = x; 137 | this.y = y; 138 | 139 | this.moveEvent = event; 140 | 141 | if (event.cancelable) { 142 | event.preventDefault(); 143 | } 144 | }; 145 | 146 | didDragMove = () => { 147 | this.animationFrameID = null; 148 | this.onMove(this.deltaX, this.deltaY, this.moveEvent); 149 | 150 | this.deltaX = 0; 151 | this.deltaY = 0; 152 | }; 153 | /** 154 | * Calls onMoveEnd passed into constructor and updates internal state. 155 | */ 156 | onDragUp = event => { 157 | if (this.animationFrameID) { 158 | this.didDragMove(); 159 | } 160 | this.onMoveEnd?.(event); 161 | }; 162 | } 163 | -------------------------------------------------------------------------------- /src/WheelHandler.ts: -------------------------------------------------------------------------------- 1 | import emptyFunction from './utils/emptyFunction'; 2 | import normalizeWheel from './utils/normalizeWheel'; 3 | import requestAnimationFramePolyfill from './requestAnimationFramePolyfill'; 4 | 5 | const swapWheelAxis = normalizedEvent => { 6 | return { 7 | spinX: normalizedEvent.spinY, 8 | spinY: normalizedEvent.spinX, 9 | pixelX: normalizedEvent.pixelY, 10 | pixelY: normalizedEvent.pixelX 11 | }; 12 | }; 13 | 14 | /** 15 | * Used to handle scrolling trackpad and mouse wheel events. 16 | */ 17 | class WheelHandler { 18 | animationFrameID = null; 19 | deltaX = 0; 20 | deltaY = 0; 21 | handleScrollX = null; 22 | handleScrollY = null; 23 | stopPropagation = null; 24 | onWheelCallback = null; 25 | 26 | constructor(onWheel, handleScrollX, handleScrollY, stopPropagation) { 27 | this.didWheel = this.didWheel.bind(this); 28 | 29 | if (typeof handleScrollX !== 'function') { 30 | handleScrollX = handleScrollX 31 | ? emptyFunction.thatReturnsTrue 32 | : emptyFunction.thatReturnsFalse; 33 | } 34 | 35 | if (typeof handleScrollY !== 'function') { 36 | handleScrollY = handleScrollY 37 | ? emptyFunction.thatReturnsTrue 38 | : emptyFunction.thatReturnsFalse; 39 | } 40 | 41 | if (typeof stopPropagation !== 'function') { 42 | stopPropagation = stopPropagation 43 | ? emptyFunction.thatReturnsTrue 44 | : emptyFunction.thatReturnsFalse; 45 | } 46 | 47 | this.handleScrollX = handleScrollX; 48 | this.handleScrollY = handleScrollY; 49 | this.stopPropagation = stopPropagation; 50 | this.onWheelCallback = onWheel; 51 | this.onWheel = this.onWheel.bind(this); 52 | } 53 | 54 | /** 55 | * Binds the wheel handler. 56 | * @param event The wheel event. 57 | */ 58 | onWheel(event) { 59 | let normalizedEvent = normalizeWheel(event); 60 | 61 | // on some platforms (e.g. Win10), browsers do not automatically swap deltas for horizontal scroll 62 | if (navigator.platform !== 'MacIntel' && event.shiftKey) { 63 | normalizedEvent = swapWheelAxis(normalizedEvent); 64 | } 65 | 66 | const deltaX = this.deltaX + normalizedEvent.pixelX; 67 | const deltaY = this.deltaY + normalizedEvent.pixelY; 68 | const handleScrollX = this.handleScrollX(deltaX, deltaY); 69 | const handleScrollY = this.handleScrollY(deltaY, deltaX); 70 | if (!handleScrollX && !handleScrollY) { 71 | return; 72 | } 73 | 74 | this.deltaX += handleScrollX ? normalizedEvent.pixelX : 0; 75 | this.deltaY += handleScrollY ? normalizedEvent.pixelY : 0; 76 | event.preventDefault(); 77 | 78 | let changed; 79 | if (this.deltaX !== 0 || this.deltaY !== 0) { 80 | if (this.stopPropagation()) { 81 | event.stopPropagation(); 82 | } 83 | changed = true; 84 | } 85 | 86 | if (changed === true && this.animationFrameID === null) { 87 | this.animationFrameID = requestAnimationFramePolyfill(this.didWheel); 88 | } 89 | } 90 | 91 | /** 92 | * Fires a callback if the wheel event has changed. 93 | */ 94 | didWheel() { 95 | this.animationFrameID = null; 96 | this.onWheelCallback(this.deltaX, this.deltaY); 97 | this.deltaX = 0; 98 | this.deltaY = 0; 99 | } 100 | } 101 | 102 | export default WheelHandler; 103 | -------------------------------------------------------------------------------- /src/addClass.ts: -------------------------------------------------------------------------------- 1 | import hasClass from './hasClass'; 2 | 3 | /** 4 | * Adds specific class to a given element 5 | * 6 | * @param target The element to add class to 7 | * @param className The class to be added 8 | * 9 | * @returns The target element 10 | */ 11 | export default function addClass(target: Element, className: string): Element { 12 | if (className) { 13 | if (target.classList) { 14 | target.classList.add(className); 15 | } else if (!hasClass(target, className)) { 16 | target.className = `${target.className} ${className}`; 17 | } 18 | } 19 | return target; 20 | } 21 | -------------------------------------------------------------------------------- /src/addStyle.ts: -------------------------------------------------------------------------------- 1 | import hyphenateStyleName from './utils/hyphenateStyleName'; 2 | import removeStyle from './removeStyle'; 3 | 4 | export interface CSSProperty { 5 | [key: string]: string | number; 6 | } 7 | 8 | /** 9 | * Apply a single CSS style rule to a given element 10 | * 11 | * @param node The element to add styles to 12 | * @param property The style property to be added 13 | * @param value The style value to be added 14 | */ 15 | function addStyle(node: Element, property: string, value: string | number): void; 16 | 17 | /** 18 | * Apply multiple CSS style rules to a given element 19 | * 20 | * @param node The element to add styles to 21 | * @param properties The key-value object of style properties to be added 22 | */ 23 | function addStyle(node: Element, properties: Partial): void; 24 | function addStyle( 25 | node: Element, 26 | property: string | Partial, 27 | value?: string | number 28 | ): void { 29 | let css = ''; 30 | let props = property; 31 | 32 | if (typeof property === 'string') { 33 | if (value === undefined) { 34 | throw new Error('value is undefined'); 35 | } 36 | (props = {})[property] = value; 37 | } 38 | 39 | if (typeof props === 'object') { 40 | for (const key in props) { 41 | if (Object.prototype.hasOwnProperty.call(props, key)) { 42 | if (!props[key] && props[key] !== 0) { 43 | removeStyle(node, hyphenateStyleName(key)); 44 | } else { 45 | css += `${hyphenateStyleName(key)}:${props[key]};`; 46 | } 47 | } 48 | } 49 | } 50 | 51 | (node as HTMLElement).style.cssText += `;${css}`; 52 | } 53 | 54 | export default addStyle; 55 | -------------------------------------------------------------------------------- /src/canUseDOM.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if the current environment is in the browser and can access and modify the DOM. 3 | */ 4 | const canUseDOM = !!( 5 | typeof window !== 'undefined' && 6 | window.document && 7 | window.document.createElement 8 | ); 9 | 10 | export default canUseDOM; 11 | -------------------------------------------------------------------------------- /src/cancelAnimationFramePolyfill.ts: -------------------------------------------------------------------------------- 1 | import getGlobal from './utils/getGlobal'; 2 | 3 | const g = getGlobal(); 4 | 5 | /** 6 | * @deprecated use `cancelAnimationFrame` instead 7 | */ 8 | const cancelAnimationFramePolyfill = g.cancelAnimationFrame || g.clearTimeout; 9 | 10 | export default cancelAnimationFramePolyfill; 11 | -------------------------------------------------------------------------------- /src/contains.ts: -------------------------------------------------------------------------------- 1 | import canUseDOM from './canUseDOM'; 2 | 3 | const fallback = (context: Element, node: (Node & ParentNode) | null) => { 4 | if (!node) return false; 5 | 6 | do { 7 | if (node === context) { 8 | return true; 9 | } 10 | } while (node.parentNode && (node = node.parentNode)); 11 | 12 | return false; 13 | }; 14 | 15 | /** 16 | * Checks if an element contains another given element. 17 | * 18 | * @param context The context element 19 | * @param node The element to check 20 | * @returns `true` if the given element is contained, `false` otherwise 21 | */ 22 | const contains = (context: Element, node: (Node & ParentNode) | null) => { 23 | if (!node) return false; 24 | 25 | if (context.contains) { 26 | return context.contains(node); 27 | } else if (context.compareDocumentPosition) { 28 | return context === node || !!(context.compareDocumentPosition(node) & 16); 29 | } 30 | 31 | return fallback(context, node); 32 | }; 33 | 34 | export default (() => (canUseDOM ? contains : fallback))(); 35 | -------------------------------------------------------------------------------- /src/getAnimationEnd.ts: -------------------------------------------------------------------------------- 1 | import canUseDOM from './canUseDOM'; 2 | 3 | const vendorMap = { 4 | animation: 'animationend', 5 | OAnimation: 'oAnimationEnd', 6 | MozAnimation: 'animationend', 7 | WebkitAnimation: 'webkitAnimationEnd' 8 | }; 9 | 10 | function getAnimationEnd() { 11 | if (!canUseDOM) { 12 | return; 13 | } 14 | 15 | let tempAnimationEnd; 16 | const style = document.createElement('div').style; 17 | for (tempAnimationEnd in vendorMap) { 18 | if (style[tempAnimationEnd] !== undefined) { 19 | return vendorMap[tempAnimationEnd]; 20 | } 21 | } 22 | } 23 | 24 | export default getAnimationEnd; 25 | -------------------------------------------------------------------------------- /src/getContainer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get a DOM container 3 | * @param container 4 | * @param defaultContainer 5 | * @returns 6 | */ 7 | export default function getContainer( 8 | container: Element | null | (() => Element | null), 9 | defaultContainer?: Element 10 | ): Element { 11 | container = typeof container === 'function' ? container() : container; 12 | return container || defaultContainer; 13 | } 14 | -------------------------------------------------------------------------------- /src/getHeight.ts: -------------------------------------------------------------------------------- 1 | import getWindow from './getWindow'; 2 | import getOffset from './getOffset'; 3 | 4 | /** 5 | * Get the height of a DOM element 6 | * @param node The DOM element 7 | * @param client Whether to get the client height 8 | * @returns The height of the DOM element 9 | */ 10 | export default function getHeight(node: Element | Window, client?: Element): number { 11 | const win = getWindow(node); 12 | 13 | if (win) { 14 | return win.innerHeight; 15 | } 16 | 17 | return client ? (node as Element).clientHeight : getOffset(node as Element).height; 18 | } 19 | -------------------------------------------------------------------------------- /src/getOffset.ts: -------------------------------------------------------------------------------- 1 | import ownerDocument from './ownerDocument'; 2 | import getWindow from './getWindow'; 3 | import contains from './contains'; 4 | 5 | export type Offset = { 6 | top: number; 7 | left: number; 8 | height: number; 9 | width: number; 10 | }; 11 | 12 | /** 13 | * Get the offset of a DOM element 14 | * @param node The DOM element 15 | * @returns The offset of the DOM element 16 | */ 17 | export default function getOffset(node: Element | null): Offset | DOMRect | null { 18 | const doc = ownerDocument(node); 19 | const win = getWindow(doc); 20 | const docElem = doc && doc.documentElement; 21 | 22 | let box = { 23 | top: 0, 24 | left: 0, 25 | height: 0, 26 | width: 0 27 | }; 28 | 29 | if (!doc) { 30 | return null; 31 | } 32 | 33 | // Make sure it's not a disconnected DOM node 34 | if (!contains(docElem, node)) { 35 | return box; 36 | } 37 | 38 | if (node?.getBoundingClientRect !== undefined) { 39 | box = node.getBoundingClientRect(); 40 | } 41 | 42 | if ((box.width || box.height) && docElem && win) { 43 | box = { 44 | top: box.top + (win.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0), 45 | left: box.left + (win.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0), 46 | width: (box.width === null ? (node as HTMLElement).offsetWidth : box.width) || 0, 47 | height: (box.height === null ? (node as HTMLElement).offsetHeight : box.height) || 0 48 | }; 49 | } 50 | 51 | return box; 52 | } 53 | -------------------------------------------------------------------------------- /src/getOffsetParent.ts: -------------------------------------------------------------------------------- 1 | import ownerDocument from './ownerDocument'; 2 | import nodeName from './nodeName'; 3 | import getStyle from './getStyle'; 4 | 5 | /** 6 | * Get the offset parent of a DOM element 7 | * @param node The DOM element 8 | * @returns The offset parent of the DOM element 9 | */ 10 | export default function getOffsetParent(node: Element): Element { 11 | const doc = ownerDocument(node); 12 | let offsetParent: Element = (node as HTMLElement)?.offsetParent; 13 | 14 | while ( 15 | offsetParent && 16 | nodeName(node) !== 'html' && 17 | getStyle(offsetParent, 'position') === 'static' 18 | ) { 19 | offsetParent = (offsetParent as HTMLElement).offsetParent; 20 | } 21 | 22 | return offsetParent || doc.documentElement; 23 | } 24 | -------------------------------------------------------------------------------- /src/getPosition.ts: -------------------------------------------------------------------------------- 1 | import getOffsetParent from './getOffsetParent'; 2 | import getOffset, { Offset } from './getOffset'; 3 | import getStyle from './getStyle'; 4 | import scrollTop from './scrollTop'; 5 | import scrollLeft from './scrollLeft'; 6 | import nodeName from './nodeName'; 7 | 8 | /** 9 | * Get the position of a DOM element 10 | * @param node The DOM element 11 | * @param offsetParent The offset parent of the DOM element 12 | * @param calcMargin Whether to calculate the margin 13 | * @returns The position of the DOM element 14 | */ 15 | export default function getPosition( 16 | node: Element, 17 | offsetParent?: Element, 18 | calcMargin = true 19 | ): Offset | DOMRect | null { 20 | const parentOffset = { 21 | top: 0, 22 | left: 0 23 | }; 24 | 25 | let offset = null; 26 | 27 | // Fixed elements are offset from window (parentOffset = {top:0, left: 0}, 28 | // because it is its only offset parent 29 | if (getStyle(node, 'position') === 'fixed') { 30 | offset = node.getBoundingClientRect(); 31 | } else { 32 | offsetParent = offsetParent || getOffsetParent(node); 33 | offset = getOffset(node); 34 | 35 | if (nodeName(offsetParent) !== 'html') { 36 | const nextParentOffset = getOffset(offsetParent); 37 | if (nextParentOffset) { 38 | parentOffset.top = nextParentOffset.top; 39 | parentOffset.left = nextParentOffset.left; 40 | } 41 | } 42 | 43 | parentOffset.top += 44 | parseInt(getStyle(offsetParent, 'borderTopWidth') as string, 10) - scrollTop(offsetParent) || 45 | 0; 46 | parentOffset.left += 47 | parseInt(getStyle(offsetParent, 'borderLeftWidth') as string, 10) - 48 | scrollLeft(offsetParent) || 0; 49 | } 50 | 51 | // Subtract parent offsets and node margins 52 | 53 | if (offset) { 54 | const marginTop = calcMargin ? parseInt(getStyle(node, 'marginTop') as string, 10) || 0 : 0; 55 | const marginLeft = calcMargin ? parseInt(getStyle(node, 'marginLeft') as string, 10) || 0 : 0; 56 | return { 57 | ...offset, 58 | top: offset.top - parentOffset.top - marginTop, 59 | left: offset.left - parentOffset.left - marginLeft 60 | }; 61 | } 62 | 63 | return null; 64 | } 65 | -------------------------------------------------------------------------------- /src/getScrollbarSize.ts: -------------------------------------------------------------------------------- 1 | import canUseDOM from './canUseDOM'; 2 | 3 | let size; 4 | 5 | /** 6 | * Returns the size of the scrollbar. 7 | * @param recalc Force recalculation. 8 | * @returns The size of the scrollbar. 9 | */ 10 | export default function getScrollbarSize(recalc?: boolean): number | void { 11 | if (size === undefined || recalc) { 12 | if (canUseDOM) { 13 | const scrollDiv = document.createElement('div'); 14 | const body: any = document.body; 15 | 16 | scrollDiv.style.position = 'absolute'; 17 | scrollDiv.style.top = '-9999px'; 18 | scrollDiv.style.width = '50px'; 19 | scrollDiv.style.height = '50px'; 20 | scrollDiv.style.overflow = 'scroll'; 21 | 22 | body.appendChild(scrollDiv); 23 | size = scrollDiv.offsetWidth - scrollDiv.clientWidth; 24 | body.removeChild(scrollDiv); 25 | } 26 | } 27 | 28 | return size; 29 | } 30 | -------------------------------------------------------------------------------- /src/getStyle.ts: -------------------------------------------------------------------------------- 1 | import camelizeStyleName from './utils/camelizeStyleName'; 2 | import getComputedStyle from './utils/getComputedStyle'; 3 | import hyphenateStyleName from './utils/hyphenateStyleName'; 4 | 5 | /** 6 | * Gets the value for a style property 7 | * @param node The DOM element 8 | * @param property The style property 9 | * @returns The value of the style property 10 | */ 11 | export default function getStyle(node: Element, property?: string) { 12 | if (property) { 13 | const value = (node as HTMLElement).style[camelizeStyleName(property)]; 14 | 15 | if (value) { 16 | return value; 17 | } 18 | 19 | const styles = getComputedStyle(node); 20 | 21 | if (styles) { 22 | return styles.getPropertyValue(hyphenateStyleName(property)); 23 | } 24 | } 25 | 26 | return (node as HTMLElement).style || getComputedStyle(node); 27 | } 28 | -------------------------------------------------------------------------------- /src/getTransitionEnd.ts: -------------------------------------------------------------------------------- 1 | import getTransitionProperties from './getTransitionProperties'; 2 | 3 | export default function getTransitionEnd() { 4 | return getTransitionProperties().end; 5 | } 6 | -------------------------------------------------------------------------------- /src/getTransitionProperties.ts: -------------------------------------------------------------------------------- 1 | import canUseDOM from './canUseDOM'; 2 | 3 | function getTransitionProperties() { 4 | if (!canUseDOM) { 5 | return {}; 6 | } 7 | 8 | const vendorMap = { 9 | O: e => `o${e.toLowerCase()}`, 10 | Moz: e => e.toLowerCase(), 11 | Webkit: e => `webkit${e}`, 12 | ms: e => `MS${e}` 13 | }; 14 | 15 | const vendors = Object.keys(vendorMap); 16 | 17 | let style = document.createElement('div').style; 18 | 19 | let tempTransitionEnd; 20 | let tempPrefix = ''; 21 | 22 | for (let i = 0; i < vendors.length; i += 1) { 23 | const vendor = vendors[i]; 24 | 25 | if (`${vendor}TransitionProperty` in style) { 26 | tempPrefix = `-${vendor.toLowerCase()}`; 27 | tempTransitionEnd = vendorMap[vendor]('TransitionEnd'); 28 | break; 29 | } 30 | } 31 | 32 | if (!tempTransitionEnd && 'transitionProperty' in style) { 33 | tempTransitionEnd = 'transitionend'; 34 | } 35 | 36 | style = null; 37 | 38 | const addPrefix = (name: string) => `${tempPrefix}-${name}`; 39 | 40 | return { 41 | end: tempTransitionEnd, 42 | backfaceVisibility: addPrefix('backface-visibility'), 43 | transform: addPrefix('transform'), 44 | property: addPrefix('transition-property'), 45 | timing: addPrefix('transition-timing-function'), 46 | delay: addPrefix('transition-delay'), 47 | duration: addPrefix('transition-duration') 48 | }; 49 | } 50 | 51 | export default getTransitionProperties; 52 | -------------------------------------------------------------------------------- /src/getWidth.ts: -------------------------------------------------------------------------------- 1 | import getWindow from './getWindow'; 2 | import getOffset from './getOffset'; 3 | 4 | /** 5 | * Get the width of a DOM element 6 | * @param node The DOM element 7 | * @param client Whether to get the client width 8 | * @returns The width of the DOM element 9 | */ 10 | export default function getWidth(node: Element | Window, client?: Element): number { 11 | const win = getWindow(node); 12 | 13 | if (win) { 14 | return win.innerWidth; 15 | } 16 | 17 | if (client) { 18 | return (node as Element).clientWidth; 19 | } 20 | 21 | const offset = getOffset(node as Element); 22 | 23 | return offset ? offset.width : 0; 24 | } 25 | -------------------------------------------------------------------------------- /src/getWindow.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the Window object of browser 3 | * @param node The DOM element 4 | * @returns The Window object of browser 5 | */ 6 | export default function getWindow(node: any): Window { 7 | if (node === node?.window) { 8 | return node; 9 | } 10 | 11 | return node?.nodeType === 9 ? node?.defaultView || node?.parentWindow : null; 12 | } 13 | -------------------------------------------------------------------------------- /src/hasClass.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check whether an element has a specific class 3 | * 4 | * @param target The element to be checked 5 | * @param className The class to be checked 6 | * 7 | * @returns `true` if the element has the class, `false` otherwise 8 | */ 9 | export default function hasClass(target: Element, className: string): boolean { 10 | if (target.classList) { 11 | return !!className && target.classList.contains(className); 12 | } 13 | return ` ${target.className} `.indexOf(` ${className} `) !== -1; 14 | } 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** events */ 2 | export { default as on } from './on'; 3 | export { default as off } from './off'; 4 | export { default as WheelHandler } from './WheelHandler'; 5 | export { default as DOMMouseMoveTracker } from './DOMMouseMoveTracker'; 6 | export { default as PointerMoveTracker } from './PointerMoveTracker'; 7 | 8 | /** classNames */ 9 | export { default as addClass } from './addClass'; 10 | export { default as removeClass } from './removeClass'; 11 | export { default as hasClass } from './hasClass'; 12 | export { default as toggleClass } from './toggleClass'; 13 | 14 | /** animation */ 15 | export { default as cancelAnimationFramePolyfill } from './cancelAnimationFramePolyfill'; 16 | export { default as requestAnimationFramePolyfill } from './requestAnimationFramePolyfill'; 17 | export { default as getAnimationEnd } from './getAnimationEnd'; 18 | 19 | /** DOM query */ 20 | export { default as ownerDocument } from './ownerDocument'; 21 | export { default as ownerWindow } from './ownerWindow'; 22 | export { default as getWindow } from './getWindow'; 23 | export { default as getContainer } from './getContainer'; 24 | export { default as canUseDOM } from './canUseDOM'; 25 | export { default as contains } from './contains'; 26 | export { default as scrollTop } from './scrollTop'; 27 | export { default as scrollLeft } from './scrollLeft'; 28 | export { default as getOffset } from './getOffset'; 29 | export { default as nodeName } from './nodeName'; 30 | export { default as getOffsetParent } from './getOffsetParent'; 31 | export { default as getPosition } from './getPosition'; 32 | export { default as isOverflowing } from './isOverflowing'; 33 | export { default as getScrollbarSize } from './getScrollbarSize'; 34 | export { default as getHeight } from './getHeight'; 35 | export { default as getWidth } from './getWidth'; 36 | export { default as isFocusable } from './isFocusable'; 37 | 38 | /** styles */ 39 | export { default as getStyle } from './getStyle'; 40 | export { default as removeStyle } from './removeStyle'; 41 | export { default as addStyle } from './addStyle'; 42 | export { default as translateDOMPositionXY } from './translateDOMPositionXY'; 43 | -------------------------------------------------------------------------------- /src/isFocusable.ts: -------------------------------------------------------------------------------- 1 | const selector = `input:not([type='hidden']):not([disabled]), 2 | select:not([disabled]), textarea:not([disabled]), a[href], 3 | button:not([disabled]),[tabindex],iframe,object, embed, area[href], 4 | audio[controls],video[controls],[contenteditable]:not([contenteditable='false'])`; 5 | 6 | function isVisible(element: Element) { 7 | const htmlElement = element as HTMLElement; 8 | return ( 9 | htmlElement.offsetWidth > 0 || 10 | htmlElement.offsetHeight > 0 || 11 | element.getClientRects().length > 0 12 | ); 13 | } 14 | 15 | /** 16 | * Checks whether `element` is focusable or not. 17 | * 18 | * ```typescript 19 | * isFocusable(document.querySelector("input")); // true 20 | * isFocusable(document.querySelector("input[tabindex='-1']")); // true 21 | * isFocusable(document.querySelector("input[hidden]")); // false 22 | * isFocusable(document.querySelector("input:disabled")); // false 23 | * ``` 24 | */ 25 | function isFocusable(element: Element): boolean { 26 | return isVisible(element) && element?.matches(selector); 27 | } 28 | 29 | export default isFocusable; 30 | -------------------------------------------------------------------------------- /src/isOverflowing.ts: -------------------------------------------------------------------------------- 1 | import getWindow from './getWindow'; 2 | import ownerDocument from './ownerDocument'; 3 | 4 | function bodyIsOverflowing(node) { 5 | const doc = ownerDocument(node); 6 | const win = getWindow(doc); 7 | const fullWidth = win.innerWidth; 8 | 9 | if (doc.body) { 10 | return doc.body.clientWidth < fullWidth; 11 | } 12 | 13 | return false; 14 | } 15 | 16 | /** 17 | * Check if the document is overflowing and account for the scrollbar width 18 | * @param container The container to check 19 | * @returns The document is overflowing 20 | */ 21 | export default function isOverflowing(container: Element) { 22 | const win = getWindow(container); 23 | const isBody = container && container.tagName.toLowerCase() === 'body'; 24 | 25 | return win || isBody 26 | ? bodyIsOverflowing(container) 27 | : container.scrollHeight > container.clientHeight; 28 | } 29 | -------------------------------------------------------------------------------- /src/nodeName.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the name of the DOM element 3 | * @param node The DOM element 4 | * @returns The name of the DOM element 5 | */ 6 | export default function nodeName(node: Element): string { 7 | return node?.nodeName && node?.nodeName?.toLowerCase(); 8 | } 9 | -------------------------------------------------------------------------------- /src/off.ts: -------------------------------------------------------------------------------- 1 | export interface CustomEventListener { 2 | (evt: T): void; 3 | } 4 | 5 | /** 6 | * Unbind `target` event `eventName`'s callback `listener`. 7 | * @param target The DOM element 8 | * @param eventName The event name 9 | * @param listener The event listener 10 | * @param options The event options 11 | */ 12 | export default function on( 13 | target: Element | Window | Document | EventTarget, 14 | eventName: K, 15 | listener: EventListenerOrEventListenerObject | CustomEventListener, 16 | options: boolean | AddEventListenerOptions = false 17 | ): void { 18 | target.removeEventListener(eventName, listener, options); 19 | } 20 | -------------------------------------------------------------------------------- /src/on.ts: -------------------------------------------------------------------------------- 1 | export interface CustomEventListener { 2 | (evt: T): void; 3 | } 4 | 5 | /** 6 | * Bind `target` event `eventName`'s callback `listener`. 7 | * @param target The DOM element 8 | * @param eventType The event name 9 | * @param listener The event listener 10 | * @param options The event options 11 | * @returns The event listener 12 | */ 13 | export default function on( 14 | target: Element | Window | Document | EventTarget, 15 | eventType: K, 16 | listener: EventListenerOrEventListenerObject | CustomEventListener, 17 | options: boolean | AddEventListenerOptions = false 18 | ): { off: () => void } { 19 | target.addEventListener(eventType, listener, options); 20 | 21 | return { 22 | off() { 23 | target.removeEventListener(eventType, listener, options); 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/ownerDocument.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the top-level document object of the node. 3 | * @param node The DOM element 4 | * @returns The top-level document object of the node 5 | */ 6 | export default function ownerDocument(node: Element | null): Document { 7 | return (node && node.ownerDocument) || document; 8 | } 9 | -------------------------------------------------------------------------------- /src/ownerWindow.ts: -------------------------------------------------------------------------------- 1 | import ownerDocument from './ownerDocument'; 2 | 3 | /** 4 | * Returns the top-level window object of the node. 5 | * @param componentOrElement The DOM element 6 | * @returns The top-level window object of the node 7 | */ 8 | export default function ownerWindow(componentOrElement: Element): Window { 9 | const doc = ownerDocument(componentOrElement); 10 | return doc.defaultView; 11 | } 12 | -------------------------------------------------------------------------------- /src/removeClass.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove a class from a given element 3 | * 4 | * @param target The element to remove the class from 5 | * @param className The class to be removed 6 | * 7 | * @returns The target element 8 | */ 9 | export default function removeClass(target: Element, className: string): Element { 10 | if (className) { 11 | if (target.classList) { 12 | target.classList.remove(className); 13 | } else { 14 | target.className = target.className 15 | .replace(new RegExp(`(^|\\s)${className}(?:\\s|$)`, 'g'), '$1') 16 | .replace(/\s+/g, ' ') // multiple spaces to one 17 | .replace(/^\s*|\s*$/g, ''); // trim the ends 18 | } 19 | } 20 | return target; 21 | } 22 | -------------------------------------------------------------------------------- /src/removeStyle.ts: -------------------------------------------------------------------------------- 1 | function _removeStyle(node: Element, key: string) { 2 | (node as HTMLElement).style?.removeProperty?.(key); 3 | } 4 | 5 | /** 6 | * Remove a style property from a DOM element 7 | * @param node The DOM element 8 | * @param keys key(s) typeof [string , array] 9 | */ 10 | export default function removeStyle(node: Element, keys: string | Array) { 11 | if (typeof keys === 'string') { 12 | _removeStyle(node, keys); 13 | } else if (Array.isArray(keys)) { 14 | keys.forEach(key => _removeStyle(node, key)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/requestAnimationFramePolyfill.ts: -------------------------------------------------------------------------------- 1 | import getGlobal from './utils/getGlobal'; 2 | 3 | const g = getGlobal(); 4 | let lastTime = 0; 5 | 6 | function _setTimeout(callback: (t: number) => void) { 7 | const currTime = Date.now(); 8 | const timeDelay = Math.max(0, 16 - (currTime - lastTime)); 9 | lastTime = currTime + timeDelay; 10 | return g.setTimeout(() => { 11 | callback(Date.now()); 12 | }, timeDelay); 13 | } 14 | 15 | /** 16 | * @deprecated Use `requestAnimationFrame` instead. 17 | */ 18 | const requestAnimationFramePolyfill = g.requestAnimationFrame || _setTimeout; 19 | 20 | export default requestAnimationFramePolyfill; 21 | -------------------------------------------------------------------------------- /src/scrollLeft.ts: -------------------------------------------------------------------------------- 1 | import getWindow from './getWindow'; 2 | 3 | /** 4 | * Gets the number of pixels to scroll the element's content from the left edge. 5 | * @param node The DOM element 6 | */ 7 | function scrollLeft(node: Element): number; 8 | 9 | /** 10 | * Sets the number of pixels to scroll the element's content from its left edge. 11 | * @param node The DOM element 12 | * @param val The number of pixels to scroll the element's content from its left edge 13 | */ 14 | function scrollLeft(node: Element, val: number): void; 15 | function scrollLeft(node: Element, val?: number): number { 16 | const win = getWindow(node); 17 | let left = node.scrollLeft; 18 | let top = 0; 19 | 20 | if (win) { 21 | left = win.pageXOffset; 22 | top = win.pageYOffset; 23 | } 24 | 25 | if (val !== undefined) { 26 | if (win) { 27 | win.scrollTo(val, top); 28 | } else { 29 | node.scrollLeft = val; 30 | } 31 | } 32 | 33 | return left; 34 | } 35 | 36 | export default scrollLeft; 37 | -------------------------------------------------------------------------------- /src/scrollTop.ts: -------------------------------------------------------------------------------- 1 | import getWindow from './getWindow'; 2 | 3 | /** 4 | * Gets the number of pixels that an element's content is scrolled vertically. 5 | * @param node The DOM element 6 | */ 7 | function scrollTop(node: Element): number; 8 | 9 | /** 10 | * Sets the number of pixels that an element's content is scrolled vertically. 11 | * @param node The DOM element 12 | * @param val The number of pixels that an element's content is scrolled vertically 13 | */ 14 | function scrollTop(node: Element, val: number): void; 15 | function scrollTop(node: Element, val?: number): number { 16 | const win = getWindow(node); 17 | let top = node.scrollTop; 18 | let left = 0; 19 | 20 | if (win) { 21 | top = win.pageYOffset; 22 | left = win.pageXOffset; 23 | } 24 | 25 | if (val !== undefined) { 26 | if (win) { 27 | win.scrollTo(left, val); 28 | } else { 29 | node.scrollTop = val; 30 | } 31 | } 32 | 33 | return top; 34 | } 35 | 36 | export default scrollTop; 37 | -------------------------------------------------------------------------------- /src/toggleClass.ts: -------------------------------------------------------------------------------- 1 | import hasClass from './hasClass'; 2 | import addClass from './addClass'; 3 | import removeClass from './removeClass'; 4 | 5 | /** 6 | * Toggle a class on an element 7 | * @param target The DOM element 8 | * @param className The class name 9 | * @returns The DOM element 10 | */ 11 | export default function toggleClass(target: Element, className: string): Element { 12 | if (hasClass(target, className)) { 13 | return removeClass(target, className); 14 | } 15 | return addClass(target, className); 16 | } 17 | -------------------------------------------------------------------------------- /src/translateDOMPositionXY.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Source code reference from: 3 | * https://github.com/facebook/fbjs/blob/d308fa83c9/packages/fbjs/src/dom/translateDOMPositionXY.js 4 | */ 5 | 6 | import BrowserSupportCore from './utils/BrowserSupportCore'; 7 | import getVendorPrefixedName from './utils/getVendorPrefixedName'; 8 | import getGlobal from './utils/getGlobal'; 9 | 10 | const g = getGlobal(); 11 | const TRANSFORM = getVendorPrefixedName('transform'); 12 | const BACKFACE_VISIBILITY = getVendorPrefixedName('backfaceVisibility'); 13 | 14 | export interface Options { 15 | enableTransform?: boolean; 16 | enable3DTransform?: boolean; 17 | forceUseTransform?: boolean; 18 | } 19 | 20 | const appendLeftAndTop = (style: CSSStyleDeclaration, x = 0, y = 0) => { 21 | style.left = `${x}px`; 22 | style.top = `${y}px`; 23 | 24 | return style; 25 | }; 26 | 27 | const appendTranslate = (style: CSSStyleDeclaration, x = 0, y = 0) => { 28 | style[TRANSFORM] = `translate(${x}px,${y}px)`; 29 | 30 | return style; 31 | }; 32 | 33 | const appendTranslate3d = (style: CSSStyleDeclaration, x = 0, y = 0) => { 34 | style[TRANSFORM] = `translate3d(${x}px,${y}px,0)`; 35 | style[BACKFACE_VISIBILITY] = 'hidden'; 36 | 37 | return style; 38 | }; 39 | 40 | export const getTranslateDOMPositionXY = (conf?: Options) => { 41 | const { enableTransform = true, enable3DTransform = true, forceUseTransform } = conf || {}; 42 | if (forceUseTransform) { 43 | return conf.enable3DTransform ? appendTranslate3d : appendTranslate; 44 | } 45 | 46 | if (BrowserSupportCore.hasCSSTransforms() && enableTransform) { 47 | const ua = g.window ? g.window.navigator.userAgent : 'UNKNOWN'; 48 | const isSafari = /Safari\//.test(ua) && !/Chrome\//.test(ua); 49 | 50 | // It appears that Safari messes up the composition order 51 | // of GPU-accelerated layers 52 | // (see bug https://bugs.webkit.org/show_bug.cgi?id=61824). 53 | // Use 2D translation instead. 54 | if (!isSafari && BrowserSupportCore.hasCSS3DTransforms() && enable3DTransform) { 55 | return appendTranslate3d; 56 | } 57 | 58 | return appendTranslate; 59 | } 60 | 61 | return appendLeftAndTop; 62 | }; 63 | 64 | const translateDOMPositionXY = getTranslateDOMPositionXY(); 65 | 66 | export default translateDOMPositionXY; 67 | -------------------------------------------------------------------------------- /src/utils/BrowserSupportCore.ts: -------------------------------------------------------------------------------- 1 | import getVendorPrefixedName from './getVendorPrefixedName'; 2 | 3 | export default { 4 | /** 5 | * @return {bool} True if browser supports css animations. 6 | */ 7 | hasCSSAnimations: () => !!getVendorPrefixedName('animationName'), 8 | 9 | /** 10 | * @return {bool} True if browser supports css transforms. 11 | */ 12 | hasCSSTransforms: () => !!getVendorPrefixedName('transform'), 13 | 14 | /** 15 | * @return {bool} True if browser supports css 3d transforms. 16 | */ 17 | hasCSS3DTransforms: () => !!getVendorPrefixedName('perspective'), 18 | 19 | /** 20 | * @return {bool} True if browser supports css transitions. 21 | */ 22 | hasCSSTransitions: () => !!getVendorPrefixedName('transition') 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils/UserAgent.ts: -------------------------------------------------------------------------------- 1 | let populated = false; 2 | 3 | // Browsers 4 | let ie; 5 | let firefox; 6 | let opera; 7 | let webkit; 8 | let chrome; 9 | 10 | // Actual IE browser for compatibility mode 11 | let ieRealVersion; 12 | 13 | // Platforms 14 | let osx; 15 | let windows; 16 | let linux; 17 | let android; 18 | 19 | // Architectures 20 | let win64; 21 | 22 | // Devices 23 | let iphone; 24 | let ipad; 25 | let native; 26 | 27 | let mobile; 28 | 29 | function populate(): null { 30 | if (populated) { 31 | return; 32 | } 33 | 34 | populated = true; 35 | 36 | // To work around buggy JS libraries that can't handle multi-digit 37 | // version numbers, Opera 10's user agent string claims it's Opera 38 | // 9, then later includes a Version/X.Y field: 39 | // 40 | // Opera/9.80 (foo) Presto/2.2.15 Version/10.10 41 | const uas = navigator.userAgent; 42 | let agent = 43 | /(?:MSIE.(\d+\.\d+))|(?:(?:Firefox|GranParadiso|Iceweasel).(\d+\.\d+))|(?:Opera(?:.+Version.|.)(\d+\.\d+))|(?:AppleWebKit.(\d+(?:\.\d+)?))|(?:Trident\/\d+\.\d+.*rv:(\d+\.\d+))/.exec( 44 | uas 45 | ); 46 | const os = /(Mac OS X)|(Windows)|(Linux)/.exec(uas); 47 | 48 | iphone = /\b(iPhone|iP[ao]d)/.exec(uas); 49 | ipad = /\b(iP[ao]d)/.exec(uas); 50 | android = /Android/i.exec(uas); 51 | native = /FBAN\/\w+;/i.exec(uas); 52 | mobile = /Mobile/i.exec(uas); 53 | 54 | // Note that the IE team blog would have you believe you should be checking 55 | // for 'Win64; x64'. But MSDN then reveals that you can actually be coming 56 | // from either x64 or ia64; so ultimately, you should just check for Win64 57 | // as in indicator of whether you're in 64-bit IE. 32-bit IE on 64-bit 58 | // Windows will send 'WOW64' instead. 59 | win64 = !!/Win64/.exec(uas); 60 | 61 | if (agent) { 62 | if (agent[1]) { 63 | ie = parseFloat(agent[1]); 64 | } else { 65 | ie = agent[5] ? parseFloat(agent[5]) : NaN; 66 | } 67 | 68 | // IE compatibility mode 69 | // @ts-ignore 70 | if (ie && document && document.documentMode) { 71 | // @ts-ignore 72 | ie = document.documentMode; 73 | } 74 | // grab the "true" ie version from the trident token if available 75 | const trident = /(?:Trident\/(\d+.\d+))/.exec(uas); 76 | ieRealVersion = trident ? parseFloat(trident[1]) + 4 : ie; 77 | 78 | firefox = agent[2] ? parseFloat(agent[2]) : NaN; 79 | opera = agent[3] ? parseFloat(agent[3]) : NaN; 80 | webkit = agent[4] ? parseFloat(agent[4]) : NaN; 81 | if (webkit) { 82 | // We do not add the regexp to the above test, because it will always 83 | // match 'safari' only since 'AppleWebKit' appears before 'Chrome' in 84 | // the userAgent string. 85 | agent = /(?:Chrome\/(\d+\.\d+))/.exec(uas); 86 | chrome = agent && agent[1] ? parseFloat(agent[1]) : NaN; 87 | } else { 88 | chrome = NaN; 89 | } 90 | } else { 91 | ie = NaN; 92 | firefox = NaN; 93 | opera = NaN; 94 | chrome = NaN; 95 | webkit = NaN; 96 | } 97 | 98 | if (os) { 99 | if (os[1]) { 100 | // Detect OS X version. If no version number matches, set osx to true. 101 | // Version examples: 10, 10_6_1, 10.7 102 | // Parses version number as a float, taking only first two sets of 103 | // digits. If only one set of digits is found, returns just the major 104 | // version number. 105 | const ver = /(?:Mac OS X (\d+(?:[._]\d+)?))/.exec(uas); 106 | 107 | osx = ver ? parseFloat(ver[1].replace('_', '.')) : true; 108 | } else { 109 | osx = false; 110 | } 111 | windows = !!os[2]; 112 | linux = !!os[3]; 113 | } else { 114 | osx = false; 115 | windows = false; 116 | linux = false; 117 | } 118 | } 119 | 120 | /** 121 | * @deprecated 122 | */ 123 | const UserAgent = { 124 | /** 125 | * Check if the UA is Internet Explorer. 126 | * 127 | * 128 | * @return float|NaN Version number (if match) or NaN. 129 | */ 130 | ie: (): boolean => populate() || ie, 131 | 132 | /** 133 | * Check if we're in Internet Explorer compatibility mode. 134 | * 135 | * @return bool true if in compatibility mode, false if 136 | * not compatibility mode or not ie 137 | */ 138 | ieCompatibilityMode: (): boolean => populate() || ieRealVersion > ie, 139 | 140 | /** 141 | * Whether the browser is 64-bit IE. Really, this is kind of weak sauce; we 142 | * only need this because Skype can't handle 64-bit IE yet. We need to remove 143 | * this when we don't need it -- tracked by #601957. 144 | */ 145 | ie64: (): boolean => UserAgent.ie() && win64, 146 | 147 | /** 148 | * Check if the UA is Firefox. 149 | * 150 | * 151 | * @return float|NaN Version number (if match) or NaN. 152 | */ 153 | firefox: (): boolean => populate() || firefox, 154 | 155 | /** 156 | * Check if the UA is Opera. 157 | * 158 | * 159 | * @return float|NaN Version number (if match) or NaN. 160 | */ 161 | opera: (): boolean => populate() || opera, 162 | 163 | /** 164 | * Check if the UA is WebKit. 165 | * 166 | * 167 | * @return float|NaN Version number (if match) or NaN. 168 | */ 169 | webkit: (): boolean => populate() || webkit, 170 | 171 | /** 172 | * For Push 173 | * WILL BE REMOVED VERY SOON. Use UserAgent_DEPRECATED.webkit 174 | */ 175 | safari: (): boolean => UserAgent.webkit(), 176 | 177 | /** 178 | * Check if the UA is a Chrome browser. 179 | * 180 | * 181 | * @return float|NaN Version number (if match) or NaN. 182 | */ 183 | chrome: (): boolean => populate() || chrome, 184 | 185 | /** 186 | * Check if the user is running Windows. 187 | * 188 | * @return bool `true' if the user's OS is Windows. 189 | */ 190 | windows: (): boolean => populate() || windows, 191 | 192 | /** 193 | * Check if the user is running Mac OS X. 194 | * 195 | * @return float|bool Returns a float if a version number is detected, 196 | * otherwise true/false. 197 | */ 198 | osx: (): boolean => populate() || osx, 199 | 200 | /** 201 | * Check if the user is running Linux. 202 | * 203 | * @return bool `true' if the user's OS is some flavor of Linux. 204 | */ 205 | linux: (): boolean => populate() || linux, 206 | 207 | /** 208 | * Check if the user is running on an iPhone or iPod platform. 209 | * 210 | * @return bool `true' if the user is running some flavor of the 211 | * iPhone OS. 212 | */ 213 | iphone: (): boolean => populate() || iphone, 214 | mobile: (): boolean => populate() || iphone || ipad || android || mobile, 215 | 216 | // webviews inside of the native apps 217 | nativeApp: (): boolean => populate() || native, 218 | android: (): boolean => populate() || android, 219 | ipad: (): boolean => populate() || ipad 220 | }; 221 | 222 | export default UserAgent; 223 | -------------------------------------------------------------------------------- /src/utils/camelize.ts: -------------------------------------------------------------------------------- 1 | const hyphenPattern = /-(.)/g; 2 | 3 | /** 4 | * Camelcases a hyphenated string, for example: 5 | * 6 | * > camelize('background-color') 7 | * < "backgroundColor" 8 | * 9 | * @param {string} string 10 | * @return {string} 11 | */ 12 | function camelize(string) { 13 | return string.replace(hyphenPattern, (_, character) => character.toUpperCase()); 14 | } 15 | 16 | export default camelize; 17 | -------------------------------------------------------------------------------- /src/utils/camelizeStyleName.ts: -------------------------------------------------------------------------------- 1 | import { camelize } from './stringFormatter'; 2 | 3 | const msPattern = /^-ms-/; 4 | 5 | export default function camelizeStyleName(name: string) { 6 | // The `-ms` prefix is converted to lowercase `ms`. 7 | // http://www.andismith.com/blog/2012/02/modernizr-prefixed/ 8 | return camelize(name.replace(msPattern, 'ms-')); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author {@link https://github.com/jashkenas/underscore underscorejs}. 3 | * @version 1.7.0 4 | * @see {@link http://underscorejs.org/#debounce underscore.debounce(function, wait, [immediate])} 5 | * @param func 6 | * @param wait 7 | * @param immediate 8 | * @returns {*} 9 | */ 10 | /* eslint-disable */ 11 | export default function debounce(func, wait, immediate) { 12 | 13 | var timeout, args, context, timestamp, result; 14 | 15 | var _now = Date.now || function () { 16 | return new Date().getTime(); 17 | }; 18 | 19 | var later = function () { 20 | var last = _now() - timestamp; 21 | if (last < wait && last >= 0) { 22 | timeout = setTimeout(later, wait - last); 23 | } else { 24 | timeout = null; 25 | if (!immediate) { 26 | result = func.apply(context, args); 27 | if (!timeout) { 28 | context = args = null; 29 | } 30 | } 31 | } 32 | }; 33 | 34 | return function () { 35 | context = this; 36 | args = arguments; 37 | timestamp = _now(); 38 | var callNow = immediate && !timeout; 39 | if (!timeout) { 40 | timeout = setTimeout(later, wait); 41 | } 42 | if (callNow) { 43 | result = func.apply(context, args); 44 | context = args = null; 45 | } 46 | return result; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/emptyFunction.ts: -------------------------------------------------------------------------------- 1 | function makeEmptyFunction(arg) { 2 | return () => arg; 3 | } 4 | 5 | function emptyFunction() {} 6 | 7 | emptyFunction.thatReturns = makeEmptyFunction; 8 | emptyFunction.thatReturnsFalse = makeEmptyFunction(false); 9 | emptyFunction.thatReturnsTrue = makeEmptyFunction(true); 10 | emptyFunction.thatReturnsNull = makeEmptyFunction(null); 11 | emptyFunction.thatReturnsThis = () => this; 12 | emptyFunction.thatReturnsArgument = arg => arg; 13 | 14 | export default emptyFunction; 15 | -------------------------------------------------------------------------------- /src/utils/getComputedStyle.ts: -------------------------------------------------------------------------------- 1 | export default (node: Element): CSSStyleDeclaration | null => { 2 | if (!node) { 3 | throw new TypeError('No Element passed to `getComputedStyle()`'); 4 | } 5 | 6 | const doc = node.ownerDocument; 7 | 8 | if ('defaultView' in doc) { 9 | if (doc.defaultView.opener) { 10 | return node.ownerDocument.defaultView.getComputedStyle(node, null); 11 | } 12 | 13 | return window.getComputedStyle(node, null); 14 | } 15 | 16 | return null; 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/getGlobal.ts: -------------------------------------------------------------------------------- 1 | // the only reliable means to get the global object is 2 | // `Function('return this')()` 3 | // However, this causes CSP violations in Chrome apps. 4 | 5 | // https://github.com/tc39/proposal-global 6 | function getGlobal() { 7 | if (typeof globalThis !== 'undefined') { 8 | return globalThis; 9 | } 10 | 11 | if (typeof self !== 'undefined') { 12 | return self; 13 | } 14 | if (typeof window !== 'undefined') { 15 | return window; 16 | } 17 | 18 | throw new Error('unable to locate global object'); 19 | } 20 | 21 | export default getGlobal; 22 | -------------------------------------------------------------------------------- /src/utils/getVendorPrefixedName.ts: -------------------------------------------------------------------------------- 1 | import canUseDOM from '../canUseDOM'; 2 | import { camelize } from './stringFormatter'; 3 | 4 | const memoized = {}; 5 | const prefixes = ['Webkit', 'ms', 'Moz', 'O']; 6 | const prefixRegex = new RegExp(`^(${prefixes.join('|')})`); 7 | const testStyle = canUseDOM ? document.createElement('div').style : {}; 8 | 9 | function getWithPrefix(name) { 10 | for (let i = 0; i < prefixes.length; i += 1) { 11 | const prefixedName = prefixes[i] + name; 12 | if (prefixedName in testStyle) { 13 | return prefixedName; 14 | } 15 | } 16 | return null; 17 | } 18 | 19 | /** 20 | * @param {string} property Name of a css property to check for. 21 | * @return {?string} property name supported in the browser, or null if not 22 | * supported. 23 | */ 24 | function getVendorPrefixedName(property: string) { 25 | const name = camelize(property); 26 | if (memoized[name] === undefined) { 27 | const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1); 28 | if (prefixRegex.test(capitalizedName)) { 29 | throw new Error( 30 | `getVendorPrefixedName must only be called with unprefixed 31 | CSS property names. It was called with ${property}` 32 | ); 33 | } 34 | memoized[name] = name in testStyle ? name : getWithPrefix(capitalizedName); 35 | } 36 | return memoized[name] || name; 37 | } 38 | 39 | export default getVendorPrefixedName; 40 | -------------------------------------------------------------------------------- /src/utils/hyphenateStyleName.ts: -------------------------------------------------------------------------------- 1 | import { hyphenate } from './stringFormatter'; 2 | 3 | const msPattern = /^ms-/; 4 | 5 | export default string => hyphenate(string).replace(msPattern, '-ms-'); 6 | -------------------------------------------------------------------------------- /src/utils/isEventSupported.ts: -------------------------------------------------------------------------------- 1 | import canUseDOM from '../canUseDOM'; 2 | 3 | let useHasFeature; 4 | if (canUseDOM) { 5 | useHasFeature = 6 | document.implementation && 7 | document.implementation.hasFeature && 8 | // always returns true in newer browsers as per the standard. 9 | // @see http://dom.spec.whatwg.org/#dom-domimplementation-hasfeature 10 | document.implementation.hasFeature('', '') !== true; 11 | } 12 | 13 | function isEventSupported(eventNameSuffix: string, capture?: boolean) { 14 | if (!canUseDOM || (capture && !('addEventListener' in document))) { 15 | return false; 16 | } 17 | 18 | const eventName = `on${eventNameSuffix}`; 19 | let isSupported = eventName in document; 20 | 21 | if (!isSupported) { 22 | const element = document.createElement('div'); 23 | element.setAttribute(eventName, 'return;'); 24 | isSupported = typeof element[eventName] === 'function'; 25 | } 26 | 27 | if (!isSupported && useHasFeature && eventNameSuffix === 'wheel') { 28 | // This is the only way to test support for the `wheel` event in IE9+. 29 | isSupported = document.implementation.hasFeature('Events.wheel', '3.0'); 30 | } 31 | 32 | return isSupported; 33 | } 34 | 35 | export default isEventSupported; 36 | -------------------------------------------------------------------------------- /src/utils/normalizeWheel.ts: -------------------------------------------------------------------------------- 1 | import UserAgent from './UserAgent'; 2 | import isEventSupported from './isEventSupported'; 3 | 4 | // Reasonable defaults 5 | const PIXEL_STEP = 10; 6 | const LINE_HEIGHT = 40; 7 | const PAGE_HEIGHT = 800; 8 | 9 | function normalizeWheel(event: any) { 10 | let sX = 0; 11 | let sY = 0; // spinX, spinY 12 | let pX = 0; 13 | let pY = 0; // pixelX, pixelY 14 | 15 | // Legacy 16 | if ('detail' in event) { 17 | sY = event.detail; 18 | } 19 | if ('wheelDelta' in event) { 20 | sY = -event.wheelDelta / 120; 21 | } 22 | if ('wheelDeltaY' in event) { 23 | sY = -event.wheelDeltaY / 120; 24 | } 25 | if ('wheelDeltaX' in event) { 26 | sX = -event.wheelDeltaX / 120; 27 | } 28 | 29 | // side scrolling on FF with DOMMouseScroll 30 | if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) { 31 | sX = sY; 32 | sY = 0; 33 | } 34 | 35 | pX = sX * PIXEL_STEP; 36 | pY = sY * PIXEL_STEP; 37 | 38 | if ('deltaY' in event) { 39 | pY = event.deltaY; 40 | } 41 | if ('deltaX' in event) { 42 | pX = event.deltaX; 43 | } 44 | 45 | if ((pX || pY) && event.deltaMode) { 46 | if (event.deltaMode === 1) { 47 | // delta in LINE units 48 | pX *= LINE_HEIGHT; 49 | pY *= LINE_HEIGHT; 50 | } else { 51 | // delta in PAGE units 52 | pX *= PAGE_HEIGHT; 53 | pY *= PAGE_HEIGHT; 54 | } 55 | } 56 | 57 | // Fall-back if spin cannot be determined 58 | if (pX && !sX) { 59 | sX = pX < 1 ? -1 : 1; 60 | } 61 | if (pY && !sY) { 62 | sY = pY < 1 ? -1 : 1; 63 | } 64 | 65 | return { 66 | spinX: sX, 67 | spinY: sY, 68 | pixelX: pX, 69 | pixelY: pY 70 | }; 71 | } 72 | 73 | /** 74 | * The best combination if you prefer spinX + spinY normalization. It favors 75 | * the older DOMMouseScroll for Firefox, as FF does not include wheelDelta with 76 | * 'wheel' event, making spin speed determination impossible. 77 | */ 78 | normalizeWheel.getEventType = () => { 79 | if (UserAgent.firefox()) { 80 | return 'DOMMouseScroll'; 81 | } 82 | 83 | return isEventSupported('wheel') ? 'wheel' : 'mousewheel'; 84 | }; 85 | 86 | export default normalizeWheel; 87 | -------------------------------------------------------------------------------- /src/utils/stringFormatter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | /** 4 | * @example 5 | * underscoreName('getList'); 6 | * => get_list 7 | */ 8 | 9 | export function underscore(string) { 10 | return string.replace(/([A-Z])/g, '_$1').toLowerCase(); 11 | } 12 | 13 | /** 14 | * @example 15 | * camelize('font-size'); 16 | * => fontSize 17 | */ 18 | export function camelize(string) { 19 | return string.replace(/\-(\w)/g, char => { 20 | return char.slice(1).toUpperCase(); 21 | }); 22 | } 23 | 24 | /** 25 | * @example 26 | * camelize('fontSize'); 27 | * => font-size 28 | */ 29 | export function hyphenate(string) { 30 | return string.replace(/([A-Z])/g, '-$1').toLowerCase(); 31 | } 32 | 33 | /** 34 | * @example 35 | * merge('{0} - A front-end {1} ','Suite','framework'); 36 | * => Suite - A front-end framework 37 | */ 38 | export function merge(pattern) { 39 | var pointer = 0, 40 | i; 41 | for (i = 1; i < arguments.length; i += 1) { 42 | pattern = pattern.split(`{${pointer}}`).join(arguments[i]); 43 | pointer += 1; 44 | } 45 | return pattern; 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/throttle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author {@link https://github.com/jashkenas/underscore underscorejs}. 3 | * @version 1.7.0 4 | * @see {@link http://underscorejs.org/#throttle underscore.throttle(function, wait, [immediate])} 5 | * @param func 6 | * @param wait 7 | * @param options 8 | * @returns {throttled} 9 | */ 10 | 11 | /* eslint-disable */ 12 | export default function throttle(func, wait, options) { 13 | var context, args, result; 14 | var timeout = null; 15 | var previous = 0; 16 | var _now = 17 | Date.now || 18 | function() { 19 | return new Date().getTime(); 20 | }; 21 | if (!options) { 22 | options = {}; 23 | } 24 | var later = function() { 25 | previous = options.leading === false ? 0 : _now(); 26 | timeout = null; 27 | result = func.apply(context, args); 28 | if (!timeout) { 29 | context = args = null; 30 | } 31 | }; 32 | return function() { 33 | var now = _now(); 34 | if (!previous && options.leading === false) { 35 | previous = now; 36 | } 37 | var remaining = wait - (now - previous); 38 | context = this; 39 | args = arguments; 40 | if (remaining <= 0 || remaining > wait) { 41 | if (timeout) { 42 | clearTimeout(timeout); 43 | timeout = null; 44 | } 45 | previous = now; 46 | result = func.apply(context, args); 47 | if (!timeout) { 48 | context = args = null; 49 | } 50 | } else if (!timeout && options.trailing !== false) { 51 | timeout = setTimeout(later, remaining); 52 | } 53 | return result; 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /test/PointerMoveTrackerSpec.js: -------------------------------------------------------------------------------- 1 | import * as lib from '../src'; 2 | import simulant from 'simulant'; 3 | 4 | describe('PointerMoveTracker', () => { 5 | beforeEach(() => { 6 | document.body.innerHTML = window.__html__['test/html/PointerMoveTracker.html']; 7 | }); 8 | 9 | it('Should track for mouse events', done => { 10 | const target = document.getElementById('drag-target'); 11 | let tracker = null; 12 | 13 | const handleDragMove = (x, y, e) => { 14 | if (e instanceof MouseEvent) { 15 | if (x && y) { 16 | expect(x).to.equal(100); 17 | expect(y).to.equal(100); 18 | } 19 | } 20 | }; 21 | 22 | const handleDragEnd = () => { 23 | tracker.releaseMoves(); 24 | tracker = null; 25 | done(); 26 | }; 27 | 28 | function handleStart(e) { 29 | if (!tracker) { 30 | tracker = new lib.PointerMoveTracker(document.body, { 31 | onMove: handleDragMove, 32 | onMoveEnd: handleDragEnd 33 | }); 34 | 35 | tracker.captureMoves(e); 36 | } 37 | } 38 | 39 | target.addEventListener('mousedown', handleStart); 40 | 41 | simulant.fire(target, 'mousedown'); 42 | simulant.fire(document.body, 'mousemove', { clientX: 100, clientY: 100 }); 43 | simulant.fire(document.body, 'mouseup'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/WheelHandlerSpec.js: -------------------------------------------------------------------------------- 1 | import { WheelHandler } from '../src'; 2 | 3 | describe('WheelHandler', () => { 4 | let mockEvent; 5 | let originalNavigator = global.navigator; 6 | 7 | beforeEach(() => { 8 | mockEvent = { 9 | preventDefault: () => {}, 10 | deltaX: 10, 11 | deltaY: 0 12 | }; 13 | 14 | Object.defineProperty(global, 'navigator', { 15 | value: { 16 | platform: 'MacIntel' 17 | } 18 | }); 19 | }); 20 | 21 | after(() => { 22 | Object.defineProperty(global, 'navigator', { 23 | value: originalNavigator 24 | }); 25 | }); 26 | 27 | it('Should return deltaX and deltaY', done => { 28 | const wheelHandler = new WheelHandler( 29 | (dX, dY) => { 30 | expect(dX).to.equal(10); 31 | expect(dY).to.equal(0); 32 | done(); 33 | }, 34 | true, 35 | true 36 | ); 37 | 38 | wheelHandler.onWheel(mockEvent); 39 | }); 40 | 41 | it('Should normalize deltas correctly when delta unit is lines', done => { 42 | const wheelHandler = new WheelHandler( 43 | (dX, dY) => { 44 | expect(dX).to.equal(8000); 45 | expect(dY).to.equal(800); 46 | done(); 47 | }, 48 | true, 49 | true 50 | ); 51 | wheelHandler.onWheel({ 52 | ...mockEvent, 53 | deltaMode: 2, 54 | deltaX: 10, 55 | deltaY: 1 56 | }); 57 | }); 58 | 59 | it('Should normalize deltas when delta unit is pages', done => { 60 | const wheelHandler = new WheelHandler( 61 | (dX, dY) => { 62 | expect(dX).to.equal(400); 63 | expect(dY).to.equal(40); 64 | done(); 65 | }, 66 | true, 67 | true 68 | ); 69 | wheelHandler.onWheel({ 70 | ...mockEvent, 71 | deltaMode: 1, 72 | deltaX: 10, 73 | deltaY: 1 74 | }); 75 | }); 76 | 77 | it('Should take horizontal scrolling with shiftKey + wheel into account on non-MacIntel platforms', done => { 78 | Object.defineProperty(global, 'navigator', { 79 | value: { 80 | platform: 'Win64' 81 | } 82 | }); 83 | 84 | const wheelHandler = new WheelHandler( 85 | (dX, dY) => { 86 | expect(dX).to.equal(10); 87 | expect(dY).to.equal(0); 88 | done(); 89 | }, 90 | true, 91 | true 92 | ); 93 | wheelHandler.onWheel({ 94 | ...mockEvent, 95 | shiftKey: true, 96 | deltaX: 0, 97 | deltaY: 10 98 | }); 99 | }); 100 | 101 | it('Should not treat as horizontal scrolling when event.shiftKey == true if platform is MacIntel', done => { 102 | Object.defineProperty(global, 'navigator', { 103 | value: { 104 | platform: 'MacIntel' 105 | } 106 | }); 107 | 108 | const wheelHandler = new WheelHandler( 109 | (dX, dY) => { 110 | expect(dX).to.equal(0); 111 | expect(dY).to.equal(10); 112 | done(); 113 | }, 114 | true, 115 | true 116 | ); 117 | wheelHandler.onWheel({ 118 | ...mockEvent, 119 | shiftKey: true, 120 | deltaX: 0, 121 | deltaY: 10 122 | }); 123 | }); 124 | 125 | it('Should treat as horizontal scrolling when side scrolling on FF', done => { 126 | const wheelHandler = new WheelHandler( 127 | (dX, dY) => { 128 | expect(dX).to.equal(500); 129 | expect(dY).to.equal(0); 130 | done(); 131 | }, 132 | true, 133 | true 134 | ); 135 | wheelHandler.onWheel({ 136 | detail: 50, 137 | axis: 1, 138 | HORIZONTAL_AXIS: 1, 139 | preventDefault: () => {} 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /test/classSpec.js: -------------------------------------------------------------------------------- 1 | import { addClass, hasClass, removeClass, toggleClass } from '../src'; 2 | 3 | describe('Class', () => { 4 | beforeEach(() => { 5 | document.body.innerHTML = window.__html__['test/html/class.html']; 6 | }); 7 | 8 | it('should add a class', () => { 9 | const el = document.getElementById('case-1'); 10 | addClass(el, 'custom-class'); 11 | expect(el.className).to.contain('custom-class'); 12 | }); 13 | 14 | it('should remove a class', () => { 15 | const el = document.getElementById('case-2'); 16 | removeClass(el, 'test-class'); 17 | expect(el.className).to.equal(''); 18 | }); 19 | 20 | it('should toggle a class', () => { 21 | const el = document.getElementById('case-3'); 22 | toggleClass(el, 'test-class'); 23 | expect(el.className).to.equal('test-class'); 24 | toggleClass(el, 'test-class'); 25 | expect(el.className).to.equal(''); 26 | }); 27 | 28 | it('should check for a class', () => { 29 | expect(hasClass(document.getElementById('case-1'), 'test-class')).to.equal(false); 30 | expect(hasClass(document.getElementById('case-2'), 'test-class')).to.equal(true); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/eventSpec.js: -------------------------------------------------------------------------------- 1 | import { on, off } from '../src'; 2 | import simulant from 'simulant'; 3 | 4 | describe('Events', () => { 5 | beforeEach(() => { 6 | document.body.innerHTML = window.__html__['test/html/events.html']; 7 | }); 8 | 9 | it('should add an event listener', done => { 10 | const el = document.getElementById('case-1'); 11 | on(el, 'click', () => done()); 12 | simulant.fire(el, 'click'); 13 | }); 14 | 15 | it('should remove an event listener', () => { 16 | const el = document.getElementById('case-1'); 17 | function handleEvent() { 18 | throw new Error('event fired'); 19 | } 20 | on(el, 'click', handleEvent); 21 | off(el, 'click', handleEvent); 22 | simulant.fire(el, 'click'); 23 | }); 24 | 25 | it('should remove an event listener', () => { 26 | const el = document.getElementById('case-1'); 27 | function handleEvent() { 28 | throw new Error('event fired'); 29 | } 30 | on(el, 'click', handleEvent).off(); 31 | simulant.fire(el, 'click'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/html/PointerMoveTracker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PointerMoveTracker 8 | 9 | 10 |
11 | 12 |
13 |
14 |

drag me (fail)

15 |
16 | 17 |
18 |

touch me

19 |
20 |
21 | 22 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /test/html/WheelHandler.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WheelHandler 8 | 9 | 10 |
11 |
12 | 😀 😁 😂 🤣 😃😄 😅 😆 😉 😊😫 😴 😌 😛 😜👆🏻 😒 😓 😔 👇🏻😑 😶 🙄 😏 😣 😞 😟 😤 😢 😭🤑 😲 13 | 🙄 🙁 😖👍 👎 👊 ✊ 🤛🙄 ✋ 🤚 🖐 🖖👍🏼 👎🏼 👊🏼 ✊🏼 🤛🏼 ☝🏽 ✋🏽 🤚🏽 🖐🏽 🖖🏽🌖 🌗 🌘 🌑 🌒💫 💥 💢 💦 14 | 💧🐠 🐟 🐬 🐳 🐋😬 😐 😕 😯 😶 😇 😏 😑 😓 😵🐥 🐣 🐔 🐛 🐤💪 ✨ 🔔 ✊ ✋👇 👊 👍 👈 👆💛 👐 15 | 👎 👌 💘 👍🏼 👎🏼 👊🏼 ✊🏼 🤛🏼☝🏽 ✋🏽 🤚🏽 🖐🏽 🖖🏽🌖 🌗 🌘 🌑 🌒💫 💥 💢 💦 💧🐠 🐟 🐬 🐳 🐋 😬 😐 😕 😯 16 | 😶😇 😏 😑 😓 😵🐥 🐣 🐔 🐛 🐤💪 ✨ 🔔 ✊ ✋👇 👊 👍 👈 👆 💛 👐 👎 👌 💘 😀 😁 😂 🤣 😃😄 17 | 😅 😆 😉 😊😫 😴 😌 😛 😜👆🏻 😒 😓 😔 👇🏻😑 😶 🙄 😏 😣 😞 😟 😤 😢 😭🤑 😲 🙄 🙁 😖👍 👎 👊 18 | ✊ 🤛🙄 ✋ 🤚 🖐 🖖👍🏼 👎🏼 👊🏼 ✊🏼 🤛🏼 ☝🏽 ✋🏽 🤚🏽 🖐🏽 🖖🏽🌖 🌗 🌘 🌑 🌒💫 💥 💢 💦 💧🐠 🐟 🐬 🐳 🐋😬 19 | 😐 😕 😯 😶 😇 😏 😑 😓 😵🐥 🐣 🐔 🐛 🐤💪 ✨ 🔔 ✊ ✋👇 👊 👍 👈 👆💛 👐 👎 👌 💘 👍🏼 👎🏼 👊🏼 20 | ✊🏼 🤛🏼☝🏽 ✋🏽 🤚🏽 🖐🏽 🖖🏽🌖 🌗 🌘 🌑 🌒💫 💥 💢 💦 💧🐠 🐟 🐬 🐳 🐋 😬 😐 😕 😯 😶😇 😏 😑 😓 😵🐥 21 | 🐣 🐔 🐛 🐤💪 ✨ 🔔 ✊ ✋👇 👊 👍 👈 👆 💛 👐 👎 👌 💘 😀 😁 😂 🤣 😃😄 😅 😆 😉 😊😫 😴 😌 22 | 😛 😜👆🏻 😒 😓 😔 👇🏻😑 😶 🙄 😏 😣 😞 😟 😤 😢 😭🤑 😲 🙄 🙁 😖👍 👎 👊 ✊ 🤛🙄 ✋ 🤚 🖐 🖖👍🏼 23 | 👎🏼 👊🏼 ✊🏼 🤛🏼 ☝🏽 ✋🏽 🤚🏽 🖐🏽 🖖🏽🌖 🌗 🌘 🌑 🌒💫 💥 💢 💦 💧🐠 🐟 🐬 🐳 🐋😬 😐 😕 😯 😶 😇 😏 😑 24 | 😓 😵🐥 🐣 🐔 🐛 🐤💪 ✨ 🔔 ✊ ✋👇 👊 👍 👈 👆💛 👐 👎 👌 💘 👍🏼 👎🏼 👊🏼 ✊🏼 🤛🏼☝🏽 ✋🏽 🤚🏽 🖐🏽 🖖🏽🌖 25 | 🌗 🌘 🌑 🌒💫 💥 💢 💦 💧🐠 🐟 🐬 🐳 🐋 😬 😐 😕 😯 😶😇 😏 😑 😓 😵🐥 🐣 🐔 🐛 🐤💪 ✨ 🔔 26 | ✊ ✋👇 👊 👍 👈 👆 💛 👐 👎 👌 💘 😀 😁 😂 🤣 😃😄 😅 😆 😉 😊😫 😴 😌 😛 😜👆🏻 😒 😓 😔 27 | 👇🏻😑 😶 🙄 😏 😣 😞 😟 😤 😢 😭🤑 😲 🙄 🙁 😖👍 👎 👊 ✊ 🤛🙄 ✋ 🤚 🖐 🖖👍🏼 👎🏼 👊🏼 ✊🏼 🤛🏼 ☝🏽 28 | ✋🏽 🤚🏽 🖐🏽 🖖🏽🌖 🌗 🌘 🌑 🌒💫 💥 💢 💦 💧🐠 🐟 🐬 🐳 🐋😬 😐 😕 😯 😶 😇 😏 😑 😓 😵🐥 🐣 🐔 29 | 🐛 🐤💪 ✨ 🔔 ✊ ✋👇 👊 👍 👈 👆💛 👐 👎 👌 💘 👍🏼 👎🏼 👊🏼 ✊🏼 🤛🏼☝🏽 ✋🏽 🤚🏽 🖐🏽 🖖🏽🌖 🌗 🌘 🌑 🌒💫 30 | 💥 💢 💦 💧🐠 🐟 🐬 🐳 🐋 😬 😐 😕 😯 😶😇 😏 😑 😓 😵🐥 🐣 🐔 🐛 🐤💪 ✨ 🔔 ✊ ✋👇 👊 👍 31 | 👈 👆 💛 👐 👎 👌 💘 32 |
33 |
34 | 35 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /test/html/class.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /test/html/events.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | -------------------------------------------------------------------------------- /test/html/query.html: -------------------------------------------------------------------------------- 1 |
2 |
-
3 |
4 |
5 |
-
6 |
7 |
8 |
-
9 |
10 |
11 |
-
12 |
13 | 14 | -------------------------------------------------------------------------------- /test/html/style.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const testsContext = require.context('.', true, /Spec$/); 2 | testsContext.keys().forEach(testsContext); 3 | -------------------------------------------------------------------------------- /test/querySpec.js: -------------------------------------------------------------------------------- 1 | import * as lib from '../src'; 2 | import $ from 'jquery'; 3 | 4 | describe('Query', () => { 5 | beforeEach(() => { 6 | document.body.innerHTML = window.__html__['test/html/query.html']; 7 | }); 8 | 9 | it('should get 100 of height', () => { 10 | const el = document.getElementById('case-1'); 11 | const height = lib.getHeight(el); 12 | expect(height).to.equal(100); 13 | }); 14 | 15 | it('should get 200 of width', () => { 16 | const el = document.getElementById('case-1'); 17 | const height = lib.getWidth(el); 18 | expect(height).to.equal(200); 19 | }); 20 | 21 | it('should handle fixed position', () => { 22 | const el = document.getElementById('case-2'); 23 | const position = lib.getPosition(el); 24 | const $position = $('#case-2').position(); 25 | expect(position.left).to.equal($position.left); 26 | expect(position.top).to.equal($position.top); 27 | }); 28 | 29 | it('should handle absolute position', () => { 30 | const el = document.getElementById('case-3'); 31 | const position = lib.getPosition(el); 32 | const $position = $('#case-3').position(); 33 | 34 | expect(position.left).to.equal($position.left); 35 | expect(position.top).to.equal($position.top); 36 | }); 37 | 38 | it('should handle scroll position', () => { 39 | const el = document.getElementById('case-4'); 40 | lib.scrollTop(el, 100); 41 | lib.scrollLeft(el, 200); 42 | 43 | expect(100).to.equal($('#case-4').scrollTop()); 44 | expect(200).to.equal($('#case-4').scrollLeft()); 45 | }); 46 | 47 | describe('contains(context, node)', () => { 48 | it('should check for contained element', () => { 49 | const el4 = document.getElementById('case-4'); 50 | const el5 = document.getElementById('case-5'); 51 | const el6 = document.getElementById('case-6'); 52 | 53 | expect(lib.contains(el5, el4)).to.equal(false); 54 | expect(lib.contains(el5, el6)).to.equal(true); 55 | }); 56 | 57 | it('should return false if node is null', () => { 58 | const el4 = document.getElementById('case-4'); 59 | expect(lib.contains(el4, null)).to.be.false; 60 | }); 61 | }); 62 | 63 | it('should container with offset', () => { 64 | const container = document.getElementById('case-7'); 65 | const node = document.getElementById('case-8'); 66 | 67 | const posi = lib.getPosition(node, container, false); 68 | expect(posi.top).to.equal(20); 69 | expect(posi.left).to.equal(10); 70 | }); 71 | 72 | describe('isFocusable', () => { 73 | function createElement(type, props = {}) { 74 | const element = document.createElement(type, props); 75 | const keys = Object.keys(props); 76 | for (const prop of keys) { 77 | const value = props[prop]; 78 | element[prop] = value; 79 | element.setAttribute(prop.toLowerCase(), `${value}`); 80 | } 81 | 82 | document.body.appendChild(element); 83 | 84 | return element; 85 | } 86 | 87 | it('should return true for focusable element', () => { 88 | expect( 89 | lib.isFocusable(document.createElementNS('http://www.w3.org/2000/svg', 'svg')) 90 | ).to.equal(false); 91 | expect(lib.isFocusable(createElement('input'))).to.equal(true); 92 | expect(lib.isFocusable(createElement('input', { tabIndex: -1 }))).to.equal(true); 93 | expect(lib.isFocusable(createElement('input', { hidden: true }))).to.equal(false); 94 | expect(lib.isFocusable(createElement('input', { disabled: true }))).to.equal(false); 95 | expect(lib.isFocusable(createElement('a'))).to.equal(false); 96 | expect(lib.isFocusable(createElement('a', { href: '' }))).to.equal(true); 97 | expect(lib.isFocusable(createElement('audio'))).to.equal(false); 98 | expect(lib.isFocusable(createElement('audio', { controls: true }))).to.equal(true); 99 | expect(lib.isFocusable(createElement('video'))).to.equal(false); 100 | expect(lib.isFocusable(createElement('video', { controls: true }))).to.equal(true); 101 | expect(lib.isFocusable(createElement('div'))).to.equal(false); 102 | expect(lib.isFocusable(createElement('div', { contentEditable: true }))).to.equal(true); 103 | expect(lib.isFocusable(createElement('div', { tabIndex: 0 }))).to.equal(true); 104 | expect(lib.isFocusable(createElement('div', { tabIndex: -1 }))).to.equal(true); 105 | }); 106 | }); 107 | 108 | describe('getContainer', () => { 109 | it('Should return the container if present', () => { 110 | const container = {}; 111 | 112 | expect(lib.getContainer(container)).to.equal(container); 113 | }); 114 | 115 | it('Should return the return value of container when container is a function', () => { 116 | const container = {}; 117 | 118 | expect(lib.getContainer(() => container)).to.equal(container); 119 | }); 120 | 121 | it('Should return defaultContainer if container is null', () => { 122 | const defaultContainer = {}; 123 | 124 | expect(lib.getContainer(null, defaultContainer)).to.equal(defaultContainer); 125 | }); 126 | 127 | it('Should return defaultContainer if container returns null', () => { 128 | const defaultContainer = {}; 129 | 130 | expect(lib.getContainer(() => null, defaultContainer)).to.equal(defaultContainer); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/styleSpec.js: -------------------------------------------------------------------------------- 1 | import { getStyle, removeStyle, addStyle, translateDOMPositionXY } from '../src'; 2 | import { getTranslateDOMPositionXY } from '../src/translateDOMPositionXY'; 3 | 4 | describe('Style', () => { 5 | beforeEach(() => { 6 | document.body.innerHTML = window.__html__['test/html/style.html']; 7 | }); 8 | 9 | describe('getStyle', () => { 10 | it('Should return complete style text', () => { 11 | const el = document.getElementById('case-1'); 12 | const style = getStyle(el); 13 | expect(style.color).to.contain('rgb(255, 0, 0)'); 14 | expect(style.marginLeft).to.contain('1px'); 15 | }); 16 | 17 | it('Should return style value of specific property', () => { 18 | const el = document.getElementById('case-1'); 19 | const color = getStyle(el, 'color'); 20 | expect(color).to.contain('rgb(255, 0, 0)'); 21 | }); 22 | }); 23 | 24 | describe('addStyle', () => { 25 | it('Should add a single style property with specific value', () => { 26 | const el = document.getElementById('case-2'); 27 | addStyle(el, 'color', '#ffffff'); 28 | 29 | const style = getStyle(el); 30 | expect(style.color).to.contain('rgb(255, 255, 255)'); 31 | }); 32 | 33 | it('Should add multiple style properties with specific values', () => { 34 | const el = document.getElementById('case-2'); 35 | addStyle(el, { 36 | background: '#ff0000', 37 | 'margin-left': '4px' 38 | }); 39 | 40 | const style = getStyle(el); 41 | expect(style.background).to.contain('rgb(255, 0, 0)'); 42 | expect(style.marginLeft).to.contain('4px'); 43 | }); 44 | }); 45 | 46 | describe('removeStyle', () => { 47 | it('Should remove a single style property', () => { 48 | const el = document.getElementById('case-3'); 49 | removeStyle(el, 'color'); 50 | const style = getStyle(el); 51 | expect(style.color).to.be.empty; 52 | expect(style.background).to.contain('rgb(255, 0, 0)'); 53 | }); 54 | 55 | it('Should remove multiple style properties', () => { 56 | const el = document.getElementById('case-3'); 57 | removeStyle(el, ['margin-left', 'margin-right']); 58 | 59 | const style = getStyle(el); 60 | expect(style.marginLeft).to.be.empty; 61 | expect(style.marginRight).to.be.empty; 62 | expect(style.background).to.contain('rgb(255, 0, 0)'); 63 | }); 64 | }); 65 | 66 | describe('translateDOMPositionXY', () => { 67 | it('Should use translate3d by default', () => { 68 | const style = {}; 69 | translateDOMPositionXY(style, 10, 20); 70 | 71 | expect(style.transform).to.contain('translate3d(10px,20px,0)'); 72 | expect(style.backfaceVisibility).to.contain('hidden'); 73 | }); 74 | 75 | it('Should be disable translate3d', () => { 76 | const translateDOMPositionXY = getTranslateDOMPositionXY({ enable3DTransform: false }); 77 | const style = {}; 78 | translateDOMPositionXY(style, 10, 20); 79 | 80 | expect(style.transform).to.contain('translate(10px,20px)'); 81 | }); 82 | 83 | it('Should be forced to use translate3d', () => { 84 | const translateDOMPositionXY = getTranslateDOMPositionXY({ 85 | forceUseTransform: true, 86 | enable3DTransform: true 87 | }); 88 | const style = {}; 89 | translateDOMPositionXY(style, 10, 20); 90 | 91 | expect(style.transform).to.contain('translate3d(10px,20px,0)'); 92 | expect(style.backfaceVisibility).to.contain('hidden'); 93 | }); 94 | 95 | it('Should be forced to use translate3d', () => { 96 | const translateDOMPositionXY = getTranslateDOMPositionXY({ 97 | forceUseTransform: true, 98 | enable3DTransform: false 99 | }); 100 | const style = {}; 101 | translateDOMPositionXY(style, 10, 20); 102 | 103 | expect(style.transform).to.contain('translate(10px,20px)'); 104 | }); 105 | it('Should be use position', () => { 106 | const translateDOMPositionXY = getTranslateDOMPositionXY({ 107 | enableTransform: false 108 | }); 109 | const style = {}; 110 | translateDOMPositionXY(style, 10, 20); 111 | 112 | expect(style.left).to.contain('10px'); 113 | expect(style.top).to.contain('20px'); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/utilsSpec.js: -------------------------------------------------------------------------------- 1 | import camelizeStyleName from '../src/utils/camelizeStyleName'; 2 | 3 | describe('Utils', () => { 4 | describe('camelizeStyleName', () => { 5 | // https://www.andismith.com/blogs/2012/02/modernizr-prefixed/ 6 | it('Should return the correct Modernizr prefix', () => { 7 | expect(camelizeStyleName('-ms-transform')).to.equal('msTransform'); 8 | expect(camelizeStyleName('-moz-transform')).to.equal('MozTransform'); 9 | expect(camelizeStyleName('-o-transform')).to.equal('OTransform'); 10 | expect(camelizeStyleName('-webkit-transform')).to.equal('WebkitTransform'); 11 | expect(camelizeStyleName('transform')).to.equal('transform'); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "allowJs": true, 5 | "allowSyntheticDefaultImports": true, 6 | "esModuleInterop": true, 7 | "noImplicitAny": false, 8 | "noUnusedParameters": true, 9 | "noUnusedLocals": true, 10 | "sourceMap": true, 11 | "moduleResolution": "node", 12 | "target": "esnext" 13 | }, 14 | "include": ["./src/**/*.ts"] 15 | } 16 | --------------------------------------------------------------------------------