// ==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 // @downloadURL none // ==/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: `
华东师大二附中“创意·创新·创造”大赛
-- 刘怀轩 东昌南校 初三2班
#{slogan}
-- #{author}
可用的测试页面:
#{testURL} `, confInput: (zone, name, hint) => ` ${name[0].toUpperCase() + name.slice(1)} ${hint} `, confApply: (zone) => ``, conf: ` Configuration
Shortcuts 快捷键
#{confInput | sc | leader | 引导}
#{confInput | sc | toggle | 开关浮窗}
#{confInput | sc | mark | 标记}
#{confInput | sc | fill | 填充}
#{confInput | sc | rset | 清存}
#{confInput | sc | conf | 设置}
#{confInput | sc | info | 关于}
#{confApply | sc}