Typeracer Floating Input

try to make long texts doable!

// ==UserScript==
// @name         Typeracer Floating Input
// @namespace    http://tampermonkey.net/
// @version      0.2.1
// @description  try to make long texts doable!
// @author       Ted Morin
// @match        https://play.typeracer.com/*
// @grant        none
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// ==/UserScript==
/* jshint asi:true */

var lastCalled = Date.now()
var setupLoopRunning = false
var lastTargetOffset = 0
var inputPanel = function() { return $('table.inputPanel') }
var activeWord = function() { return $('.nonHideableWords > span:nth-child(2)') }
var completedText = function() { return $('.nonHideableWords > span:nth-child(1)') }
var inputBox = function() { return $('.inputPanel > tbody > tr:nth-child(2) > td:first-child') }
var commonPrefixLength = function(first, second) {
    for (var i = 0; i < first.length; i++) {
        if (first[i] !== second[i]) {
            return i
        }
    }
    return first.length
}
var inputBoxInput = function() { return $('.inputPanel input.txtInput') }
var updateProgressBar = function() {
    var progress = $('.floating-progress-bar')
    if (!progress.length) {
        inputBox().prepend('<div class="progress-bar-container"><div class="floating-progress-bar"></div></div>')
        var container = $('.progress-bar-container')
        container.css('width', '98%')
        container.css('border', '1px solid #eee')
        container.css('margin', '0 auto')
        container.css('margin-top', '3px')
        progress = $('.floating-progress-bar')
        progress.css('height', '5px')
        progress.css('background-color', 'rgba(50, 50, 50, 0.5)')
        progress.css('transition', 'all 150ms')
    }
    progress.css('width', quotePercentage() + '%')
    return progress
}
var quotePercentage = function() {
    var totalQuoteCharacters = $('.nonHideableWords').text().trim().length
    var completedQuoteCharacters = completedText().text().length
    var currentWordProgress = commonPrefixLength(activeWord().text(), inputBoxInput().val())
    return 100 * (completedQuoteCharacters + currentWordProgress) / totalQuoteCharacters
}
var relativeActiveWordOffset = function() {
    var inputPanelOffset = inputPanel().offset()
    var activeWordOffset = activeWord().offset()
    if (!inputPanelOffset || !activeWordOffset) return {}
    return { top: activeWordOffset.top - inputPanelOffset.top, left: activeWordOffset.left - inputPanelOffset.left }
}

var setup = function() {
    setupLoopRunning = true
    inputPanel().css('position', 'relative')
    inputBox().css('position', 'absolute')
    inputBox().css('width', '98%')
    inputBox().css('background-color', 'white')
    inputBox().css('transition', 'all 100ms')
    inputBox().css('box-shadow', '0px 0px 5px')
    inputBoxInput().on('keyup', moveInputBox)
    inputBoxInput().css('margin-left', '5px')
    if (!lastTargetOffset) {
        moveInputBox()
        setTimeout(setup, 1000)
    } else {
        setupLoopRunning = false
    }

}
var moveInputBox = function() {
    // Prevent lag by only making the modification within 50ms
    if ((Date.now() - lastCalled) < 50) return
    lastCalled = Date.now()

    var relativeOffset = relativeActiveWordOffset()
    var inputBoxOffset = inputBox().css('top')
    if (relativeOffset.top) {
        var targetTop = relativeOffset.top + 60
        if (inputBoxOffset.top !== targetTop) {
            lastTargetOffset = targetTop
            inputBox().css('top', targetTop)
        }
    }
    setTimeout(updateProgressBar, 50)
}

;(function() {
    'use strict';
    // Set whole quote + input table to position relative
    setup()
    setInterval(function() {
        if (inputBox().css('position') !== 'absolute' && !setupLoopRunning) {
            lastTargetOffset = 0
            setup()
        }
    }, 2000)
})();