├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── dist ├── css │ ├── x-ray.css │ └── x-ray.min.css └── js │ ├── x-ray.js │ └── x-ray.min.js ├── docs ├── assets │ └── css │ │ └── custom.css ├── dist │ ├── css │ │ ├── x-ray.css │ │ └── x-ray.min.css │ └── js │ │ ├── x-ray.js │ │ └── x-ray.min.js └── index.html ├── gulpfile.js ├── package.json └── src ├── docs ├── _templates │ ├── _footer.html │ └── _header.html ├── assets │ └── css │ │ └── custom.css └── index.md ├── js └── x-ray.js └── sass ├── components └── _x-ray.scss └── x-ray.scss /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | test/results 4 | test/coverage 5 | 6 | ## OS X 7 | .DS_Store 8 | ._* 9 | .Spotlight-V100 10 | .Trashes -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install -g gulp 6 | script: gulp -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Go Make Things, LLC 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # X-Ray [![Build Status](https://travis-ci.org/cferdinandi/x-ray.svg)](https://travis-ci.org/cferdinandi/x-ray) 2 | A script that lets users toggle password visibility in forms. 3 | 4 | [Download X-Ray](https://github.com/cferdinandi/x-ray/archive/master.zip) / [View the demo](http://cferdinandi.github.io/x-ray/) 5 | 6 | 7 |
8 | 9 | ### Want to learn how to write your own vanilla JS plugins? Check out ["The Vanilla JS Guidebook"](https://gomakethings.com/vanilla-js-guidebook/) and level-up as a web developer. 🚀 10 | 11 |
12 | 13 | 14 | 15 | ## Getting Started 16 | 17 | Compiled and production-ready code can be found in the `dist` directory. The `src` directory contains development code. 18 | 19 | ### 1. Include X-Ray on your site. 20 | 21 | ```html 22 | 23 | 24 | ``` 25 | 26 | ### 2. Add the markup to your HTML. 27 | 28 | ```html 29 |
30 |
31 | 32 | 33 |
34 |
35 | 39 |
40 |
41 | ``` 42 | 43 | Turn any link or button into a password visibility toggle by adding the `.x-ray` class and `[data-x-ray]` attribute. The value of the `[data-x-ray]` attribute should match the selector for the target password field. If you would like passwords to be visible by default, set the optional `[data-default]` attribute to `show`. 44 | 45 | Use `` elements with the `.x-ray-show` class and `[data-x-ray-show]` data attribute or `.x-ray-hide` class and `[data-x-ray-hide]` data attribute to change the toggle element based on whether or not the password is visible. 46 | 47 | #### Using Checkboxes 48 | 49 | If you'd prefer, you can use a checkbox instead of a button to toggle password visibility. 50 | 51 | ```html 52 |
53 |
54 | 55 | 56 |
57 |
58 | 59 | 60 |
61 |
62 | 66 |
67 |
68 | ``` 69 | 70 | #### Toggling Multiple Password Fields 71 | 72 | You can toggle multiple password fields with one button or checkbox by using a class selector instead of an ID. 73 | 74 | ```html 75 |
76 |
77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 | 89 |
90 |
91 | ``` 92 | 93 | ### 3. Initialize X-Ray. 94 | 95 | ```html 96 | 99 | ``` 100 | 101 | In the footer of your page, after the content, initialize X-Ray. And that's it, you're done. Nice work! 102 | 103 | 104 | 105 | ## Installing with Package Managers 106 | 107 | You can install X-Ray with your favorite package manager. 108 | 109 | * **NPM:** `npm install cferdinandi/x-ray` 110 | * **Bower:** `bower install https://github.com/cferdinandi/x-ray.git` 111 | * **Component:** `component install cferdinandi/x-ray` 112 | 113 | 114 | 115 | ## Working with the Source Files 116 | 117 | If you would prefer, you can work with the development code in the `src` directory using the included [Gulp build system](http://gulpjs.com/). This compiles, lints, and minifies code. 118 | 119 | ### Dependencies 120 | Make sure these are installed first. 121 | 122 | * [Node.js](http://nodejs.org) 123 | * [Gulp](http://gulpjs.com) `sudo npm install -g gulp` 124 | 125 | ### Quick Start 126 | 127 | 1. In bash/terminal/command line, `cd` into your project directory. 128 | 2. Run `npm install` to install required files. 129 | 3. When it's done installing, run one of the task runners to get going: 130 | * `gulp` manually compiles files. 131 | * `gulp watch` automatically compiles files and applies changes using [LiveReload](http://livereload.com/). 132 | 133 | 134 | 135 | ## Options and Settings 136 | 137 | X-Ray includes smart defaults and works right out of the box. But if you want to customize things, it also has a robust API that provides multiple ways for you to adjust the default options and settings. 138 | 139 | ### Global Settings 140 | 141 | You can pass options and callbacks into X-Ray through the `init()` function: 142 | 143 | ```javascript 144 | xray.init({ 145 | selector: '[data-x-ray]', // Selector for the password toggle (must be a valid CSS selector) 146 | selectorShow: '[data-x-ray-show]', // Selector for the "show password" text (must be a valid CSS selector) 147 | selectorHide: '[data-x-ray-hide]', // Selector for the "hide password" text (must be a valid CSS selector) 148 | toggleActiveClass: 'active', // Class added to active password toggle button 149 | initClass: 'js-x-ray', // Class added to element when initiated 150 | callback: function ( toggle, pwID ) {} // Function that's run after password visibility is toggled 151 | }); 152 | ``` 153 | 154 | ### Use X-Ray events in your own scripts 155 | 156 | You can also call X-Ray's toggle password event in your own scripts. 157 | 158 | #### runToggle() 159 | Toggle password visibility on or off. 160 | 161 | ```javascript 162 | xray.runToggle( 163 | toggle, // Node that toggles the password visibility. ex. document.querySelector('[data-x-ray="#pw"]') 164 | pwID, // The ID or class of the password area(s) to show. ex. '#pw' 165 | options, // Classes and callbacks. Same options as those passed into the init() function. 166 | event // Optional, if a DOM event was triggered. 167 | ); 168 | ``` 169 | 170 | **Example** 171 | 172 | ```javascript 173 | var toggle = document.querySelector('[data-x-ray="#pw"]'); 174 | xray.runToggle( toggle, '#pw' ); 175 | ``` 176 | 177 | #### destroy() 178 | Destroy the current `xray.init()`. This is called automatically during the init function to remove any existing initializations. 179 | 180 | ```javascript 181 | xray.destroy(); 182 | ``` 183 | 184 | 185 | 186 | ## Browser Compatibility 187 | 188 | X-Ray works in all modern browsers, and IE 10 and above. You can extend browser support back to IE 9 with the [classList.js polyfill](https://github.com/eligrey/classList.js/). 189 | 190 | X-Ray is built with modern JavaScript APIs, and uses progressive enhancement. If the JavaScript file fails to load, or if your site is viewed on older and less capable browsers, passwords will be masked by default. 191 | 192 | 193 | 194 | ## How to Contribute 195 | 196 | In lieu of a formal style guide, take care to maintain the existing coding style. Please apply fixes to both the development and production code. Don't forget to update the version number, and when applicable, the documentation. 197 | 198 | 199 | 200 | ## License 201 | 202 | The code is available under the [MIT License](LICENSE.md). -------------------------------------------------------------------------------- /dist/css/x-ray.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * x-ray v9.2.0: Toggle password visibility 3 | * (c) 2016 Chris Ferdinandi 4 | * MIT License 5 | * http://github.com/cferdinandi/x-ray 6 | */ 7 | 8 | /* Hide show/hide labels by default */ 9 | /* line 2, /Users/cferdinandi/Sites/x-ray/src/sass/components/_x-ray.scss */ 10 | .x-ray, 11 | .x-ray-show, 12 | .x-ray-hide { 13 | display: none; 14 | visibility: hidden; 15 | } 16 | 17 | /* Display show/hide toggle when modern JS API's supported. 18 | * Display show/hide labels when active. */ 19 | /* line 11, /Users/cferdinandi/Sites/x-ray/src/sass/components/_x-ray.scss */ 20 | .js-x-ray .x-ray, 21 | .x-ray-show.active, 22 | .x-ray-hide.active { 23 | display: inline; 24 | visibility: visible; 25 | } 26 | -------------------------------------------------------------------------------- /dist/css/x-ray.min.css: -------------------------------------------------------------------------------- 1 | /*! x-ray v9.2.0 | (c) 2016 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/x-ray */ 2 | .x-ray,.x-ray-hide,.x-ray-show{display:none;visibility:hidden}.js-x-ray .x-ray,.x-ray-hide.active,.x-ray-show.active{display:inline;visibility:visible} -------------------------------------------------------------------------------- /dist/js/x-ray.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * x-ray v9.2.0: Toggle password visibility 3 | * (c) 2016 Chris Ferdinandi 4 | * MIT License 5 | * http://github.com/cferdinandi/x-ray 6 | */ 7 | 8 | (function (root, factory) { 9 | if ( typeof define === 'function' && define.amd ) { 10 | define([], factory(root)); 11 | } else if ( typeof exports === 'object' ) { 12 | module.exports = factory(root); 13 | } else { 14 | root.xray = factory(root); 15 | } 16 | })(typeof global !== 'undefined' ? global : this.window || this.global, function (root) { 17 | 18 | 'use strict'; 19 | 20 | // 21 | // Variables 22 | // 23 | 24 | var xray = {}; // Object for public APIs 25 | var supports = 'querySelector' in document && 'addEventListener' in root && 'classList' in document.createElement('_'); // Feature test 26 | var settings, toggles; 27 | 28 | // Default settings 29 | var defaults = { 30 | selector: '[data-x-ray]', 31 | selectorShow: '[data-x-ray-show]', 32 | selectorHide: '[data-x-ray-hide]', 33 | toggleActiveClass: 'active', 34 | initClass: 'js-x-ray', 35 | callback: function () {} 36 | }; 37 | 38 | 39 | // 40 | // Methods 41 | // 42 | 43 | /** 44 | * A simple forEach() implementation for Arrays, Objects and NodeLists. 45 | * @private 46 | * @author Todd Motto 47 | * @link https://github.com/toddmotto/foreach 48 | * @param {Array|Object|NodeList} collection Collection of items to iterate 49 | * @param {Function} callback Callback function for each iteration 50 | * @param {Array|Object|NodeList} scope Object/NodeList/Array that forEach is iterating over (aka `this`) 51 | */ 52 | var forEach = function ( collection, callback, scope ) { 53 | if ( Object.prototype.toString.call( collection ) === '[object Object]' ) { 54 | for ( var prop in collection ) { 55 | if ( Object.prototype.hasOwnProperty.call( collection, prop ) ) { 56 | callback.call( scope, collection[prop], prop, collection ); 57 | } 58 | } 59 | } else { 60 | for ( var i = 0, len = collection.length; i < len; i++ ) { 61 | callback.call( scope, collection[i], i, collection ); 62 | } 63 | } 64 | }; 65 | 66 | /** 67 | * Merge two or more objects. Returns a new object. 68 | * @private 69 | * @param {Boolean} deep If true, do a deep (or recursive) merge [optional] 70 | * @param {Object} objects The objects to merge together 71 | * @returns {Object} Merged values of defaults and options 72 | */ 73 | var extend = function () { 74 | 75 | // Variables 76 | var extended = {}; 77 | var deep = false; 78 | var i = 0; 79 | var length = arguments.length; 80 | 81 | // Check if a deep merge 82 | if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { 83 | deep = arguments[0]; 84 | i++; 85 | } 86 | 87 | // Merge the object into the extended object 88 | var merge = function (obj) { 89 | for ( var prop in obj ) { 90 | if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { 91 | // If deep merge and property is an object, merge properties 92 | if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { 93 | extended[prop] = extend( true, extended[prop], obj[prop] ); 94 | } else { 95 | extended[prop] = obj[prop]; 96 | } 97 | } 98 | } 99 | }; 100 | 101 | // Loop through each object and conduct a merge 102 | for ( ; i < length; i++ ) { 103 | var obj = arguments[i]; 104 | merge(obj); 105 | } 106 | 107 | return extended; 108 | 109 | }; 110 | 111 | /** 112 | * Get the closest matching element up the DOM tree. 113 | * @private 114 | * @param {Element} elem Starting element 115 | * @param {String} selector Selector to match against 116 | * @return {Boolean|Element} Returns null if not match found 117 | */ 118 | var getClosest = function ( elem, selector ) { 119 | 120 | // Element.matches() polyfill 121 | if (!Element.prototype.matches) { 122 | Element.prototype.matches = 123 | Element.prototype.matchesSelector || 124 | Element.prototype.mozMatchesSelector || 125 | Element.prototype.msMatchesSelector || 126 | Element.prototype.oMatchesSelector || 127 | Element.prototype.webkitMatchesSelector || 128 | function(s) { 129 | var matches = (this.document || this.ownerDocument).querySelectorAll(s), 130 | i = matches.length; 131 | while (--i >= 0 && matches.item(i) !== this) {} 132 | return i > -1; 133 | }; 134 | } 135 | 136 | // Get closest match 137 | for ( ; elem && elem !== document; elem = elem.parentNode ) { 138 | if ( elem.matches( selector ) ) return elem; 139 | } 140 | 141 | return null; 142 | 143 | }; 144 | 145 | /** 146 | * Toggle password visibility 147 | * @private 148 | * @param {NodeList} pws Password fields to toggle 149 | */ 150 | var togglePW = function ( pws ) { 151 | forEach(pws, function (pw) { 152 | var pwType = pw.type.toLowerCase(); 153 | if ( pwType === 'password' ) { 154 | pw.type = 'text'; 155 | } else if ( pwType === 'text' ) { 156 | pw.type = 'password'; 157 | } 158 | }); 159 | }; 160 | 161 | /** 162 | * Load default visibility 163 | * @private 164 | * @param {Element} toggle The element that toggles password visibility 165 | * @param {String} visibility Should the password be visible or hidden by default? 166 | * @param {String} pwSelector ID of the password field 167 | * @param {Object} settings 168 | */ 169 | var loadDefaultVisibility = function ( toggle, visibility, pwSelector, settings ) { 170 | var showText = toggle.querySelector( settings.selectorShow ); 171 | var hideText = toggle.querySelector( settings.selectorHide ); 172 | var pws = document.querySelectorAll(pwSelector); 173 | if ( visibility === 'show' ) { 174 | togglePW(pws); 175 | if ( hideText ) { 176 | hideText.classList.add( settings.toggleActiveClass ); 177 | } 178 | } else { 179 | if ( showText ) { 180 | showText.classList.add( settings.toggleActiveClass ); 181 | } 182 | } 183 | }; 184 | 185 | /** 186 | * Update toggle text 187 | * @private 188 | * @param {Element} toggle The element that toggles password visibility 189 | * @param {Object} settings 190 | */ 191 | var updateToggleText = function ( toggle, settings ) { 192 | var showText = toggle.querySelector('.x-ray-show'); 193 | var hideText = toggle.querySelector('.x-ray-hide'); 194 | if ( hideText ) { 195 | hideText.classList.toggle( settings.toggleActiveClass ); 196 | } 197 | if ( showText ) { 198 | showText.classList.toggle( settings.toggleActiveClass ); 199 | } 200 | }; 201 | 202 | /** 203 | * Show or hide password visibility 204 | * @public 205 | * @param {Element} toggle The element that toggles password visibility 206 | * @param {String} pwSelector The selector for the password fields 207 | * @param {Object} options 208 | * @param {Event} event 209 | */ 210 | xray.runToggle = function ( toggle, pwSelector, options, event ) { 211 | 212 | // Selectors and variables 213 | var settings = extend( settings || defaults, options || {} ); // Merge user options with defaults 214 | var pws = document.querySelectorAll( pwSelector ); 215 | 216 | togglePW( pws ); // Show/Hide password 217 | updateToggleText( toggle, settings ); // Change the toggle text 218 | 219 | settings.callback( toggle, pwSelector ); // Run callbacks after password visibility toggle 220 | 221 | }; 222 | 223 | /** 224 | * Handle toggle click events 225 | * @private 226 | */ 227 | var eventHandler = function (event) { 228 | var toggle = getClosest( event.target, settings.selector ); 229 | if ( toggle ) { 230 | if ( toggle.tagName.toLowerCase() === 'a' || toggle.tagName.toLowerCase() === 'button' ) { 231 | event.preventDefault(); 232 | } 233 | xray.runToggle( toggle, toggle.getAttribute('data-x-ray'), settings ); 234 | } 235 | }; 236 | 237 | /** 238 | * Destroy the current initialization. 239 | * @public 240 | */ 241 | xray.destroy = function () { 242 | if ( !settings ) return; 243 | document.documentElement.classList.remove( settings.initClass ); 244 | document.removeEventListener('click', eventHandler, false); 245 | if ( toggles ) { 246 | forEach( toggles, function ( toggle ) { 247 | 248 | // Get elements 249 | var pws = document.querySelectorAll( toggle.getAttribute('data-x-ray') ); 250 | var showText = toggle.querySelector( settings.selectorShow ); 251 | var hideText = toggle.querySelector( settings.selectorHide ); 252 | 253 | // Reset to default password state 254 | forEach( pws, function ( pw ) { 255 | pw.type = 'password'; 256 | }); 257 | showText.classList.remove(settings.toggleActiveClass); 258 | hideText.classList.remove(settings.toggleActiveClass); 259 | 260 | }); 261 | } 262 | settings = null; 263 | toggles = null; 264 | }; 265 | 266 | /** 267 | * Initialize X-Ray 268 | * @public 269 | * @param {Object} options User settings 270 | */ 271 | xray.init = function ( options ) { 272 | 273 | // feature test 274 | if ( !supports ) return; 275 | 276 | // Destroy any existing initializations 277 | xray.destroy(); 278 | 279 | // Selectors and variables 280 | settings = extend( defaults, options || {} ); // Merge user options with defaults 281 | toggles = document.querySelectorAll( settings.selector ); // Get show/hide password toggles 282 | 283 | document.documentElement.classList.add( settings.initClass ); // Add class to HTML element to activate conditional CSS 284 | 285 | // Initialize password visibility defaults 286 | forEach(toggles, function (toggle, index) { 287 | var visibility = toggle.getAttribute('data-default'); 288 | var pwID = toggle.getAttribute('data-x-ray'); 289 | loadDefaultVisibility( toggle, visibility, pwID, settings ); 290 | }); 291 | 292 | // Listen for click events 293 | document.addEventListener('click', eventHandler, false); 294 | 295 | }; 296 | 297 | 298 | // 299 | // Public APIs 300 | // 301 | 302 | return xray; 303 | 304 | }); -------------------------------------------------------------------------------- /dist/js/x-ray.min.js: -------------------------------------------------------------------------------- 1 | /*! x-ray v9.2.0 | (c) 2016 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/x-ray */ 2 | !function(e,t){"function"==typeof define&&define.amd?define([],t(e)):"object"==typeof exports?module.exports=t(e):e.xray=t(e)}("undefined"!=typeof global?global:this.window||this.global,function(e){"use strict";var t,o,r={},l="querySelector"in document&&"addEventListener"in e&&"classList"in document.createElement("_"),c={selector:"[data-x-ray]",selectorShow:"[data-x-ray-show]",selectorHide:"[data-x-ray-hide]",toggleActiveClass:"active",initClass:"js-x-ray",callback:function(){}},n=function(e,t,o){if("[object Object]"===Object.prototype.toString.call(e))for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.call(o,e[r],r,e);else for(var l=0,c=e.length;l=0&&t.item(o)!==this;);return o>-1});e&&e!==document;e=e.parentNode)if(e.matches(t))return e;return null},i=function(e){n(e,function(e){var t=e.type.toLowerCase();"password"===t?e.type="text":"text"===t&&(e.type="password")})},u=function(e,t,o,r){var l=e.querySelector(r.selectorShow),c=e.querySelector(r.selectorHide),n=document.querySelectorAll(o);"show"===t?(i(n),c&&c.classList.add(r.toggleActiveClass)):l&&l.classList.add(r.toggleActiveClass)},d=function(e,t){var o=e.querySelector(".x-ray-show"),r=e.querySelector(".x-ray-hide");r&&r.classList.toggle(t.toggleActiveClass),o&&o.classList.toggle(t.toggleActiveClass)};r.runToggle=function(e,t,o,r){var l=a(l||c,o||{}),n=document.querySelectorAll(t);i(n),d(e,l),l.callback(e,t)};var y=function(e){var o=s(e.target,t.selector);o&&("a"!==o.tagName.toLowerCase()&&"button"!==o.tagName.toLowerCase()||e.preventDefault(),r.runToggle(o,o.getAttribute("data-x-ray"),t))};return r.destroy=function(){t&&(document.documentElement.classList.remove(t.initClass),document.removeEventListener("click",y,!1),o&&n(o,function(e){var o=document.querySelectorAll(e.getAttribute("data-x-ray")),r=e.querySelector(t.selectorShow),l=e.querySelector(t.selectorHide);n(o,function(e){e.type="password"}),r.classList.remove(t.toggleActiveClass),l.classList.remove(t.toggleActiveClass)}),t=null,o=null)},r.init=function(e){l&&(r.destroy(),t=a(c,e||{}),o=document.querySelectorAll(t.selector),document.documentElement.classList.add(t.initClass),n(o,function(e,o){var r=e.getAttribute("data-default"),l=e.getAttribute("data-x-ray");u(e,r,l,t)}),document.addEventListener("click",y,!1))},r}); -------------------------------------------------------------------------------- /docs/assets/css/custom.css: -------------------------------------------------------------------------------- 1 | @-webkit-viewport { width: device-width; zoom: 1.0; } 2 | @-moz-viewport { width: device-width; zoom: 1.0; } 3 | @-ms-viewport { width: device-width; zoom: 1.0; } 4 | @-o-viewport { width: device-width; zoom: 1.0; } 5 | @viewport { width: device-width; zoom: 1.0; } 6 | 7 | html { overflow-y: auto; } 8 | 9 | img, audio, video, canvas { 10 | max-width: 100%; 11 | height: auto; 12 | } 13 | 14 | /* Sets body width */ 15 | .container { 16 | max-width: 40em; 17 | width: 88%; 18 | margin-left: auto; 19 | margin-right: auto; 20 | } -------------------------------------------------------------------------------- /docs/dist/css/x-ray.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * x-ray v9.2.0: Toggle password visibility 3 | * (c) 2016 Chris Ferdinandi 4 | * MIT License 5 | * http://github.com/cferdinandi/x-ray 6 | */ 7 | 8 | /* Hide show/hide labels by default */ 9 | /* line 2, /Users/cferdinandi/Sites/x-ray/src/sass/components/_x-ray.scss */ 10 | .x-ray, 11 | .x-ray-show, 12 | .x-ray-hide { 13 | display: none; 14 | visibility: hidden; 15 | } 16 | 17 | /* Display show/hide toggle when modern JS API's supported. 18 | * Display show/hide labels when active. */ 19 | /* line 11, /Users/cferdinandi/Sites/x-ray/src/sass/components/_x-ray.scss */ 20 | .js-x-ray .x-ray, 21 | .x-ray-show.active, 22 | .x-ray-hide.active { 23 | display: inline; 24 | visibility: visible; 25 | } 26 | -------------------------------------------------------------------------------- /docs/dist/css/x-ray.min.css: -------------------------------------------------------------------------------- 1 | /*! x-ray v9.2.0 | (c) 2016 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/x-ray */ 2 | .x-ray,.x-ray-hide,.x-ray-show{display:none;visibility:hidden}.js-x-ray .x-ray,.x-ray-hide.active,.x-ray-show.active{display:inline;visibility:visible} -------------------------------------------------------------------------------- /docs/dist/js/x-ray.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * x-ray v9.2.0: Toggle password visibility 3 | * (c) 2016 Chris Ferdinandi 4 | * MIT License 5 | * http://github.com/cferdinandi/x-ray 6 | */ 7 | 8 | (function (root, factory) { 9 | if ( typeof define === 'function' && define.amd ) { 10 | define([], factory(root)); 11 | } else if ( typeof exports === 'object' ) { 12 | module.exports = factory(root); 13 | } else { 14 | root.xray = factory(root); 15 | } 16 | })(typeof global !== 'undefined' ? global : this.window || this.global, function (root) { 17 | 18 | 'use strict'; 19 | 20 | // 21 | // Variables 22 | // 23 | 24 | var xray = {}; // Object for public APIs 25 | var supports = 'querySelector' in document && 'addEventListener' in root && 'classList' in document.createElement('_'); // Feature test 26 | var settings, toggles; 27 | 28 | // Default settings 29 | var defaults = { 30 | selector: '[data-x-ray]', 31 | selectorShow: '[data-x-ray-show]', 32 | selectorHide: '[data-x-ray-hide]', 33 | toggleActiveClass: 'active', 34 | initClass: 'js-x-ray', 35 | callback: function () {} 36 | }; 37 | 38 | 39 | // 40 | // Methods 41 | // 42 | 43 | /** 44 | * A simple forEach() implementation for Arrays, Objects and NodeLists. 45 | * @private 46 | * @author Todd Motto 47 | * @link https://github.com/toddmotto/foreach 48 | * @param {Array|Object|NodeList} collection Collection of items to iterate 49 | * @param {Function} callback Callback function for each iteration 50 | * @param {Array|Object|NodeList} scope Object/NodeList/Array that forEach is iterating over (aka `this`) 51 | */ 52 | var forEach = function ( collection, callback, scope ) { 53 | if ( Object.prototype.toString.call( collection ) === '[object Object]' ) { 54 | for ( var prop in collection ) { 55 | if ( Object.prototype.hasOwnProperty.call( collection, prop ) ) { 56 | callback.call( scope, collection[prop], prop, collection ); 57 | } 58 | } 59 | } else { 60 | for ( var i = 0, len = collection.length; i < len; i++ ) { 61 | callback.call( scope, collection[i], i, collection ); 62 | } 63 | } 64 | }; 65 | 66 | /** 67 | * Merge two or more objects. Returns a new object. 68 | * @private 69 | * @param {Boolean} deep If true, do a deep (or recursive) merge [optional] 70 | * @param {Object} objects The objects to merge together 71 | * @returns {Object} Merged values of defaults and options 72 | */ 73 | var extend = function () { 74 | 75 | // Variables 76 | var extended = {}; 77 | var deep = false; 78 | var i = 0; 79 | var length = arguments.length; 80 | 81 | // Check if a deep merge 82 | if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { 83 | deep = arguments[0]; 84 | i++; 85 | } 86 | 87 | // Merge the object into the extended object 88 | var merge = function (obj) { 89 | for ( var prop in obj ) { 90 | if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { 91 | // If deep merge and property is an object, merge properties 92 | if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { 93 | extended[prop] = extend( true, extended[prop], obj[prop] ); 94 | } else { 95 | extended[prop] = obj[prop]; 96 | } 97 | } 98 | } 99 | }; 100 | 101 | // Loop through each object and conduct a merge 102 | for ( ; i < length; i++ ) { 103 | var obj = arguments[i]; 104 | merge(obj); 105 | } 106 | 107 | return extended; 108 | 109 | }; 110 | 111 | /** 112 | * Get the closest matching element up the DOM tree. 113 | * @private 114 | * @param {Element} elem Starting element 115 | * @param {String} selector Selector to match against 116 | * @return {Boolean|Element} Returns null if not match found 117 | */ 118 | var getClosest = function ( elem, selector ) { 119 | 120 | // Element.matches() polyfill 121 | if (!Element.prototype.matches) { 122 | Element.prototype.matches = 123 | Element.prototype.matchesSelector || 124 | Element.prototype.mozMatchesSelector || 125 | Element.prototype.msMatchesSelector || 126 | Element.prototype.oMatchesSelector || 127 | Element.prototype.webkitMatchesSelector || 128 | function(s) { 129 | var matches = (this.document || this.ownerDocument).querySelectorAll(s), 130 | i = matches.length; 131 | while (--i >= 0 && matches.item(i) !== this) {} 132 | return i > -1; 133 | }; 134 | } 135 | 136 | // Get closest match 137 | for ( ; elem && elem !== document; elem = elem.parentNode ) { 138 | if ( elem.matches( selector ) ) return elem; 139 | } 140 | 141 | return null; 142 | 143 | }; 144 | 145 | /** 146 | * Toggle password visibility 147 | * @private 148 | * @param {NodeList} pws Password fields to toggle 149 | */ 150 | var togglePW = function ( pws ) { 151 | forEach(pws, function (pw) { 152 | var pwType = pw.type.toLowerCase(); 153 | if ( pwType === 'password' ) { 154 | pw.type = 'text'; 155 | } else if ( pwType === 'text' ) { 156 | pw.type = 'password'; 157 | } 158 | }); 159 | }; 160 | 161 | /** 162 | * Load default visibility 163 | * @private 164 | * @param {Element} toggle The element that toggles password visibility 165 | * @param {String} visibility Should the password be visible or hidden by default? 166 | * @param {String} pwSelector ID of the password field 167 | * @param {Object} settings 168 | */ 169 | var loadDefaultVisibility = function ( toggle, visibility, pwSelector, settings ) { 170 | var showText = toggle.querySelector( settings.selectorShow ); 171 | var hideText = toggle.querySelector( settings.selectorHide ); 172 | var pws = document.querySelectorAll(pwSelector); 173 | if ( visibility === 'show' ) { 174 | togglePW(pws); 175 | if ( hideText ) { 176 | hideText.classList.add( settings.toggleActiveClass ); 177 | } 178 | } else { 179 | if ( showText ) { 180 | showText.classList.add( settings.toggleActiveClass ); 181 | } 182 | } 183 | }; 184 | 185 | /** 186 | * Update toggle text 187 | * @private 188 | * @param {Element} toggle The element that toggles password visibility 189 | * @param {Object} settings 190 | */ 191 | var updateToggleText = function ( toggle, settings ) { 192 | var showText = toggle.querySelector('.x-ray-show'); 193 | var hideText = toggle.querySelector('.x-ray-hide'); 194 | if ( hideText ) { 195 | hideText.classList.toggle( settings.toggleActiveClass ); 196 | } 197 | if ( showText ) { 198 | showText.classList.toggle( settings.toggleActiveClass ); 199 | } 200 | }; 201 | 202 | /** 203 | * Show or hide password visibility 204 | * @public 205 | * @param {Element} toggle The element that toggles password visibility 206 | * @param {String} pwSelector The selector for the password fields 207 | * @param {Object} options 208 | * @param {Event} event 209 | */ 210 | xray.runToggle = function ( toggle, pwSelector, options, event ) { 211 | 212 | // Selectors and variables 213 | var settings = extend( settings || defaults, options || {} ); // Merge user options with defaults 214 | var pws = document.querySelectorAll( pwSelector ); 215 | 216 | togglePW( pws ); // Show/Hide password 217 | updateToggleText( toggle, settings ); // Change the toggle text 218 | 219 | settings.callback( toggle, pwSelector ); // Run callbacks after password visibility toggle 220 | 221 | }; 222 | 223 | /** 224 | * Handle toggle click events 225 | * @private 226 | */ 227 | var eventHandler = function (event) { 228 | var toggle = getClosest( event.target, settings.selector ); 229 | if ( toggle ) { 230 | if ( toggle.tagName.toLowerCase() === 'a' || toggle.tagName.toLowerCase() === 'button' ) { 231 | event.preventDefault(); 232 | } 233 | xray.runToggle( toggle, toggle.getAttribute('data-x-ray'), settings ); 234 | } 235 | }; 236 | 237 | /** 238 | * Destroy the current initialization. 239 | * @public 240 | */ 241 | xray.destroy = function () { 242 | if ( !settings ) return; 243 | document.documentElement.classList.remove( settings.initClass ); 244 | document.removeEventListener('click', eventHandler, false); 245 | if ( toggles ) { 246 | forEach( toggles, function ( toggle ) { 247 | 248 | // Get elements 249 | var pws = document.querySelectorAll( toggle.getAttribute('data-x-ray') ); 250 | var showText = toggle.querySelector( settings.selectorShow ); 251 | var hideText = toggle.querySelector( settings.selectorHide ); 252 | 253 | // Reset to default password state 254 | forEach( pws, function ( pw ) { 255 | pw.type = 'password'; 256 | }); 257 | showText.classList.remove(settings.toggleActiveClass); 258 | hideText.classList.remove(settings.toggleActiveClass); 259 | 260 | }); 261 | } 262 | settings = null; 263 | toggles = null; 264 | }; 265 | 266 | /** 267 | * Initialize X-Ray 268 | * @public 269 | * @param {Object} options User settings 270 | */ 271 | xray.init = function ( options ) { 272 | 273 | // feature test 274 | if ( !supports ) return; 275 | 276 | // Destroy any existing initializations 277 | xray.destroy(); 278 | 279 | // Selectors and variables 280 | settings = extend( defaults, options || {} ); // Merge user options with defaults 281 | toggles = document.querySelectorAll( settings.selector ); // Get show/hide password toggles 282 | 283 | document.documentElement.classList.add( settings.initClass ); // Add class to HTML element to activate conditional CSS 284 | 285 | // Initialize password visibility defaults 286 | forEach(toggles, function (toggle, index) { 287 | var visibility = toggle.getAttribute('data-default'); 288 | var pwID = toggle.getAttribute('data-x-ray'); 289 | loadDefaultVisibility( toggle, visibility, pwID, settings ); 290 | }); 291 | 292 | // Listen for click events 293 | document.addEventListener('click', eventHandler, false); 294 | 295 | }; 296 | 297 | 298 | // 299 | // Public APIs 300 | // 301 | 302 | return xray; 303 | 304 | }); -------------------------------------------------------------------------------- /docs/dist/js/x-ray.min.js: -------------------------------------------------------------------------------- 1 | /*! x-ray v9.2.0 | (c) 2016 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/x-ray */ 2 | !function(e,t){"function"==typeof define&&define.amd?define([],t(e)):"object"==typeof exports?module.exports=t(e):e.xray=t(e)}("undefined"!=typeof global?global:this.window||this.global,function(e){"use strict";var t,o,r={},l="querySelector"in document&&"addEventListener"in e&&"classList"in document.createElement("_"),c={selector:"[data-x-ray]",selectorShow:"[data-x-ray-show]",selectorHide:"[data-x-ray-hide]",toggleActiveClass:"active",initClass:"js-x-ray",callback:function(){}},n=function(e,t,o){if("[object Object]"===Object.prototype.toString.call(e))for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.call(o,e[r],r,e);else for(var l=0,c=e.length;l=0&&t.item(o)!==this;);return o>-1});e&&e!==document;e=e.parentNode)if(e.matches(t))return e;return null},i=function(e){n(e,function(e){var t=e.type.toLowerCase();"password"===t?e.type="text":"text"===t&&(e.type="password")})},u=function(e,t,o,r){var l=e.querySelector(r.selectorShow),c=e.querySelector(r.selectorHide),n=document.querySelectorAll(o);"show"===t?(i(n),c&&c.classList.add(r.toggleActiveClass)):l&&l.classList.add(r.toggleActiveClass)},d=function(e,t){var o=e.querySelector(".x-ray-show"),r=e.querySelector(".x-ray-hide");r&&r.classList.toggle(t.toggleActiveClass),o&&o.classList.toggle(t.toggleActiveClass)};r.runToggle=function(e,t,o,r){var l=a(l||c,o||{}),n=document.querySelectorAll(t);i(n),d(e,l),l.callback(e,t)};var y=function(e){var o=s(e.target,t.selector);o&&("a"!==o.tagName.toLowerCase()&&"button"!==o.tagName.toLowerCase()||e.preventDefault(),r.runToggle(o,o.getAttribute("data-x-ray"),t))};return r.destroy=function(){t&&(document.documentElement.classList.remove(t.initClass),document.removeEventListener("click",y,!1),o&&n(o,function(e){var o=document.querySelectorAll(e.getAttribute("data-x-ray")),r=e.querySelector(t.selectorShow),l=e.querySelector(t.selectorHide);n(o,function(e){e.type="password"}),r.classList.remove(t.toggleActiveClass),l.classList.remove(t.toggleActiveClass)}),t=null,o=null)},r.init=function(e){l&&(r.destroy(),t=a(c,e||{}),o=document.querySelectorAll(t.selector),document.documentElement.classList.add(t.initClass),n(o,function(e,o){var r=e.getAttribute("data-default"),l=e.getAttribute("data-x-ray");u(e,r,l,t)}),document.addEventListener("click",y,!1))},r}); -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | X-Ray 7 | 8 | 9 | 10 | 11 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 30 | 31 |
32 |

With a button

33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 |
43 | 47 |
48 |
49 | 50 |



51 |

With a checkbox

52 |
53 |
54 | 55 | 56 |
57 |
58 | 59 | 60 |
61 |
62 | 66 |
67 |
68 | 69 |



70 |

Multiple passwords

71 |
72 |
73 | 74 | 75 |
76 |
77 | 78 | 79 |
80 |
81 | 85 |
86 |
87 |
88 |
89 | 90 | 91 | 92 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gulp Packages 3 | */ 4 | 5 | // General 6 | var gulp = require('gulp'); 7 | var fs = require('fs'); 8 | var del = require('del'); 9 | var lazypipe = require('lazypipe'); 10 | var plumber = require('gulp-plumber'); 11 | var flatten = require('gulp-flatten'); 12 | var tap = require('gulp-tap'); 13 | var rename = require('gulp-rename'); 14 | var header = require('gulp-header'); 15 | var footer = require('gulp-footer'); 16 | var watch = require('gulp-watch'); 17 | var livereload = require('gulp-livereload'); 18 | var package = require('./package.json'); 19 | 20 | // Scripts 21 | var jshint = require('gulp-jshint'); 22 | var stylish = require('jshint-stylish'); 23 | var concat = require('gulp-concat'); 24 | var uglify = require('gulp-uglify'); 25 | var optimizejs = require('gulp-optimize-js'); 26 | 27 | // Styles 28 | var sass = require('gulp-sass'); 29 | var prefix = require('gulp-autoprefixer'); 30 | var minify = require('gulp-cssnano'); 31 | 32 | // Docs 33 | var markdown = require('gulp-markdown'); 34 | var fileinclude = require('gulp-file-include'); 35 | 36 | 37 | /** 38 | * Paths to project folders 39 | */ 40 | 41 | var paths = { 42 | input: 'src/**/*', 43 | output: 'dist/', 44 | scripts: { 45 | input: 'src/js/*', 46 | output: 'dist/js/' 47 | }, 48 | styles: { 49 | input: 'src/sass/**/*.{scss,sass}', 50 | output: 'dist/css/' 51 | }, 52 | docs: { 53 | input: 'src/docs/*.{html,md,markdown}', 54 | output: 'docs/', 55 | templates: 'src/docs/_templates/', 56 | assets: 'src/docs/assets/**' 57 | } 58 | }; 59 | 60 | 61 | /** 62 | * Template for banner to add to file headers 63 | */ 64 | 65 | var banner = { 66 | full : 67 | '/*!\n' + 68 | ' * <%= package.name %> v<%= package.version %>: <%= package.description %>\n' + 69 | ' * (c) ' + new Date().getFullYear() + ' <%= package.author.name %>\n' + 70 | ' * MIT License\n' + 71 | ' * <%= package.repository.url %>\n' + 72 | ' */\n\n', 73 | min : 74 | '/*!' + 75 | ' <%= package.name %> v<%= package.version %>' + 76 | ' | (c) ' + new Date().getFullYear() + ' <%= package.author.name %>' + 77 | ' | MIT License' + 78 | ' | <%= package.repository.url %>' + 79 | ' */\n' 80 | }; 81 | 82 | 83 | /** 84 | * Gulp Taks 85 | */ 86 | 87 | // Lint, minify, and concatenate scripts 88 | gulp.task('build:scripts', ['clean:dist'], function() { 89 | var jsTasks = lazypipe() 90 | .pipe(header, banner.full, { package : package }) 91 | .pipe(gulp.dest, paths.scripts.output) 92 | .pipe(rename, { suffix: '.min' }) 93 | .pipe(uglify) 94 | .pipe(header, banner.min, { package : package }) 95 | .pipe(gulp.dest, paths.scripts.output); 96 | 97 | return gulp.src(paths.scripts.input) 98 | .pipe(plumber()) 99 | .pipe(tap(function (file, t) { 100 | if ( file.isDirectory() ) { 101 | var name = file.relative + '.js'; 102 | return gulp.src(file.path + '/*.js') 103 | .pipe(concat(name)) 104 | .pipe(jsTasks()); 105 | } 106 | })) 107 | .pipe(jsTasks()); 108 | }); 109 | 110 | // Process, lint, and minify Sass files 111 | gulp.task('build:styles', ['clean:dist'], function() { 112 | return gulp.src(paths.styles.input) 113 | .pipe(plumber()) 114 | .pipe(sass({ 115 | outputStyle: 'expanded', 116 | sourceComments: true 117 | })) 118 | .pipe(flatten()) 119 | .pipe(prefix({ 120 | browsers: ['last 2 version', '> 1%'], 121 | cascade: true, 122 | remove: true 123 | })) 124 | .pipe(header(banner.full, { package : package })) 125 | .pipe(gulp.dest(paths.styles.output)) 126 | .pipe(rename({ suffix: '.min' })) 127 | .pipe(minify({ 128 | discardComments: { 129 | removeAll: true 130 | } 131 | })) 132 | .pipe(header(banner.min, { package : package })) 133 | .pipe(gulp.dest(paths.styles.output)); 134 | }); 135 | 136 | // Lint scripts 137 | gulp.task('lint:scripts', function () { 138 | return gulp.src(paths.scripts.input) 139 | .pipe(plumber()) 140 | .pipe(jshint()) 141 | .pipe(jshint.reporter('jshint-stylish')); 142 | }); 143 | 144 | // Remove pre-existing content from output folder 145 | gulp.task('clean:dist', function () { 146 | del.sync([ 147 | paths.output 148 | ]); 149 | }); 150 | 151 | // Generate documentation 152 | gulp.task('build:docs', ['compile', 'clean:docs'], function() { 153 | return gulp.src(paths.docs.input) 154 | .pipe(plumber()) 155 | .pipe(fileinclude({ 156 | prefix: '@@', 157 | basepath: '@file' 158 | })) 159 | .pipe(tap(function (file, t) { 160 | if ( /\.md|\.markdown/.test(file.path) ) { 161 | return t.through(markdown); 162 | } 163 | })) 164 | .pipe(header(fs.readFileSync(paths.docs.templates + '/_header.html', 'utf8'))) 165 | .pipe(footer(fs.readFileSync(paths.docs.templates + '/_footer.html', 'utf8'))) 166 | .pipe(gulp.dest(paths.docs.output)); 167 | }); 168 | 169 | // Copy distribution files to docs 170 | gulp.task('copy:dist', ['compile', 'clean:docs'], function() { 171 | return gulp.src(paths.output + '/**') 172 | .pipe(plumber()) 173 | .pipe(gulp.dest(paths.docs.output + '/dist')); 174 | }); 175 | 176 | // Copy documentation assets to docs 177 | gulp.task('copy:assets', ['clean:docs'], function() { 178 | return gulp.src(paths.docs.assets) 179 | .pipe(plumber()) 180 | .pipe(gulp.dest(paths.docs.output + '/assets')); 181 | }); 182 | 183 | // Remove prexisting content from docs folder 184 | gulp.task('clean:docs', function () { 185 | return del.sync(paths.docs.output); 186 | }); 187 | 188 | // Spin up livereload server and listen for file changes 189 | gulp.task('listen', function () { 190 | livereload.listen(); 191 | gulp.watch(paths.input).on('change', function(file) { 192 | gulp.start('default'); 193 | gulp.start('refresh'); 194 | }); 195 | }); 196 | 197 | // Run livereload after file change 198 | gulp.task('refresh', ['compile', 'docs'], function () { 199 | livereload.changed(); 200 | }); 201 | 202 | 203 | /** 204 | * Task Runners 205 | */ 206 | 207 | // Compile files 208 | gulp.task('compile', [ 209 | 'lint:scripts', 210 | 'clean:dist', 211 | 'build:scripts', 212 | 'build:styles' 213 | ]); 214 | 215 | // Generate documentation 216 | gulp.task('docs', [ 217 | 'clean:docs', 218 | 'build:docs', 219 | 'copy:dist', 220 | 'copy:assets' 221 | ]); 222 | 223 | // Compile files and generate docs (default) 224 | gulp.task('default', [ 225 | 'compile', 226 | 'docs' 227 | ]); 228 | 229 | // Compile files and generate docs when something changes 230 | gulp.task('watch', [ 231 | 'listen', 232 | 'default' 233 | ]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "x-ray", 3 | "version": "9.2.0", 4 | "description": "Toggle password visibility", 5 | "main": "./dist/js/x-ray.min.js", 6 | "author": { 7 | "name": "Chris Ferdinandi", 8 | "url": "http://gomakethings.com" 9 | }, 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "http://github.com/cferdinandi/x-ray" 14 | }, 15 | "devDependencies": { 16 | "gulp": "^3.9.1", 17 | "node-fs": "^0.1.7", 18 | "del": "^2.2.0", 19 | "lazypipe": "^1.0.1", 20 | "gulp-plumber": "^1.1.0", 21 | "gulp-flatten": "^0.3.1", 22 | "gulp-tap": "^0.1.3", 23 | "gulp-rename": "^1.2.2", 24 | "gulp-header": "^1.8.8", 25 | "gulp-footer": "^1.0.5", 26 | "gulp-watch": "^4.3.9", 27 | "gulp-livereload": "^3.8.1", 28 | "gulp-jshint": "^2.0.1", 29 | "jshint-stylish": "^2.2.1", 30 | "gulp-concat": "^2.6.0", 31 | "gulp-uglify": "^2.0.0", 32 | "gulp-optimize-js": "^1.0.2", 33 | "gulp-sass": "^2.3.2", 34 | "gulp-cssnano": "^2.1.2", 35 | "gulp-autoprefixer": "^3.1.1", 36 | "gulp-svgmin": "^1.2.3", 37 | "gulp-markdown": "^1.2.0", 38 | "gulp-file-include": "^0.14.0" 39 | } 40 | } -------------------------------------------------------------------------------- /src/docs/_templates/_footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/docs/_templates/_header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | X-Ray 7 | 8 | 9 | 10 | 11 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 30 | 31 |
32 | -------------------------------------------------------------------------------- /src/docs/assets/css/custom.css: -------------------------------------------------------------------------------- 1 | @-webkit-viewport { width: device-width; zoom: 1.0; } 2 | @-moz-viewport { width: device-width; zoom: 1.0; } 3 | @-ms-viewport { width: device-width; zoom: 1.0; } 4 | @-o-viewport { width: device-width; zoom: 1.0; } 5 | @viewport { width: device-width; zoom: 1.0; } 6 | 7 | html { overflow-y: auto; } 8 | 9 | img, audio, video, canvas { 10 | max-width: 100%; 11 | height: auto; 12 | } 13 | 14 | /* Sets body width */ 15 | .container { 16 | max-width: 40em; 17 | width: 88%; 18 | margin-left: auto; 19 | margin-right: auto; 20 | } -------------------------------------------------------------------------------- /src/docs/index.md: -------------------------------------------------------------------------------- 1 | ## With a button 2 | 3 |
4 |
5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 | 17 |
18 |
19 | 20 |

21 | 22 | ## With a checkbox 23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 38 |
39 |
40 | 41 |

42 | 43 | ## Multiple passwords 44 | 45 |
46 |
47 | 48 | 49 |
50 |
51 | 52 | 53 |
54 |
55 | 59 |
60 |
-------------------------------------------------------------------------------- /src/js/x-ray.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if ( typeof define === 'function' && define.amd ) { 3 | define([], factory(root)); 4 | } else if ( typeof exports === 'object' ) { 5 | module.exports = factory(root); 6 | } else { 7 | root.xray = factory(root); 8 | } 9 | })(typeof global !== 'undefined' ? global : this.window || this.global, function (root) { 10 | 11 | 'use strict'; 12 | 13 | // 14 | // Variables 15 | // 16 | 17 | var xray = {}; // Object for public APIs 18 | var supports = 'querySelector' in document && 'addEventListener' in root && 'classList' in document.createElement('_'); // Feature test 19 | var settings, toggles; 20 | 21 | // Default settings 22 | var defaults = { 23 | selector: '[data-x-ray]', 24 | selectorShow: '[data-x-ray-show]', 25 | selectorHide: '[data-x-ray-hide]', 26 | toggleActiveClass: 'active', 27 | initClass: 'js-x-ray', 28 | callback: function () {} 29 | }; 30 | 31 | 32 | // 33 | // Methods 34 | // 35 | 36 | /** 37 | * A simple forEach() implementation for Arrays, Objects and NodeLists. 38 | * @private 39 | * @author Todd Motto 40 | * @link https://github.com/toddmotto/foreach 41 | * @param {Array|Object|NodeList} collection Collection of items to iterate 42 | * @param {Function} callback Callback function for each iteration 43 | * @param {Array|Object|NodeList} scope Object/NodeList/Array that forEach is iterating over (aka `this`) 44 | */ 45 | var forEach = function ( collection, callback, scope ) { 46 | if ( Object.prototype.toString.call( collection ) === '[object Object]' ) { 47 | for ( var prop in collection ) { 48 | if ( Object.prototype.hasOwnProperty.call( collection, prop ) ) { 49 | callback.call( scope, collection[prop], prop, collection ); 50 | } 51 | } 52 | } else { 53 | for ( var i = 0, len = collection.length; i < len; i++ ) { 54 | callback.call( scope, collection[i], i, collection ); 55 | } 56 | } 57 | }; 58 | 59 | /** 60 | * Merge two or more objects. Returns a new object. 61 | * @private 62 | * @param {Boolean} deep If true, do a deep (or recursive) merge [optional] 63 | * @param {Object} objects The objects to merge together 64 | * @returns {Object} Merged values of defaults and options 65 | */ 66 | var extend = function () { 67 | 68 | // Variables 69 | var extended = {}; 70 | var deep = false; 71 | var i = 0; 72 | var length = arguments.length; 73 | 74 | // Check if a deep merge 75 | if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { 76 | deep = arguments[0]; 77 | i++; 78 | } 79 | 80 | // Merge the object into the extended object 81 | var merge = function (obj) { 82 | for ( var prop in obj ) { 83 | if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { 84 | // If deep merge and property is an object, merge properties 85 | if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { 86 | extended[prop] = extend( true, extended[prop], obj[prop] ); 87 | } else { 88 | extended[prop] = obj[prop]; 89 | } 90 | } 91 | } 92 | }; 93 | 94 | // Loop through each object and conduct a merge 95 | for ( ; i < length; i++ ) { 96 | var obj = arguments[i]; 97 | merge(obj); 98 | } 99 | 100 | return extended; 101 | 102 | }; 103 | 104 | /** 105 | * Get the closest matching element up the DOM tree. 106 | * @private 107 | * @param {Element} elem Starting element 108 | * @param {String} selector Selector to match against 109 | * @return {Boolean|Element} Returns null if not match found 110 | */ 111 | var getClosest = function ( elem, selector ) { 112 | 113 | // Element.matches() polyfill 114 | if (!Element.prototype.matches) { 115 | Element.prototype.matches = 116 | Element.prototype.matchesSelector || 117 | Element.prototype.mozMatchesSelector || 118 | Element.prototype.msMatchesSelector || 119 | Element.prototype.oMatchesSelector || 120 | Element.prototype.webkitMatchesSelector || 121 | function(s) { 122 | var matches = (this.document || this.ownerDocument).querySelectorAll(s), 123 | i = matches.length; 124 | while (--i >= 0 && matches.item(i) !== this) {} 125 | return i > -1; 126 | }; 127 | } 128 | 129 | // Get closest match 130 | for ( ; elem && elem !== document; elem = elem.parentNode ) { 131 | if ( elem.matches( selector ) ) return elem; 132 | } 133 | 134 | return null; 135 | 136 | }; 137 | 138 | /** 139 | * Toggle password visibility 140 | * @private 141 | * @param {NodeList} pws Password fields to toggle 142 | */ 143 | var togglePW = function ( pws ) { 144 | forEach(pws, function (pw) { 145 | var pwType = pw.type.toLowerCase(); 146 | if ( pwType === 'password' ) { 147 | pw.type = 'text'; 148 | } else if ( pwType === 'text' ) { 149 | pw.type = 'password'; 150 | } 151 | }); 152 | }; 153 | 154 | /** 155 | * Load default visibility 156 | * @private 157 | * @param {Element} toggle The element that toggles password visibility 158 | * @param {String} visibility Should the password be visible or hidden by default? 159 | * @param {String} pwSelector ID of the password field 160 | * @param {Object} settings 161 | */ 162 | var loadDefaultVisibility = function ( toggle, visibility, pwSelector, settings ) { 163 | var showText = toggle.querySelector( settings.selectorShow ); 164 | var hideText = toggle.querySelector( settings.selectorHide ); 165 | var pws = document.querySelectorAll(pwSelector); 166 | if ( visibility === 'show' ) { 167 | togglePW(pws); 168 | if ( hideText ) { 169 | hideText.classList.add( settings.toggleActiveClass ); 170 | } 171 | } else { 172 | if ( showText ) { 173 | showText.classList.add( settings.toggleActiveClass ); 174 | } 175 | } 176 | }; 177 | 178 | /** 179 | * Update toggle text 180 | * @private 181 | * @param {Element} toggle The element that toggles password visibility 182 | * @param {Object} settings 183 | */ 184 | var updateToggleText = function ( toggle, settings ) { 185 | var showText = toggle.querySelector('.x-ray-show'); 186 | var hideText = toggle.querySelector('.x-ray-hide'); 187 | if ( hideText ) { 188 | hideText.classList.toggle( settings.toggleActiveClass ); 189 | } 190 | if ( showText ) { 191 | showText.classList.toggle( settings.toggleActiveClass ); 192 | } 193 | }; 194 | 195 | /** 196 | * Show or hide password visibility 197 | * @public 198 | * @param {Element} toggle The element that toggles password visibility 199 | * @param {String} pwSelector The selector for the password fields 200 | * @param {Object} options 201 | * @param {Event} event 202 | */ 203 | xray.runToggle = function ( toggle, pwSelector, options, event ) { 204 | 205 | // Selectors and variables 206 | var settings = extend( settings || defaults, options || {} ); // Merge user options with defaults 207 | var pws = document.querySelectorAll( pwSelector ); 208 | 209 | togglePW( pws ); // Show/Hide password 210 | updateToggleText( toggle, settings ); // Change the toggle text 211 | 212 | settings.callback( toggle, pwSelector ); // Run callbacks after password visibility toggle 213 | 214 | }; 215 | 216 | /** 217 | * Handle toggle click events 218 | * @private 219 | */ 220 | var eventHandler = function (event) { 221 | var toggle = getClosest( event.target, settings.selector ); 222 | if ( toggle ) { 223 | if ( toggle.tagName.toLowerCase() === 'a' || toggle.tagName.toLowerCase() === 'button' ) { 224 | event.preventDefault(); 225 | } 226 | xray.runToggle( toggle, toggle.getAttribute('data-x-ray'), settings ); 227 | } 228 | }; 229 | 230 | /** 231 | * Destroy the current initialization. 232 | * @public 233 | */ 234 | xray.destroy = function () { 235 | if ( !settings ) return; 236 | document.documentElement.classList.remove( settings.initClass ); 237 | document.removeEventListener('click', eventHandler, false); 238 | if ( toggles ) { 239 | forEach( toggles, function ( toggle ) { 240 | 241 | // Get elements 242 | var pws = document.querySelectorAll( toggle.getAttribute('data-x-ray') ); 243 | var showText = toggle.querySelector( settings.selectorShow ); 244 | var hideText = toggle.querySelector( settings.selectorHide ); 245 | 246 | // Reset to default password state 247 | forEach( pws, function ( pw ) { 248 | pw.type = 'password'; 249 | }); 250 | showText.classList.remove(settings.toggleActiveClass); 251 | hideText.classList.remove(settings.toggleActiveClass); 252 | 253 | }); 254 | } 255 | settings = null; 256 | toggles = null; 257 | }; 258 | 259 | /** 260 | * Initialize X-Ray 261 | * @public 262 | * @param {Object} options User settings 263 | */ 264 | xray.init = function ( options ) { 265 | 266 | // feature test 267 | if ( !supports ) return; 268 | 269 | // Destroy any existing initializations 270 | xray.destroy(); 271 | 272 | // Selectors and variables 273 | settings = extend( defaults, options || {} ); // Merge user options with defaults 274 | toggles = document.querySelectorAll( settings.selector ); // Get show/hide password toggles 275 | 276 | document.documentElement.classList.add( settings.initClass ); // Add class to HTML element to activate conditional CSS 277 | 278 | // Initialize password visibility defaults 279 | forEach(toggles, function (toggle, index) { 280 | var visibility = toggle.getAttribute('data-default'); 281 | var pwID = toggle.getAttribute('data-x-ray'); 282 | loadDefaultVisibility( toggle, visibility, pwID, settings ); 283 | }); 284 | 285 | // Listen for click events 286 | document.addEventListener('click', eventHandler, false); 287 | 288 | }; 289 | 290 | 291 | // 292 | // Public APIs 293 | // 294 | 295 | return xray; 296 | 297 | }); -------------------------------------------------------------------------------- /src/sass/components/_x-ray.scss: -------------------------------------------------------------------------------- 1 | /* Hide show/hide labels by default */ 2 | .x-ray, 3 | .x-ray-show, 4 | .x-ray-hide { 5 | display: none; 6 | visibility: hidden; 7 | } 8 | 9 | /* Display show/hide toggle when modern JS API's supported. 10 | * Display show/hide labels when active. */ 11 | .js-x-ray .x-ray, 12 | .x-ray-show.active, 13 | .x-ray-hide.active { 14 | display: inline; 15 | visibility: visible; 16 | } -------------------------------------------------------------------------------- /src/sass/x-ray.scss: -------------------------------------------------------------------------------- 1 | // Import the table styles for compiling 2 | @import 3 | "components/x-ray"; 4 | --------------------------------------------------------------------------------