frostyshield / Password Visibility Toggle

// ==UserScript==
// @name         Password Visibility Toggle
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Adds a show/hide toggle to password fields on specified websites without modifying server-side code.
// @author       Your Name
// @match        https://your-target-website.com/*
// @match        http://your-target-website.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    /**
     * Configuration Variables
     */
    const config = {
        // SVG Icons for Show and Hide (You can replace these with your preferred icons)
        showIcon: `
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="20" height="20" fill="#555">
                <path d="M572.52 241.4C518.3 135.6 407.3 64 288 64S57.7 135.6 3.48 241.4a48.16 48.16 0 0 0 0 29.2C57.7 376.4 168.7 448 288 448s230.3-71.6 284.52-177.4a48.16 48.16 0 0 0 0-29.2zM288 400c-79.4 0-152.1-35.1-200-88 47.9-52.9 119.6-88 200-88s152.1 35.1 200 88c-47.9 52.9-119.6 88-200 88zm0-176a88 88 0 1 0 88 88 88.1 88.1 0 0 0-88-88zm0 144a56 56 0 1 1 56-56 56 56 0 0 1-56 56z"/>
            </svg>
        `,
        hideIcon: `
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="20" height="20" fill="#555">
                <path d="M320 400c-79.4 0-152.1-35.1-200-88 22.1-24.5 47.9-47.6 75.4-68.2l-46.5-46.5c-6.3 2.7-12.7 6.1-19 10.1C89.5 194.1 32 224 32 224s55.6 112.3 160 192c19.1 12.7 39.1 23.1 59.6 31.8l-46.5-46.5c20.6-27.5 43.7-53.3 68.2-75.4C384.9 272.1 320 400 320 400zM633.8 458.1l-86.7-86.7C588.1 329.7 608 274.4 608 224c0-48.6-35.3-89.6-80-104.6l-52.8-52.8C476.1 69.6 502.5 64 528 64c106 0 160 112.3 160 112.3s-55.6 112.3-160 192c-17.6 11.2-35.7 21.4-54.5 30.2l-52.8-52.8c-14.9-44.7-55.9-80-104.6-80-50.4 0-105.7 19.9-138.4 54.6l-86.7-86.7c-14.2-14.2-37.2-14.2-51.4 0l-16.1 16.1c-14.2 14.2-14.2 37.2 0 51.4l86.7 86.7C70.3 274.3 64 299.4 64 325.8c0 5.6.9 11.2 2.8 16.5l-46.5 46.5C19.2 388.4 0 423.2 0 464c0 48.6 35.3 89.6 80 104.6l52.8 52.8C163.9 560.4 190.3 565 216 565c106 0 160-112.3 160-112.3s-55.6-112.3-160-192c-19.1-12.7-39.1-23.1-59.6-31.8l46.5-46.5c6.3 2.7 12.7 6.1 19 10.1C550.5 317.9 608 288 608 288s-55.6-112.3-160-192c-19.1-12.7-39.1-23.1-59.6-31.8l46.5-46.5c20.6 27.5 43.7 53.3 68.2 75.4C479.1 239.9 544 112 544 112s-64-128-160-128c-25.5 0-51 5.9-73.3 16.2l-46.5-46.5c-19.9 19.9-35.2 43-46.6 68.4l-86.7-86.7c-14.2-14.2-37.2-14.2-51.4 0l-16.1 16.1c-14.2 14.2-14.2 37.2 0 51.4l86.7 86.7C70.3 274.3 64 299.4 64 325.8c0 5.6.9 11.2 2.8 16.5l-46.5 46.5C19.2 388.4 0 423.2 0 464c0 48.6 35.3 89.6 80 104.6l52.8 52.8C163.9 560.4 190.3 565 216 565c106 0 160-112.3 160-112.3s-55.6-112.3-160-192c-19.1-12.7-39.1-23.1-59.6-31.8l46.5-46.5c6.3 2.7 12.7 6.1 19 10.1C550.5 317.9 608 288 608 288s-55.6-112.3-160-192c-19.1-12.7-39.1-23.1-59.6-31.8l46.5-46.5c20.6 27.5 43.7 53.3 68.2 75.4C479.1 239.9 544 112 544 112s-64-128-160-128c-25.5 0-51 5.9-73.3 16.2l-46.5-46.5c-19.9 19.9-35.2 43-46.6 68.4z"/>
            </svg>
    `,

        // Toggle Button Styles
        toggleButtonStyles: {
            position: 'absolute',
            right: '10px',
            top: '50%',
            transform: 'translateY(-50%)',
            background: 'none',
            border: 'none',
            cursor: 'pointer',
            padding: '0',
            margin: '0',
            zIndex: '2',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
        },

        // Wrapper Styles
        wrapperStyles: {
            position: 'relative',
            display: 'inline-block',
            width: '100%',
        },

        // Icon Styles (if any additional styles are needed)
        iconStyles: {
            width: '20px',
            height: '20px',
            fill: '#555',
        },

        // Toggle Button Hover Color
        toggleHoverColor: '#000',

        // Match URLs (Specify the exact websites where you want to inject the script)
        // Example: Only run on example.com
        // @match rules are already defined in the metadata block above
    };

    /**
     * Utility function to create elements with specific attributes and styles.
     */
    function createElement(tag, attributes = {}, styles = {}) {
        const element = document.createElement(tag);
        for (let key in attributes) {
            if (key === 'innerHTML') {
                element.innerHTML = attributes[key];
            } else {
                element.setAttribute(key, attributes[key]);
            }
        }
        for (let style in styles) {
            element.style[style] = styles[style];
        }
        return element;
    }

    /**
     * Inject CSS styles for the toggle button and wrapper.
     */
    function injectStyles() {
        const style = document.createElement('style');
        style.innerHTML = `
            .password-toggle-button:hover .password-toggle-icon {
                fill: ${config.toggleHoverColor};
            }
        `;
        document.head.appendChild(style);
    }

    /**
     * Function to add toggle button to a password input field.
     */
    function addToggleButton(passwordInput) {
        // Prevent adding multiple toggle buttons to the same input
        if (passwordInput.parentElement.classList.contains('password-field-wrapper')) {
            return;
        }

        // Wrap the input in a relative container
        const wrapper = createElement('div', {}, config.wrapperStyles);
        passwordInput.parentNode.insertBefore(wrapper, passwordInput);
        wrapper.appendChild(passwordInput);

        // Create the toggle button
        const toggleButton = createElement('button', {
            type: 'button',
            class: 'password-toggle-button',
            'aria-label': 'Show password',
            innerHTML: config.showIcon,
        }, config.toggleButtonStyles);

        // Append the toggle button to the wrapper
        wrapper.appendChild(toggleButton);

        // Event listener for the toggle button
        toggleButton.addEventListener('click', function() {
            const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
            passwordInput.setAttribute('type', type);

            // Update the aria-label and icon
            if (type === 'password') {
                toggleButton.setAttribute('aria-label', 'Show password');
                toggleButton.innerHTML = config.showIcon;
            } else {
                toggleButton.setAttribute('aria-label', 'Hide password');
                toggleButton.innerHTML = config.hideIcon;
            }
        });
    }

    /**
     * Function to initialize the script.
     */
    function initialize() {
        injectStyles();

        // Select all password input fields
        const passwordInputs = document.querySelectorAll('input[type="password"]');

        passwordInputs.forEach(input => {
            addToggleButton(input);
        });

        // Observe for dynamically added password fields
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === 1) { // Element node
                        if (node.matches('input[type="password"]')) {
                            addToggleButton(node);
                        }

                        // Also check within the node for password inputs
                        const nestedPasswordInputs = node.querySelectorAll('input[type="password"]');
                        nestedPasswordInputs.forEach(nestedInput => {
                            addToggleButton(nestedInput);
                        });
                    }
                });
            });
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    // Initialize the script when the DOM is fully loaded
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }

})();