// ==UserScript== // @name WhereIsMyForm // @namespace https://github.com/ForkFG // @version 0.3.2 // @description 管理你的表单,不让他们走丢。适用场景:问卷,发帖,…… // @author ForkKILLET // @match *://*/* // @noframes // @grant unsafeWindow // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @require https://code.jquery.com/jquery-1.11.0.min.js // @downloadURL none // ==/UserScript== const $ = this.$ // Debug: Hack eslint warnings in TM editor. const debug = true function expose(o) { if (debug) for (let i in o) unsafeWindow[i] = o[i] } function Throw(msg, detail) { msg = `[WIMF] ${msg}` arguments.length === 2 ? console.error(msg + "\n%o", detail) : console.error(msg) } // Note: `dat.xxx.yyy = zzz` doesn't work. Now have to use `dat._.xxx_yyy = zzz`. function Dat({ getter, setter, useWrapper, getW, setW, dataW }) { const pn = (p, n) => p ? p + "_" + n : n function dat(opt, src = dat, p) { const R = src === dat, r = new Proxy(src, useWrapper ? { get: (__, k) => { if (k === "_" && R) return _ return _[pn(p, k)] }, set: (__, k, v) => { if (k === "_" && R) Throw("[Dat] Set _.") _[pn(p, k)] = v } } : { get: (_, k) => getter(pn(p, k), k), set: (_, k, v) => setter(pn(p, k), k, v) } ) for (let n in opt) { if (typeof opt[n] === "object" && ! Array.isArray(opt[n])) { if (r[n] === undefined) r[n] = {} src[n] = dat(opt[n], src[n], pn(p, n)) } else if (r[n] === undefined) r[n] = opt[n] } return r } 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 ] if (now == null) Throw("[Dat]: Saw undefined when _.") return _parse(idx + 1, now[k]) } return _parse(0, src) } const _ = useWrapper ? new Proxy({}, { get: (_, p) => { const r = parse(p, getW()) return r[0][r[1]] }, set: (_, p, v) => { const d = getW(), r = parse(p, d) r[0][r[1]] = v setW(dataW ? dataW(d) : d) } }) : null return dat } const ts = Dat({ useWrapper: true, getW: () => GM_getValue("app") ?? {}, setW: v => GM_setValue("app", v) })({ window: { state: "open", top: 0, right: 0, }, key: { leader: "Alt-w", shortcut: { toggle: "&q", mark: "&m", fill: "&f", rset: "&r", conf: "&c", info: "&i" } }, operation: {} })._ 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 op = Dat({ getter: (_, n) => { if (n === "here") n = location.origin + location.pathname return ts.operation[n] ?? [] }, setter: (_, n, v) => { if (n === "here") n = location.origin + location.pathname const o = ts.operation o[n] = v; ts.operation = o } })({}) $.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, a, b) { const v = this.css("display") === "none" if (type === "fadeio") type = v ? "fadein" : "fadeout" if (b == null) b = type === "fadein" ? "show" : "" if (a == null) a = type === "fadein" ? "" : "hide" this[b]() this.css("animation", `melting-${type} ${time}s`) time *= 1000 setTimeout(() => this[a](), time > 100 ? time - 100 : time * 0.9) // Note: A bit shorter than the animation duration for avoid "flash back". return v }, ""() {} }) function scan({ hl, root } = { root: "body" }) { const o = op.here, u = () => { op.here = o } 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 o) { if (o[i].type === "text" && o[i].path === path) { o[i].val = val f = false; break } } if (f) o.push({ path, val, type: "text" }) u() }) $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 o) { if (o[i].type === "radio") { if (o[i].path === path){ f = false; break } // Note: Replace the old choice. if ($(o[i].path).attr("name") === $_.attr("name")) { o[i].path = path f = false; break } } } if (f) o.push({ path, label, type: "radio" }) u() }) $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 o) { if (o[i].type === "checkbox" && o[i].path === path){ f = false; break } } if (f) o.push({ path, label, type: "checkbox" }) u() }) 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), $r = $(".WIMF"), sc = ts.key_shortcut, lk = ts.key_leader, 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 === "" ? lk : i) const c_k = { toggle() { ts.window_state = $(".WIMF").melt("fadeio", 1.5) ? "open" : "close" }, 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: `
华东师大二附中“创意·创新·创造”大赛
-- 刘怀轩 东昌南校 初三2班
#{slogan}
-- #{author}
可用的测试页面:
#{testURL} `, confInput: (zone, name, hint) => ` ${name.replace(/^[a-z]+_/, "").replace(/^./, c => c.toUpperCase())} ${hint} `, confApply: (zone) => ``, conf: ` Configuration
Key 按键
#{confInput | key | leader | 引导}
#{confInput | key | shortcut_toggle | 开关浮窗}
#{confInput | key | shortcut_mark | 标记}
#{confInput | key | shortcut_fill | 填充}
#{confInput | key | shortcut_rset | 清存}
#{confInput | key | shortcut_conf | 设置}
#{confInput | key | shortcut_info | 关于}
#{confApply | key}