Greasy Fork

Greasy Fork is available in English.

WhereIsMyForm

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

当前为 2020-11-15 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         WhereIsMyForm
// @namespace    https://github.com/ForkFG
// @version      0.3.1
// @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) {
        const hide = this.css("display") === "none"
        if (type === "fadeio")
            type = hide ? "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".
        return c => c(! hide, this)
    }
})

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)(hide => ts.quit = hide)
        },
        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)
        ts.quit = 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)

    const $r = $(".WIMF"), $m = $(".WIMF-main"), $w = $(unsafeWindow)
    if (ts.quit) $r.hide()
    UI.move()

    $(".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({
        quit: false,
        top: 0,
        right: 0,
        sc: {
            leader: "Alt-w",
            toggle: "&q",
            mark: "&m",
            fill: "&f",
            rset: "&r",
            conf: "&c",
            info: "&i"
        }
    })

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