Greasy Fork

Greasy Fork is available in English.

WhereIsMyForm

管理你的表单,不让他们走丢。适用场景:问卷,发帖,……

目前为 2020-11-15 提交的版本。查看 最新版本

// ==UserScript==
// @name         WhereIsMyForm
// @namespace    https://github.com/ForkFG
// @version      0.3
// @description  管理你的表单,不让他们走丢。适用场景:问卷,发帖,……
// @author       ForkKILLET
// @match        *://*/*
// @noframes
// @grant        unsafeWindow
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_listValues
// @require      https://code.jquery.com/jquery-1.11.0.min.js
// ==/UserScript==

function Throw(msg, detail) {
    msg = `[WIMF] ${msg}`
    arguments.length === 2
        ? console.error(msg + "\n%o", detail)
        : console.error(msg)
}

function Dat({ getter, setter, useWrapper, getW, setW, dataW }) {
    function dat(opt, src = dat, path) {
        for (let n in opt) {
            const p = path ? path + "." + n : n
            Object.defineProperty(src, n, useWrapper
                ? {
                    get: () => dat._[p],
                    set: v => dat._[p] = v
                }
                : {
                    get: () => getter(p, n),
                    set: v => setter(p, n, v)
                }
            )
            if (typeof opt[n] === "object" && ! Array.isArray(opt[n])) {
                if (src[n] == null) src[n] = {}
                dat(opt[n], dat[n], p)
            }
            else if (src[n] == null) src[n] = opt[n]
        }
    }

    function parse(path, src = dat) {
        const keys = path.split("."), len = keys.length
        function _parse(idx, now) {
            let k = keys[idx]
            if (len - idx <= 1) return [ now, k ]
            return _parse(idx + 1, now[k])
        }
        return _parse(0, src)
    }

    dat._ = new Proxy(dat, {
        get: (_, path) => {
            const r = parse(path, getW())
            return r[0][r[1]]
        },
        set: (_, path, val) => {
            const d = getW(), r = parse(path, d)
            r[0][r[1]] = val
            setW(dataW ? dataW(d) : d)
        }
    })

    return dat
}

const ls = Dat({
    useWrapper: true,
    getW: () => JSON.parse(unsafeWindow.localStorage.getItem("WIMF") ?? "{}"),
    setW: v => unsafeWindow.localStorage.setItem("WIMF", v),
    dataW: v => JSON.stringify(v)
})
const ts = Dat({
    useWrapper: true,
    getW: () => GM_getValue("app") ?? {},
    setW: v => GM_setValue("app", v)
})

$.fn.extend({
    path() {
        // Note: Too strict. We need a smarter path.
        //       It doesn't work on dynamic pages sometimes.
        return (function _path(e, p = "", f = true) {
            if (! e) return p
            const $e = $(e), t = e.tagName.toLowerCase()
            let pn = t
            if (e.id) pn += `#${e.id}`
            if (e.name) pn += `[name=${e.name}]`
            if (! e.id && $e.parent().children(t).length > 1) pn += `:nth-of-type(${
                $e.prevAll(t).length + 1
            })`
            return _path(e.parentElement, pn + (f ? "" : `>${p}`), false)
        })(this[0])
    },
    one(event, func) {
        return this.off(event).on(event, func)
    },
    forWhat() {
        if (! this.is("label")) return null
        let for_ = this.attr("for")
        if (for_) return $(`#${for_}`)
        for (let i of [ "prev", "next", "children" ]) {
            let $i = this[i]("input[type=checkbox]")
            if ($i.length) return $i
        }
        return null
    },
    melt(type, time, rm) {
        if (type === "fadeio")
            type = this.css("display") === "none" ? "fadein" : "fadeout"
        if (type === "fadein") this.show()
        this.css("animation", `melting-${type} ${time}s`)
        time *= 1000
        setTimeout(() => {
            if (type !== "fadein") rm ? this.remove() : this.hide()
        }, time > 100 ? time - 100 : time * 0.9)
        // Note: A bit shorter than the animation duration for avoid "flash back".
    }
})

function scan({ hl, root } = {
    root: "body"
}) {
    const op = ls.op

    const $t = $(`${root} input[type=text],textarea`),
          $r = $(`${root} input[type=radio],label`),
          $c = $(`${root} input[type=checkbox],label`),
          $A = [ $t, $r, $c ]

    $t.one("change.WIMF", function() {
        const $_ = $(this), path = $_.path(), val = $_.val()
        let f = true; for (let i in op) {
            if (op[i].type === "text" && op[i].path === path){
                op[i].val = val
                f = false; break
            }
        }
        if (f) op.push({ path, val, type: "text" })
        ls.op = op
    })
    $r.one("click.WIMF", function() {
        let $_ = $(this)
        let path = $_.path(), label
        if ($_.is("label")) {
            label = path
            $_ = $_.forWhat()
            path = $_.path()
        }
        if (! $_.is("[type=radio]")) return

        let f = true; for (let i in op) {
            if (op[i].type === "radio") {
                if (op[i].path === path){
                    f = false; break
                }
                // Note: Replace the old choice.
                if ($(op[i].path).attr("name") === $_.attr("name")) {
                    op[i].path = path
                    f = false; break
                }
            }
        }
        if (f) op.push({ path, label, type: "radio" })
        ls.op = op
    })
    $c.one("click.WIMF", function() {
        let $_ = $(this)
        let path = $_.path(), label
        if ($_.is("label")) {
            label = path
            $_ = $_.forWhat()
            path = $_.path()
        }
        if (! $_.is("[type=checkbox]")) return

        let f = true; for (let i in op)
            if (op[i].type === "checkbox" && op[i].path === path){
                f = false; break
            }
        if (f) op.push({ path, label, type: "checkbox" })
        ls.op = op
    })

    if (typeof hl === "function") for (let $i of $A) hl($i)
}

function shortcut() {
    let t_pk
    const pk = []
    pk.last = () => pk[pk.length - 1]

    const $w = $(unsafeWindow), sc = ts.sc,
          sc_rm = () => {
              for (let i in sc) sc[i].m = 0
          },
          ct = () => {
              clearTimeout(t_pk)
              pk.splice(0)
              pk.sdk =  false
              t_pk = null
              sc_rm()
          },
          st = () => {
              clearTimeout(t_pk)
              t_pk = setTimeout(ct, 800)
          }

    for (let i in sc) sc[i] = sc[i].split("&").map(i => i === "" ? sc.leader[0] : i)
    const c_k = {
        toggle: () => $(".WIMF").melt("fadeio", 1.5),
        mark: UI.action.mark,
        fill: UI.action.fill,
        rset: UI.action.rset,
        conf: UI.action.conf,
        info: UI.action.info
    }

    ct()
    $w.one("keydown.WIMF", e => {
        st(); let ck = "", sdk = false
        for (let dk of [ "alt", "ctrl", "shift", "meta" ]) {
            if (e[dk + "Key"]) {
                ck += dk = dk[0].toUpperCase() + dk.slice(1)
                if (e.key === dk || e.key === "Control") {
                    sdk = true; break
                }
                ck += "-"
            }
        }
        if (! sdk) ck += e.key.toLowerCase()

        if (pk.sdk && ck.includes(pk.last())) {
            pk.pop()
        }
        pk.sdk = sdk
        pk.push(ck)

        for (let i in sc) {
            const k = sc[i]
            if (k.m === k.length) continue
            if (k[k.m] === ck) {
                if (++k.m === k.length) {
                    if (i !== "leader") ct()
                    if (c_k[i]) c_k[i]()
                }
            }
            else if (pk.sdk && k[k.m].includes(ck)) ;
            else k.m = 0
        }
    })
}

const UI = {}
UI.meta = {
    author: "ForkKILLET",
    slogan: "管理你的表单,不让他们走丢",
    aboutCompetition: `
<p>华东师大二附中“创意·创新·创造”大赛 <br/>
    <i>-- 刘怀轩 东昌南校 初三2班
</p>`,

    mainButton: (name, emoji) => `
<span class="WIMF-button" name="${name}">${emoji}</span>
`,
    html: `
<div class="WIMF">
    <div class="WIMF-main">
        <b class="WIMF-title">WhereIsMyForm</b>
        #{mainButton | mark 标记 | 🔍}
        #{mainButton | fill 填充 | 📃}
        #{mainButton | rset 清存 | 🗑️}
        #{mainButton | conf 设置 | ⚙️}
        #{mainButton | info 关于 | ℹ️}
        #{mainButton | quit 退出 | ❌}
    </div>
    <div class="WIMF-text"></div>
    <div class="WIMF-task"></div>
</div>
`,
    testURL: "https://www.wjx.cn/newsurveys.aspx",
    info: `
<b class="WIMF-title">Infomation</b> <br/>

<p>#{slogan} <br/>
    <i>-- #{author}</i>
</p> <br/> <br/>

#{aboutCompetition} <br/> <br/>

<p>可用的测试页面:</p>
<a href="#{testURL}">#{testURL}</a>
`,
    confInput: (zone, name, hint) => `
${name[0].toUpperCase() + name.slice(1)} ${hint}
<input type="text" name="${zone}_${name}"/>
`,
    confApply: (zone) => `<button data-zone="${zone}">OK</button>`,
    conf: `
<b class="WIMF-title">Configuration</b> <br/>

<p>
    <b>Shortcuts 快捷键</b> <br/>
    #{confInput | sc | leader | 引导}
    #{confInput | sc | toggle | 开关浮窗}
    #{confInput | sc | mark | 标记}
    #{confInput | sc | fill | 填充}
    #{confInput | sc | rset | 清存}
    #{confInput | sc | conf | 设置}
    #{confInput | sc | info | 关于}
    #{confApply | sc}
</p>
`,
    styl: `
/* :: Animation */

@keyframes melting-sudden {
    0%, 70% { opacity: 1; }
    100% { opacity: 0; }
}
@keyframes melting-fadeout {
    0% { opacity: 1; }
    100% { opacity: 0; }
}
@keyframes melting-fadein {
    0% { opacity: 0; }
    100% { opacity: 1; }
}

/* :: Skeleton */

.WIMF {
    position: fixed;
    z-index: 1919810;
    user-select: none;

    opacity: 1;
    transition: top 1s, right 1s;
    transform: scale(.9);
}
.WIMF, .WIMF * {
    box-sizing: content-box;
}

.WIMF-main, .WIMF-text, .WIMF-task p {
    width: 100px;

    padding: 0 3px 0 4.5px;
    border-radius: 12px;
    font-size: 12px;
    background-color: #fff;
    box-shadow: 0 0 4px #aaa;
}
.WIMF-main {
    position: absolute;
    top: 0;
    right: 0;
    height: 80px;
}

.WIMF-task {
    position: absolute;
    top: 0;
    right: 115px;
}

/* :: Modification */

.WIMF-mark {
    background-color: #ffff81;
}
.WIMF-title {
    display: block;
    text-align: center;
}

/* :: Cell */

.WIMF-main::after { /* Note: A cover. */
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    pointer-events: none;

    content: "";
    border-radius: 12px;
    background-color: black;

    opacity: 0;
    transition: opacity .8s;
}
.WIMF-main.dragging::after {
    opacity: .5;
}

.WIMF-button {
    display: inline-block;
    width: 17px;
    height: 17px;

    padding: 2px 3px 3px 3px;
    margin: 3px;

    border-radius: 7px;
    font-size: 12px;
    text-align: center;
    box-shadow: 0 0 3px #bbb;

    background-color: #fff;
    transition: background-color .8s;
}
.WIMF-button:hover, .WIMF-button.active {
    background-color: #bbb;
}
.WIMF-button:hover::before {
    position: absolute;
    right: 114px;
    width: 75px;

    content: attr(name);
    padding: 0 3px;

    font-size: 14px;
    border-radius: 4px;
    background-color: #fff;
    box-shadow: 0 0 4px #aaa;
}

.WIMF-text {
    position: absolute;
    display: none;
    top: 85px;
    right: 0;
    height: 300px;

    overflow: -moz-scrollbars-none;
    overflow-y: scroll;
    -ms-overflow-style: none;
}
.WIMF-text::-webkit-scrollbar {
    display: none;
}
.WIMF-text a {
    overflow-wrap: anywhere;
}

.WIMF-text input {
    width: 95px;
    margin: 3px 0;

    border: none;
    border-radius: 3px;
    outline: none;
    box-shadow: 0 0 3px #aaa;
}

.WIMF-text button {
    margin: 3px 0;
    padding: 0 5px;

    border: none;
    border-radius: 3px;
    outline: none;

    box-shadow: 0 0 3px #aaa;
    background-color: #fff;
    transition: background-color .8s;
}
.WIMF-text button:hover {
    background-color: #bbb;
}

.WIMF-task p {
    margin-bottom: 3px;
    background-color: #9f9;
}
`
}
UI.M = new Proxy(UI.meta, {
    get: (t, n) => t[n].replace(/#{(.*?)}/g, (_, s) => {
        const [ k, ...a ] = s.split(/ *\| */), m = t[k]
        if (a.length && typeof m === "function") return m(...a)
        return m
    })
})

UI.$btn = n => $(`.WIMF-button[name^=${n}]`)
UI.action = {
    mark() {
        const $b = UI.$btn("mark")
        if ($b.is(".active")) {
            $(".WIMF-mark").removeClass("WIMF-mark")
            UI.task("表单高亮已取消。", "Form highlight is canceled.")
        }
        else {
            scan({
                hl: $i => $i.addClass("WIMF-mark")
            })
            UI.task("表单已高亮。", "Forms are highlighted.")
        }
        $b.toggleClass("active")
    },
    fill() {
        let c = 0; for (let o of ls.op) {
            const $i = $(o.path)
            if (! $i.length) Throw("Form path not found")
            switch (o.type) {
                case "text":
                    $i.val(o.val)
                    break
                case "radio":
                case "checkbox":
                    // Hack: HTMLElement:.click is stabler than $.click sometimes.
                    //       If user clicks <label> instead of <input>, we also do that.
                    if (o.label) $(o.label)[0].click()
                    else $i[0].click()
                    break
                default:
                    Throw("Unknown form type.")
            }
            c++
        }
        UI.task(`已填充 ${c} 个表单项。`, `${c} form field(s) is filled.`)
    },
    rset() {
        ls.op = []
        UI.task("保存的表单已清除。", "Saved form is reset.")
    },
    conf() {
        UI.text.show("conf")

        const $A = $(".WIMF-text button")
        for (let i = 0; i < $A.length; i++) {
            const $b = $($A[i]),
                  zone = $b.data("zone"),
                  $t = $b.prevAll(`input[type=text][name^=${zone}_]`),
                  c_b = {
                      sc: shortcut
                  }

            function map(it) {
                for (let j = $t.length - 1; j >= 0; j--) {
                    const $e = $($t[j]), sp = $e.attr("name").replace("_", ".")
                    it($e, sp)
                }
            }
            map(($_, sp) => $_.val(ts._[sp]))
            $b.on("click", () => {
                map(($_, sp) => ts._[sp] = $_.val())
                if (c_b[zone]) c_b[zone]()
                UI.task(`设置块 ${zone} 已应用。`, `Configuration zone ${zone} is applied.`)
            })
        }
    },
    info() {
        UI.text.show("info")
    },
    quit() {
        $(".WIMF").melt("fadeout", 1.5, true)
    },
    back() {
        $(".WIMF-text").hide()
        UI.$btn("back").attr("name", "quit 退出")
        UI.hideText()
    }
}
UI.text = {
    hide: () => UI.$btn(UI.textActive).removeClass("active"),
        show: (n) => {
        UI.text.hide()
        $(".WIMF-text").show().html(UI.M[n])
        UI.$btn(n).addClass("active")
        UI.textActive = n
        UI.$btn("quit").attr("name", "back 返回")
    }
}
UI.task = (m) => $(`<p>${m}</p>`).prependTo($(".WIMF-task")).melt("sudden", 3, true)
UI.move = (t, r) => {
    if (t != null) ts.top = Math.max(t, 0)
    if (r != null) ts.right = Math.max(r, 0)
    $(".WIMF").css("top", ts.top + "px").css("right", ts.right + "px")
}
UI.init = () => {
    GM_addStyle(UI.M.styl)
    $("body").after(UI.M.html)
    UI.move()

    const $m = $(".WIMF-main"), $w = $(unsafeWindow)

    $(".WIMF-button").on("click", function() {
         UI.action[$(this).attr("name").split(" ")[0]]()
    })

    $m.on("mousedown", e => {
        const { clientX: x0, clientY: y0 } = e

        $w.on("mouseup", finish)

        let c = false
        const t_f = setTimeout(finish, 1800),
              t_c = setTimeout(() => {
            c = true
            $m.addClass("dragging")
        }, 200) // Note: Differentiate from clickings.

        function finish(f) {
            clearTimeout(t_f); clearTimeout(t_c)
            if (c && f) {
                const { clientX: x1, clientY: y1 } = f,
                      dx = x1 - x0, dy = y1 - y0
                UI.move(ts.top + dy, ts.right - dx)
            }
            if (c) $m.removeClass("dragging").off("mousemove")
            $w.off("mouseup")
        }
    })
}

$(function init() {
    ls({
        op: []
    })
    ts({
        top: 0,
        right: 0,
        sc: {
            leader: "Alt-w",
            toggle: "&q",
            mark: "&m",
            fill: "&f",
            rset: "&r",
            conf: "&c",
            info: "&i"
        }
    })

    UI.init()
    scan()
    shortcut()
})