From 46d4bd35972130029b2520a086f8918810403c7c Mon Sep 17 00:00:00 2001 From: iw0 Date: Thu, 15 Feb 2024 20:29:38 +0100 Subject: [PATCH] convenience field focus, entering address data, a bit of jsdoc --- content_script.js | 321 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 250 insertions(+), 71 deletions(-) diff --git a/content_script.js b/content_script.js index 6c816ce..7732a5d 100644 --- a/content_script.js +++ b/content_script.js @@ -1,15 +1,44 @@ "use strict"; // Put all the javascript code here, that you want to execute after page load. +/** + * + * @typedef {Object} Stage + * @property {string} name + * @property {function.} match + * @property {function.} execute + * @property {('mutation'|'node'|undefined)} expects + */ +/** + * @type Stage + */ let currentStage; +const settings = browser.storage.sync; + +const _clickEv = () => { return new Event('click', { bubbles: true }) }; +/** @param {string} s */ +const $ = s => document.querySelector(s); +/** + * @param {Element} n + * @param {string} s + */ +const $$ = (n, s) => n.querySelector(s); +const pressKey = (...keys) => { + keys.forEach(k => { + let d = document.dispatchEvent; + d(new KeyboardEvent('keydown', { bubbles: true, key: k })); + d(new KeyboardEvent('keyup', { bubbles: true, key: k })); + }) +} +/** @param {Node} node */ function executeStage(node) { if (node.nodeType === 1) { if (currentStage === undefined) { currentStage = stages.shift(); } if (currentStage.match(node)) { - console.log(currentStage.name, "matched"); + console.log(currentStage.name, "matched: ", node); console.log(currentStage.name, currentStage.execute(node) ? "executed" : "execution failed"); if (stages.length > 0) { currentStage = stages.shift(); @@ -17,32 +46,85 @@ function executeStage(node) { observer.disconnect(); } } else { - console.log(currentStage.name, "did not match: ", node); + console.log(currentStage.name, "did not match: ", mutation); } } } let stages; -const personalDataConfigKeys = ["addr__appellation", "addr__firstName", "addr__surName", "addr__email", "addr__street", "addr__postcode", "addr__placename"]; +const personalDataConfigKeys = [ + "addr__appellation", "addr__title", "addr__firstName", "addr__surName", + "addr__email", "addr__street", "addr__postcode", "addr__placename"]; const bankDetailConfigKeys = ["pymt__iban", "pymt__bic"]; +/** + * @param {Node} n + */ +function processSingleAddedNode(n) { + if (currentStage.match(n)) { + console.log(currentStage.name, "matched: ", n); + console.log(currentStage.name, currentStage.execute(n) ? "executed" : "execution failed"); + nextStage(); + } else { + console.log(currentStage.name, "did not match: ", n); + } +} +/** + * + * @param {MutationRecord[]} mutationList + * @param {MutationObserver} observer + */ function processMutations(mutationList, observer) { + if (currentStage === undefined) { + currentStage = stages.shift(); + } for (const mutation of mutationList) { - if (mutation.type === "childList") { - mutation.addedNodes.forEach(executeStage); + if (Object.keys(currentStage).includes('expects')) { + if (currentStage.expects == 'mutation') { + if (currentStage.match(mutation)) { + console.log(currentStage.name, "matched: ", mutation); + console.log(currentStage.name, currentStage.execute(mutation) ? "executed" : "execution failed"); + nextStage(); + } else { + console.log(currentStage.name, "did not match: ", mutation); + } + } else if (currentStage.expects == 'node' && mutation.type === "childList") { + mutation.addedNodes.forEach(processSingleAddedNode); + } + } else if (mutation.type === "childList") { + //last resort + mutation.addedNodes.forEach(n => { + if (n.nodeType === Node.ELEMENT_NODE) { + processSingleAddedNode(n); + } else if (!([Node.COMMENT_NODE, Node.TEXT_NODE].includes(n.nodeType))) { + console.log("skipping node", n); + } + }); + } else { + console.log("err: could not dispatch mutation", mutation); } + } } let clickThroughForms; let hasConfiguredBankDetails, hasConfiguredPersonalData; let observer = new MutationObserver(processMutations); +function nextStage() { + if (stages.length > 0) { + currentStage = stages.shift(); + } else { + observer.disconnect(); + } +} const addObserver = () => { - browser.storage.sync.get(['autocontinue', 'enable'].concat(personalDataConfigKeys, bankDetailConfigKeys)).then(v => { - clickThroughForms = !!v.autocontinue; - hasConfiguredPersonalData = Object.keys(v).filter(k => personalDataConfigKeys.includes(k)).length > 0; - hasConfiguredBankDetails = Object.keys(v).filter(k => bankDetailConfigKeys.includes(k)).length > 0; - if (!!v.enable) { + settings.get( + ['autocontinue', 'enable'].concat(personalDataConfigKeys, bankDetailConfigKeys) + ).then(userSettings => { + clickThroughForms = !!userSettings.autocontinue; + hasConfiguredPersonalData = Object.keys(userSettings).filter(k => personalDataConfigKeys.includes(k)).length > 0; + hasConfiguredBankDetails = Object.keys(userSettings).filter(k => bankDetailConfigKeys.includes(k)).length > 0; + if (!!userSettings.enable) { observer.observe(document.body, { childList: true, subtree: true }) @@ -53,72 +135,54 @@ const addObserver = () => { addObserver(); function fillTextInput(parentNode, selector, value) { - const node = parentNode.querySelector(selector); + const node = $$(parentNode, selector); node.value = value; - node.dispatchEvent(new Event("input", { bubbles: true })); + // node.focus(); + // node.dispatchEvent(new FocusEvent("focus")); + // node.dispatchEvent(new FocusEvent("focusin", { bubbles: true })); + node.dispatchEvent(new Event("input", { bubbles: true, inputType: "insertFromPaste", data: value })); + // node.dispatchEvent(new FocusEvent("focusout", { bubbles: true })); + } const startClaim = { name: "startClaim", - match: node => node.classList.contains("antrag-starten"), + match: node => node.classList.contains("main-layout"), execute: node => { const startenButton = node.querySelector('button.test-antrag-starten-button'); if (startenButton instanceof HTMLButtonElement) { - startenButton.dispatchEvent(new Event('click', { bubbles: true })); + startenButton.dispatchEvent(_clickEv()); return true; } return false; } } -function fillBcnum(bcNumberInput) { - browser.storage.sync.get('bcnum').then(v => { - let bcNum = v.bcnum || null; - if (bcNum !== null && bcNum !== "") { - bcNumberInput.value = bcNum; - bcNumberInput.dispatchEvent(new Event('input', { bubbles: true })); - return true; - } - }) - return false; -} -function fillBday(birthdayInput) { - browser.storage.sync.get('bday').then(v => { - const bDay = v.bday || null; - if (bDay !== null && bDay !== "") { - birthdayInput.value = bDay; - birthdayInput.dispatchEvent(new Event('input', { bubbles: true })); - return true; - } - }) - return false; -} - const fillData = { name: "fillData", match: node => node.classList.contains("fahrgastrechte-bahn-card-auswahl"), execute: node => { - let bcNumField, bdayField; - node.querySelectorAll('input').forEach(e => { - if (e.name === "fahrgastrechte-bahn-card-nummer") { - bcNumField = e; - } else if (e.name === "fahrgastrechte-bahn-card-auswahl-geburts-datum") { - bdayField = e; + settings.get(["bcnum", "bday"]).then(cfg => { + for (const [k, v] of Object.entries(cfg)) { + if (k == "bcnum" && !!v) { + fillTextInput(node, '#fahrgastrechte-bahn-card-auswahl-nummer--db-web-text-input', v); + } + if (k == "bday" && !!v) { + fillTextInput(node, "#fahrgastrechte-bahn-card-auswahl-geburts-datum--db-web-text-input", v); + } } - }) - fillBcnum(bcNumField); - fillBday(bdayField); + }); return true; } } const clickContinue = { name: "clickContinue", - match: () => true, + match: () => $('#fahrgastrechte-bahn-card-auswahl-geburts-datum--db-web-text-input').value !== "", execute: e => { - const continueButton = document.querySelector('.fahrgastrechte-bahn-card-auswahl button.fahrgastrechte-continue-button'); + const continueButton = $('.fahrgastrechte-bahn-card-auswahl button.fahrgastrechte-continue-button'); if (continueButton instanceof Element) { - continueButton.dispatchEvent(new Event('click')); + continueButton.dispatchEvent(_clickEv()); return true; } return false; @@ -130,7 +194,7 @@ const iWasDelayed = { name: "iWasDelayed", match: node => node.classList.contains("antrags-typ-auswahl") && clickThroughForms, execute: node => { - const delay = node.querySelector('input#antragstyp-verspaetung'); + const delay = $$(node, 'input#antragstyp-verspaetung'); if (delay instanceof HTMLInputElement) { delay.dispatchEvent(new Event('change')); return true; @@ -148,20 +212,118 @@ const moreThan60Minutes = { const continueToForm = { name: "continueToForm", match: node => node.classList.contains("verspaetung-bestaetigung") && clickThroughForms, - execute: node => node.querySelector('button.fahrgastrechte-continue-button').dispatchEvent(new Event('click', { bubbles: true })) + execute: node => $$(node, 'button.fahrgastrechte-continue-button').dispatchEvent(_clickEv()) } -const enterPersonalData = { - name: "enterPersonalData", +const focusDepartureInput = { + name: "focusDepartureInput", + match: node => node.classList.contains("fahrplan") && clickThroughForms, + execute: node => { + const depInput = $$(node, '.fahrplan__start .fahrplan__haltestelle input'); + const obs = new IntersectionObserver((entries, intObserver) => { + if (!(entries.some(e => e.isIntersecting))) return false; + console.log("observer fired:", entries); + depInput.focus(); + intObserver.disconnect(); + }, { threshold: 1 }); + obs.observe(depInput); + return true; + } +} + +const jumpToTimeInput = { + name: "jumpToTimeInput", + match: node => node.classList.contains("ankunft-zeit") && clickThroughForms, + execute: node => { + $$(node, '#fahrgastrechte-ankunft-uhrzeit--db-web-text-input').focus(); + return true; + } +} + +const activateAppellationDropdown = { + name: "activateAppellationDropdown", match: node => node.classList.contains("persoenlicheangaben") && hasConfiguredPersonalData, execute: node => { - browser.storage.sync.get(personalDataConfigKeys).then(foundKeys => { + settings.get("addr__appellation").then(foundKeys => { console.log("storage returned", foundKeys); - //TODO the dropdowns are crazy - // if (foundKeys.keys().contains("addr__appellation")){ - // let dropDownSelectList = node.querySelector('.test-name-anrede ul.db-web-select-list'); + if (Object.keys(foundKeys).includes("addr__appellation")) { + const selectList = node.querySelector('.test-name-anrede.db-web-select'); + selectList.querySelector('button').dispatchEvent(_clickEv()); + } + }); + return true; + } +} +/**@type Stage */ +const enterAppellationAndActivateTitleDropdown = { + name: "enterAppellationAndActivateTitleDropdown", + match: node => node.classList.contains("db-web-dropdown-outer-container") && node.querySelector(".db-web-select-list") !== null, + execute: node => { + settings.get("addr__appellation").then(foundKeys => { + console.log("storage returned", foundKeys); + if (Object.keys(foundKeys).includes("addr__appellation")) { + const selectList = $$(node, "ul"); + selectList.querySelector(`[data-value=${foundKeys.addr__appellation}]`).dispatchEvent(_clickEv()); + } else { + node.parentElement.parentElement.parentElement.querySelector("button").dispatchEvent(_clickEv()); + } + $('.test-name-titel.db-web-select button').dispatchEvent(_clickEv()); + }); + return true; + } +} - // } +const enterTitle = { + name: "enterTitle", + /**@param {Element} node */ + match: node => node.classList.contains("db-web-dropdown-outer-container") && node.querySelector(".db-web-select-list") !== null, + /**@param {Element} node */ + execute: node => { + settings.get("addr__title").then(foundKeys => { + console.log("storage returned", foundKeys); + if (Object.keys(foundKeys).includes("addr__title")) { + const selectList = $$(node, "ul"); + selectList.querySelector(`[data-value=${foundKeys.addr__title}]`).dispatchEvent(_clickEv()); + } else { + node.parentElement.parentElement.parentElement.querySelector("button").dispatchEvent(_clickEv()); + } + }); + return true; + } +} +const enterFirstName = { + name: "enterFirstName", + expects: "mutation", + match: mutation => { + return mutation.target.parentNode.parentNode.classList.contains("test-name-titel") && + Array.from(mutation.removedNodes).some( + n => n.nodeType === Node.ELEMENT_NODE && + n.classList.contains("db-web-dropdown-outer-container")) + }, + execute: () => { + let node = document; + settings.get() + } +} + +/** + * @type Stage + */ +const enterTextPersonalData = { + name: "enterTextPersonalData", + expects: 'mutation', + /** @param {MutationRecord} mutation */ + match: mutation => { + return (mutation.target.parentNode.parentNode.classList.contains("test-name-titel") && + Array.from(mutation.removedNodes).some( + n => n.nodeType === Node.ELEMENT_NODE && + n.classList.contains("db-web-dropdown-outer-container"))) + }, + execute: () => { + let node = document; + let delay = 100; + settings.get(personalDataConfigKeys).then(foundKeys => { + console.log("storage returned", foundKeys); const configKey_Selector = { "addr__firstName": ".test-name-vorname input", "addr__surName": ".test-name-nachname input", @@ -170,32 +332,46 @@ const enterPersonalData = { "addr__postcode": ".test-adresse-plz input", "addr__placename": ".test-adresse-ort input" } - for (const [k, v] of Object.entries(foundKeys)) { if (Object.keys(configKey_Selector).includes(k)) { //TODO WIP this only works on some fields - console.log("filling", configKey_Selector, "with", v); - fillTextInput(node, configKey_Selector[k], v); + console.log("filling", configKey_Selector[k], "with", v); + setTimeout(() => { + fillTextInput(node, configKey_Selector[k], v) + }, delay); + delay += 100; } else { console.log("no selector found for config key", k); } } - const continueBtn = document.querySelector(".fahrgastrechte-editable__buttons button.fahrgastrechte-continue-button"); - if (continueBtn.querySelector("span span.db-web-button__label").textContent == "OK, weiter" && clickThroughForms) { - continueBtn.dispatchEvent(new Event("click", { bubbles: true })); - } + setTimeout(() => { + const continueBtn = $(".fahrgastrechte-editable__buttons button.fahrgastrechte-continue-button"); + continueBtn.focus(); + continueBtn.dispatchEvent(_clickEv()); + }, delay); return true; }) }, } +/** @type Stage */ +const continueToPayout = { + name: "continueToPayout", + expects: "mutation", + match: () => Object.is(document.activeElement, $(".fahrgastrechte-editable__buttons button.fahrgastrechte-continue-button")) + , + execute: () => document.activeElement.dispatchEvent(_clickEv()), +} + const enterPaymentDetails = { name: "enterPaymentDetails", - match: node => node.classList.contains("entschaedigung") && hasConfiguredBankDetails, + match: node => node.querySelector(".entschaedigung") && + hasConfiguredBankDetails, execute: node => { - node.querySelector('#ueberweisung').dispatchEvent(new Event('change')); - browser.storage.sync.get(bankDetailConfigKeys).then(results => { + const xfrRadio = node.querySelector('#ueberweisung'); + xfrRadio.dispatchEvent(new Event('change')); + settings.get(bankDetailConfigKeys).then(results => { console.log(results); for (const [k, v] of Object.entries(results)) { switch (k) { @@ -211,8 +387,11 @@ const enterPaymentDetails = { true }, } - const defaultStages = [ - startClaim, fillData, clickContinue, iWasDelayed, moreThan60Minutes, continueToForm, enterPersonalData, enterPaymentDetails + startClaim, fillData, clickContinue, + iWasDelayed, moreThan60Minutes, continueToForm, focusDepartureInput, jumpToTimeInput, + activateAppellationDropdown, enterAppellationAndActivateTitleDropdown, enterTitle, + enterTextPersonalData, /* continueToPayout */, enterPaymentDetails ]; -stages = defaultStages; +/** @type Stage[] */ +stages = defaultStages; \ No newline at end of file