├── .babelrc ├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .markdownlint.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── class-change.esm.js ├── class-change.esm.js.map ├── class-change.esm.min.js ├── class-change.esm.min.js.map ├── class-change.js ├── class-change.js.map ├── class-change.min.js └── class-change.min.js.map ├── docs ├── .nojekyll ├── assets │ └── img │ │ ├── code.svg │ │ ├── github.svg │ │ ├── npm.svg │ │ └── twitter.svg ├── index.html ├── index.md └── sidebar.md ├── karma.conf.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── server.js ├── src ├── add.js ├── attrs.js ├── index.js ├── listener.js ├── remove.js ├── toggle.js └── util.js └── tests ├── add.test.js ├── attrs.test.js ├── listener.test.js ├── remove.test.js └── toggle.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules/**"], 3 | "presets": [ 4 | ["@babel/preset-env", { 5 | "targets": "ie >= 9" 6 | }] 7 | ], 8 | "plugins": [ 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'commonjs': true, 5 | 'es6': true, 6 | 'mocha': true, 7 | 'node': true 8 | }, 9 | 'extends': [ 10 | 'eslint:recommended', 11 | 'plugin:mocha/recommended' 12 | ], 13 | 'ignorePatterns': [ 14 | 'dist' 15 | ], 16 | 'parserOptions': { 17 | 'sourceType': 'module' 18 | }, 19 | 'plugins': [ 20 | 'chai-expect', 21 | 'html', 22 | 'mocha' 23 | ], 24 | 'rules': { 25 | 'array-bracket-spacing' : ['error', 'never'], 26 | 'array-callback-return' : ['error'], 27 | 'block-scoped-var' : ['error'], 28 | 'block-spacing' : ['error', 'always'], 29 | 'curly' : ['error'], 30 | 'dot-notation' : ['error'], 31 | 'eqeqeq' : ['error'], 32 | 'indent' : ['error', 4], 33 | 'no-console' : ['warn'], 34 | 'no-floating-decimal' : ['error'], 35 | 'no-implicit-coercion' : ['error'], 36 | 'no-implicit-globals' : ['error'], 37 | 'no-loop-func' : ['error'], 38 | 'no-return-assign' : ['error'], 39 | 'no-template-curly-in-string': ['error'], 40 | 'no-unneeded-ternary' : ['error'], 41 | 'no-unused-vars' : ['error', { 'args': 'none' }], 42 | 'no-useless-computed-key' : ['error'], 43 | 'no-useless-return' : ['error'], 44 | 'no-var' : ['error'], 45 | 'prefer-const' : ['error'], 46 | 'quotes' : ['error', 'single'], 47 | 'semi' : ['error', 'always'], 48 | 49 | 'mocha/no-hooks-for-single-case': ['off'], 50 | 'mocha/no-top-level-hooks' : ['off'], 51 | 'mocha/no-setup-in-describe' : ['off'] 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jhildenbiddle 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'Build & Test' 2 | 3 | on: [push, pull_request_target] 4 | 5 | jobs: 6 | build: 7 | name: Build (${{ matrix.os }}) 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [macos-latest, ubuntu-latest, windows-latest] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Install 17 | run: npm ci --ignore-scripts 18 | 19 | - name: Lint 20 | run: npm run lint 21 | 22 | - name: Build 23 | run: npm run build 24 | 25 | test: 26 | name: Test 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Install 33 | run: npm ci --ignore-scripts 34 | 35 | - name: Build 36 | run: npm run build 37 | 38 | - name: Setup BrowserStack environment 39 | uses: browserstack/github-actions/setup-env@master 40 | with: 41 | username: ${{ secrets.BROWSERSTACK_USERNAME }} 42 | access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} 43 | 44 | - name: Start BrowserStack tunnel 45 | uses: browserstack/github-actions/setup-local@master 46 | with: 47 | local-testing: start 48 | local-identifier: random 49 | 50 | - name: Run tests on BrowserStack 51 | run: npm run test-remote 52 | 53 | - name: Stop BrowserStack tunnel 54 | uses: browserstack/github-actions/setup-local@master 55 | with: 56 | local-testing: stop 57 | 58 | - name: Report code coverage 59 | uses: codacy/codacy-coverage-reporter-action@v1 60 | with: 61 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 62 | coverage-reports: coverage/lcov.info 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | coverage 3 | node_modules 4 | 5 | # Files 6 | *.log 7 | 8 | # OS 9 | ._* 10 | .cache 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD001": false, 4 | "MD004": { "style": "consistent" }, 5 | "MD013": false, 6 | "MD033": false, 7 | "MD036": false 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Codacy", 4 | "jhildenbiddle", 5 | "myclass" 6 | ] 7 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.1.8 4 | 5 | *2024-02-06* 6 | 7 | - Fix GitHub workflow badge 8 | 9 | ## 1.1.7 10 | 11 | *2022-08-16* 12 | 13 | - Update dependencies 14 | - Update README.md 15 | 16 | ## 1.1.6 17 | 18 | *2019-07-19* 19 | 20 | - Update dependencies 21 | - Switch to `mocha/recommended` ESLint rules 22 | 23 | ## 1.1.5 24 | 25 | *2019-01-08* 26 | 27 | - Update dependencies 28 | - Update unit test configuration (Karma+Travis) 29 | - Update CDN links (switch from unpkg to jsdelivr) 30 | 31 | ## 1.1.4 32 | 33 | *2018-12-07* 34 | 35 | - Update dependencies 36 | - Fix website landscape display on notched devices 37 | - Fix rollup plugin configuration 38 | 39 | ## 1.1.3 40 | 41 | *2018-06-11* 42 | 43 | - Updated description and README 44 | 45 | ## 1.1.1/2 46 | 47 | *2018-06-11* 48 | 49 | - Updated description and documentation 50 | 51 | ## 1.1.0 52 | 53 | *2018-06-09* 54 | 55 | **Added** 56 | 57 | - Added attrs() method, allowing class changes to be triggered using HTML 58 | data attributes. 59 | - Added automated tests 60 | 61 | ## 1.0.3 62 | 63 | *2016-09-18* 64 | 65 | **Fixed** 66 | 67 | - Iterable class list check that prevented classChange from working in IE 68 | 69 | ## 1.0.2 70 | 71 | *2016-08-04* 72 | 73 | **Added** 74 | 75 | - Switched build to webpack 76 | 77 | ## 1.0.1 78 | 79 | *2016-08-03* 80 | 81 | **Added** 82 | 83 | - Minor updated to Bower config, .gitignore and README.md 84 | 85 | ## 1.0.0 86 | 87 | *2016-08-01* 88 | 89 | **Breaking changes from 0.x** 90 | 91 | - The delegate() method has been renamed to listener() 92 | - The delegate() "matches" option has been renamed "match" for listener() 93 | - The delegate() arguments have changed for listener() 94 | 95 | **Added** 96 | 97 | - add(), remove() and toggle() support Functions as "classNames" arg 98 | - listener() supports Functions as option values 99 | - Adding library to Bower 100 | 101 | **Changed** 102 | 103 | - add(), remove() and toggle() now return an element when a single element 104 | is passed as "target" (previously an array with one item was returned) 105 | - listener() "target" option will now create an event listener for all Nodes 106 | specified. This allows for adding event listeners directly to multiple nodes 107 | instead of delegating a single event to a parent node. 108 | - Updated README.md 109 | 110 | **Fixed** 111 | 112 | - Passing "falsey" values as class names to add(), remove(), toggle() and 113 | listen() will no longer throw an error 114 | 115 | ## 0.0.4 116 | 117 | *2016-03-07* 118 | 119 | **Added** 120 | 121 | - New demos 122 | - Library documentation added to README 123 | 124 | **Changed** 125 | 126 | - destroy() method renamed to remove() 127 | 128 | **Fixed** 129 | 130 | - toggle() force feature with IE10/11 131 | - Changing multiple class names with IE10/11 132 | 133 | ## 0.0.3 134 | 135 | *2016-03-06* 136 | 137 | **Added** 138 | 139 | - Demo page 140 | - Specify elements using CSS selector with add/remove/toggle methods 141 | - Handle multiple elements being passed to add/remove/toggle methods 142 | 143 | ## 0.0.2 144 | 145 | *2016-03-05* 146 | 147 | **Changed** 148 | 149 | - Switched to browserify and separate modules for each method 150 | 151 | ## 0.0.1 152 | 153 | *2016-03-05* 154 | 155 | - Initial release 156 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 John Hildenbiddle 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 | # class-change.js 2 | 3 | [![NPM](https://img.shields.io/npm/v/class-change.svg?style=flat-square)](https://www.npmjs.com/package/class-change) 4 | [![GitHub Workflow Status (master)](https://img.shields.io/github/actions/workflow/status/jhildenbiddle/class-change/test.yml?branch=master&label=checks&style=flat-square)](https://github.com/jhildenbiddle/class-change/actions?query=branch%3Amaster+) 5 | [![Codacy code quality](https://img.shields.io/codacy/grade/d656ba140a6e488ab9db2f33183f760e/master?style=flat-square)](https://app.codacy.com/gh/jhildenbiddle/class-change/dashboard?branch=master) 6 | [![Codacy branch coverage](https://img.shields.io/codacy/coverage/d656ba140a6e488ab9db2f33183f760e/master?style=flat-square)](https://app.codacy.com/gh/jhildenbiddle/class-change/dashboard?branch=master) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://github.com/jhildenbiddle/class-change/blob/master/LICENSE) 8 | [![Sponsor this project](https://img.shields.io/static/v1?style=flat-square&label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/jhildenbiddle) 9 | 10 | A versatile [Element.classList](https://developer.mozilla.org/en/DOM/element.classList) alternative for manipulating CSS class names, triggering class change events using HTML data attributes, and creating class-related event listeners using a simple, declarative API. 11 | 12 | - [Documentation](https://jhildenbiddle.github.io/class-change) 13 | - [Codepen Demo](https://codepen.io/jhildenbiddle/pen/wvmYVML) 14 | 15 | ## Why? 16 | 17 | CSS class names change. A lot. 18 | 19 | Native methods for manipulating CSS class names are rudimentary given how often we need them. [Element.classList](https://developer.mozilla.org/en/DOM/element.classList) provides a basic API for working with classes, but changes can only be applied to a single element and separate event listeners must be created for each class change event. Legacy browsers also suffer from [incomplete implementations](http://caniuse.com/#feat=classlist) or lack support entirely. The result is unnecessary code bloat and complexity from repeated loops and boilerplate code, polyfills for legacy browsers, and potential performance issues caused by a high volume of event listeners. This micro-library addresses these issues by reducing and simplifying the code required for handling CSS class changes for modern and legacy browsers. 20 | 21 | ## Features 22 | 23 | - Apply class changes to Arrays, CSS Selectors, HTMLCollections, and NodeLists 24 | - Trigger class changes using HTML data attributes 25 | - Create class change event listeners using a simple, declarative API 26 | - Legacy browser support (IE9+) 27 | - ES and UMD modules available 28 | - Lightweight (1.6k min+gzip) and dependency-free 29 | 30 | ## Usage & Options 31 | 32 | See the [documentation site](https://jhildenbiddle.github.io/class-change/) for details. 33 | 34 | ## Sponsorship 35 | 36 | A [sponsorship](https://github.com/sponsors/jhildenbiddle) is more than just a way to show appreciation for the open-source authors and projects we rely on; it can be the spark that ignites the next big idea, the inspiration to create something new, and the motivation to share so that others may benefit. 37 | 38 | If you benefit from this project, please consider lending your support and encouraging future efforts by [becoming a sponsor](https://github.com/sponsors/jhildenbiddle). 39 | 40 | Thank you! 🙏🏻 41 | 42 | ## Contact & Support 43 | 44 | - Follow 👨🏻‍💻 **@jhildenbiddle** on [Twitter](https://twitter.com/jhildenbiddle) and [GitHub](https://github.com/jhildenbiddle) for announcements 45 | - Create a 💬 [GitHub issue](https://github.com/jhildenbiddle/class-change/issues) for bug reports, feature requests, or questions 46 | - Add a ⭐️ [star on GitHub](https://github.com/jhildenbiddle/class-change) and 🐦 [tweet](https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fjhildenbiddle%2Fclass-change&hashtags=css,developers,frontend,javascript) to promote the project 47 | - Become a 💖 [sponsor](https://github.com/sponsors/jhildenbiddle) to support the project and future efforts 48 | 49 | ## License 50 | 51 | This project is licensed under the MIT License. See the [LICENSE](https://github.com/jhildenbiddle/class-change/blob/master/LICENSE) for details. 52 | 53 | Copyright (c) John Hildenbiddle ([@jhildenbiddle](https://twitter.com/jhildenbiddle)) 54 | -------------------------------------------------------------------------------- /dist/class-change.esm.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * class-change 3 | * v1.1.8 4 | * https://jhildenbiddle.github.io/class-change 5 | * (c) 2016-2024 John Hildenbiddle 6 | * MIT license 7 | */ 8 | function classNamesToArray(classNames) { 9 | if (typeof classNames === "string") { 10 | classNames = classNames.trim().replace(/\s+/g, " ").split(" "); 11 | } 12 | if (Array.isArray(classNames)) { 13 | classNames = classNames.map((function(name) { 14 | return name && name.length ? name.trim() : null; 15 | })); 16 | classNames = classNames.filter(Boolean); 17 | } 18 | return classNames; 19 | } 20 | 21 | function elementsToArray(elements) { 22 | if (typeof elements === "string") { 23 | elements = Array.apply(null, document.querySelectorAll(elements)); 24 | } else if (elements instanceof window.HTMLCollection || elements instanceof window.NodeList) { 25 | elements = Array.apply(null, elements); 26 | } else if (elements && !Array.isArray(elements)) { 27 | elements = [ elements ]; 28 | } 29 | if (Array.isArray(elements)) { 30 | return elements.filter((function(value, index, self) { 31 | return self.indexOf(value) === index; 32 | })); 33 | } else { 34 | return []; 35 | } 36 | } 37 | 38 | function getClosest(elm, matchSelector) { 39 | var matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector; 40 | var matchedElm = null; 41 | var testElm = elm; 42 | while (testElm && testElm !== document) { 43 | if (matches.call(testElm, matchSelector)) { 44 | matchedElm = testElm; 45 | break; 46 | } 47 | testElm = testElm.parentNode; 48 | } 49 | return matchedElm; 50 | } 51 | 52 | function getParents(elm, matchSelector) { 53 | var matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector; 54 | var parentElms = []; 55 | var testElm = elm.parentNode; 56 | while (testElm && testElm !== document) { 57 | if (!matchSelector || matchSelector && matches.call(testElm, matchSelector)) { 58 | parentElms.push(testElm); 59 | } 60 | testElm = testElm.parentNode; 61 | } 62 | return parentElms; 63 | } 64 | 65 | function matchesSelector(elm, selector) { 66 | var matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector; 67 | return matches.call(elm, selector); 68 | } 69 | 70 | function addClass(target, classNames) { 71 | var elms = elementsToArray(target); 72 | elms.forEach((function(elm, i) { 73 | var classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames); 74 | if (classArray && classArray.length) { 75 | var elmClassArray = elm.className.length ? elm.className.split(" ") : []; 76 | var newClassArray = classArray.filter((function(className) { 77 | return elmClassArray.indexOf(className) === -1; 78 | })); 79 | var finalClassArray = elmClassArray.concat(newClassArray); 80 | elm.className = finalClassArray.join(" "); 81 | } 82 | })); 83 | return elms.length === 1 ? elms[0] : elms; 84 | } 85 | 86 | function removeClass(target, classNames) { 87 | var elms = elementsToArray(target); 88 | elms.forEach((function(elm, i) { 89 | var classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames); 90 | if (elm.className.trim().length && classArray && classArray.length) { 91 | var elmClassArray = elm.className.split(" "); 92 | var finalClassArray = elmClassArray.filter((function(className) { 93 | return classArray.indexOf(className) === -1; 94 | })); 95 | if (finalClassArray.length) { 96 | elm.className = finalClassArray.join(" "); 97 | } else { 98 | elm.removeAttribute("class"); 99 | } 100 | } 101 | })); 102 | return elms.length === 1 ? elms[0] : elms; 103 | } 104 | 105 | var classChange$2 = { 106 | add: addClass, 107 | remove: removeClass 108 | }; 109 | 110 | function toggleClass(target, classNames, forceTrueFalse) { 111 | if (forceTrueFalse === true) { 112 | return classChange$2.add(target, classNames); 113 | } else if (forceTrueFalse === false) { 114 | return classChange$2.remove(target, classNames); 115 | } else { 116 | var elms = elementsToArray(target); 117 | elms.forEach((function(elm, i) { 118 | var classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames); 119 | if (classArray && classArray.length) { 120 | var elmClassArray = elm.className.length ? elm.className.split(" ") : []; 121 | var keepClassArray = elmClassArray.filter((function(className) { 122 | return classArray.indexOf(className) === -1; 123 | })); 124 | var newClassArray = classArray.filter((function(className) { 125 | return elmClassArray.indexOf(className) === -1; 126 | })); 127 | var finalClassArray = keepClassArray.concat(newClassArray); 128 | elm.className = finalClassArray.join(" "); 129 | } 130 | })); 131 | return elms.length === 1 ? elms[0] : elms; 132 | } 133 | } 134 | 135 | var classChange$1 = { 136 | add: addClass, 137 | remove: removeClass, 138 | toggle: toggleClass 139 | }; 140 | 141 | function addRemoveAttrListener() { 142 | var listenerTarget = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document; 143 | var addTrueRemoveFalse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; 144 | addTrueRemoveFalse = typeof listenerTarget === "boolean" ? listenerTarget : addTrueRemoveFalse; 145 | listenerTarget = typeof listenerTarget === "boolean" ? document : listenerTarget; 146 | var elms = elementsToArray(listenerTarget); 147 | var method = listenerTarget === false || addTrueRemoveFalse === false ? "removeEventListener" : "addEventListener"; 148 | elms.forEach((function(elm) { 149 | elm[method]("click", handleAttrEvent); 150 | })); 151 | return { 152 | remove: function remove() { 153 | elms.forEach((function(elm) { 154 | elm.removeEventListener("click", handleAttrEvent); 155 | })); 156 | } 157 | }; 158 | } 159 | 160 | function handleAttrEvent(evt) { 161 | var elms = [ evt.target ].concat(getParents(evt.target)); 162 | var matchSelector = "[data-class-add],[data-class-remove],[data-class-toggle]"; 163 | var methods = [ "add", "remove", "toggle" ]; 164 | elms.forEach((function(elm) { 165 | var hasAttr = matchesSelector(elm, matchSelector); 166 | if (hasAttr) { 167 | var changeTasks = {}; 168 | methods.forEach((function(method) { 169 | var classNames = elm.getAttribute("data-class-".concat(method)); 170 | if (classNames && classNames.length) { 171 | var closestAttr = elm.getAttribute("data-class-".concat(method, "-closest")) || elm.getAttribute("data-class-closest"); 172 | var parentsAttr = elm.getAttribute("data-class-".concat(method, "-parents")) || elm.getAttribute("data-class-parents"); 173 | var siblingsAttr = elm.getAttribute("data-class-".concat(method, "-siblings")) || elm.getAttribute("data-class-siblings"); 174 | var targetAttr = elm.getAttribute("data-class-".concat(method, "-target")) || elm.getAttribute("data-class-target"); 175 | var changeElms = []; 176 | if (closestAttr) { 177 | var _elms = getClosest(elm, closestAttr); 178 | changeElms = changeElms.concat(_elms); 179 | } 180 | if (parentsAttr) { 181 | var _elms2 = getParents(elm, parentsAttr); 182 | changeElms = changeElms.concat(_elms2); 183 | } 184 | if (siblingsAttr) { 185 | var siblingElms = elementsToArray(elm.parentNode.children); 186 | siblingElms.forEach((function(siblingElm) { 187 | var isSibling = siblingElm !== elm; 188 | var isMatch = matchesSelector(siblingElm, siblingsAttr); 189 | if (isSibling && isMatch) { 190 | changeElms.push(siblingElm); 191 | } 192 | })); 193 | } 194 | if (targetAttr) { 195 | var _elms3 = elementsToArray(document.querySelectorAll(targetAttr)); 196 | changeElms = changeElms.concat(_elms3); 197 | } 198 | changeTasks[method] = { 199 | target: changeElms.length ? changeElms : elm, 200 | classNames: classNames 201 | }; 202 | } 203 | })); 204 | methods.forEach((function(method) { 205 | if (changeTasks[method]) { 206 | classChange$1[method](changeTasks[method].target, changeTasks[method].classNames); 207 | } 208 | })); 209 | } 210 | })); 211 | } 212 | 213 | function _typeof(obj) { 214 | "@babel/helpers - typeof"; 215 | return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(obj) { 216 | return typeof obj; 217 | } : function(obj) { 218 | return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 219 | }, _typeof(obj); 220 | } 221 | 222 | var classChange = { 223 | add: addClass, 224 | remove: removeClass, 225 | toggle: toggleClass 226 | }; 227 | 228 | function addChangeListener(options) { 229 | var settings = { 230 | target: elementsToArray(options.target || document.body), 231 | event: options.event || "click", 232 | match: options.match || true, 233 | change: options.change || true, 234 | add: options.add || null, 235 | remove: options.remove || null, 236 | toggle: options.toggle || null 237 | }; 238 | function triggerChangeEvent(evt) { 239 | handleChangeEvent(evt, settings); 240 | } 241 | settings.target.forEach((function(target) { 242 | target.addEventListener(settings.event, triggerChangeEvent); 243 | })); 244 | return { 245 | remove: function remove() { 246 | settings.target.forEach((function(target) { 247 | target.removeEventListener(settings.event, triggerChangeEvent); 248 | })); 249 | } 250 | }; 251 | } 252 | 253 | function handleChangeEvent(evt, settings) { 254 | var matchElms = settings.match instanceof Function ? settings.match(evt) : settings.match; 255 | var matchedElm; 256 | if (matchElms === true) { 257 | matchElms = [ evt.target ]; 258 | matchedElm = evt.target; 259 | } else if (typeof matchElms === "string") { 260 | var isMatch = matchesSelector(evt.target, matchElms); 261 | matchedElm = isMatch ? evt.target : getParents(evt.target).filter((function(elm) { 262 | return matchesSelector(elm, matchElms); 263 | }))[0] || null; 264 | matchElms = elementsToArray(matchElms); 265 | } else if (_typeof(matchElms) === "object") { 266 | var _isMatch = evt.target === matchElms; 267 | matchElms = elementsToArray(matchElms); 268 | matchedElm = _isMatch ? evt.target : matchElms[matchElms.indexOf(evt.target)] || getParents(evt.target).filter((function(elm) { 269 | return matchElms.indexOf(elm) !== -1; 270 | }))[0] || null; 271 | } 272 | if (matchedElm) { 273 | var matchedElmIndex = matchElms.indexOf(matchedElm); 274 | var changeElms = settings.change instanceof Function ? settings.change(evt, matchedElm, matchedElmIndex) : settings.change; 275 | changeElms = changeElms === true ? [ evt.target ] : elementsToArray(changeElms); 276 | [ "toggle", "remove", "add" ].forEach((function(changeType) { 277 | if (settings[changeType] instanceof Function) { 278 | changeElms.forEach((function(changeElm, changeElmIndex) { 279 | var classNames = settings[changeType](evt, matchedElm, matchedElmIndex, changeElm, changeElmIndex); 280 | classChange[changeType](changeElm, classNames); 281 | })); 282 | } else { 283 | var classNames = settings[changeType]; 284 | classChange[changeType](changeElms, classNames); 285 | } 286 | })); 287 | } 288 | } 289 | 290 | var index = { 291 | add: addClass, 292 | attrs: addRemoveAttrListener, 293 | listener: addChangeListener, 294 | remove: removeClass, 295 | toggle: toggleClass 296 | }; 297 | 298 | export { index as default }; 299 | //# sourceMappingURL=class-change.esm.js.map 300 | -------------------------------------------------------------------------------- /dist/class-change.esm.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"class-change.esm.js","sources":["../src/util.js","../src/add.js","../src/remove.js","../src/toggle.js","../src/attrs.js","../src/listener.js","../src/index.js"],"sourcesContent":["// Exports\n// =============================================================================\n/**\n * Converts space-separates list of class names to an array (if necessary) then\n * trims each array item.\n *\n * @export\n * @param {(array|string)} classNames\n * @returns {array}\n */\nexport function classNamesToArray(classNames) {\n // String - Trim and convert to Array\n if (typeof classNames === 'string') {\n classNames = classNames.trim().replace(/\\s+/g, ' ').split(' ');\n }\n\n if (Array.isArray(classNames)) {\n // Trim items\n classNames = classNames.map(name => name && name.length ? name.trim() : null);\n\n // Filter out \"falsey\" values\n classNames = classNames.filter(Boolean);\n }\n\n return classNames;\n}\n\n/**\n * Converts a CSS selector (string), Element, HTMLCollection or NodeList to an\n * array (returns array as-is).\n * - Array: [Element, Element, ...]\n * - Element: document.body\n * - HTMLCollection: document.getElementsByTagName('p')\n * - NodeList: document.querySelectorAll('p')\n * - String (CSS selector): 'p'\n *\n * @export\n * @param {(array|element|htmlcollection|nodelist|string)} elements\n * @param {boolean} [removeDuplicates=true]\n * @returns {array}\n */\nexport function elementsToArray(elements) {\n // CSS Selector\n if (typeof elements === 'string') {\n elements = Array.apply(null, document.querySelectorAll(elements));\n }\n // HTMLCollection / NodeList\n else if (elements instanceof window.HTMLCollection || elements instanceof window.NodeList) {\n elements = Array.apply(null, elements);\n }\n // Node/Element (assumed)\n else if (elements && !Array.isArray(elements)) {\n elements = [elements];\n }\n\n if (Array.isArray(elements)) {\n // Remove duplicate\n return elements.filter((value, index, self) => self.indexOf(value) === index);\n }\n else {\n return [];\n }\n}\n\n/**\n * Matches self or finds closest ancestor (excluding document) node that match a\n * CSS selector\n *\n * @export\n * @param {element} elm\n * @param {sting} matchSelector\n * @returns {array}\n */\nexport function getClosest(elm, matchSelector) {\n /* istanbul ignore next */\n const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector;\n\n let matchedElm = null;\n let testElm = elm;\n\n while (testElm && testElm !== document) {\n if (matches.call(testElm, matchSelector)) {\n matchedElm = testElm;\n break;\n }\n\n testElm = testElm.parentNode;\n }\n\n return matchedElm;\n}\n\n/**\n * Finds all parent nodes (excluding document), optionally limited to only those\n * that match a CSS selector\n *\n * @export\n * @param {element} elm\n * @param {sting} matchSelector\n * @returns {array}\n */\nexport function getParents(elm, matchSelector) {\n /* istanbul ignore next */\n const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector;\n const parentElms = [];\n\n let testElm = elm.parentNode;\n\n while (testElm && testElm !== document) {\n if (!matchSelector || matchSelector && matches.call(testElm, matchSelector)) {\n parentElms.push(testElm);\n }\n\n testElm = testElm.parentNode;\n }\n\n return parentElms;\n}\n\n/**\n * Cross-browser wrapper for native \"matches\" method\n *\n * @export\n * @param {element} elm\n * @param {string} selector\n * @returns {boolean}\n */\nexport function matchesSelector(elm, selector) {\n /* istanbul ignore next */\n const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector;\n\n return matches.call(elm, selector);\n}\n","// Modules\n// =============================================================================\nimport { elementsToArray, classNamesToArray } from './util.js';\n\n\n// Functions\n// =============================================================================\n/**\n * Add class name(s) to target element(s)\n *\n * @param {(array|element|htmlcollection|nodelist|string)} target -\n * Element(s) to add class name(s) to\n * @param {(array|function|string)} classNames - Array, space-separated list,\n * or function that returns array/string of class name(s)\n * @returns {(array|element)} - Target(s)\n */\nfunction addClass(target, classNames) {\n const elms = elementsToArray(target);\n\n elms.forEach(function(elm, i) {\n const classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames);\n\n if (classArray && classArray.length) {\n const elmClassArray = elm.className.length ? elm.className.split(' ') : [];\n const newClassArray = classArray.filter(className => elmClassArray.indexOf(className) === -1);\n const finalClassArray = elmClassArray.concat(newClassArray);\n\n elm.className = finalClassArray.join(' ');\n }\n });\n\n return elms.length === 1 ? elms[0] : elms;\n}\n\n\n// Exports\n// =============================================================================\nexport default addClass;\n","// Modules\n// =============================================================================\nimport { elementsToArray, classNamesToArray } from './util.js';\n\n\n// Functions\n// =============================================================================\n/**\n * Remove class name(s) from target element(s)\n *\n * @param {(array|element|htmlcollection|nodelist|string)} target -\n * Element(s) to remove class name(s) from\n * @param {(array|function|string)} classNames - Array, space-separated list,\n * or function that returns array/string of class name(s)\n * @returns {(array|element)} - Target(s)\n */\nfunction removeClass(target, classNames) {\n const elms = elementsToArray(target);\n\n elms.forEach(function(elm, i) {\n const classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames);\n\n if (elm.className.trim().length && classArray && classArray.length) {\n const elmClassArray = elm.className.split(' ');\n const finalClassArray = elmClassArray.filter(className => classArray.indexOf(className) === -1);\n\n // Standardize result of setting empty \"class\" attribute.\n // Internet Explorer and Edge automatically remove the \"class\"\n // attribute when it is set to \"\". Other browsers (Chrome, Firefox,\n // Safari) will set the attribute to \"\". The difference in these\n // behaviors throws off unit tests, so the following code emulates\n // IE/Edge behavior of removing the attribute when the value is \"\".\n if (finalClassArray.length) {\n elm.className = finalClassArray.join(' ');\n }\n else {\n elm.removeAttribute('class');\n }\n }\n });\n\n return elms.length === 1 ? elms[0] : elms;\n}\n\n\n// Exports\n// =============================================================================\nexport default removeClass;\n","// Modules\n// =============================================================================\nimport add from './add.js';\nimport remove from './remove.js';\nimport { elementsToArray, classNamesToArray } from './util.js';\n\n\n// Variables\n// =============================================================================\nconst classChange = { add, remove };\n\n\n// Functions\n// =============================================================================\n/**\n * Toggle class name(s) on target element(s)\n *\n * @param {(array|element|htmlcollection|nodelist|string)} target -\n * Element(s) to toggle class name(s) on\n * @param {(array|function|string)} classNames - Array, space-separated list, or\n * function that returns array/string of class name(s)\n * @param {boolean} [forceTrueFalse] - Force add when true, remove when false\n * @returns {(array|element)} - Target(s)\n */\nfunction toggleClass(target, classNames, forceTrueFalse) {\n if (forceTrueFalse === true) {\n return classChange.add(target, classNames);\n }\n else if (forceTrueFalse === false) {\n return classChange.remove(target, classNames);\n }\n else {\n const elms = elementsToArray(target);\n\n elms.forEach(function(elm, i) {\n const classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames);\n\n if (classArray && classArray.length) {\n const elmClassArray = elm.className.length ? elm.className.split(' ') : [];\n const keepClassArray = elmClassArray.filter(className => classArray.indexOf(className) === -1);\n const newClassArray = classArray.filter(className => elmClassArray.indexOf(className) === -1);\n const finalClassArray = keepClassArray.concat(newClassArray);\n\n elm.className = finalClassArray.join(' ');\n }\n });\n\n return elms.length === 1 ? elms[0] : elms;\n }\n}\n\n\n// Exports\n// =============================================================================\nexport default toggleClass;\n","// Modules\n// =============================================================================\nimport add from './add.js';\nimport remove from './remove.js';\nimport toggle from './toggle.js';\nimport { elementsToArray, matchesSelector, getClosest, getParents } from './util.js';\n\n\n// Variables\n// =============================================================================\nconst classChange = { add, remove, toggle };\n\n\n// Functions\n// =============================================================================\n/**\n * Adds or removes click/tap event listener(s) on elements that have\n * data-class-* attributes and trigger associated method(s).\n *\n * @param {(array|element|htmlcollection|nodelist|string)} [listenerTarget=document]\n * @param {boolean} [addTrueRemoveFalse=true]\n */\nfunction addRemoveAttrListener(listenerTarget = document, addTrueRemoveFalse = true) {\n // Allow boolean for listenerTarget\n // true = add default listener, false = remove default listener\n addTrueRemoveFalse = typeof(listenerTarget) === 'boolean' ? listenerTarget : addTrueRemoveFalse;\n listenerTarget = typeof(listenerTarget) === 'boolean' ? document : listenerTarget;\n\n const elms = elementsToArray(listenerTarget);\n const method = listenerTarget === false || addTrueRemoveFalse === false ? 'removeEventListener' : 'addEventListener';\n\n elms.forEach(function(elm) {\n elm[method]('click', handleAttrEvent);\n });\n\n // Return object containing remove method\n return {\n remove() {\n elms.forEach(function(elm) {\n elm.removeEventListener('click', handleAttrEvent);\n });\n }\n };\n}\n\n/**\n * Handles click/tap events triggered via data-class-* attributes.\n *\n * @param {object} evt\n */\nfunction handleAttrEvent(evt) {\n const elms = [evt.target].concat(getParents(evt.target));\n const matchSelector = '[data-class-add],[data-class-remove],[data-class-toggle]';\n const methods = ['add', 'remove', 'toggle'];\n\n elms.forEach(function(elm) {\n const hasAttr = matchesSelector(elm, matchSelector);\n\n if (hasAttr) {\n const changeTasks = {};\n\n methods.forEach(function(method) {\n const classNames = elm.getAttribute(`data-class-${method}`);\n\n if (classNames && classNames.length) {\n const closestAttr = elm.getAttribute(`data-class-${method}-closest`) || elm.getAttribute('data-class-closest');\n const parentsAttr = elm.getAttribute(`data-class-${method}-parents`) || elm.getAttribute('data-class-parents');\n const siblingsAttr = elm.getAttribute(`data-class-${method}-siblings`) || elm.getAttribute('data-class-siblings');\n const targetAttr = elm.getAttribute(`data-class-${method}-target`) || elm.getAttribute('data-class-target');\n\n let changeElms = [];\n\n if (closestAttr) {\n const elms = getClosest(elm, closestAttr);\n changeElms = changeElms.concat(elms);\n }\n if (parentsAttr) {\n const elms = getParents(elm, parentsAttr);\n changeElms = changeElms.concat(elms);\n }\n if (siblingsAttr) {\n const siblingElms = elementsToArray(elm.parentNode.children);\n\n siblingElms.forEach(function(siblingElm) {\n const isSibling = siblingElm !== elm;\n const isMatch = matchesSelector(siblingElm, siblingsAttr);\n\n if (isSibling && isMatch) {\n changeElms.push(siblingElm);\n }\n });\n }\n if (targetAttr) {\n const elms = elementsToArray(document.querySelectorAll(targetAttr));\n changeElms = changeElms.concat(elms);\n }\n\n changeTasks[method] = {\n target: changeElms.length ? changeElms : elm,\n classNames\n };\n }\n });\n\n methods.forEach(function(method) {\n if (changeTasks[method]) {\n classChange[method](changeTasks[method].target, changeTasks[method].classNames);\n }\n });\n }\n });\n}\n\n\n// Exports\n// =============================================================================\nexport default addRemoveAttrListener;\n","// Modules\n// =============================================================================\nimport add from './add.js';\nimport remove from './remove.js';\nimport toggle from './toggle.js';\nimport * as util from './util.js';\n\n\n// Variables\n// =============================================================================\nconst classChange = { add, remove, toggle };\n\n\n// Functions\n// =============================================================================\n/**\n * Adds classChange event listener(s) and returns a remove() method\n *\n * @param {object} options\n * @param {(array|element|htmlcollection|nodelist|string)} [options.target=document]\n * @param {string} [options.event=\"click\"]\n * @param {(array|boolean|element|function|htmlcollection|nodelist|string)} [options.match=true]\n * @param {(array|boolean|element|function|htmlcollection|nodelist|string)} [options.change=true]\n * @param {(array|function|string)} [options.add]\n * @param {(array|function|string)} [options.remove]\n * @param {(array|function|string)} [options.toggle]\n * @returns {object} Remove method\n */\nfunction addChangeListener(options) {\n const settings = {\n target: util.elementsToArray(options.target || document.body),\n event : options.event || 'click',\n match : options.match || true,\n change: options.change || true,\n add : options.add || null,\n remove: options.remove || null,\n toggle: options.toggle || null\n };\n\n function triggerChangeEvent(evt) {\n handleChangeEvent(evt, settings);\n }\n\n settings.target.forEach(function(target) {\n target.addEventListener(settings.event, triggerChangeEvent);\n });\n\n // Return object containing remove method\n return {\n remove() {\n settings.target.forEach(function(target) {\n target.removeEventListener(settings.event, triggerChangeEvent);\n });\n }\n };\n}\n\n/**\n * Detects if an event matches the one defined in settings and changes class\n * names on elements accordingly\n *\n * @param {object} evt - Event object\n * @param {object} settings - Listener settings\n */\nfunction handleChangeEvent(evt, settings) {\n let matchElms = settings.match instanceof Function ? settings.match(evt) : settings.match;\n let matchedElm;\n\n // Match: Event target\n /* istanbul ignore else */\n if (matchElms === true) {\n matchElms = [evt.target];\n matchedElm = evt.target;\n }\n // Match: CSS selector\n else if (typeof matchElms === 'string') {\n const isMatch = util.matchesSelector(evt.target, matchElms);\n\n matchedElm = isMatch ? evt.target : util.getParents(evt.target).filter(elm => util.matchesSelector(elm, matchElms))[0] || null;\n matchElms = util.elementsToArray(matchElms);\n }\n // Match: Array, Element, HTMLCollection, NodeList\n else if (typeof matchElms === 'object') {\n const isMatch = evt.target === matchElms;\n\n matchElms = util.elementsToArray(matchElms);\n matchedElm = isMatch ? evt.target : matchElms[matchElms.indexOf(evt.target)] || util.getParents(evt.target).filter(elm => matchElms.indexOf(elm) !== -1)[0] || null;\n }\n\n // Change\n if (matchedElm) {\n const matchedElmIndex = matchElms.indexOf(matchedElm);\n let changeElms = settings.change instanceof Function ? settings.change(evt, matchedElm, matchedElmIndex) : settings.change;\n\n changeElms = changeElms === true ? [evt.target] : util.elementsToArray(changeElms);\n\n ['toggle', 'remove', 'add'].forEach(changeType => {\n // If settings value is a function, call for each element with args\n if (settings[changeType] instanceof Function) {\n changeElms.forEach((changeElm, changeElmIndex) => {\n const classNames = settings[changeType](evt, matchedElm, matchedElmIndex, changeElm, changeElmIndex);\n\n classChange[changeType](changeElm, classNames);\n });\n }\n else {\n const classNames = settings[changeType];\n\n classChange[changeType](changeElms, classNames);\n }\n });\n }\n}\n\n\n// Exports\n// =============================================================================\nexport default addChangeListener;\n","// Modules\n// =============================================================================\nimport add from './add.js';\nimport attrs from './attrs.js';\nimport listener from './listener.js';\nimport remove from './remove.js';\nimport toggle from './toggle.js';\n\n\n// Exports\n// =============================================================================\nexport default { add, attrs, listener, remove, toggle };\n"],"names":["classNamesToArray","classNames","trim","replace","split","Array","isArray","map","name","length","filter","Boolean","elementsToArray","elements","apply","document","querySelectorAll","window","HTMLCollection","NodeList","value","index","self","indexOf","getClosest","elm","matchSelector","matches","matchesSelector","webkitMatchesSelector","mozMatchesSelector","msMatchesSelector","oMatchesSelector","matchedElm","testElm","call","parentNode","getParents","parentElms","push","selector","addClass","target","elms","forEach","i","classArray","Function","elmClassArray","className","newClassArray","finalClassArray","concat","join","removeClass","removeAttribute","classChange","add","remove","toggleClass","forceTrueFalse","keepClassArray","toggle","addRemoveAttrListener","listenerTarget","addTrueRemoveFalse","method","handleAttrEvent","removeEventListener","evt","methods","hasAttr","changeTasks","getAttribute","closestAttr","parentsAttr","siblingsAttr","targetAttr","changeElms","siblingElms","children","siblingElm","isSibling","isMatch","addChangeListener","options","settings","util","body","event","match","change","triggerChangeEvent","handleChangeEvent","addEventListener","matchElms","_typeof","matchedElmIndex","changeType","changeElm","changeElmIndex","attrs","listener"],"mappings":";;;;;;;AAUO,SAASA,kBAAkBC;IAE9B,WAAWA,eAAe,UAAU;QAChCA,aAAaA,WAAWC,OAAOC,QAAQ,QAAQ,KAAKC,MAAM;AAC7D;IAED,IAAIC,MAAMC,QAAQL,aAAa;QAE3BA,aAAaA,WAAWM,KAAI,SAAAC;YAAI,OAAIA,QAAQA,KAAKC,SAASD,KAAKN,SAAS;;QAGxED,aAAaA,WAAWS,OAAOC;AAClC;IAED,OAAOV;AACV;;AAgBM,SAASW,gBAAgBC;IAE5B,WAAWA,aAAa,UAAU;QAC9BA,WAAWR,MAAMS,MAAM,MAAMC,SAASC,iBAAiBH;AAD3D,WAIK,IAAIA,oBAAoBI,OAAOC,kBAAkBL,oBAAoBI,OAAOE,UAAU;QACvFN,WAAWR,MAAMS,MAAM,MAAMD;AAD5B,WAIA,IAAIA,aAAaR,MAAMC,QAAQO,WAAW;QAC3CA,WAAW,EAACA;AACf;IAED,IAAIR,MAAMC,QAAQO,WAAW;QAEzB,OAAOA,SAASH,QAAO,SAACU,OAAOC,OAAOC;YAAf,OAAwBA,KAAKC,QAAQH,WAAWC;AAAhD;AAC1B,WACI;QACD,OAAO;AACV;AACJ;;AAWM,SAASG,WAAWC,KAAKC;IAE5B,IAAMC,UAAUF,IAAIE,WAAWF,IAAIG,mBAAmBH,IAAII,yBAAyBJ,IAAIK,sBAAsBL,IAAIM,qBAAqBN,IAAIO;IAE1I,IAAIC,aAAa;IACjB,IAAIC,UAAaT;IAEjB,OAAOS,WAAWA,YAAYnB,UAAU;QACpC,IAAIY,QAAQQ,KAAKD,SAASR,gBAAgB;YACtCO,aAAaC;YACb;AACH;QAEDA,UAAUA,QAAQE;AACrB;IAED,OAAOH;AACV;;AAWM,SAASI,WAAWZ,KAAKC;IAE5B,IAAMC,UAAaF,IAAIE,WAAWF,IAAIG,mBAAmBH,IAAII,yBAAyBJ,IAAIK,sBAAsBL,IAAIM,qBAAqBN,IAAIO;IAC7I,IAAMM,aAAa;IAEnB,IAAIJ,UAAUT,IAAIW;IAElB,OAAOF,WAAWA,YAAYnB,UAAU;QACpC,KAAKW,iBAAiBA,iBAAiBC,QAAQQ,KAAKD,SAASR,gBAAgB;YACzEY,WAAWC,KAAKL;AACnB;QAEDA,UAAUA,QAAQE;AACrB;IAED,OAAOE;AACV;;AAUM,SAASV,gBAAgBH,KAAKe;IAEjC,IAAMb,UAAUF,IAAIE,WAAWF,IAAIG,mBAAmBH,IAAII,yBAAyBJ,IAAIK,sBAAsBL,IAAIM,qBAAqBN,IAAIO;IAE1I,OAAOL,QAAQQ,KAAKV,KAAKe;AAC5B;;ACpHD,SAASC,SAASC,QAAQzC;IACtB,IAAM0C,OAAO/B,gBAAgB8B;IAE7BC,KAAKC,SAAQ,SAASnB,KAAKoB;QACvB,IAAMC,aAAa9C,kBAAkBC,sBAAsB8C,WAAW9C,WAAWwB,KAAKoB,KAAK5C;QAE3F,IAAI6C,cAAcA,WAAWrC,QAAQ;YACjC,IAAMuC,gBAAkBvB,IAAIwB,UAAUxC,SAASgB,IAAIwB,UAAU7C,MAAM,OAAO;YAC1E,IAAM8C,gBAAkBJ,WAAWpC,QAAO,SAAAuC;gBAAS,OAAID,cAAczB,QAAQ0B,gBAAgB;AAA1C;YACnD,IAAME,kBAAkBH,cAAcI,OAAOF;YAE7CzB,IAAIwB,YAAYE,gBAAgBE,KAAK;AACxC;;IAGL,OAAOV,KAAKlC,WAAW,IAAIkC,KAAK,KAAKA;AACxC;;AChBD,SAASW,YAAYZ,QAAQzC;IACzB,IAAM0C,OAAO/B,gBAAgB8B;IAE7BC,KAAKC,SAAQ,SAASnB,KAAKoB;QACvB,IAAMC,aAAa9C,kBAAkBC,sBAAsB8C,WAAW9C,WAAWwB,KAAKoB,KAAK5C;QAE3F,IAAIwB,IAAIwB,UAAU/C,OAAOO,UAAUqC,cAAcA,WAAWrC,QAAQ;YAChE,IAAMuC,gBAAkBvB,IAAIwB,UAAU7C,MAAM;YAC5C,IAAM+C,kBAAkBH,cAActC,QAAO,SAAAuC;gBAAS,OAAIH,WAAWvB,QAAQ0B,gBAAgB;;YAQ7F,IAAIE,gBAAgB1C,QAAQ;gBACxBgB,IAAIwB,YAAYE,gBAAgBE,KAAK;AACxC,mBACI;gBACD5B,IAAI8B,gBAAgB;AACvB;AACJ;;IAGL,OAAOZ,KAAKlC,WAAW,IAAIkC,KAAK,KAAKA;AACxC;;ACjCD,IAAMa,gBAAc;IAAEC,KAAAA;IAAKC,QAAAA;;;AAe3B,SAASC,YAAYjB,QAAQzC,YAAY2D;IACrC,IAAIA,mBAAmB,MAAM;QACzB,OAAOJ,cAAYC,IAAIf,QAAQzC;AAClC,WACI,IAAI2D,mBAAmB,OAAO;QAC/B,OAAOJ,cAAYE,OAAOhB,QAAQzC;AACrC,WACI;QACD,IAAM0C,OAAO/B,gBAAgB8B;QAE7BC,KAAKC,SAAQ,SAASnB,KAAKoB;YACvB,IAAMC,aAAa9C,kBAAkBC,sBAAsB8C,WAAW9C,WAAWwB,KAAKoB,KAAK5C;YAE3F,IAAI6C,cAAcA,WAAWrC,QAAQ;gBACjC,IAAMuC,gBAAkBvB,IAAIwB,UAAUxC,SAASgB,IAAIwB,UAAU7C,MAAM,OAAO;gBAC1E,IAAMyD,iBAAkBb,cAActC,QAAO,SAAAuC;oBAAS,OAAIH,WAAWvB,QAAQ0B,gBAAgB;AAAvC;gBACtD,IAAMC,gBAAkBJ,WAAWpC,QAAO,SAAAuC;oBAAS,OAAID,cAAczB,QAAQ0B,gBAAgB;AAA1C;gBACnD,IAAME,kBAAkBU,eAAeT,OAAOF;gBAE9CzB,IAAIwB,YAAYE,gBAAgBE,KAAK;AACxC;;QAGL,OAAOV,KAAKlC,WAAW,IAAIkC,KAAK,KAAKA;AACxC;AACJ;;ACvCD,IAAMa,gBAAc;IAAEC,KAAAA;IAAKC,QAAAA;IAAQI,QAAAA;;;AAYnC,SAASC;IAA4E,IAAtDC,qFAAiBjD;IAAqC,IAA3BkD,yFAAqB;IAG3EA,4BAA4BD,mBAAoB,YAAYA,iBAAiBC;IAC7ED,wBAAwBA,mBAAoB,YAAYjD,WAAWiD;IAEnE,IAAMrB,OAAS/B,gBAAgBoD;IAC/B,IAAME,SAASF,mBAAmB,SAASC,uBAAuB,QAAQ,wBAAwB;IAElGtB,KAAKC,SAAQ,SAASnB;QAClBA,IAAIyC,QAAQ,SAASC;AACxB;IAGD,OAAO;QACHT,QAAS,SAAAA;YACLf,KAAKC,SAAQ,SAASnB;gBAClBA,IAAI2C,oBAAoB,SAASD;;AAExC;;AAER;;AAOD,SAASA,gBAAgBE;IACrB,IAAM1B,OAAgB,EAAC0B,IAAI3B,SAAQU,OAAOf,WAAWgC,IAAI3B;IACzD,IAAMhB,gBAAgB;IACtB,IAAM4C,UAAgB,EAAC,OAAO,UAAU;IAExC3B,KAAKC,SAAQ,SAASnB;QAClB,IAAM8C,UAAU3C,gBAAgBH,KAAKC;QAErC,IAAI6C,SAAS;YACT,IAAMC,cAAc,CAAA;YAEpBF,QAAQ1B,SAAQ,SAASsB;gBACrB,IAAMjE,aAAawB,IAAIgD,aAAJ,cAAArB,OAA+Bc;gBAElD,IAAIjE,cAAcA,WAAWQ,QAAQ;oBACjC,IAAMiE,cAAejD,IAAIgD,aAAJ,cAAArB,OAA+Bc,QAA/B,gBAAoDzC,IAAIgD,aAAa;oBAC1F,IAAME,cAAelD,IAAIgD,aAAJ,cAAArB,OAA+Bc,QAA/B,gBAAoDzC,IAAIgD,aAAa;oBAC1F,IAAMG,eAAenD,IAAIgD,aAAJ,cAAArB,OAA+Bc,QAA/B,iBAAqDzC,IAAIgD,aAAa;oBAC3F,IAAMI,aAAepD,IAAIgD,aAAJ,cAAArB,OAA+Bc,QAA/B,eAAmDzC,IAAIgD,aAAa;oBAEzF,IAAIK,aAAa;oBAEjB,IAAIJ,aAAa;wBACb,IAAM/B,QAAOnB,WAAWC,KAAKiD;wBAC7BI,aAAaA,WAAW1B,OAAOT;AAClC;oBACD,IAAIgC,aAAa;wBACb,IAAMhC,SAAON,WAAWZ,KAAKkD;wBAC7BG,aAAaA,WAAW1B,OAAOT;AAClC;oBACD,IAAIiC,cAAc;wBACd,IAAMG,cAAcnE,gBAAgBa,IAAIW,WAAW4C;wBAEnDD,YAAYnC,SAAQ,SAASqC;4BACzB,IAAMC,YAAYD,eAAexD;4BACjC,IAAM0D,UAAYvD,gBAAgBqD,YAAYL;4BAE9C,IAAIM,aAAaC,SAAS;gCACtBL,WAAWvC,KAAK0C;AACnB;;AAER;oBACD,IAAIJ,YAAY;wBACZ,IAAMlC,SAAO/B,gBAAgBG,SAASC,iBAAiB6D;wBACvDC,aAAaA,WAAW1B,OAAOT;AAClC;oBAED6B,YAAYN,UAAU;wBAClBxB,QAAQoC,WAAWrE,SAASqE,aAAarD;wBACzCxB,YAAAA;;AAEP;;YAGLqE,QAAQ1B,SAAQ,SAASsB;gBACrB,IAAIM,YAAYN,SAAS;oBACrBV,cAAYU,QAAQM,YAAYN,QAAQxB,QAAQ8B,YAAYN,QAAQjE;AACvE;;AAER;;AAER;;;;;;;;;;;ACrGD,IAAMuD,cAAc;IAAEC,KAAAA;IAAKC,QAAAA;IAAQI,QAAAA;;;AAkBnC,SAASsB,kBAAkBC;IACvB,IAAMC,WAAW;QACb5C,QAAQ6C,gBAAqBF,QAAQ3C,UAAU3B,SAASyE;QACxDC,OAAQJ,QAAQI,SAAU;QAC1BC,OAAQL,QAAQK,SAAU;QAC1BC,QAAQN,QAAQM,UAAU;QAC1BlC,KAAQ4B,QAAQ5B,OAAU;QAC1BC,QAAQ2B,QAAQ3B,UAAU;QAC1BI,QAAQuB,QAAQvB,UAAU;;IAG9B,SAAS8B,mBAAmBvB;QACxBwB,kBAAkBxB,KAAKiB;AAC1B;IAEDA,SAAS5C,OAAOE,SAAQ,SAASF;QAC7BA,OAAOoD,iBAAiBR,SAASG,OAAOG;AAC3C;IAGD,OAAO;QACHlC,QAAS,SAAAA;YACL4B,SAAS5C,OAAOE,SAAQ,SAASF;gBAC7BA,OAAO0B,oBAAoBkB,SAASG,OAAOG;;AAElD;;AAER;;AASD,SAASC,kBAAkBxB,KAAKiB;IAC5B,IAAIS,YAAYT,SAASI,iBAAiB3C,WAAWuC,SAASI,MAAMrB,OAAOiB,SAASI;IACpF,IAAIzD;IAIJ,IAAI8D,cAAc,MAAM;QACpBA,YAAa,EAAC1B,IAAI3B;QAClBT,aAAaoC,IAAI3B;AAFrB,WAKK,WAAWqD,cAAc,UAAU;QACpC,IAAMZ,UAAUI,gBAAqBlB,IAAI3B,QAAQqD;QAEjD9D,aAAakD,UAAUd,IAAI3B,SAAS6C,WAAgBlB,IAAI3B,QAAQhC,QAAO,SAAAe;YAAG,OAAI8D,gBAAqB9D,KAAKsE;YAAY,MAAM;QAC1HA,YAAaR,gBAAqBQ;AAJjC,WAOA,IAAIC,QAAOD,eAAc,UAAU;QACpC,IAAMZ,WAAUd,IAAI3B,WAAWqD;QAE/BA,YAAaR,gBAAqBQ;QAClC9D,aAAakD,WAAUd,IAAI3B,SAASqD,UAAUA,UAAUxE,QAAQ8C,IAAI3B,YAAY6C,WAAgBlB,IAAI3B,QAAQhC,QAAO,SAAAe;YAAG,OAAIsE,UAAUxE,QAAQE,UAAU;AAAhC,YAAmC,MAAM;AAtB7H;IA0BtC,IAAIQ,YAAY;QACZ,IAAMgE,kBAAkBF,UAAUxE,QAAQU;QAC1C,IAAM6C,aAAkBQ,SAASK,kBAAkB5C,WAAWuC,SAASK,OAAOtB,KAAKpC,YAAYgE,mBAAmBX,SAASK;QAE3Hb,aAAaA,eAAe,OAAO,EAACT,IAAI3B,WAAU6C,gBAAqBT;QAEvE,EAAC,UAAU,UAAU,QAAOlC,SAAQ,SAAAsD;YAEhC,IAAIZ,SAASY,uBAAuBnD,UAAU;gBAC1C+B,WAAWlC,SAAQ,SAACuD,WAAWC;oBAC3B,IAAMnG,aAAaqF,SAASY,YAAY7B,KAAKpC,YAAYgE,iBAAiBE,WAAWC;oBAErF5C,YAAY0C,YAAYC,WAAWlG;;AAE1C,mBACI;gBACD,IAAMA,aAAaqF,SAASY;gBAE5B1C,YAAY0C,YAAYpB,YAAY7E;AACvC;;AAER;AACJ;;ACrGD,IAAeoB,QAAA;IAAEoC,KAAAA;IAAK4C,OAAAA;IAAOC,UAAAA;IAAU5C,QAAAA;IAAQI,QAAAA;;;"} -------------------------------------------------------------------------------- /dist/class-change.esm.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * class-change 3 | * v1.1.8 4 | * https://jhildenbiddle.github.io/class-change 5 | * (c) 2016-2024 John Hildenbiddle 6 | * MIT license 7 | */ 8 | function t(t){return"string"==typeof t&&(t=t.trim().replace(/\s+/g," ").split(" ")),Array.isArray(t)&&(t=(t=t.map((function(t){return t&&t.length?t.trim():null}))).filter(Boolean)),t}function e(t){return"string"==typeof t?t=Array.apply(null,document.querySelectorAll(t)):t instanceof window.HTMLCollection||t instanceof window.NodeList?t=Array.apply(null,t):t&&!Array.isArray(t)&&(t=[t]),Array.isArray(t)?t.filter((function(t,e,n){return n.indexOf(t)===e})):[]}function n(t,e){for(var n=t.matches||t.matchesSelector||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector||t.oMatchesSelector,a=[],r=t.parentNode;r&&r!==document;)(!e||e&&n.call(r,e))&&a.push(r),r=r.parentNode;return a}function a(t,e){return(t.matches||t.matchesSelector||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector||t.oMatchesSelector).call(t,e)}function r(n,a){var r=e(n);return r.forEach((function(e,n){var r=t(a instanceof Function?a(e,n):a);if(r&&r.length){var c=e.className.length?e.className.split(" "):[],o=r.filter((function(t){return-1===c.indexOf(t)})),i=c.concat(o);e.className=i.join(" ")}})),1===r.length?r[0]:r}function c(n,a){var r=e(n);return r.forEach((function(e,n){var r=t(a instanceof Function?a(e,n):a);if(e.className.trim().length&&r&&r.length){var c=e.className.split(" ").filter((function(t){return-1===r.indexOf(t)}));c.length?e.className=c.join(" "):e.removeAttribute("class")}})),1===r.length?r[0]:r}var o={add:r,remove:c};function i(n,a,r){if(!0===r)return o.add(n,a);if(!1===r)return o.remove(n,a);var c=e(n);return c.forEach((function(e,n){var r=t(a instanceof Function?a(e,n):a);if(r&&r.length){var c=e.className.length?e.className.split(" "):[],o=c.filter((function(t){return-1===r.indexOf(t)})),i=r.filter((function(t){return-1===c.indexOf(t)})),l=o.concat(i);e.className=l.join(" ")}})),1===c.length?c[0]:c}var l={add:r,remove:c,toggle:i};function s(t){var r=[t.target].concat(n(t.target)),c=["add","remove","toggle"];r.forEach((function(t){if(a(t,"[data-class-add],[data-class-remove],[data-class-toggle]")){var r={};c.forEach((function(c){var o=t.getAttribute("data-class-".concat(c));if(o&&o.length){var i=t.getAttribute("data-class-".concat(c,"-closest"))||t.getAttribute("data-class-closest"),l=t.getAttribute("data-class-".concat(c,"-parents"))||t.getAttribute("data-class-parents"),s=t.getAttribute("data-class-".concat(c,"-siblings"))||t.getAttribute("data-class-siblings"),f=t.getAttribute("data-class-".concat(c,"-target"))||t.getAttribute("data-class-target"),u=[];if(i){var g=function(t,e){for(var n=t.matches||t.matchesSelector||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector||t.oMatchesSelector,a=null,r=t;r&&r!==document;){if(n.call(r,e)){a=r;break}r=r.parentNode}return a}(t,i);u=u.concat(g)}if(l){var d=n(t,l);u=u.concat(d)}if(s)e(t.parentNode.children).forEach((function(e){var n=e!==t,r=a(e,s);n&&r&&u.push(e)}));if(f){var m=e(document.querySelectorAll(f));u=u.concat(m)}r[c]={target:u.length?u:t,classNames:o}}})),c.forEach((function(t){r[t]&&l[t](r[t].target,r[t].classNames)}))}}))}function f(t){return f="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},f(t)}var u={add:r,remove:c,toggle:i};var g={add:r,attrs:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:document,n=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];n="boolean"==typeof t?t:n;var a=e(t="boolean"==typeof t?document:t),r=!1===t||!1===n?"removeEventListener":"addEventListener";return a.forEach((function(t){t[r]("click",s)})),{remove:function(){a.forEach((function(t){t.removeEventListener("click",s)}))}}},listener:function(t){var r={target:e(t.target||document.body),event:t.event||"click",match:t.match||!0,change:t.change||!0,add:t.add||null,remove:t.remove||null,toggle:t.toggle||null};function c(t){!function(t,r){var c,o=r.match instanceof Function?r.match(t):r.match;if(!0===o)o=[t.target],c=t.target;else if("string"==typeof o){var i=a(t.target,o);c=i?t.target:n(t.target).filter((function(t){return a(t,o)}))[0]||null,o=e(o)}else if("object"===f(o)){var l=t.target===o;o=e(o),c=l?t.target:o[o.indexOf(t.target)]||n(t.target).filter((function(t){return-1!==o.indexOf(t)}))[0]||null}if(c){var s=o.indexOf(c),g=r.change instanceof Function?r.change(t,c,s):r.change;g=!0===g?[t.target]:e(g),["toggle","remove","add"].forEach((function(e){if(r[e]instanceof Function)g.forEach((function(n,a){var o=r[e](t,c,s,n,a);u[e](n,o)}));else{var n=r[e];u[e](g,n)}}))}}(t,r)}return r.target.forEach((function(t){t.addEventListener(r.event,c)})),{remove:function(){r.target.forEach((function(t){t.removeEventListener(r.event,c)}))}}},remove:c,toggle:i};export{g as default}; 9 | //# sourceMappingURL=class-change.esm.min.js.map 10 | -------------------------------------------------------------------------------- /dist/class-change.esm.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"class-change.esm.min.js","sources":["../src/util.js","../src/add.js","../src/remove.js","../src/toggle.js","../src/attrs.js","../src/listener.js","../src/index.js"],"sourcesContent":["// Exports\n// =============================================================================\n/**\n * Converts space-separates list of class names to an array (if necessary) then\n * trims each array item.\n *\n * @export\n * @param {(array|string)} classNames\n * @returns {array}\n */\nexport function classNamesToArray(classNames) {\n // String - Trim and convert to Array\n if (typeof classNames === 'string') {\n classNames = classNames.trim().replace(/\\s+/g, ' ').split(' ');\n }\n\n if (Array.isArray(classNames)) {\n // Trim items\n classNames = classNames.map(name => name && name.length ? name.trim() : null);\n\n // Filter out \"falsey\" values\n classNames = classNames.filter(Boolean);\n }\n\n return classNames;\n}\n\n/**\n * Converts a CSS selector (string), Element, HTMLCollection or NodeList to an\n * array (returns array as-is).\n * - Array: [Element, Element, ...]\n * - Element: document.body\n * - HTMLCollection: document.getElementsByTagName('p')\n * - NodeList: document.querySelectorAll('p')\n * - String (CSS selector): 'p'\n *\n * @export\n * @param {(array|element|htmlcollection|nodelist|string)} elements\n * @param {boolean} [removeDuplicates=true]\n * @returns {array}\n */\nexport function elementsToArray(elements) {\n // CSS Selector\n if (typeof elements === 'string') {\n elements = Array.apply(null, document.querySelectorAll(elements));\n }\n // HTMLCollection / NodeList\n else if (elements instanceof window.HTMLCollection || elements instanceof window.NodeList) {\n elements = Array.apply(null, elements);\n }\n // Node/Element (assumed)\n else if (elements && !Array.isArray(elements)) {\n elements = [elements];\n }\n\n if (Array.isArray(elements)) {\n // Remove duplicate\n return elements.filter((value, index, self) => self.indexOf(value) === index);\n }\n else {\n return [];\n }\n}\n\n/**\n * Matches self or finds closest ancestor (excluding document) node that match a\n * CSS selector\n *\n * @export\n * @param {element} elm\n * @param {sting} matchSelector\n * @returns {array}\n */\nexport function getClosest(elm, matchSelector) {\n /* istanbul ignore next */\n const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector;\n\n let matchedElm = null;\n let testElm = elm;\n\n while (testElm && testElm !== document) {\n if (matches.call(testElm, matchSelector)) {\n matchedElm = testElm;\n break;\n }\n\n testElm = testElm.parentNode;\n }\n\n return matchedElm;\n}\n\n/**\n * Finds all parent nodes (excluding document), optionally limited to only those\n * that match a CSS selector\n *\n * @export\n * @param {element} elm\n * @param {sting} matchSelector\n * @returns {array}\n */\nexport function getParents(elm, matchSelector) {\n /* istanbul ignore next */\n const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector;\n const parentElms = [];\n\n let testElm = elm.parentNode;\n\n while (testElm && testElm !== document) {\n if (!matchSelector || matchSelector && matches.call(testElm, matchSelector)) {\n parentElms.push(testElm);\n }\n\n testElm = testElm.parentNode;\n }\n\n return parentElms;\n}\n\n/**\n * Cross-browser wrapper for native \"matches\" method\n *\n * @export\n * @param {element} elm\n * @param {string} selector\n * @returns {boolean}\n */\nexport function matchesSelector(elm, selector) {\n /* istanbul ignore next */\n const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector;\n\n return matches.call(elm, selector);\n}\n","// Modules\n// =============================================================================\nimport { elementsToArray, classNamesToArray } from './util.js';\n\n\n// Functions\n// =============================================================================\n/**\n * Add class name(s) to target element(s)\n *\n * @param {(array|element|htmlcollection|nodelist|string)} target -\n * Element(s) to add class name(s) to\n * @param {(array|function|string)} classNames - Array, space-separated list,\n * or function that returns array/string of class name(s)\n * @returns {(array|element)} - Target(s)\n */\nfunction addClass(target, classNames) {\n const elms = elementsToArray(target);\n\n elms.forEach(function(elm, i) {\n const classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames);\n\n if (classArray && classArray.length) {\n const elmClassArray = elm.className.length ? elm.className.split(' ') : [];\n const newClassArray = classArray.filter(className => elmClassArray.indexOf(className) === -1);\n const finalClassArray = elmClassArray.concat(newClassArray);\n\n elm.className = finalClassArray.join(' ');\n }\n });\n\n return elms.length === 1 ? elms[0] : elms;\n}\n\n\n// Exports\n// =============================================================================\nexport default addClass;\n","// Modules\n// =============================================================================\nimport { elementsToArray, classNamesToArray } from './util.js';\n\n\n// Functions\n// =============================================================================\n/**\n * Remove class name(s) from target element(s)\n *\n * @param {(array|element|htmlcollection|nodelist|string)} target -\n * Element(s) to remove class name(s) from\n * @param {(array|function|string)} classNames - Array, space-separated list,\n * or function that returns array/string of class name(s)\n * @returns {(array|element)} - Target(s)\n */\nfunction removeClass(target, classNames) {\n const elms = elementsToArray(target);\n\n elms.forEach(function(elm, i) {\n const classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames);\n\n if (elm.className.trim().length && classArray && classArray.length) {\n const elmClassArray = elm.className.split(' ');\n const finalClassArray = elmClassArray.filter(className => classArray.indexOf(className) === -1);\n\n // Standardize result of setting empty \"class\" attribute.\n // Internet Explorer and Edge automatically remove the \"class\"\n // attribute when it is set to \"\". Other browsers (Chrome, Firefox,\n // Safari) will set the attribute to \"\". The difference in these\n // behaviors throws off unit tests, so the following code emulates\n // IE/Edge behavior of removing the attribute when the value is \"\".\n if (finalClassArray.length) {\n elm.className = finalClassArray.join(' ');\n }\n else {\n elm.removeAttribute('class');\n }\n }\n });\n\n return elms.length === 1 ? elms[0] : elms;\n}\n\n\n// Exports\n// =============================================================================\nexport default removeClass;\n","// Modules\n// =============================================================================\nimport add from './add.js';\nimport remove from './remove.js';\nimport { elementsToArray, classNamesToArray } from './util.js';\n\n\n// Variables\n// =============================================================================\nconst classChange = { add, remove };\n\n\n// Functions\n// =============================================================================\n/**\n * Toggle class name(s) on target element(s)\n *\n * @param {(array|element|htmlcollection|nodelist|string)} target -\n * Element(s) to toggle class name(s) on\n * @param {(array|function|string)} classNames - Array, space-separated list, or\n * function that returns array/string of class name(s)\n * @param {boolean} [forceTrueFalse] - Force add when true, remove when false\n * @returns {(array|element)} - Target(s)\n */\nfunction toggleClass(target, classNames, forceTrueFalse) {\n if (forceTrueFalse === true) {\n return classChange.add(target, classNames);\n }\n else if (forceTrueFalse === false) {\n return classChange.remove(target, classNames);\n }\n else {\n const elms = elementsToArray(target);\n\n elms.forEach(function(elm, i) {\n const classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames);\n\n if (classArray && classArray.length) {\n const elmClassArray = elm.className.length ? elm.className.split(' ') : [];\n const keepClassArray = elmClassArray.filter(className => classArray.indexOf(className) === -1);\n const newClassArray = classArray.filter(className => elmClassArray.indexOf(className) === -1);\n const finalClassArray = keepClassArray.concat(newClassArray);\n\n elm.className = finalClassArray.join(' ');\n }\n });\n\n return elms.length === 1 ? elms[0] : elms;\n }\n}\n\n\n// Exports\n// =============================================================================\nexport default toggleClass;\n","// Modules\n// =============================================================================\nimport add from './add.js';\nimport remove from './remove.js';\nimport toggle from './toggle.js';\nimport { elementsToArray, matchesSelector, getClosest, getParents } from './util.js';\n\n\n// Variables\n// =============================================================================\nconst classChange = { add, remove, toggle };\n\n\n// Functions\n// =============================================================================\n/**\n * Adds or removes click/tap event listener(s) on elements that have\n * data-class-* attributes and trigger associated method(s).\n *\n * @param {(array|element|htmlcollection|nodelist|string)} [listenerTarget=document]\n * @param {boolean} [addTrueRemoveFalse=true]\n */\nfunction addRemoveAttrListener(listenerTarget = document, addTrueRemoveFalse = true) {\n // Allow boolean for listenerTarget\n // true = add default listener, false = remove default listener\n addTrueRemoveFalse = typeof(listenerTarget) === 'boolean' ? listenerTarget : addTrueRemoveFalse;\n listenerTarget = typeof(listenerTarget) === 'boolean' ? document : listenerTarget;\n\n const elms = elementsToArray(listenerTarget);\n const method = listenerTarget === false || addTrueRemoveFalse === false ? 'removeEventListener' : 'addEventListener';\n\n elms.forEach(function(elm) {\n elm[method]('click', handleAttrEvent);\n });\n\n // Return object containing remove method\n return {\n remove() {\n elms.forEach(function(elm) {\n elm.removeEventListener('click', handleAttrEvent);\n });\n }\n };\n}\n\n/**\n * Handles click/tap events triggered via data-class-* attributes.\n *\n * @param {object} evt\n */\nfunction handleAttrEvent(evt) {\n const elms = [evt.target].concat(getParents(evt.target));\n const matchSelector = '[data-class-add],[data-class-remove],[data-class-toggle]';\n const methods = ['add', 'remove', 'toggle'];\n\n elms.forEach(function(elm) {\n const hasAttr = matchesSelector(elm, matchSelector);\n\n if (hasAttr) {\n const changeTasks = {};\n\n methods.forEach(function(method) {\n const classNames = elm.getAttribute(`data-class-${method}`);\n\n if (classNames && classNames.length) {\n const closestAttr = elm.getAttribute(`data-class-${method}-closest`) || elm.getAttribute('data-class-closest');\n const parentsAttr = elm.getAttribute(`data-class-${method}-parents`) || elm.getAttribute('data-class-parents');\n const siblingsAttr = elm.getAttribute(`data-class-${method}-siblings`) || elm.getAttribute('data-class-siblings');\n const targetAttr = elm.getAttribute(`data-class-${method}-target`) || elm.getAttribute('data-class-target');\n\n let changeElms = [];\n\n if (closestAttr) {\n const elms = getClosest(elm, closestAttr);\n changeElms = changeElms.concat(elms);\n }\n if (parentsAttr) {\n const elms = getParents(elm, parentsAttr);\n changeElms = changeElms.concat(elms);\n }\n if (siblingsAttr) {\n const siblingElms = elementsToArray(elm.parentNode.children);\n\n siblingElms.forEach(function(siblingElm) {\n const isSibling = siblingElm !== elm;\n const isMatch = matchesSelector(siblingElm, siblingsAttr);\n\n if (isSibling && isMatch) {\n changeElms.push(siblingElm);\n }\n });\n }\n if (targetAttr) {\n const elms = elementsToArray(document.querySelectorAll(targetAttr));\n changeElms = changeElms.concat(elms);\n }\n\n changeTasks[method] = {\n target: changeElms.length ? changeElms : elm,\n classNames\n };\n }\n });\n\n methods.forEach(function(method) {\n if (changeTasks[method]) {\n classChange[method](changeTasks[method].target, changeTasks[method].classNames);\n }\n });\n }\n });\n}\n\n\n// Exports\n// =============================================================================\nexport default addRemoveAttrListener;\n","// Modules\n// =============================================================================\nimport add from './add.js';\nimport remove from './remove.js';\nimport toggle from './toggle.js';\nimport * as util from './util.js';\n\n\n// Variables\n// =============================================================================\nconst classChange = { add, remove, toggle };\n\n\n// Functions\n// =============================================================================\n/**\n * Adds classChange event listener(s) and returns a remove() method\n *\n * @param {object} options\n * @param {(array|element|htmlcollection|nodelist|string)} [options.target=document]\n * @param {string} [options.event=\"click\"]\n * @param {(array|boolean|element|function|htmlcollection|nodelist|string)} [options.match=true]\n * @param {(array|boolean|element|function|htmlcollection|nodelist|string)} [options.change=true]\n * @param {(array|function|string)} [options.add]\n * @param {(array|function|string)} [options.remove]\n * @param {(array|function|string)} [options.toggle]\n * @returns {object} Remove method\n */\nfunction addChangeListener(options) {\n const settings = {\n target: util.elementsToArray(options.target || document.body),\n event : options.event || 'click',\n match : options.match || true,\n change: options.change || true,\n add : options.add || null,\n remove: options.remove || null,\n toggle: options.toggle || null\n };\n\n function triggerChangeEvent(evt) {\n handleChangeEvent(evt, settings);\n }\n\n settings.target.forEach(function(target) {\n target.addEventListener(settings.event, triggerChangeEvent);\n });\n\n // Return object containing remove method\n return {\n remove() {\n settings.target.forEach(function(target) {\n target.removeEventListener(settings.event, triggerChangeEvent);\n });\n }\n };\n}\n\n/**\n * Detects if an event matches the one defined in settings and changes class\n * names on elements accordingly\n *\n * @param {object} evt - Event object\n * @param {object} settings - Listener settings\n */\nfunction handleChangeEvent(evt, settings) {\n let matchElms = settings.match instanceof Function ? settings.match(evt) : settings.match;\n let matchedElm;\n\n // Match: Event target\n /* istanbul ignore else */\n if (matchElms === true) {\n matchElms = [evt.target];\n matchedElm = evt.target;\n }\n // Match: CSS selector\n else if (typeof matchElms === 'string') {\n const isMatch = util.matchesSelector(evt.target, matchElms);\n\n matchedElm = isMatch ? evt.target : util.getParents(evt.target).filter(elm => util.matchesSelector(elm, matchElms))[0] || null;\n matchElms = util.elementsToArray(matchElms);\n }\n // Match: Array, Element, HTMLCollection, NodeList\n else if (typeof matchElms === 'object') {\n const isMatch = evt.target === matchElms;\n\n matchElms = util.elementsToArray(matchElms);\n matchedElm = isMatch ? evt.target : matchElms[matchElms.indexOf(evt.target)] || util.getParents(evt.target).filter(elm => matchElms.indexOf(elm) !== -1)[0] || null;\n }\n\n // Change\n if (matchedElm) {\n const matchedElmIndex = matchElms.indexOf(matchedElm);\n let changeElms = settings.change instanceof Function ? settings.change(evt, matchedElm, matchedElmIndex) : settings.change;\n\n changeElms = changeElms === true ? [evt.target] : util.elementsToArray(changeElms);\n\n ['toggle', 'remove', 'add'].forEach(changeType => {\n // If settings value is a function, call for each element with args\n if (settings[changeType] instanceof Function) {\n changeElms.forEach((changeElm, changeElmIndex) => {\n const classNames = settings[changeType](evt, matchedElm, matchedElmIndex, changeElm, changeElmIndex);\n\n classChange[changeType](changeElm, classNames);\n });\n }\n else {\n const classNames = settings[changeType];\n\n classChange[changeType](changeElms, classNames);\n }\n });\n }\n}\n\n\n// Exports\n// =============================================================================\nexport default addChangeListener;\n","// Modules\n// =============================================================================\nimport add from './add.js';\nimport attrs from './attrs.js';\nimport listener from './listener.js';\nimport remove from './remove.js';\nimport toggle from './toggle.js';\n\n\n// Exports\n// =============================================================================\nexport default { add, attrs, listener, remove, toggle };\n"],"names":["classNamesToArray","classNames","trim","replace","split","Array","isArray","map","name","length","filter","Boolean","elementsToArray","elements","apply","document","querySelectorAll","window","HTMLCollection","NodeList","value","index","self","indexOf","getParents","elm","matchSelector","matches","matchesSelector","webkitMatchesSelector","mozMatchesSelector","msMatchesSelector","oMatchesSelector","parentElms","testElm","parentNode","call","push","selector","addClass","target","elms","forEach","i","classArray","Function","elmClassArray","className","newClassArray","finalClassArray","concat","join","removeClass","removeAttribute","classChange","add","remove","toggleClass","forceTrueFalse","keepClassArray","toggle","handleAttrEvent","evt","methods","changeTasks","method","getAttribute","closestAttr","parentsAttr","siblingsAttr","targetAttr","changeElms","matchedElm","getClosest","children","siblingElm","isSibling","isMatch","attrs","listenerTarget","addTrueRemoveFalse","removeEventListener","listener","options","settings","util","body","event","match","change","triggerChangeEvent","matchElms","_typeof","matchedElmIndex","changeType","changeElm","changeElmIndex","handleChangeEvent","addEventListener"],"mappings":"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"} -------------------------------------------------------------------------------- /dist/class-change.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * class-change 3 | * v1.1.8 4 | * https://jhildenbiddle.github.io/class-change 5 | * (c) 2016-2024 John Hildenbiddle 6 | * MIT license 7 | */ 8 | (function(global, factory) { 9 | typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, 10 | global.classChange = factory()); 11 | })(this, (function() { 12 | "use strict"; 13 | function classNamesToArray(classNames) { 14 | if (typeof classNames === "string") { 15 | classNames = classNames.trim().replace(/\s+/g, " ").split(" "); 16 | } 17 | if (Array.isArray(classNames)) { 18 | classNames = classNames.map((function(name) { 19 | return name && name.length ? name.trim() : null; 20 | })); 21 | classNames = classNames.filter(Boolean); 22 | } 23 | return classNames; 24 | } 25 | function elementsToArray(elements) { 26 | if (typeof elements === "string") { 27 | elements = Array.apply(null, document.querySelectorAll(elements)); 28 | } else if (elements instanceof window.HTMLCollection || elements instanceof window.NodeList) { 29 | elements = Array.apply(null, elements); 30 | } else if (elements && !Array.isArray(elements)) { 31 | elements = [ elements ]; 32 | } 33 | if (Array.isArray(elements)) { 34 | return elements.filter((function(value, index, self) { 35 | return self.indexOf(value) === index; 36 | })); 37 | } else { 38 | return []; 39 | } 40 | } 41 | function getClosest(elm, matchSelector) { 42 | var matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector; 43 | var matchedElm = null; 44 | var testElm = elm; 45 | while (testElm && testElm !== document) { 46 | if (matches.call(testElm, matchSelector)) { 47 | matchedElm = testElm; 48 | break; 49 | } 50 | testElm = testElm.parentNode; 51 | } 52 | return matchedElm; 53 | } 54 | function getParents(elm, matchSelector) { 55 | var matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector; 56 | var parentElms = []; 57 | var testElm = elm.parentNode; 58 | while (testElm && testElm !== document) { 59 | if (!matchSelector || matchSelector && matches.call(testElm, matchSelector)) { 60 | parentElms.push(testElm); 61 | } 62 | testElm = testElm.parentNode; 63 | } 64 | return parentElms; 65 | } 66 | function matchesSelector(elm, selector) { 67 | var matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector; 68 | return matches.call(elm, selector); 69 | } 70 | function addClass(target, classNames) { 71 | var elms = elementsToArray(target); 72 | elms.forEach((function(elm, i) { 73 | var classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames); 74 | if (classArray && classArray.length) { 75 | var elmClassArray = elm.className.length ? elm.className.split(" ") : []; 76 | var newClassArray = classArray.filter((function(className) { 77 | return elmClassArray.indexOf(className) === -1; 78 | })); 79 | var finalClassArray = elmClassArray.concat(newClassArray); 80 | elm.className = finalClassArray.join(" "); 81 | } 82 | })); 83 | return elms.length === 1 ? elms[0] : elms; 84 | } 85 | function removeClass(target, classNames) { 86 | var elms = elementsToArray(target); 87 | elms.forEach((function(elm, i) { 88 | var classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames); 89 | if (elm.className.trim().length && classArray && classArray.length) { 90 | var elmClassArray = elm.className.split(" "); 91 | var finalClassArray = elmClassArray.filter((function(className) { 92 | return classArray.indexOf(className) === -1; 93 | })); 94 | if (finalClassArray.length) { 95 | elm.className = finalClassArray.join(" "); 96 | } else { 97 | elm.removeAttribute("class"); 98 | } 99 | } 100 | })); 101 | return elms.length === 1 ? elms[0] : elms; 102 | } 103 | var classChange$2 = { 104 | add: addClass, 105 | remove: removeClass 106 | }; 107 | function toggleClass(target, classNames, forceTrueFalse) { 108 | if (forceTrueFalse === true) { 109 | return classChange$2.add(target, classNames); 110 | } else if (forceTrueFalse === false) { 111 | return classChange$2.remove(target, classNames); 112 | } else { 113 | var elms = elementsToArray(target); 114 | elms.forEach((function(elm, i) { 115 | var classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames); 116 | if (classArray && classArray.length) { 117 | var elmClassArray = elm.className.length ? elm.className.split(" ") : []; 118 | var keepClassArray = elmClassArray.filter((function(className) { 119 | return classArray.indexOf(className) === -1; 120 | })); 121 | var newClassArray = classArray.filter((function(className) { 122 | return elmClassArray.indexOf(className) === -1; 123 | })); 124 | var finalClassArray = keepClassArray.concat(newClassArray); 125 | elm.className = finalClassArray.join(" "); 126 | } 127 | })); 128 | return elms.length === 1 ? elms[0] : elms; 129 | } 130 | } 131 | var classChange$1 = { 132 | add: addClass, 133 | remove: removeClass, 134 | toggle: toggleClass 135 | }; 136 | function addRemoveAttrListener() { 137 | var listenerTarget = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document; 138 | var addTrueRemoveFalse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; 139 | addTrueRemoveFalse = typeof listenerTarget === "boolean" ? listenerTarget : addTrueRemoveFalse; 140 | listenerTarget = typeof listenerTarget === "boolean" ? document : listenerTarget; 141 | var elms = elementsToArray(listenerTarget); 142 | var method = listenerTarget === false || addTrueRemoveFalse === false ? "removeEventListener" : "addEventListener"; 143 | elms.forEach((function(elm) { 144 | elm[method]("click", handleAttrEvent); 145 | })); 146 | return { 147 | remove: function remove() { 148 | elms.forEach((function(elm) { 149 | elm.removeEventListener("click", handleAttrEvent); 150 | })); 151 | } 152 | }; 153 | } 154 | function handleAttrEvent(evt) { 155 | var elms = [ evt.target ].concat(getParents(evt.target)); 156 | var matchSelector = "[data-class-add],[data-class-remove],[data-class-toggle]"; 157 | var methods = [ "add", "remove", "toggle" ]; 158 | elms.forEach((function(elm) { 159 | var hasAttr = matchesSelector(elm, matchSelector); 160 | if (hasAttr) { 161 | var changeTasks = {}; 162 | methods.forEach((function(method) { 163 | var classNames = elm.getAttribute("data-class-".concat(method)); 164 | if (classNames && classNames.length) { 165 | var closestAttr = elm.getAttribute("data-class-".concat(method, "-closest")) || elm.getAttribute("data-class-closest"); 166 | var parentsAttr = elm.getAttribute("data-class-".concat(method, "-parents")) || elm.getAttribute("data-class-parents"); 167 | var siblingsAttr = elm.getAttribute("data-class-".concat(method, "-siblings")) || elm.getAttribute("data-class-siblings"); 168 | var targetAttr = elm.getAttribute("data-class-".concat(method, "-target")) || elm.getAttribute("data-class-target"); 169 | var changeElms = []; 170 | if (closestAttr) { 171 | var _elms = getClosest(elm, closestAttr); 172 | changeElms = changeElms.concat(_elms); 173 | } 174 | if (parentsAttr) { 175 | var _elms2 = getParents(elm, parentsAttr); 176 | changeElms = changeElms.concat(_elms2); 177 | } 178 | if (siblingsAttr) { 179 | var siblingElms = elementsToArray(elm.parentNode.children); 180 | siblingElms.forEach((function(siblingElm) { 181 | var isSibling = siblingElm !== elm; 182 | var isMatch = matchesSelector(siblingElm, siblingsAttr); 183 | if (isSibling && isMatch) { 184 | changeElms.push(siblingElm); 185 | } 186 | })); 187 | } 188 | if (targetAttr) { 189 | var _elms3 = elementsToArray(document.querySelectorAll(targetAttr)); 190 | changeElms = changeElms.concat(_elms3); 191 | } 192 | changeTasks[method] = { 193 | target: changeElms.length ? changeElms : elm, 194 | classNames: classNames 195 | }; 196 | } 197 | })); 198 | methods.forEach((function(method) { 199 | if (changeTasks[method]) { 200 | classChange$1[method](changeTasks[method].target, changeTasks[method].classNames); 201 | } 202 | })); 203 | } 204 | })); 205 | } 206 | function _typeof(obj) { 207 | "@babel/helpers - typeof"; 208 | return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(obj) { 209 | return typeof obj; 210 | } : function(obj) { 211 | return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 212 | }, _typeof(obj); 213 | } 214 | var classChange = { 215 | add: addClass, 216 | remove: removeClass, 217 | toggle: toggleClass 218 | }; 219 | function addChangeListener(options) { 220 | var settings = { 221 | target: elementsToArray(options.target || document.body), 222 | event: options.event || "click", 223 | match: options.match || true, 224 | change: options.change || true, 225 | add: options.add || null, 226 | remove: options.remove || null, 227 | toggle: options.toggle || null 228 | }; 229 | function triggerChangeEvent(evt) { 230 | handleChangeEvent(evt, settings); 231 | } 232 | settings.target.forEach((function(target) { 233 | target.addEventListener(settings.event, triggerChangeEvent); 234 | })); 235 | return { 236 | remove: function remove() { 237 | settings.target.forEach((function(target) { 238 | target.removeEventListener(settings.event, triggerChangeEvent); 239 | })); 240 | } 241 | }; 242 | } 243 | function handleChangeEvent(evt, settings) { 244 | var matchElms = settings.match instanceof Function ? settings.match(evt) : settings.match; 245 | var matchedElm; 246 | if (matchElms === true) { 247 | matchElms = [ evt.target ]; 248 | matchedElm = evt.target; 249 | } else if (typeof matchElms === "string") { 250 | var isMatch = matchesSelector(evt.target, matchElms); 251 | matchedElm = isMatch ? evt.target : getParents(evt.target).filter((function(elm) { 252 | return matchesSelector(elm, matchElms); 253 | }))[0] || null; 254 | matchElms = elementsToArray(matchElms); 255 | } else if (_typeof(matchElms) === "object") { 256 | var _isMatch = evt.target === matchElms; 257 | matchElms = elementsToArray(matchElms); 258 | matchedElm = _isMatch ? evt.target : matchElms[matchElms.indexOf(evt.target)] || getParents(evt.target).filter((function(elm) { 259 | return matchElms.indexOf(elm) !== -1; 260 | }))[0] || null; 261 | } 262 | if (matchedElm) { 263 | var matchedElmIndex = matchElms.indexOf(matchedElm); 264 | var changeElms = settings.change instanceof Function ? settings.change(evt, matchedElm, matchedElmIndex) : settings.change; 265 | changeElms = changeElms === true ? [ evt.target ] : elementsToArray(changeElms); 266 | [ "toggle", "remove", "add" ].forEach((function(changeType) { 267 | if (settings[changeType] instanceof Function) { 268 | changeElms.forEach((function(changeElm, changeElmIndex) { 269 | var classNames = settings[changeType](evt, matchedElm, matchedElmIndex, changeElm, changeElmIndex); 270 | classChange[changeType](changeElm, classNames); 271 | })); 272 | } else { 273 | var classNames = settings[changeType]; 274 | classChange[changeType](changeElms, classNames); 275 | } 276 | })); 277 | } 278 | } 279 | var index = { 280 | add: addClass, 281 | attrs: addRemoveAttrListener, 282 | listener: addChangeListener, 283 | remove: removeClass, 284 | toggle: toggleClass 285 | }; 286 | return index; 287 | })); 288 | //# sourceMappingURL=class-change.js.map 289 | -------------------------------------------------------------------------------- /dist/class-change.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * class-change 3 | * v1.1.8 4 | * https://jhildenbiddle.github.io/class-change 5 | * (c) 2016-2024 John Hildenbiddle 6 | * MIT license 7 | */ 8 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).classChange=e()}(this,(function(){"use strict";function t(t){return"string"==typeof t&&(t=t.trim().replace(/\s+/g," ").split(" ")),Array.isArray(t)&&(t=(t=t.map((function(t){return t&&t.length?t.trim():null}))).filter(Boolean)),t}function e(t){return"string"==typeof t?t=Array.apply(null,document.querySelectorAll(t)):t instanceof window.HTMLCollection||t instanceof window.NodeList?t=Array.apply(null,t):t&&!Array.isArray(t)&&(t=[t]),Array.isArray(t)?t.filter((function(t,e,n){return n.indexOf(t)===e})):[]}function n(t,e){for(var n=t.matches||t.matchesSelector||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector||t.oMatchesSelector,a=[],r=t.parentNode;r&&r!==document;)(!e||e&&n.call(r,e))&&a.push(r),r=r.parentNode;return a}function a(t,e){return(t.matches||t.matchesSelector||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector||t.oMatchesSelector).call(t,e)}function r(n,a){var r=e(n);return r.forEach((function(e,n){var r=t(a instanceof Function?a(e,n):a);if(r&&r.length){var o=e.className.length?e.className.split(" "):[],c=r.filter((function(t){return-1===o.indexOf(t)})),i=o.concat(c);e.className=i.join(" ")}})),1===r.length?r[0]:r}function o(n,a){var r=e(n);return r.forEach((function(e,n){var r=t(a instanceof Function?a(e,n):a);if(e.className.trim().length&&r&&r.length){var o=e.className.split(" ").filter((function(t){return-1===r.indexOf(t)}));o.length?e.className=o.join(" "):e.removeAttribute("class")}})),1===r.length?r[0]:r}var c={add:r,remove:o};function i(n,a,r){if(!0===r)return c.add(n,a);if(!1===r)return c.remove(n,a);var o=e(n);return o.forEach((function(e,n){var r=t(a instanceof Function?a(e,n):a);if(r&&r.length){var o=e.className.length?e.className.split(" "):[],c=o.filter((function(t){return-1===r.indexOf(t)})),i=r.filter((function(t){return-1===o.indexOf(t)})),l=c.concat(i);e.className=l.join(" ")}})),1===o.length?o[0]:o}var l={add:r,remove:o,toggle:i};function s(t){var r=[t.target].concat(n(t.target)),o=["add","remove","toggle"];r.forEach((function(t){if(a(t,"[data-class-add],[data-class-remove],[data-class-toggle]")){var r={};o.forEach((function(o){var c=t.getAttribute("data-class-".concat(o));if(c&&c.length){var i=t.getAttribute("data-class-".concat(o,"-closest"))||t.getAttribute("data-class-closest"),l=t.getAttribute("data-class-".concat(o,"-parents"))||t.getAttribute("data-class-parents"),s=t.getAttribute("data-class-".concat(o,"-siblings"))||t.getAttribute("data-class-siblings"),f=t.getAttribute("data-class-".concat(o,"-target"))||t.getAttribute("data-class-target"),u=[];if(i){var g=function(t,e){for(var n=t.matches||t.matchesSelector||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector||t.oMatchesSelector,a=null,r=t;r&&r!==document;){if(n.call(r,e)){a=r;break}r=r.parentNode}return a}(t,i);u=u.concat(g)}if(l){var d=n(t,l);u=u.concat(d)}if(s)e(t.parentNode.children).forEach((function(e){var n=e!==t,r=a(e,s);n&&r&&u.push(e)}));if(f){var m=e(document.querySelectorAll(f));u=u.concat(m)}r[o]={target:u.length?u:t,classNames:c}}})),o.forEach((function(t){r[t]&&l[t](r[t].target,r[t].classNames)}))}}))}function f(t){return f="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},f(t)}var u={add:r,remove:o,toggle:i};var g={add:r,attrs:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:document,n=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];n="boolean"==typeof t?t:n;var a=e(t="boolean"==typeof t?document:t),r=!1===t||!1===n?"removeEventListener":"addEventListener";return a.forEach((function(t){t[r]("click",s)})),{remove:function(){a.forEach((function(t){t.removeEventListener("click",s)}))}}},listener:function(t){var r={target:e(t.target||document.body),event:t.event||"click",match:t.match||!0,change:t.change||!0,add:t.add||null,remove:t.remove||null,toggle:t.toggle||null};function o(t){!function(t,r){var o,c=r.match instanceof Function?r.match(t):r.match;if(!0===c)c=[t.target],o=t.target;else if("string"==typeof c){var i=a(t.target,c);o=i?t.target:n(t.target).filter((function(t){return a(t,c)}))[0]||null,c=e(c)}else if("object"===f(c)){var l=t.target===c;c=e(c),o=l?t.target:c[c.indexOf(t.target)]||n(t.target).filter((function(t){return-1!==c.indexOf(t)}))[0]||null}if(o){var s=c.indexOf(o),g=r.change instanceof Function?r.change(t,o,s):r.change;g=!0===g?[t.target]:e(g),["toggle","remove","add"].forEach((function(e){if(r[e]instanceof Function)g.forEach((function(n,a){var c=r[e](t,o,s,n,a);u[e](n,c)}));else{var n=r[e];u[e](g,n)}}))}}(t,r)}return r.target.forEach((function(t){t.addEventListener(r.event,o)})),{remove:function(){r.target.forEach((function(t){t.removeEventListener(r.event,o)}))}}},remove:o,toggle:i};return g})); 9 | //# sourceMappingURL=class-change.min.js.map 10 | -------------------------------------------------------------------------------- /dist/class-change.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"class-change.min.js","sources":["../src/util.js","../src/add.js","../src/remove.js","../src/toggle.js","../src/attrs.js","../src/listener.js","../src/index.js"],"sourcesContent":["// Exports\n// =============================================================================\n/**\n * Converts space-separates list of class names to an array (if necessary) then\n * trims each array item.\n *\n * @export\n * @param {(array|string)} classNames\n * @returns {array}\n */\nexport function classNamesToArray(classNames) {\n // String - Trim and convert to Array\n if (typeof classNames === 'string') {\n classNames = classNames.trim().replace(/\\s+/g, ' ').split(' ');\n }\n\n if (Array.isArray(classNames)) {\n // Trim items\n classNames = classNames.map(name => name && name.length ? name.trim() : null);\n\n // Filter out \"falsey\" values\n classNames = classNames.filter(Boolean);\n }\n\n return classNames;\n}\n\n/**\n * Converts a CSS selector (string), Element, HTMLCollection or NodeList to an\n * array (returns array as-is).\n * - Array: [Element, Element, ...]\n * - Element: document.body\n * - HTMLCollection: document.getElementsByTagName('p')\n * - NodeList: document.querySelectorAll('p')\n * - String (CSS selector): 'p'\n *\n * @export\n * @param {(array|element|htmlcollection|nodelist|string)} elements\n * @param {boolean} [removeDuplicates=true]\n * @returns {array}\n */\nexport function elementsToArray(elements) {\n // CSS Selector\n if (typeof elements === 'string') {\n elements = Array.apply(null, document.querySelectorAll(elements));\n }\n // HTMLCollection / NodeList\n else if (elements instanceof window.HTMLCollection || elements instanceof window.NodeList) {\n elements = Array.apply(null, elements);\n }\n // Node/Element (assumed)\n else if (elements && !Array.isArray(elements)) {\n elements = [elements];\n }\n\n if (Array.isArray(elements)) {\n // Remove duplicate\n return elements.filter((value, index, self) => self.indexOf(value) === index);\n }\n else {\n return [];\n }\n}\n\n/**\n * Matches self or finds closest ancestor (excluding document) node that match a\n * CSS selector\n *\n * @export\n * @param {element} elm\n * @param {sting} matchSelector\n * @returns {array}\n */\nexport function getClosest(elm, matchSelector) {\n /* istanbul ignore next */\n const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector;\n\n let matchedElm = null;\n let testElm = elm;\n\n while (testElm && testElm !== document) {\n if (matches.call(testElm, matchSelector)) {\n matchedElm = testElm;\n break;\n }\n\n testElm = testElm.parentNode;\n }\n\n return matchedElm;\n}\n\n/**\n * Finds all parent nodes (excluding document), optionally limited to only those\n * that match a CSS selector\n *\n * @export\n * @param {element} elm\n * @param {sting} matchSelector\n * @returns {array}\n */\nexport function getParents(elm, matchSelector) {\n /* istanbul ignore next */\n const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector;\n const parentElms = [];\n\n let testElm = elm.parentNode;\n\n while (testElm && testElm !== document) {\n if (!matchSelector || matchSelector && matches.call(testElm, matchSelector)) {\n parentElms.push(testElm);\n }\n\n testElm = testElm.parentNode;\n }\n\n return parentElms;\n}\n\n/**\n * Cross-browser wrapper for native \"matches\" method\n *\n * @export\n * @param {element} elm\n * @param {string} selector\n * @returns {boolean}\n */\nexport function matchesSelector(elm, selector) {\n /* istanbul ignore next */\n const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector;\n\n return matches.call(elm, selector);\n}\n","// Modules\n// =============================================================================\nimport { elementsToArray, classNamesToArray } from './util.js';\n\n\n// Functions\n// =============================================================================\n/**\n * Add class name(s) to target element(s)\n *\n * @param {(array|element|htmlcollection|nodelist|string)} target -\n * Element(s) to add class name(s) to\n * @param {(array|function|string)} classNames - Array, space-separated list,\n * or function that returns array/string of class name(s)\n * @returns {(array|element)} - Target(s)\n */\nfunction addClass(target, classNames) {\n const elms = elementsToArray(target);\n\n elms.forEach(function(elm, i) {\n const classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames);\n\n if (classArray && classArray.length) {\n const elmClassArray = elm.className.length ? elm.className.split(' ') : [];\n const newClassArray = classArray.filter(className => elmClassArray.indexOf(className) === -1);\n const finalClassArray = elmClassArray.concat(newClassArray);\n\n elm.className = finalClassArray.join(' ');\n }\n });\n\n return elms.length === 1 ? elms[0] : elms;\n}\n\n\n// Exports\n// =============================================================================\nexport default addClass;\n","// Modules\n// =============================================================================\nimport { elementsToArray, classNamesToArray } from './util.js';\n\n\n// Functions\n// =============================================================================\n/**\n * Remove class name(s) from target element(s)\n *\n * @param {(array|element|htmlcollection|nodelist|string)} target -\n * Element(s) to remove class name(s) from\n * @param {(array|function|string)} classNames - Array, space-separated list,\n * or function that returns array/string of class name(s)\n * @returns {(array|element)} - Target(s)\n */\nfunction removeClass(target, classNames) {\n const elms = elementsToArray(target);\n\n elms.forEach(function(elm, i) {\n const classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames);\n\n if (elm.className.trim().length && classArray && classArray.length) {\n const elmClassArray = elm.className.split(' ');\n const finalClassArray = elmClassArray.filter(className => classArray.indexOf(className) === -1);\n\n // Standardize result of setting empty \"class\" attribute.\n // Internet Explorer and Edge automatically remove the \"class\"\n // attribute when it is set to \"\". Other browsers (Chrome, Firefox,\n // Safari) will set the attribute to \"\". The difference in these\n // behaviors throws off unit tests, so the following code emulates\n // IE/Edge behavior of removing the attribute when the value is \"\".\n if (finalClassArray.length) {\n elm.className = finalClassArray.join(' ');\n }\n else {\n elm.removeAttribute('class');\n }\n }\n });\n\n return elms.length === 1 ? elms[0] : elms;\n}\n\n\n// Exports\n// =============================================================================\nexport default removeClass;\n","// Modules\n// =============================================================================\nimport add from './add.js';\nimport remove from './remove.js';\nimport { elementsToArray, classNamesToArray } from './util.js';\n\n\n// Variables\n// =============================================================================\nconst classChange = { add, remove };\n\n\n// Functions\n// =============================================================================\n/**\n * Toggle class name(s) on target element(s)\n *\n * @param {(array|element|htmlcollection|nodelist|string)} target -\n * Element(s) to toggle class name(s) on\n * @param {(array|function|string)} classNames - Array, space-separated list, or\n * function that returns array/string of class name(s)\n * @param {boolean} [forceTrueFalse] - Force add when true, remove when false\n * @returns {(array|element)} - Target(s)\n */\nfunction toggleClass(target, classNames, forceTrueFalse) {\n if (forceTrueFalse === true) {\n return classChange.add(target, classNames);\n }\n else if (forceTrueFalse === false) {\n return classChange.remove(target, classNames);\n }\n else {\n const elms = elementsToArray(target);\n\n elms.forEach(function(elm, i) {\n const classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames);\n\n if (classArray && classArray.length) {\n const elmClassArray = elm.className.length ? elm.className.split(' ') : [];\n const keepClassArray = elmClassArray.filter(className => classArray.indexOf(className) === -1);\n const newClassArray = classArray.filter(className => elmClassArray.indexOf(className) === -1);\n const finalClassArray = keepClassArray.concat(newClassArray);\n\n elm.className = finalClassArray.join(' ');\n }\n });\n\n return elms.length === 1 ? elms[0] : elms;\n }\n}\n\n\n// Exports\n// =============================================================================\nexport default toggleClass;\n","// Modules\n// =============================================================================\nimport add from './add.js';\nimport remove from './remove.js';\nimport toggle from './toggle.js';\nimport { elementsToArray, matchesSelector, getClosest, getParents } from './util.js';\n\n\n// Variables\n// =============================================================================\nconst classChange = { add, remove, toggle };\n\n\n// Functions\n// =============================================================================\n/**\n * Adds or removes click/tap event listener(s) on elements that have\n * data-class-* attributes and trigger associated method(s).\n *\n * @param {(array|element|htmlcollection|nodelist|string)} [listenerTarget=document]\n * @param {boolean} [addTrueRemoveFalse=true]\n */\nfunction addRemoveAttrListener(listenerTarget = document, addTrueRemoveFalse = true) {\n // Allow boolean for listenerTarget\n // true = add default listener, false = remove default listener\n addTrueRemoveFalse = typeof(listenerTarget) === 'boolean' ? listenerTarget : addTrueRemoveFalse;\n listenerTarget = typeof(listenerTarget) === 'boolean' ? document : listenerTarget;\n\n const elms = elementsToArray(listenerTarget);\n const method = listenerTarget === false || addTrueRemoveFalse === false ? 'removeEventListener' : 'addEventListener';\n\n elms.forEach(function(elm) {\n elm[method]('click', handleAttrEvent);\n });\n\n // Return object containing remove method\n return {\n remove() {\n elms.forEach(function(elm) {\n elm.removeEventListener('click', handleAttrEvent);\n });\n }\n };\n}\n\n/**\n * Handles click/tap events triggered via data-class-* attributes.\n *\n * @param {object} evt\n */\nfunction handleAttrEvent(evt) {\n const elms = [evt.target].concat(getParents(evt.target));\n const matchSelector = '[data-class-add],[data-class-remove],[data-class-toggle]';\n const methods = ['add', 'remove', 'toggle'];\n\n elms.forEach(function(elm) {\n const hasAttr = matchesSelector(elm, matchSelector);\n\n if (hasAttr) {\n const changeTasks = {};\n\n methods.forEach(function(method) {\n const classNames = elm.getAttribute(`data-class-${method}`);\n\n if (classNames && classNames.length) {\n const closestAttr = elm.getAttribute(`data-class-${method}-closest`) || elm.getAttribute('data-class-closest');\n const parentsAttr = elm.getAttribute(`data-class-${method}-parents`) || elm.getAttribute('data-class-parents');\n const siblingsAttr = elm.getAttribute(`data-class-${method}-siblings`) || elm.getAttribute('data-class-siblings');\n const targetAttr = elm.getAttribute(`data-class-${method}-target`) || elm.getAttribute('data-class-target');\n\n let changeElms = [];\n\n if (closestAttr) {\n const elms = getClosest(elm, closestAttr);\n changeElms = changeElms.concat(elms);\n }\n if (parentsAttr) {\n const elms = getParents(elm, parentsAttr);\n changeElms = changeElms.concat(elms);\n }\n if (siblingsAttr) {\n const siblingElms = elementsToArray(elm.parentNode.children);\n\n siblingElms.forEach(function(siblingElm) {\n const isSibling = siblingElm !== elm;\n const isMatch = matchesSelector(siblingElm, siblingsAttr);\n\n if (isSibling && isMatch) {\n changeElms.push(siblingElm);\n }\n });\n }\n if (targetAttr) {\n const elms = elementsToArray(document.querySelectorAll(targetAttr));\n changeElms = changeElms.concat(elms);\n }\n\n changeTasks[method] = {\n target: changeElms.length ? changeElms : elm,\n classNames\n };\n }\n });\n\n methods.forEach(function(method) {\n if (changeTasks[method]) {\n classChange[method](changeTasks[method].target, changeTasks[method].classNames);\n }\n });\n }\n });\n}\n\n\n// Exports\n// =============================================================================\nexport default addRemoveAttrListener;\n","// Modules\n// =============================================================================\nimport add from './add.js';\nimport remove from './remove.js';\nimport toggle from './toggle.js';\nimport * as util from './util.js';\n\n\n// Variables\n// =============================================================================\nconst classChange = { add, remove, toggle };\n\n\n// Functions\n// =============================================================================\n/**\n * Adds classChange event listener(s) and returns a remove() method\n *\n * @param {object} options\n * @param {(array|element|htmlcollection|nodelist|string)} [options.target=document]\n * @param {string} [options.event=\"click\"]\n * @param {(array|boolean|element|function|htmlcollection|nodelist|string)} [options.match=true]\n * @param {(array|boolean|element|function|htmlcollection|nodelist|string)} [options.change=true]\n * @param {(array|function|string)} [options.add]\n * @param {(array|function|string)} [options.remove]\n * @param {(array|function|string)} [options.toggle]\n * @returns {object} Remove method\n */\nfunction addChangeListener(options) {\n const settings = {\n target: util.elementsToArray(options.target || document.body),\n event : options.event || 'click',\n match : options.match || true,\n change: options.change || true,\n add : options.add || null,\n remove: options.remove || null,\n toggle: options.toggle || null\n };\n\n function triggerChangeEvent(evt) {\n handleChangeEvent(evt, settings);\n }\n\n settings.target.forEach(function(target) {\n target.addEventListener(settings.event, triggerChangeEvent);\n });\n\n // Return object containing remove method\n return {\n remove() {\n settings.target.forEach(function(target) {\n target.removeEventListener(settings.event, triggerChangeEvent);\n });\n }\n };\n}\n\n/**\n * Detects if an event matches the one defined in settings and changes class\n * names on elements accordingly\n *\n * @param {object} evt - Event object\n * @param {object} settings - Listener settings\n */\nfunction handleChangeEvent(evt, settings) {\n let matchElms = settings.match instanceof Function ? settings.match(evt) : settings.match;\n let matchedElm;\n\n // Match: Event target\n /* istanbul ignore else */\n if (matchElms === true) {\n matchElms = [evt.target];\n matchedElm = evt.target;\n }\n // Match: CSS selector\n else if (typeof matchElms === 'string') {\n const isMatch = util.matchesSelector(evt.target, matchElms);\n\n matchedElm = isMatch ? evt.target : util.getParents(evt.target).filter(elm => util.matchesSelector(elm, matchElms))[0] || null;\n matchElms = util.elementsToArray(matchElms);\n }\n // Match: Array, Element, HTMLCollection, NodeList\n else if (typeof matchElms === 'object') {\n const isMatch = evt.target === matchElms;\n\n matchElms = util.elementsToArray(matchElms);\n matchedElm = isMatch ? evt.target : matchElms[matchElms.indexOf(evt.target)] || util.getParents(evt.target).filter(elm => matchElms.indexOf(elm) !== -1)[0] || null;\n }\n\n // Change\n if (matchedElm) {\n const matchedElmIndex = matchElms.indexOf(matchedElm);\n let changeElms = settings.change instanceof Function ? settings.change(evt, matchedElm, matchedElmIndex) : settings.change;\n\n changeElms = changeElms === true ? [evt.target] : util.elementsToArray(changeElms);\n\n ['toggle', 'remove', 'add'].forEach(changeType => {\n // If settings value is a function, call for each element with args\n if (settings[changeType] instanceof Function) {\n changeElms.forEach((changeElm, changeElmIndex) => {\n const classNames = settings[changeType](evt, matchedElm, matchedElmIndex, changeElm, changeElmIndex);\n\n classChange[changeType](changeElm, classNames);\n });\n }\n else {\n const classNames = settings[changeType];\n\n classChange[changeType](changeElms, classNames);\n }\n });\n }\n}\n\n\n// Exports\n// =============================================================================\nexport default addChangeListener;\n","// Modules\n// =============================================================================\nimport add from './add.js';\nimport attrs from './attrs.js';\nimport listener from './listener.js';\nimport remove from './remove.js';\nimport toggle from './toggle.js';\n\n\n// Exports\n// =============================================================================\nexport default { add, attrs, listener, remove, toggle };\n"],"names":["classNamesToArray","classNames","trim","replace","split","Array","isArray","map","name","length","filter","Boolean","elementsToArray","elements","apply","document","querySelectorAll","window","HTMLCollection","NodeList","value","index","self","indexOf","getParents","elm","matchSelector","matches","matchesSelector","webkitMatchesSelector","mozMatchesSelector","msMatchesSelector","oMatchesSelector","parentElms","testElm","parentNode","call","push","selector","addClass","target","elms","forEach","i","classArray","Function","elmClassArray","className","newClassArray","finalClassArray","concat","join","removeClass","removeAttribute","classChange","add","remove","toggleClass","forceTrueFalse","keepClassArray","toggle","handleAttrEvent","evt","methods","changeTasks","method","getAttribute","closestAttr","parentsAttr","siblingsAttr","targetAttr","changeElms","matchedElm","getClosest","children","siblingElm","isSibling","isMatch","attrs","listenerTarget","addTrueRemoveFalse","removeEventListener","listener","options","settings","util","body","event","match","change","triggerChangeEvent","matchElms","_typeof","matchedElmIndex","changeType","changeElm","changeElmIndex","handleChangeEvent","addEventListener"],"mappings":"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"} -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhildenbiddle/class-change/27f17afd4083e681ea1dba935f250571d2cf6cf1/docs/.nojekyll -------------------------------------------------------------------------------- /docs/assets/img/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/img/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/img/npm.svg: -------------------------------------------------------------------------------- 1 | NPM icon -------------------------------------------------------------------------------- /docs/assets/img/twitter.svg: -------------------------------------------------------------------------------- 1 | Twitter icon -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | class-change - A versatile Element.classList alternative 10 | 11 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 |
29 | 30 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /docs/sidebar.md: -------------------------------------------------------------------------------- 1 | 2 | - [Documentation](/) 3 | - [Changelog](changelog.md) 4 | - **Links** 5 | - [![Code](assets/img/code.svg)Codepen Demo](https://codepen.io/jhildenbiddle/pen/wvmYVML) 6 | - [![Github](assets/img/github.svg)Github](https://github.com/jhildenbiddle/class-change) 7 | - [![NPM](assets/img/npm.svg)NPM](https://www.npmjs.com/package/class-change) 8 | - [![Twitter](assets/img/twitter.svg)@jhildenbiddle](http://twitter.com/jhildenbiddle) 9 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | // ============================================================================= 3 | const fs = require('fs'); 4 | const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 5 | const { execSync } = require('child_process'); 6 | 7 | 8 | // Variables 9 | // ============================================================================= 10 | const files = { 11 | test: './tests/**/*.test.js' 12 | }; 13 | const gitInfo = { 14 | branch : execSync('git rev-parse --abbrev-ref HEAD').toString().trim(), 15 | commitMsg: execSync('git log -1 --pretty=%B').toString().trim(), 16 | isClean : Boolean(execSync('[[ -n $(git status -s) ]] || echo "clean"').toString().trim()), 17 | isDirty : Boolean(execSync('[[ -z $(git status -s) ]] || echo "dirty"').toString().trim()) 18 | }; 19 | 20 | 21 | // Settings 22 | // ============================================================================= 23 | const settings = { 24 | files: [ 25 | 'node_modules/@babel/polyfill/dist/polyfill.js', 26 | files.test 27 | ], 28 | preprocessors: { 29 | [files.test]: ['eslint', 'webpack', 'sourcemap'] 30 | }, 31 | frameworks: ['mocha', 'chai'], 32 | reporters : ['mocha', 'coverage-istanbul'], // 'Browserstack' added below 33 | webpack: { 34 | mode : 'development', 35 | module: { 36 | rules: [ 37 | { 38 | test : /\.js$/, 39 | exclude: [/node_modules/], 40 | use : [ 41 | { 42 | loader : 'babel-loader', 43 | options: { 44 | // See .babelrc 45 | plugins: [ 46 | ['istanbul', { exclude: 'tests/*' }] 47 | ] 48 | }, 49 | } 50 | ] 51 | } 52 | ] 53 | } 54 | }, 55 | webpackMiddleware: { 56 | // https://webpack.js.org/configuration/stats/ 57 | stats: 'minimal' 58 | }, 59 | // Code coverage 60 | // https://github.com/mattlewis92/karma-coverage-istanbul-reporter 61 | coverageIstanbulReporter: { 62 | reports : ['lcovonly', 'text-summary'], 63 | combineBrowserReports : true, 64 | fixWebpackSourcePaths : true, 65 | skipFilesWithNoCoverage: true 66 | }, 67 | mochaReporter: { 68 | // https://www.npmjs.com/package/karma-mocha-reporter 69 | output: 'autowatch' 70 | }, 71 | autoWatch : false, 72 | colors : true, 73 | concurrency: Infinity, 74 | port : 9876, 75 | singleRun : true, 76 | // browserDisconnectTimeout : 1000*10, // default 2000 77 | // browserDisconnectTolerance: 1, // default 0 78 | // browserNoActivityTimeout : 1000*30, // default 10000 79 | // captureTimeout : 1000*60, // default 60000 80 | client: { 81 | mocha: { 82 | // timeout: 1000*20 // default 2000 83 | } 84 | } 85 | }; 86 | 87 | 88 | // Export 89 | // ============================================================================= 90 | module.exports = function(config) { 91 | const isRemote = Boolean(process.argv.indexOf('--remote') > -1); 92 | 93 | // Remote test 94 | if (isRemote) { 95 | // Browsers 96 | // https://www.browserstack.com/automate/capabilities 97 | settings.customLaunchers = { 98 | sl_chrome: { 99 | base : 'BrowserStack', 100 | browser : 'Chrome', 101 | browser_version: '48.0', 102 | os : 'Windows', 103 | os_version : '10' 104 | }, 105 | sl_edge: { 106 | base : 'BrowserStack', 107 | browser : 'Edge', 108 | browser_version: '18.0', 109 | os : 'Windows', 110 | os_version : '10' 111 | }, 112 | sl_firefox: { 113 | base : 'BrowserStack', 114 | browser : 'Firefox', 115 | browser_version: '32', 116 | os : 'Windows', 117 | os_version : '10' 118 | }, 119 | sl_ie_11: { 120 | base : 'BrowserStack', 121 | browser : 'IE', 122 | browser_version: '11.0', 123 | os : 'Windows', 124 | os_version : '10' 125 | }, 126 | sl_safari: { 127 | base : 'BrowserStack', 128 | browser : 'Safari', 129 | os : 'OS X', 130 | os_version : 'Sierra' 131 | } 132 | }; 133 | settings.browsers = Object.keys(settings.customLaunchers); 134 | 135 | // BrowserStack 136 | settings.reporters.push('BrowserStack'); 137 | settings.browserStack = { 138 | username : process.env.BROWSERSTACK_USERNAME, 139 | accessKey: process.env.BROWSERSTACK_ACCESS_KEY, 140 | build : [ 141 | `${process.env.GITHUB_RUN_ID ? 'GitHub' : 'Local'}:${gitInfo.branch} -`, 142 | gitInfo.isClean ? gitInfo.commitMsg : 'Uncommitted changes', 143 | `@ ${new Date().toLocaleString('en-US', { month: 'numeric', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'short', hour12: true })}` 144 | ].join(' '), 145 | project : pkg.name, 146 | video : false 147 | }; 148 | } 149 | // Local 150 | else { 151 | settings.browsers = ['ChromeHeadless']; 152 | settings.webpack.devtool = 'inline-source-map'; 153 | settings.coverageIstanbulReporter.reports.push('html'); 154 | 155 | // eslint-disable-next-line 156 | console.log([ 157 | '============================================================\n', 158 | `KARMA: localhost:${settings.port}/debug.html\n`, 159 | '============================================================\n' 160 | ].join('')); 161 | } 162 | 163 | // Logging: LOG_DISABLE, LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG 164 | settings.logLevel = config.LOG_INFO; 165 | config.set(settings); 166 | }; 167 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "class-change", 3 | "version": "1.1.8", 4 | "description": "A versatile Element.classList alternative for manipulating CSS class names, triggering change events using HTML data attributes, and creating class-related event listeners using a simple, declarative API.", 5 | "author": "John Hildenbiddle ", 6 | "license": "MIT", 7 | "homepage": "https://jhildenbiddle.github.io/class-change", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://jhildenbiddle@github.com/jhildenbiddle/class-change.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/jhildenbiddle/class-change/issues" 14 | }, 15 | "keywords": [ 16 | "attribute", 17 | "change", 18 | "class", 19 | "classChange", 20 | "classList", 21 | "css", 22 | "data", 23 | "event", 24 | "html", 25 | "listener", 26 | "polyfill", 27 | "ponyfill" 28 | ], 29 | "browserslist": [ 30 | "ie >= 9" 31 | ], 32 | "main": "dist/class-change.js", 33 | "module": "dist/class-change.esm.js", 34 | "unpkg": "dist/class-change.min.js", 35 | "scripts": { 36 | "prepare": "run-s clean build", 37 | "build": "rollup -c", 38 | "clean": "rimraf build/* coverage/* dist/*", 39 | "lint": "eslint . && markdownlint . --ignore node_modules", 40 | "serve": "node server.js", 41 | "start": "run-p watch serve", 42 | "test": "npm run lint && karma start", 43 | "test-watch": "karma start --auto-watch --no-single-run", 44 | "test-remote": "karma start --remote", 45 | "version": "npm run prepare && npm test && git add -A dist", 46 | "watch": "run-p 'build -- -w'" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.6.0", 50 | "@babel/polyfill": "^7.6.0", 51 | "@babel/preset-env": "^7.6.0", 52 | "@rollup/plugin-babel": "^5.3.0", 53 | "@rollup/plugin-commonjs": "^22.0.2", 54 | "@rollup/plugin-json": "^4.1.0", 55 | "@rollup/plugin-node-resolve": "^13.0.4", 56 | "babel-loader": "^8.0.5", 57 | "babel-plugin-external-helpers": "^6.22.0", 58 | "babel-plugin-istanbul": "^6.0.0", 59 | "browser-sync": "^2.26.7", 60 | "chai": "^4.2.0", 61 | "compression": "^1.7.4", 62 | "eslint": "^7.32.0", 63 | "eslint-plugin-chai-expect": "^3.0.0", 64 | "eslint-plugin-html": "^7.1.0", 65 | "eslint-plugin-mocha": "^10.1.0", 66 | "karma": "^6.3.4", 67 | "karma-browserstack-launcher": "^1.6.0", 68 | "karma-chai": "^0.1.0", 69 | "karma-chrome-launcher": "^3.1.0", 70 | "karma-coverage-istanbul-reporter": "^3.0.3", 71 | "karma-eslint": "^2.2.0", 72 | "karma-mocha": "^2.0.1", 73 | "karma-mocha-reporter": "^2.2.5", 74 | "karma-sourcemap-loader": "^0.3.7", 75 | "karma-webpack": "^5.0.0", 76 | "lodash.merge": "^4.6.1", 77 | "markdownlint-cli": "^0.32.1", 78 | "mocha": "^9.2.2", 79 | "npm-run-all": "^4.1.5", 80 | "rimraf": "^3.0.0", 81 | "rollup": "^2.56.3", 82 | "rollup-plugin-eslint": "^7.0.0", 83 | "rollup-plugin-terser": "^7.0.2", 84 | "webpack": "^5.52.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | // ============================================================================= 3 | const path = require('path'); 4 | 5 | import { babel } from '@rollup/plugin-babel'; 6 | import commonjs from '@rollup/plugin-commonjs'; 7 | import { eslint } from 'rollup-plugin-eslint'; 8 | import json from '@rollup/plugin-json'; 9 | import merge from 'lodash.merge'; 10 | import pkg from './package.json'; 11 | import resolve from '@rollup/plugin-node-resolve'; 12 | import { terser } from 'rollup-plugin-terser'; 13 | 14 | 15 | // Settings 16 | // ============================================================================= 17 | // Copyright 18 | const currentYear = (new Date()).getFullYear(); 19 | const releaseYear = 2016; 20 | 21 | // Output 22 | const entryFile = path.resolve(__dirname, 'src', 'index.js'); 23 | const outputFile = path.resolve(__dirname, 'dist', `${pkg.name}.js`); 24 | const outputName = 'classChange'; 25 | 26 | // Banner 27 | const bannerData = [ 28 | `${pkg.name}`, 29 | `v${pkg.version}`, 30 | `${pkg.homepage}`, 31 | `(c) ${releaseYear}${currentYear === releaseYear ? '' : '-' + currentYear} ${pkg.author}`, 32 | `${pkg.license} license` 33 | ]; 34 | 35 | // Plugins 36 | const pluginSettings = { 37 | eslint: { 38 | exclude : ['node_modules/**', './package.json'], 39 | throwOnWarning: false, 40 | throwOnError : true 41 | }, 42 | babel: { 43 | // See .babelrc 44 | babelHelpers: 'bundled' 45 | }, 46 | terser: { 47 | beautify: { 48 | compress: false, 49 | mangle : false, 50 | output: { 51 | beautify: true, 52 | comments: /(?:^!|@(?:license|preserve))/ 53 | } 54 | }, 55 | minify: { 56 | compress: true, 57 | mangle : true, 58 | output : { 59 | comments: new RegExp(pkg.name) 60 | } 61 | } 62 | } 63 | }; 64 | 65 | 66 | // Config 67 | // ============================================================================= 68 | // Base 69 | const config = { 70 | input : entryFile, 71 | output: { 72 | file : outputFile, 73 | name : outputName, 74 | banner : `/*!\n * ${ bannerData.join('\n * ') }\n */`, 75 | sourcemap: true 76 | }, 77 | plugins: [ 78 | resolve(), 79 | commonjs(), 80 | json(), 81 | eslint(pluginSettings.eslint), 82 | babel(pluginSettings.babel) 83 | ], 84 | watch: { 85 | clearScreen: false 86 | } 87 | }; 88 | 89 | // Formats 90 | // ----------------------------------------------------------------------------- 91 | // ES Module 92 | const esm = merge({}, config, { 93 | output: { 94 | file : config.output.file.replace(/\.js$/, '.esm.js'), 95 | format: 'esm' 96 | }, 97 | plugins: [ 98 | terser(pluginSettings.terser.beautify) 99 | ] 100 | }); 101 | 102 | // ES Module (Minified) 103 | const esmMinified = merge({}, config, { 104 | output: { 105 | file : esm.output.file.replace(/\.js$/, '.min.js'), 106 | format: esm.output.format 107 | }, 108 | plugins: [ 109 | terser(pluginSettings.terser.minify) 110 | ] 111 | }); 112 | 113 | // UMD 114 | const umd = merge({}, config, { 115 | output: { 116 | format: 'umd' 117 | }, 118 | plugins: config.plugins.concat([ 119 | terser(pluginSettings.terser.beautify) 120 | ]) 121 | }); 122 | 123 | // UMD (Minified) 124 | const umdMinified = merge({}, config, { 125 | output: { 126 | file : umd.output.file.replace(/\.js$/, '.min.js'), 127 | format: umd.output.format 128 | }, 129 | plugins: config.plugins.concat([ 130 | terser(pluginSettings.terser.minify) 131 | ]) 132 | }); 133 | 134 | 135 | // Exports 136 | // ============================================================================= 137 | export default [ 138 | esm, 139 | esmMinified, 140 | umd, 141 | umdMinified 142 | ]; 143 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // ============================================================================= 3 | const browserSync = require('browser-sync').create(); 4 | const compression = require('compression'); 5 | 6 | browserSync.init({ 7 | files: [ 8 | 'docs/**.*' 9 | ], 10 | ghostMode: { 11 | clicks: false, 12 | forms : false, 13 | scroll: false 14 | }, 15 | open: false, 16 | notify: false, 17 | reloadOnRestart: false, 18 | server: { 19 | baseDir: [ 20 | './docs/' 21 | ], 22 | middleware: [ 23 | compression() 24 | ], 25 | routes: { 26 | '/dist': './dist' 27 | } 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /src/add.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // ============================================================================= 3 | import { elementsToArray, classNamesToArray } from './util.js'; 4 | 5 | 6 | // Functions 7 | // ============================================================================= 8 | /** 9 | * Add class name(s) to target element(s) 10 | * 11 | * @param {(array|element|htmlcollection|nodelist|string)} target - 12 | * Element(s) to add class name(s) to 13 | * @param {(array|function|string)} classNames - Array, space-separated list, 14 | * or function that returns array/string of class name(s) 15 | * @returns {(array|element)} - Target(s) 16 | */ 17 | function addClass(target, classNames) { 18 | const elms = elementsToArray(target); 19 | 20 | elms.forEach(function(elm, i) { 21 | const classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames); 22 | 23 | if (classArray && classArray.length) { 24 | const elmClassArray = elm.className.length ? elm.className.split(' ') : []; 25 | const newClassArray = classArray.filter(className => elmClassArray.indexOf(className) === -1); 26 | const finalClassArray = elmClassArray.concat(newClassArray); 27 | 28 | elm.className = finalClassArray.join(' '); 29 | } 30 | }); 31 | 32 | return elms.length === 1 ? elms[0] : elms; 33 | } 34 | 35 | 36 | // Exports 37 | // ============================================================================= 38 | export default addClass; 39 | -------------------------------------------------------------------------------- /src/attrs.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // ============================================================================= 3 | import add from './add.js'; 4 | import remove from './remove.js'; 5 | import toggle from './toggle.js'; 6 | import { elementsToArray, matchesSelector, getClosest, getParents } from './util.js'; 7 | 8 | 9 | // Variables 10 | // ============================================================================= 11 | const classChange = { add, remove, toggle }; 12 | 13 | 14 | // Functions 15 | // ============================================================================= 16 | /** 17 | * Adds or removes click/tap event listener(s) on elements that have 18 | * data-class-* attributes and trigger associated method(s). 19 | * 20 | * @param {(array|element|htmlcollection|nodelist|string)} [listenerTarget=document] 21 | * @param {boolean} [addTrueRemoveFalse=true] 22 | */ 23 | function addRemoveAttrListener(listenerTarget = document, addTrueRemoveFalse = true) { 24 | // Allow boolean for listenerTarget 25 | // true = add default listener, false = remove default listener 26 | addTrueRemoveFalse = typeof(listenerTarget) === 'boolean' ? listenerTarget : addTrueRemoveFalse; 27 | listenerTarget = typeof(listenerTarget) === 'boolean' ? document : listenerTarget; 28 | 29 | const elms = elementsToArray(listenerTarget); 30 | const method = listenerTarget === false || addTrueRemoveFalse === false ? 'removeEventListener' : 'addEventListener'; 31 | 32 | elms.forEach(function(elm) { 33 | elm[method]('click', handleAttrEvent); 34 | }); 35 | 36 | // Return object containing remove method 37 | return { 38 | remove() { 39 | elms.forEach(function(elm) { 40 | elm.removeEventListener('click', handleAttrEvent); 41 | }); 42 | } 43 | }; 44 | } 45 | 46 | /** 47 | * Handles click/tap events triggered via data-class-* attributes. 48 | * 49 | * @param {object} evt 50 | */ 51 | function handleAttrEvent(evt) { 52 | const elms = [evt.target].concat(getParents(evt.target)); 53 | const matchSelector = '[data-class-add],[data-class-remove],[data-class-toggle]'; 54 | const methods = ['add', 'remove', 'toggle']; 55 | 56 | elms.forEach(function(elm) { 57 | const hasAttr = matchesSelector(elm, matchSelector); 58 | 59 | if (hasAttr) { 60 | const changeTasks = {}; 61 | 62 | methods.forEach(function(method) { 63 | const classNames = elm.getAttribute(`data-class-${method}`); 64 | 65 | if (classNames && classNames.length) { 66 | const closestAttr = elm.getAttribute(`data-class-${method}-closest`) || elm.getAttribute('data-class-closest'); 67 | const parentsAttr = elm.getAttribute(`data-class-${method}-parents`) || elm.getAttribute('data-class-parents'); 68 | const siblingsAttr = elm.getAttribute(`data-class-${method}-siblings`) || elm.getAttribute('data-class-siblings'); 69 | const targetAttr = elm.getAttribute(`data-class-${method}-target`) || elm.getAttribute('data-class-target'); 70 | 71 | let changeElms = []; 72 | 73 | if (closestAttr) { 74 | const elms = getClosest(elm, closestAttr); 75 | changeElms = changeElms.concat(elms); 76 | } 77 | if (parentsAttr) { 78 | const elms = getParents(elm, parentsAttr); 79 | changeElms = changeElms.concat(elms); 80 | } 81 | if (siblingsAttr) { 82 | const siblingElms = elementsToArray(elm.parentNode.children); 83 | 84 | siblingElms.forEach(function(siblingElm) { 85 | const isSibling = siblingElm !== elm; 86 | const isMatch = matchesSelector(siblingElm, siblingsAttr); 87 | 88 | if (isSibling && isMatch) { 89 | changeElms.push(siblingElm); 90 | } 91 | }); 92 | } 93 | if (targetAttr) { 94 | const elms = elementsToArray(document.querySelectorAll(targetAttr)); 95 | changeElms = changeElms.concat(elms); 96 | } 97 | 98 | changeTasks[method] = { 99 | target: changeElms.length ? changeElms : elm, 100 | classNames 101 | }; 102 | } 103 | }); 104 | 105 | methods.forEach(function(method) { 106 | if (changeTasks[method]) { 107 | classChange[method](changeTasks[method].target, changeTasks[method].classNames); 108 | } 109 | }); 110 | } 111 | }); 112 | } 113 | 114 | 115 | // Exports 116 | // ============================================================================= 117 | export default addRemoveAttrListener; 118 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // ============================================================================= 3 | import add from './add.js'; 4 | import attrs from './attrs.js'; 5 | import listener from './listener.js'; 6 | import remove from './remove.js'; 7 | import toggle from './toggle.js'; 8 | 9 | 10 | // Exports 11 | // ============================================================================= 12 | export default { add, attrs, listener, remove, toggle }; 13 | -------------------------------------------------------------------------------- /src/listener.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // ============================================================================= 3 | import add from './add.js'; 4 | import remove from './remove.js'; 5 | import toggle from './toggle.js'; 6 | import * as util from './util.js'; 7 | 8 | 9 | // Variables 10 | // ============================================================================= 11 | const classChange = { add, remove, toggle }; 12 | 13 | 14 | // Functions 15 | // ============================================================================= 16 | /** 17 | * Adds classChange event listener(s) and returns a remove() method 18 | * 19 | * @param {object} options 20 | * @param {(array|element|htmlcollection|nodelist|string)} [options.target=document] 21 | * @param {string} [options.event="click"] 22 | * @param {(array|boolean|element|function|htmlcollection|nodelist|string)} [options.match=true] 23 | * @param {(array|boolean|element|function|htmlcollection|nodelist|string)} [options.change=true] 24 | * @param {(array|function|string)} [options.add] 25 | * @param {(array|function|string)} [options.remove] 26 | * @param {(array|function|string)} [options.toggle] 27 | * @returns {object} Remove method 28 | */ 29 | function addChangeListener(options) { 30 | const settings = { 31 | target: util.elementsToArray(options.target || document.body), 32 | event : options.event || 'click', 33 | match : options.match || true, 34 | change: options.change || true, 35 | add : options.add || null, 36 | remove: options.remove || null, 37 | toggle: options.toggle || null 38 | }; 39 | 40 | function triggerChangeEvent(evt) { 41 | handleChangeEvent(evt, settings); 42 | } 43 | 44 | settings.target.forEach(function(target) { 45 | target.addEventListener(settings.event, triggerChangeEvent); 46 | }); 47 | 48 | // Return object containing remove method 49 | return { 50 | remove() { 51 | settings.target.forEach(function(target) { 52 | target.removeEventListener(settings.event, triggerChangeEvent); 53 | }); 54 | } 55 | }; 56 | } 57 | 58 | /** 59 | * Detects if an event matches the one defined in settings and changes class 60 | * names on elements accordingly 61 | * 62 | * @param {object} evt - Event object 63 | * @param {object} settings - Listener settings 64 | */ 65 | function handleChangeEvent(evt, settings) { 66 | let matchElms = settings.match instanceof Function ? settings.match(evt) : settings.match; 67 | let matchedElm; 68 | 69 | // Match: Event target 70 | /* istanbul ignore else */ 71 | if (matchElms === true) { 72 | matchElms = [evt.target]; 73 | matchedElm = evt.target; 74 | } 75 | // Match: CSS selector 76 | else if (typeof matchElms === 'string') { 77 | const isMatch = util.matchesSelector(evt.target, matchElms); 78 | 79 | matchedElm = isMatch ? evt.target : util.getParents(evt.target).filter(elm => util.matchesSelector(elm, matchElms))[0] || null; 80 | matchElms = util.elementsToArray(matchElms); 81 | } 82 | // Match: Array, Element, HTMLCollection, NodeList 83 | else if (typeof matchElms === 'object') { 84 | const isMatch = evt.target === matchElms; 85 | 86 | matchElms = util.elementsToArray(matchElms); 87 | matchedElm = isMatch ? evt.target : matchElms[matchElms.indexOf(evt.target)] || util.getParents(evt.target).filter(elm => matchElms.indexOf(elm) !== -1)[0] || null; 88 | } 89 | 90 | // Change 91 | if (matchedElm) { 92 | const matchedElmIndex = matchElms.indexOf(matchedElm); 93 | let changeElms = settings.change instanceof Function ? settings.change(evt, matchedElm, matchedElmIndex) : settings.change; 94 | 95 | changeElms = changeElms === true ? [evt.target] : util.elementsToArray(changeElms); 96 | 97 | ['toggle', 'remove', 'add'].forEach(changeType => { 98 | // If settings value is a function, call for each element with args 99 | if (settings[changeType] instanceof Function) { 100 | changeElms.forEach((changeElm, changeElmIndex) => { 101 | const classNames = settings[changeType](evt, matchedElm, matchedElmIndex, changeElm, changeElmIndex); 102 | 103 | classChange[changeType](changeElm, classNames); 104 | }); 105 | } 106 | else { 107 | const classNames = settings[changeType]; 108 | 109 | classChange[changeType](changeElms, classNames); 110 | } 111 | }); 112 | } 113 | } 114 | 115 | 116 | // Exports 117 | // ============================================================================= 118 | export default addChangeListener; 119 | -------------------------------------------------------------------------------- /src/remove.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // ============================================================================= 3 | import { elementsToArray, classNamesToArray } from './util.js'; 4 | 5 | 6 | // Functions 7 | // ============================================================================= 8 | /** 9 | * Remove class name(s) from target element(s) 10 | * 11 | * @param {(array|element|htmlcollection|nodelist|string)} target - 12 | * Element(s) to remove class name(s) from 13 | * @param {(array|function|string)} classNames - Array, space-separated list, 14 | * or function that returns array/string of class name(s) 15 | * @returns {(array|element)} - Target(s) 16 | */ 17 | function removeClass(target, classNames) { 18 | const elms = elementsToArray(target); 19 | 20 | elms.forEach(function(elm, i) { 21 | const classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames); 22 | 23 | if (elm.className.trim().length && classArray && classArray.length) { 24 | const elmClassArray = elm.className.split(' '); 25 | const finalClassArray = elmClassArray.filter(className => classArray.indexOf(className) === -1); 26 | 27 | // Standardize result of setting empty "class" attribute. 28 | // Internet Explorer and Edge automatically remove the "class" 29 | // attribute when it is set to "". Other browsers (Chrome, Firefox, 30 | // Safari) will set the attribute to "". The difference in these 31 | // behaviors throws off unit tests, so the following code emulates 32 | // IE/Edge behavior of removing the attribute when the value is "". 33 | if (finalClassArray.length) { 34 | elm.className = finalClassArray.join(' '); 35 | } 36 | else { 37 | elm.removeAttribute('class'); 38 | } 39 | } 40 | }); 41 | 42 | return elms.length === 1 ? elms[0] : elms; 43 | } 44 | 45 | 46 | // Exports 47 | // ============================================================================= 48 | export default removeClass; 49 | -------------------------------------------------------------------------------- /src/toggle.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // ============================================================================= 3 | import add from './add.js'; 4 | import remove from './remove.js'; 5 | import { elementsToArray, classNamesToArray } from './util.js'; 6 | 7 | 8 | // Variables 9 | // ============================================================================= 10 | const classChange = { add, remove }; 11 | 12 | 13 | // Functions 14 | // ============================================================================= 15 | /** 16 | * Toggle class name(s) on target element(s) 17 | * 18 | * @param {(array|element|htmlcollection|nodelist|string)} target - 19 | * Element(s) to toggle class name(s) on 20 | * @param {(array|function|string)} classNames - Array, space-separated list, or 21 | * function that returns array/string of class name(s) 22 | * @param {boolean} [forceTrueFalse] - Force add when true, remove when false 23 | * @returns {(array|element)} - Target(s) 24 | */ 25 | function toggleClass(target, classNames, forceTrueFalse) { 26 | if (forceTrueFalse === true) { 27 | return classChange.add(target, classNames); 28 | } 29 | else if (forceTrueFalse === false) { 30 | return classChange.remove(target, classNames); 31 | } 32 | else { 33 | const elms = elementsToArray(target); 34 | 35 | elms.forEach(function(elm, i) { 36 | const classArray = classNamesToArray(classNames instanceof Function ? classNames(elm, i) : classNames); 37 | 38 | if (classArray && classArray.length) { 39 | const elmClassArray = elm.className.length ? elm.className.split(' ') : []; 40 | const keepClassArray = elmClassArray.filter(className => classArray.indexOf(className) === -1); 41 | const newClassArray = classArray.filter(className => elmClassArray.indexOf(className) === -1); 42 | const finalClassArray = keepClassArray.concat(newClassArray); 43 | 44 | elm.className = finalClassArray.join(' '); 45 | } 46 | }); 47 | 48 | return elms.length === 1 ? elms[0] : elms; 49 | } 50 | } 51 | 52 | 53 | // Exports 54 | // ============================================================================= 55 | export default toggleClass; 56 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | // Exports 2 | // ============================================================================= 3 | /** 4 | * Converts space-separates list of class names to an array (if necessary) then 5 | * trims each array item. 6 | * 7 | * @export 8 | * @param {(array|string)} classNames 9 | * @returns {array} 10 | */ 11 | export function classNamesToArray(classNames) { 12 | // String - Trim and convert to Array 13 | if (typeof classNames === 'string') { 14 | classNames = classNames.trim().replace(/\s+/g, ' ').split(' '); 15 | } 16 | 17 | if (Array.isArray(classNames)) { 18 | // Trim items 19 | classNames = classNames.map(name => name && name.length ? name.trim() : null); 20 | 21 | // Filter out "falsey" values 22 | classNames = classNames.filter(Boolean); 23 | } 24 | 25 | return classNames; 26 | } 27 | 28 | /** 29 | * Converts a CSS selector (string), Element, HTMLCollection or NodeList to an 30 | * array (returns array as-is). 31 | * - Array: [Element, Element, ...] 32 | * - Element: document.body 33 | * - HTMLCollection: document.getElementsByTagName('p') 34 | * - NodeList: document.querySelectorAll('p') 35 | * - String (CSS selector): 'p' 36 | * 37 | * @export 38 | * @param {(array|element|htmlcollection|nodelist|string)} elements 39 | * @param {boolean} [removeDuplicates=true] 40 | * @returns {array} 41 | */ 42 | export function elementsToArray(elements) { 43 | // CSS Selector 44 | if (typeof elements === 'string') { 45 | elements = Array.apply(null, document.querySelectorAll(elements)); 46 | } 47 | // HTMLCollection / NodeList 48 | else if (elements instanceof window.HTMLCollection || elements instanceof window.NodeList) { 49 | elements = Array.apply(null, elements); 50 | } 51 | // Node/Element (assumed) 52 | else if (elements && !Array.isArray(elements)) { 53 | elements = [elements]; 54 | } 55 | 56 | if (Array.isArray(elements)) { 57 | // Remove duplicate 58 | return elements.filter((value, index, self) => self.indexOf(value) === index); 59 | } 60 | else { 61 | return []; 62 | } 63 | } 64 | 65 | /** 66 | * Matches self or finds closest ancestor (excluding document) node that match a 67 | * CSS selector 68 | * 69 | * @export 70 | * @param {element} elm 71 | * @param {sting} matchSelector 72 | * @returns {array} 73 | */ 74 | export function getClosest(elm, matchSelector) { 75 | /* istanbul ignore next */ 76 | const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector; 77 | 78 | let matchedElm = null; 79 | let testElm = elm; 80 | 81 | while (testElm && testElm !== document) { 82 | if (matches.call(testElm, matchSelector)) { 83 | matchedElm = testElm; 84 | break; 85 | } 86 | 87 | testElm = testElm.parentNode; 88 | } 89 | 90 | return matchedElm; 91 | } 92 | 93 | /** 94 | * Finds all parent nodes (excluding document), optionally limited to only those 95 | * that match a CSS selector 96 | * 97 | * @export 98 | * @param {element} elm 99 | * @param {sting} matchSelector 100 | * @returns {array} 101 | */ 102 | export function getParents(elm, matchSelector) { 103 | /* istanbul ignore next */ 104 | const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector; 105 | const parentElms = []; 106 | 107 | let testElm = elm.parentNode; 108 | 109 | while (testElm && testElm !== document) { 110 | if (!matchSelector || matchSelector && matches.call(testElm, matchSelector)) { 111 | parentElms.push(testElm); 112 | } 113 | 114 | testElm = testElm.parentNode; 115 | } 116 | 117 | return parentElms; 118 | } 119 | 120 | /** 121 | * Cross-browser wrapper for native "matches" method 122 | * 123 | * @export 124 | * @param {element} elm 125 | * @param {string} selector 126 | * @returns {boolean} 127 | */ 128 | export function matchesSelector(elm, selector) { 129 | /* istanbul ignore next */ 130 | const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector; 131 | 132 | return matches.call(elm, selector); 133 | } 134 | -------------------------------------------------------------------------------- /tests/add.test.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // ============================================================================= 3 | import add from '../src/add.js'; 4 | import { expect } from 'chai'; 5 | 6 | 7 | // Variables 8 | // ============================================================================= 9 | const classChange = { add }; 10 | const testClass = 'test'; 11 | const testClassArray = ['test1', 'test2']; 12 | const testClassList = testClassArray.join(' '); 13 | 14 | 15 | // Functions 16 | // ============================================================================= 17 | function sharedSetup(options) { 18 | beforeEach(function() { 19 | document.body.innerHTML = options.htmlBefore; 20 | }); 21 | 22 | afterEach(function() { 23 | expect(document.body.innerHTML).to.equal(options.htmlAfter); 24 | }); 25 | } 26 | 27 | 28 | // Tests 29 | // ============================================================================= 30 | describe('add.js', function() { 31 | describe('Targets', function() { 32 | describe('Single element', function() { 33 | sharedSetup({ 34 | htmlBefore: '

', 35 | htmlAfter : `

` 36 | }); 37 | 38 | it('Can add a class name to an element (element)', function() { 39 | classChange.add(document.querySelector('p'), testClass); 40 | }); 41 | it('Can add a class name to an element (selector)', function() { 42 | classChange.add('p', testClass); 43 | }); 44 | }); 45 | 46 | describe('Multiple elements', function() { 47 | sharedSetup({ 48 | htmlBefore: '

', 49 | htmlAfter : `

` 50 | }); 51 | 52 | it('Can add a class name to multiple elements (array)', function() { 53 | const elmArray = Array.apply(null, document.getElementsByTagName('p')); 54 | 55 | classChange.add(elmArray, testClass); 56 | }); 57 | it('Can add a class name to multiple elements (htmlcollection)', function() { 58 | classChange.add(document.getElementsByTagName('p'), testClass); 59 | }); 60 | it('Can add a class name to multiple elements (nodelist)', function() { 61 | classChange.add(document.querySelectorAll('p'), testClass); 62 | }); 63 | it('Can add a class name to multiple elements (selector)', function() { 64 | classChange.add('p', testClass); 65 | }); 66 | }); 67 | 68 | describe('Empty / Null', function() { 69 | sharedSetup({ 70 | htmlBefore: '

', 71 | htmlAfter : '

' 72 | }); 73 | 74 | it('Can handle target as null', function() { 75 | classChange.add(null, 'foo'); 76 | }); 77 | it('Can handle target as empty array', function() { 78 | classChange.add('div', 'foo'); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('Class names', function() { 84 | describe('Single class name', function() { 85 | sharedSetup({ 86 | htmlBefore: '

', 87 | htmlAfter : `

` 88 | }); 89 | 90 | it('Can add a class name (string) to an element', function() { 91 | classChange.add('p', testClass); 92 | }); 93 | it('Can add a class name (array) to an element', function() { 94 | classChange.add('p', [testClass]); 95 | }); 96 | it('Can add a class name (function) to an element', function() { 97 | classChange.add('p', function() { return testClass; }); 98 | }); 99 | }); 100 | 101 | describe('Multiple class names', function() { 102 | sharedSetup({ 103 | htmlBefore: '

', 104 | htmlAfter : `

` 105 | }); 106 | 107 | it('Can add multiple class names (string) to multiple elements', function() { 108 | classChange.add('p', testClassList); 109 | }); 110 | it('Can add multiple class names (array) to multiple elements', function() { 111 | classChange.add('p', testClassArray); 112 | }); 113 | it('Can add multiple class names (function) to multiple elements', function() { 114 | classChange.add('p', function() { return testClassList; }); 115 | }); 116 | }); 117 | 118 | describe('Function index', function() { 119 | const testClassList0 = testClassArray.map(className => `${className}-0`).join(' '); 120 | const testClassList1 = testClassArray.map(className => `${className}-1`).join(' '); 121 | 122 | sharedSetup({ 123 | htmlBefore: '

', 124 | htmlAfter : `

` 125 | }); 126 | 127 | it('Can add multiple class names with index (function) to multiple elements', function() { 128 | classChange.add('p', function(elm, elmIndex) { return testClassArray.map(className => `${className}-${elmIndex}`).join(' '); }); 129 | }); 130 | }); 131 | 132 | describe('Empty / Null', function() { 133 | sharedSetup({ 134 | htmlBefore: '

', 135 | htmlAfter : '

' 136 | }); 137 | 138 | it('Can handle classNames as null', function() { 139 | classChange.add('p', null); 140 | }); 141 | it('Can handle classNames as empty array', function() { 142 | classChange.add('p', []); 143 | }); 144 | it('Can handle classNames as empty string', function() { 145 | classChange.add('p', ''); 146 | }); 147 | it('Can handle classNames as function without return value', function() { 148 | classChange.add('p', function() {}); 149 | }); 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /tests/attrs.test.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // ============================================================================= 3 | import attrs from '../src/attrs.js'; 4 | import { expect } from 'chai'; 5 | 6 | 7 | // Variables 8 | // ============================================================================= 9 | const classChange = { attrs }; 10 | 11 | 12 | // Tests 13 | // ============================================================================= 14 | describe('attrs.js', function() { 15 | describe('Add/remove listeners', function() { 16 | it('Can remove listener using arguments', function() { 17 | document.body.innerHTML = ''; 18 | const clickElm = document.querySelector('button'); 19 | classChange.attrs(); 20 | classChange.attrs(false); 21 | clickElm.click(); 22 | expect(clickElm.className).to.equal('foo'); 23 | }); 24 | 25 | it('Can remove listener using remove() method', function() { 26 | document.body.innerHTML = ''; 27 | const clickElm = document.querySelector('button'); 28 | const listener = classChange.attrs(); 29 | listener.remove(); 30 | clickElm.click(); 31 | expect(clickElm.className).to.equal('foo'); 32 | }); 33 | }); 34 | 35 | describe('Apply class changes', function() { 36 | before(function() { 37 | // Add event listener 38 | classChange.attrs(); 39 | }); 40 | 41 | after(function() { 42 | // Remove event listener 43 | classChange.attrs(false); 44 | }); 45 | 46 | it('Can add class name to self', function() { 47 | document.body.innerHTML = ''; 48 | const clickElm = document.querySelector('button'); 49 | clickElm.click(); 50 | expect(clickElm.className).to.equal('foo bar'); 51 | }); 52 | 53 | it('Can remove class name from self', function() { 54 | document.body.innerHTML = ''; 55 | const clickElm = document.querySelector('button'); 56 | clickElm.click(); 57 | expect(clickElm.className).to.equal('foo'); 58 | }); 59 | 60 | it('Can toggle class names on self', function() { 61 | document.body.innerHTML = ''; 62 | const clickElm = document.querySelector('button'); 63 | clickElm.click(); 64 | expect(clickElm.className).to.equal('bar'); 65 | }); 66 | 67 | it('Can add, remove and toggle class names on self', function() { 68 | document.body.innerHTML = ''; 69 | const clickElm = document.querySelector('button'); 70 | clickElm.click(); 71 | expect(clickElm.className).to.equal('bar baz'); 72 | }); 73 | 74 | it('Can add, remove and toggle class names on closest ancestor', function() { 75 | document.body.innerHTML = ` 76 |
77 |

78 | 79 | 80 | 81 |

82 |
83 | `; 84 | const clickElm = document.querySelector('button'); 85 | clickElm.click(); 86 | expect(document.querySelector('div').className).to.equal(''); 87 | expect(document.querySelector('p').className).to.equal('foo'); 88 | expect(document.querySelector('span').className).to.equal('bar baz'); 89 | expect(clickElm.className).to.equal(''); 90 | }); 91 | 92 | it('Can add, remove and toggle class names on parents', function() { 93 | document.body.innerHTML = ` 94 |
95 |

96 | 97 | 98 | 99 |

100 |
101 | `; 102 | const clickElm = document.querySelector('button'); 103 | clickElm.click(); 104 | expect(document.querySelector('div').className).to.equal(''); 105 | expect(document.querySelector('p').className).to.equal('bar baz'); 106 | expect(document.querySelector('span').className).to.equal('bar baz'); 107 | expect(clickElm.className).to.equal(''); 108 | }); 109 | 110 | it('Can add, remove and toggle class names on siblings', function() { 111 | document.body.innerHTML = ` 112 |
113 |

114 | 115 | 116 | `; 117 | const clickElm = document.querySelector('button'); 118 | clickElm.click(); 119 | expect(document.querySelector('div').className).to.equal(''); 120 | expect(document.querySelector('p').className).to.equal('bar baz'); 121 | expect(document.querySelector('span').className).to.equal('bar baz'); 122 | expect(clickElm.className).to.equal(''); 123 | }); 124 | 125 | it('Can add, remove and toggle class names on target', function() { 126 | document.body.innerHTML = ` 127 |
128 |

129 | 130 | 131 | 132 |

133 | `; 134 | const clickElm = document.querySelector('button'); 135 | clickElm.click(); 136 | expect(document.querySelector('div').className).to.equal('bar baz'); 137 | expect(document.querySelector('p').className).to.equal(''); 138 | expect(document.querySelector('span').className).to.equal('bar baz'); 139 | expect(clickElm.className).to.equal(''); 140 | }); 141 | 142 | it('Can bubble add, remove and toggle class change events', function() { 143 | document.body.innerHTML = ` 144 |

145 | 152 | `; 153 | const clickElm = document.querySelector('button span span span'); 154 | const targetElm = document.querySelector('p'); 155 | clickElm.click(); 156 | expect(targetElm.className).to.equal('baz bar'); 157 | }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /tests/listener.test.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // ============================================================================= 3 | import listener from '../src/listener.js'; 4 | import { expect } from 'chai'; 5 | 6 | 7 | // Variables 8 | // ============================================================================= 9 | const classChange = { listener }; 10 | 11 | 12 | // Tests 13 | // ============================================================================= 14 | describe('listener.js', function() { 15 | it('Can detect non-match', function() { 16 | document.body.innerHTML = ''; 17 | const clickElm = document.querySelector('button'); 18 | const listener = classChange.listener({ 19 | match: 'p', 20 | add : 'bar' 21 | }); 22 | clickElm.click(); 23 | listener.remove(); 24 | expect(clickElm.className).to.equal('foo'); 25 | }); 26 | 27 | it('Can add class names on self', function() { 28 | document.body.innerHTML = ''; 29 | const clickElm = document.querySelector('button'); 30 | const listener = classChange.listener({ 31 | add: 'bar' 32 | }); 33 | clickElm.click(); 34 | listener.remove(); 35 | expect(clickElm.className).to.equal('foo bar'); 36 | }); 37 | 38 | it('Can remove class names on self', function() { 39 | document.body.innerHTML = ''; 40 | const clickElm = document.querySelector('button'); 41 | const listener = classChange.listener({ 42 | remove: 'bar' 43 | }); 44 | clickElm.click(); 45 | listener.remove(); 46 | expect(clickElm.className).to.equal('foo'); 47 | }); 48 | 49 | it('Can toggle class names on self', function() { 50 | document.body.innerHTML = ''; 51 | const clickElm = document.querySelector('button'); 52 | const listener = classChange.listener({ 53 | toggle: 'foo bar' 54 | }); 55 | clickElm.click(); 56 | listener.remove(); 57 | expect(clickElm.className).to.equal('bar'); 58 | }); 59 | 60 | it('Can add, remove and toggle class names on self', function() { 61 | document.body.innerHTML = ''; 62 | const clickElm = document.querySelector('button'); 63 | const listener = classChange.listener({ 64 | add : 'bar', 65 | remove: 'foo', 66 | toggle: 'baz' 67 | }); 68 | clickElm.click(); 69 | listener.remove(); 70 | expect(clickElm.className).to.equal('baz bar'); 71 | }); 72 | 73 | it('Can add, remove and toggle class names on "change" element', function() { 74 | document.body.innerHTML = '

'; 75 | const clickElm = document.querySelector('button'); 76 | const listener = classChange.listener({ 77 | change: 'p', 78 | add : 'bar', 79 | remove: 'foo', 80 | toggle: 'baz' 81 | }); 82 | clickElm.click(); 83 | listener.remove(); 84 | expect(document.querySelector('p').className).to.equal('baz bar'); 85 | }); 86 | 87 | it('Can change class names using "target", "match" and "change" options (string)', function() { 88 | document.body.innerHTML = ` 89 |
90 |

91 | 92 |
`; 93 | const clickElm = document.querySelector('button'); 94 | const listener = classChange.listener({ 95 | target: 'div', 96 | match : 'button', 97 | change: 'p', 98 | add : 'bar' 99 | }); 100 | clickElm.click(); 101 | listener.remove(); 102 | expect(document.querySelector('p').className).to.equal('foo bar'); 103 | }); 104 | 105 | it('Can change class names using "target", "match" and "change" options (element)', function() { 106 | document.body.innerHTML = ` 107 |
108 |

109 | 110 |
`; 111 | const clickElm = document.querySelector('button'); 112 | const listener = classChange.listener({ 113 | target: document.querySelector('div'), 114 | match : document.querySelector('button'), 115 | change: document.querySelector('p'), 116 | add : 'bar' 117 | }); 118 | clickElm.click(); 119 | listener.remove(); 120 | expect(document.querySelector('p').className).to.equal('foo bar'); 121 | }); 122 | 123 | it('Can change class names using "target", "match" and "change" options (htmlcollection)', function() { 124 | document.body.innerHTML = ` 125 |
126 |

127 |

128 | 129 |
`; 130 | const clickElm = document.querySelector('button'); 131 | const listener = classChange.listener({ 132 | target: document.getElementsByTagName('div'), 133 | match : document.getElementsByTagName('button'), 134 | change: document.getElementsByTagName('p'), 135 | add : 'bar' 136 | }); 137 | clickElm.click(); 138 | listener.remove(); 139 | expect(document.getElementsByTagName('p')[0].className).to.equal('foo bar'); 140 | expect(document.getElementsByTagName('p')[1].className).to.equal('baz bar'); 141 | }); 142 | 143 | it('Can change class names using "target", "match" and "change" options (nodelist)', function() { 144 | document.body.innerHTML = ` 145 |
146 |

147 |

148 | 149 |
`; 150 | const clickElm = document.querySelector('button'); 151 | const listener = classChange.listener({ 152 | target: document.querySelectorAll('div'), 153 | match : document.querySelectorAll('button'), 154 | change: document.querySelectorAll('p'), 155 | add : 'bar' 156 | }); 157 | clickElm.click(); 158 | listener.remove(); 159 | expect(document.querySelectorAll('p')[0].className).to.equal('foo bar'); 160 | expect(document.querySelectorAll('p')[1].className).to.equal('baz bar'); 161 | }); 162 | 163 | it('Can change class names using emulated event bubbling', function() { 164 | document.body.innerHTML = ` 165 |

166 | `; 173 | const clickElm = document.querySelector('button span span span'); 174 | const listener = classChange.listener({ 175 | match : 'button', 176 | change: 'p', 177 | add : 'bar' 178 | }); 179 | clickElm.click(); 180 | listener.remove(); 181 | expect(document.querySelector('p').className).to.equal('foo bar'); 182 | }); 183 | 184 | it('Can handle function return values', function() { 185 | document.body.innerHTML = '

'; 186 | const listener = classChange.listener({ 187 | match : function() { return 'button'; }, 188 | change: function() { return 'p'; }, 189 | add : function() { return 'bar'; }, 190 | remove: function() { return 'foo'; }, 191 | toggle: function() { return 'baz'; } 192 | }); 193 | document.querySelector('button').click(); 194 | listener.remove(); 195 | expect(document.querySelector('p').className).to.equal('baz bar'); 196 | }); 197 | 198 | it('Can use function arguments to generate return values', function() { 199 | document.body.innerHTML = ` 200 |

201 |

202 | 203 | `; 204 | const listener = classChange.listener({ 205 | match: function(evt) { 206 | return document.querySelectorAll('button'); 207 | }, 208 | change: function(evt, matchElm, matchElmIndex) { 209 | return document.querySelectorAll('p'); 210 | }, 211 | // This is a silly function, but the goal is to test each argument 212 | add: function(evt, matchElm, matchElmIndex, changeElm, changeElmIndex) { 213 | if (matchElmIndex === changeElmIndex) { 214 | changeElm.className += ` ${matchElm.className} index-${matchElmIndex}`; 215 | } 216 | }, 217 | }); 218 | document.querySelectorAll('button')[0].click(); 219 | document.querySelectorAll('button')[1].click(); 220 | listener.remove(); 221 | expect(document.querySelectorAll('p')[0].className).to.equal('foo baz index-0'); 222 | expect(document.querySelectorAll('p')[1].className).to.equal('bar buzz index-1'); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /tests/remove.test.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // ============================================================================= 3 | import remove from '../src/remove.js'; 4 | import { expect } from 'chai'; 5 | 6 | 7 | // Variables 8 | // ============================================================================= 9 | const classChange = { remove }; 10 | const testClass = 'test'; 11 | const testClassArray = ['test1', 'test2']; 12 | const testClassList = testClassArray.join(' '); 13 | 14 | 15 | // Functions 16 | // ============================================================================= 17 | function sharedSetup(options) { 18 | beforeEach(function() { 19 | document.body.innerHTML = options.htmlBefore; 20 | }); 21 | 22 | afterEach(function() { 23 | expect(document.body.innerHTML).to.equal(options.htmlAfter); 24 | }); 25 | } 26 | 27 | 28 | // Tests 29 | // ============================================================================= 30 | describe('remove.js', function() { 31 | describe('Targets', function() { 32 | describe('Single element', function() { 33 | sharedSetup({ 34 | htmlBefore: `

`, 35 | htmlAfter : '

' 36 | }); 37 | 38 | it('Can remove a class name from an element (element)', function() { 39 | classChange.remove(document.querySelector('p'), testClass); 40 | }); 41 | it('Can remove a class name from an element (selector)', function() { 42 | classChange.remove('p', testClass); 43 | }); 44 | }); 45 | 46 | describe('Multiple elements', function() { 47 | sharedSetup({ 48 | htmlBefore: `

`, 49 | htmlAfter : '

' 50 | }); 51 | 52 | it('Can remove a class name from multiple elements (array)', function() { 53 | const elmArray = Array.apply(null, document.getElementsByTagName('p')); 54 | 55 | classChange.remove(elmArray, testClass); 56 | }); 57 | it('Can remove a class name from multiple elements (htmlcollection)', function() { 58 | classChange.remove(document.getElementsByTagName('p'), testClass); 59 | }); 60 | it('Can remove a class name from multiple elements (nodelist)', function() { 61 | classChange.remove(document.querySelectorAll('p'), testClass); 62 | }); 63 | it('Can remove a class name from multiple elements (selector)', function() { 64 | classChange.remove('p', testClass); 65 | }); 66 | }); 67 | 68 | describe('Empty / Null', function() { 69 | sharedSetup({ 70 | htmlBefore: '

', 71 | htmlAfter : '

' 72 | }); 73 | 74 | it('Can handle target as null', function() { 75 | classChange.remove(null, 'foo'); 76 | }); 77 | it('Can handle target as empty array', function() { 78 | classChange.remove('div', 'foo'); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('Class names', function() { 84 | describe('Single class name', function() { 85 | sharedSetup({ 86 | htmlBefore: `

`, 87 | htmlAfter : '

' 88 | }); 89 | 90 | it('Can remove a class name (string) from an element', function() { 91 | classChange.remove('p', testClass); 92 | }); 93 | it('Can remove a class name (array) from an element', function() { 94 | classChange.remove('p', [testClass]); 95 | }); 96 | it('Can remove a class name (function) from an element', function() { 97 | classChange.remove('p', function() { return testClass; }); 98 | }); 99 | }); 100 | 101 | describe('Multiple class names', function() { 102 | sharedSetup({ 103 | htmlBefore: `

`, 104 | htmlAfter : '

' 105 | }); 106 | 107 | it('Can remove multiple class names (string) from multiple elements', function() { 108 | classChange.remove('p', testClassList); 109 | }); 110 | it('Can remove multiple class names (array) from multiple elements', function() { 111 | classChange.remove('p', testClassArray); 112 | }); 113 | it('Can remove multiple class names (function) from multiple elements', function() { 114 | classChange.remove('p', function() { return testClassList; }); 115 | }); 116 | }); 117 | 118 | describe('Function index', function() { 119 | const testClassList0 = testClassArray.map(className => `${className}-0`).join(' '); 120 | const testClassList1 = testClassArray.map(className => `${className}-1`).join(' '); 121 | 122 | sharedSetup({ 123 | htmlBefore: `

`, 124 | htmlAfter : '

' 125 | }); 126 | 127 | it('Can remove multiple class names with index (function) from multiple elements', function() { 128 | classChange.remove('p', function(elm, elmIndex) { return testClassArray.map(className => `${className}-${elmIndex}`).join(' '); }); 129 | }); 130 | }); 131 | 132 | describe('Empty / Null', function() { 133 | sharedSetup({ 134 | htmlBefore: '

', 135 | htmlAfter : '

' 136 | }); 137 | 138 | it('Can handle classNames as null', function() { 139 | classChange.remove('p', null); 140 | }); 141 | it('Can handle classNames as empty array', function() { 142 | classChange.remove('p', []); 143 | }); 144 | it('Can handle classNames as empty string', function() { 145 | classChange.remove('p', ''); 146 | }); 147 | it('Can handle classNames as function without return value', function() { 148 | classChange.remove('p', function() {}); 149 | }); 150 | }); 151 | }); 152 | }); -------------------------------------------------------------------------------- /tests/toggle.test.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // ============================================================================= 3 | import toggle from '../src/toggle.js'; 4 | import { expect } from 'chai'; 5 | 6 | 7 | // Variables 8 | // ============================================================================= 9 | const classChange = { toggle }; 10 | const testClass = 'test'; 11 | const testClassArray = ['test1', 'test2']; 12 | const testClassList = testClassArray.join(' '); 13 | 14 | 15 | // Functions 16 | // ============================================================================= 17 | function sharedSetup(options) { 18 | beforeEach(function() { 19 | document.body.innerHTML = options.htmlBefore; 20 | }); 21 | 22 | afterEach(function() { 23 | expect(document.body.innerHTML).to.equal(options.htmlAfter); 24 | }); 25 | } 26 | 27 | 28 | // Tests 29 | // ============================================================================= 30 | describe('toggle.js', function() { 31 | describe('Targets', function() { 32 | describe('Single element', function() { 33 | sharedSetup({ 34 | htmlBefore: '

', 35 | htmlAfter : `

` 36 | }); 37 | 38 | it('Can toggle class names on an element (element)', function() { 39 | classChange.toggle(document.querySelector('p'), testClassArray); 40 | }); 41 | it('Can toggle class names on an element (selector)', function() { 42 | classChange.toggle('p', testClassArray); 43 | }); 44 | }); 45 | 46 | describe('Multiple elements', function() { 47 | sharedSetup({ 48 | htmlBefore: '

', 49 | htmlAfter : `

`, 50 | }); 51 | 52 | it('Can toggle class names on multiple elements (array)', function() { 53 | const elmArray = Array.apply(null, document.getElementsByTagName('p')); 54 | 55 | classChange.toggle(elmArray, testClassArray); 56 | }); 57 | it('Can toggle class names on multiple elements (htmlcollection)', function() { 58 | classChange.toggle(document.getElementsByTagName('p'), testClassArray); 59 | }); 60 | it('Can toggle class names on multiple elements (nodelist)', function() { 61 | classChange.toggle(document.querySelectorAll('p'), testClassArray); 62 | }); 63 | it('Can toggle class names on multiple elements (selector)', function() { 64 | classChange.toggle('p', testClassArray); 65 | }); 66 | }); 67 | 68 | describe('Empty / Null', function() { 69 | sharedSetup({ 70 | htmlBefore: '

', 71 | htmlAfter : '

' 72 | }); 73 | 74 | it('Can handle target as null', function() { 75 | classChange.toggle(null, 'foo'); 76 | }); 77 | it('Can handle target as empty array', function() { 78 | classChange.toggle('div', 'foo'); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('Class names', function() { 84 | describe('Add single class name:', function() { 85 | sharedSetup({ 86 | htmlBefore: '

', 87 | htmlAfter : `

` 88 | }); 89 | 90 | it('Can toggle (add) a class name (string) on an element', function() { 91 | classChange.toggle('p', testClass); 92 | }); 93 | it('Can toggle (add) a class name (array) on an element', function() { 94 | classChange.toggle('p', [testClass]); 95 | }); 96 | it('Can toggle (add) a class name (function) on an element', function() { 97 | classChange.toggle('p', function() { return testClass; }); 98 | }); 99 | it('Can toggle (add) a class name using forceTrueFalse (true)', function() { 100 | classChange.toggle('p', testClass, true); 101 | }); 102 | }); 103 | 104 | describe('Remove single class name', function() { 105 | sharedSetup({ 106 | htmlBefore: `

`, 107 | htmlAfter : '

' 108 | }); 109 | 110 | it('Can toggle (remove) a class name (string) on an element', function() { 111 | classChange.toggle('p', testClass); 112 | }); 113 | it('Can toggle (remove) a class name (array) on an element', function() { 114 | classChange.toggle('p', [testClass]); 115 | }); 116 | it('Can toggle (remove) a class name (function) on an element', function() { 117 | classChange.toggle('p', function() { return testClass; }); 118 | }); 119 | it('Can toggle (remove) a class name using forceTrueFalse (false)', function() { 120 | classChange.toggle('p', testClass, false); 121 | }); 122 | }); 123 | 124 | describe('Multiple class names', function() { 125 | sharedSetup({ 126 | htmlBefore: `

`, 127 | htmlAfter : `

`, 128 | }); 129 | 130 | it('Can toggle multiple class names (string) on multiple elements', function() { 131 | classChange.toggle('p', testClassList); 132 | }); 133 | it('Can toggle multiple class names (array) on multiple elements', function() { 134 | classChange.toggle('p', testClassArray); 135 | }); 136 | it('Can toggle multiple class names (function) on multiple elements', function() { 137 | classChange.toggle('p', function() { return testClassArray; }); 138 | }); 139 | }); 140 | 141 | describe('Function index', function() { 142 | const testClassList0 = testClassArray.map(className => `${className}-0`).join(' '); 143 | const testClassList1 = testClassArray.map(className => `${className}-1`).join(' '); 144 | 145 | sharedSetup({ 146 | htmlBefore: '

', 147 | htmlAfter : `

` 148 | }); 149 | 150 | it('Can toggle multiple class names with index (function) on multiple elements', function() { 151 | classChange.toggle('p', function(elm, elmIndex) { return testClassArray.map(className => `${className}-${elmIndex}`).join(' '); }); 152 | }); 153 | }); 154 | 155 | describe('Empty / Null', function() { 156 | sharedSetup({ 157 | htmlBefore: '

', 158 | htmlAfter : '

' 159 | }); 160 | 161 | it('Can handle classNames as null', function() { 162 | classChange.toggle('p', null); 163 | }); 164 | it('Can handle classNames as empty array', function() { 165 | classChange.toggle('p', []); 166 | }); 167 | it('Can handle classNames as empty string', function() { 168 | classChange.toggle('p', ''); 169 | }); 170 | it('Can handle classNames as function without return value', function() { 171 | classChange.toggle('p', function() {}); 172 | }); 173 | }); 174 | }); 175 | }); 176 | --------------------------------------------------------------------------------