KyleMit / Prefill MS Forms

// ==UserScript==
// @name         Prefill MS Forms
// @namespace    http://tampermonkey.net/
// @version      0.8.2
// @description  Pre-populate fields in Microsoft Forms via URL parameter
// @author       KyleMit
// @copyright    2018, KyleMit
// @license      MIT
// @match        https://forms.office.com/Pages/ResponsePage.aspx*
// @grant        none
// @homepage     https://github.com/KyleMit/CustomizeTheWeb#readme
// @supportURL   https://github.com/KyleMit/CustomizeTheWeb/issues
// ==/UserScript==

// ==OpenUserJS==
// @author       KyleMit
// ==/OpenUserJS==

/* global URLSearchParams */


(function() {
    'use strict';

    // prior art
    // [Pre-populate fields in Microsoft Forms via URL parameter?](https://techcommunity.microsoft.com/t5/microsoft-forms/p/m-p/113803)
    // [Allow pre-populated data via URL parameter](https://microsoftforms.uservoice.com/forums/386451/suggestions/18741331)

    // sample form
    // https://forms.office.com/Pages/ResponsePage.aspx?id=O5O0IK26PEOcAnDtzHVZxoUFQnFGPs9NnXgmxPQfHEFUQVBRUTQzRjc0OVNXM1VYSklHTlk3MkhMNi4u

    // 1. handle page load
    //      parse query param
    //      apply data to forms

    // 2. handle form change
    //      extract data
    //      update URL

    // let formTypes = [...document.querySelectorAll('[id$="-questiontype"]')].map(el => ({id: el.id, text: el.textContent}))

    let questionTypes = [{
            type: "Single line text",
            matches: (el) => { return el.querySelector('[id$="-questiontype"]').textContent == "Single line text" },
            getValue: (el) => { return el.querySelector("input.office-form-question-textbox").value },
            setValue: (el, val) => { el.querySelector("input.office-form-question-textbox").value = val }
        },
        {
            type: "Multi line text",
            matches: (el) => { return el.querySelector('[id$="-questiontype"]').textContent == "Multi Line Text" },
            getValue: (el) => { return el.querySelector("textarea.office-form-question-textbox").value },
            setValue: (el, val) => { el.querySelector("textarea.office-form-question-textbox").value = val }
        },
        {
            type: "Date",
            matches: (el) => { return el.querySelector('[id$="-questiontype"]').textContent == "Date" },
            getValue: (el) => { return el.querySelector("input.office-form-question-textbox").value },
            setValue: (el, val) => { el.querySelector("input.office-form-question-textbox").value = val }
        },
        {
            type: "Single choice (select)",
            matches: (el) => { return el.querySelector(".select-placeholder") },
            getValue: (el) => { return el.querySelector(".select-placeholder-text").textContent },
            setValue: (el, val) => {
                // get fake select
                let sel = el.querySelector("select-placeholder")

                // open select
                sel.click();

                // give it a second
                setTimeout(function() {
                    // get fake dropdown options
                    let options = [...el.querySelectorAll(".select-option-menu-container li")]

                    // get new option
                    let newOption = options.find(opt => opt.querySelector(".select-option-content").textContent == val)

                    // select new option
                    newOption.click()
                }, 30)

            }
        },
        {
            type: "Single choice (radio)",
            matches: (el) => { return el.querySelector(".office-form-question-choice .radio") },
            getValue: (el) => {
                let option = [...el.querySelectorAll("input[type='radio']")].find(o => o.checked)
                return option ? option.value : null
            },
            setValue: (el, val) => {
                let options = [...el.querySelectorAll("input[type='radio']")]

                // get new option
                let newOption = options.find(opt => opt.value == val)

                // set new option
                newOption.checked = true
            }
        },
        {
            type: "Multiple choice",
            matches: (el) => { return el.querySelector('[id$="-questiontype"]').textContent == "Multiple choice" },
            getValue: (el) => { return [...el.querySelectorAll("input[type='checkbox']")].filter(o => o.checked).map(o => o.value) },
            setValue: (el, val) => {
                let options = [...el.querySelectorAll("input[type='checkbox']")]

                // get new option
                let selOptions = options.filter(opt => val.includes(opt.value))

                // set new option
                selOptions.forEach(o => { o.checked = true })
            }
        },
        {
            type: "Rating",
            matches: (el) => { return el.querySelector('[id$="-questiontype"]').textContent == "Rating" },
            getValue: (el) => {
                return null
            },
            setValue: (el, val) => {
                // todo
                // // menu must be visible for events to fire https://stackoverflow.com/a/33971082/1366033
                // let range = el.querySelector(".rateit-range")
                // // only listens for mousedown event (not click) https://stackoverflow.com/a/20567978/1366033
                // range.dispatchEvent(new Event('mouseup'));

                // // menu must be visible for events to fire https://stackoverflow.com/a/33971082/1366033
                // let menu = el.querySelector(".rateit-hover")
                // menu.style.display = "flex"

                // let opts = [...el.querySelectorAll(".rateit-slected span")]
                // let opt = opts[val - 1]

                // // only listens for mousedown event (not click) https://stackoverflow.com/a/20567978/1366033
                // opt.dispatchEvent(new Event('mouseup'));

            }
        },
        {
            type: "Ranking",
            matches: (el) => { return el.querySelector('[id$="-questiontype"]').textContent == "Ranking" },
            getValue: (el) => {
                return null
            },
            setValue: (el, val) => {
                // todo
            }
        },
        {
            type: "Likert",
            matches: (el) => { return el.querySelector('[id$="-questiontype"]').textContent == "Likert" },
            getValue: (el) => {
                return null
            },
            setValue: (el, val) => {
                // todo
            }
        },
    ]

    let getQuestionNum = (el) => el.querySelector(".ordinal-number").textContent.replace(".", "")
    let getQuestionType = (el) => questionTypes.find(q => q.matches(el))


    let origUrl = window.location.pathname + window.location.search


    let updateUrlWithData = () => {
        // extract values
        let questions = [...document.querySelectorAll('.office-form-question')]
        let formDataEntries = questions.map(el => {
            let num = getQuestionNum(el)

            // get question type (determines how to extract value)
            let type = getQuestionType(el)

            let key = 'q' + num
            let value = type.getValue(el)

            return [key, value]
        })
        let formData = Object.fromEntries(formDataEntries.filter(ent => ent[1]))

        let queryParams = new URLSearchParams(formData).toString()

        let newUrl = `${origUrl}&${queryParams}`

        history.pushState({}, '', newUrl)
    }

    let updateDataWithUrl = () => {
        // get values from URL
        let urlData = Object.fromEntries(new URLSearchParams(location.search));

        // apply values for each question
        let questions = [...document.querySelectorAll('.office-form-question')]
        questions.forEach(el => {
            let num = getQuestionNum(el)

            // get question type (determines how to extract value)
            let type = getQuestionType(el)

            // get question value
            let key = 'q' + num
            let value = urlData[key]

            if (value) {
                type.setValue(el, value)
            }
        })
    }


    // handle page load event and parse data from URL into form
    // https://stackoverflow.com/a/36096571/1366033
    document.addEventListener("DOMContentLoaded", function() {})
    window.addEventListener("load", updateDataWithUrl);

    // handle form change events and push data to URL
    document.addEventListener('change', updateUrlWithData)
})();