// ==UserScript==
// @name Reddit Login Autofill helper
// @namespace http://tampermonkey.net/
// @version 2.0
// @description Make login button go to /login page, then create autofill-compliant dummy form to let Firefox autofill credentials.
// @author bedweb
// @match https://www.reddit.com/*
// @grant none
// @license GNU GPLv3
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
loginUrl: 'https://www.reddit.com/login',
mobileBreakpoint: 768,
autofillDelay: 1500,
styles: {
dummyForm: {
position: 'fixed',
top: '0px',
left: '0px',
//width: '1px',
//height: '1px',
//overflow: 'hidden',
//'z-index':'999',
opacity: '1'
}
}
};
/* Shared Utilities */
const DomUtils = {
waitForElement: (selector) => new Promise(resolve => {
if (document.querySelector(selector)) return resolve();
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}),
traverseShadow: (root = document, selector) => {
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_ELEMENT,
{ acceptNode: node => node.shadowRoot ?
NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP }
);
while (walker.nextNode()) {
const shadowRoot = walker.currentNode.shadowRoot;
const element = shadowRoot.querySelector(selector);
if (element) return element;
const nested = DomUtils.traverseShadow(shadowRoot, selector);
if (nested) return nested;
}
return null;
}
};
function cloneToLoseEventsAndIntercepts(el){
const elClone = el.cloneNode(true);
el.parentNode.replaceChild(elClone, el);
}
/* Login Button Modification */
const ButtonModifier = {
init: function() {
if (window.location.href.startsWith(CONFIG.loginUrl)) return;
const observer = new MutationObserver(() => {
const loginButton = DomUtils.traverseShadow(document, '[href*="login"]');
if (loginButton && !loginButton.href) this.convertToLink(loginButton);
});
document.querySelectorAll('faceplate-tracker[noun="login"]').forEach(tracker => {
cloneToLoseEventsAndIntercepts(tracker);
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
},
convertToLink: function(element) {
const link = document.createElement('a');
link.href = CONFIG.loginUrl;
link.innerHTML = element.innerHTML;
link.className = element.className;
link.style.cssText = element.style.cssText;
element.parentNode.replaceChild(link, element);
}
};
/* Autofill Manager */
const AutofillManager = {
init: function() {
if (!window.location.href.startsWith(CONFIG.loginUrl)) return;
this.createDummyForm();
this.setupObservers();
this.triggerAutofill();
},
createDummyForm: function() {
const form = document.createElement('form');
Object.assign(form.style, CONFIG.styles.dummyForm);
this.dummyUser = this.createInput('text', 'username');
this.dummyPass = this.createInput('password', 'current-password');
form.appendChild(this.dummyUser);
form.appendChild(this.dummyPass);
document.querySelector(".bg-ui-modalbackground").appendChild(form);
},
createInput: (type, autocomplete) => {
const input = document.createElement('input');
input.id = "dummy" + autocomplete;
input.type = type;
input.autocomplete = autocomplete;
//input.autocapitalize = 'none';
input.style = 'background-color:white;color:black';
return input;
},
setupObservers: function() {
this.observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
//if (mutation.attributeName === 'value') {
this.transferValues();
//}
});
});
this.observer.observe(this.dummyUser, { attributes: true });
this.observer.observe(this.dummyPass, { attributes: true });
},
transferValues: function() {
const transferField = (dummy, name) => {
const real = DomUtils.traverseShadow(document, `input[name="${name}"]`);
if (real && dummy.value) {
real.value = dummy.value;
real.dispatchEvent(new Event('input', { bubbles: true }));
real.dispatchEvent(new Event('change', { bubbles: true }));
}
};
transferField(this.dummyUser, 'username');
transferField(this.dummyPass, 'password');
},
triggerAutofill: function() {
const isMobile = window.innerWidth <= CONFIG.mobileBreakpoint;
setTimeout(() => {
this.dummyUser.focus();
setTimeout(() => this.dummyPass.focus(), isMobile ? 100 : 50);
}, isMobile ? 1000 : 500);
}
};
/* Main Execution Flow */
window.addEventListener('load', () => {
const isMobile = window.innerWidth <= CONFIG.mobileBreakpoint;
setTimeout(() => {
ButtonModifier.init();
AutofillManager.init();
}, isMobile ? 1000 : 500);
}, { once: true });
})();