WhereIsMyForm

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

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

  1. // ==UserScript==
  2. // @name WhereIsMyForm
  3. // @namespace https://github.com/ForkFG
  4. // @version 0.3
  5. // @description 管理你的表单,不让他们走丢。适用场景:问卷,发帖,……
  6. // @author ForkKILLET
  7. // @match *://*/*
  8. // @noframes
  9. // @grant unsafeWindow
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_listValues
  14. // @require https://code.jquery.com/jquery-1.11.0.min.js
  15. // ==/UserScript==
  16.  
  17. function Throw(msg, detail) {
  18. msg = `[WIMF] ${msg}`
  19. arguments.length === 2
  20. ? console.error(msg + "\n%o", detail)
  21. : console.error(msg)
  22. }
  23.  
  24. function Dat({ getter, setter, useWrapper, getW, setW, dataW }) {
  25. function dat(opt, src = dat, path) {
  26. for (let n in opt) {
  27. const p = path ? path + "." + n : n
  28. Object.defineProperty(src, n, useWrapper
  29. ? {
  30. get: () => dat._[p],
  31. set: v => dat._[p] = v
  32. }
  33. : {
  34. get: () => getter(p, n),
  35. set: v => setter(p, n, v)
  36. }
  37. )
  38. if (typeof opt[n] === "object" && ! Array.isArray(opt[n])) {
  39. if (src[n] == null) src[n] = {}
  40. dat(opt[n], dat[n], p)
  41. }
  42. else if (src[n] == null) src[n] = opt[n]
  43. }
  44. }
  45.  
  46. function parse(path, src = dat) {
  47. const keys = path.split("."), len = keys.length
  48. function _parse(idx, now) {
  49. let k = keys[idx]
  50. if (len - idx <= 1) return [ now, k ]
  51. return _parse(idx + 1, now[k])
  52. }
  53. return _parse(0, src)
  54. }
  55.  
  56. dat._ = new Proxy(dat, {
  57. get: (_, path) => {
  58. const r = parse(path, getW())
  59. return r[0][r[1]]
  60. },
  61. set: (_, path, val) => {
  62. const d = getW(), r = parse(path, d)
  63. r[0][r[1]] = val
  64. setW(dataW ? dataW(d) : d)
  65. }
  66. })
  67.  
  68. return dat
  69. }
  70.  
  71. const ls = Dat({
  72. useWrapper: true,
  73. getW: () => JSON.parse(unsafeWindow.localStorage.getItem("WIMF") ?? "{}"),
  74. setW: v => unsafeWindow.localStorage.setItem("WIMF", v),
  75. dataW: v => JSON.stringify(v)
  76. })
  77. const ts = Dat({
  78. useWrapper: true,
  79. getW: () => GM_getValue("app") ?? {},
  80. setW: v => GM_setValue("app", v)
  81. })
  82.  
  83. $.fn.extend({
  84. path() {
  85. // Note: Too strict. We need a smarter path.
  86. // It doesn't work on dynamic pages sometimes.
  87. return (function _path(e, p = "", f = true) {
  88. if (! e) return p
  89. const $e = $(e), t = e.tagName.toLowerCase()
  90. let pn = t
  91. if (e.id) pn += `#${e.id}`
  92. if (e.name) pn += `[name=${e.name}]`
  93. if (! e.id && $e.parent().children(t).length > 1) pn += `:nth-of-type(${
  94. $e.prevAll(t).length + 1
  95. })`
  96. return _path(e.parentElement, pn + (f ? "" : `>${p}`), false)
  97. })(this[0])
  98. },
  99. one(event, func) {
  100. return this.off(event).on(event, func)
  101. },
  102. forWhat() {
  103. if (! this.is("label")) return null
  104. let for_ = this.attr("for")
  105. if (for_) return $(`#${for_}`)
  106. for (let i of [ "prev", "next", "children" ]) {
  107. let $i = this[i]("input[type=checkbox]")
  108. if ($i.length) return $i
  109. }
  110. return null
  111. },
  112. melt(type, time, rm) {
  113. if (type === "fadeio")
  114. type = this.css("display") === "none" ? "fadein" : "fadeout"
  115. if (type === "fadein") this.show()
  116. this.css("animation", `melting-${type} ${time}s`)
  117. time *= 1000
  118. setTimeout(() => {
  119. if (type !== "fadein") rm ? this.remove() : this.hide()
  120. }, time > 100 ? time - 100 : time * 0.9)
  121. // Note: A bit shorter than the animation duration for avoid "flash back".
  122. }
  123. })
  124.  
  125. function scan({ hl, root } = {
  126. root: "body"
  127. }) {
  128. const op = ls.op
  129.  
  130. const $t = $(`${root} input[type=text],textarea`),
  131. $r = $(`${root} input[type=radio],label`),
  132. $c = $(`${root} input[type=checkbox],label`),
  133. $A = [ $t, $r, $c ]
  134.  
  135. $t.one("change.WIMF", function() {
  136. const $_ = $(this), path = $_.path(), val = $_.val()
  137. let f = true; for (let i in op) {
  138. if (op[i].type === "text" && op[i].path === path){
  139. op[i].val = val
  140. f = false; break
  141. }
  142. }
  143. if (f) op.push({ path, val, type: "text" })
  144. ls.op = op
  145. })
  146. $r.one("click.WIMF", function() {
  147. let $_ = $(this)
  148. let path = $_.path(), label
  149. if ($_.is("label")) {
  150. label = path
  151. $_ = $_.forWhat()
  152. path = $_.path()
  153. }
  154. if (! $_.is("[type=radio]")) return
  155.  
  156. let f = true; for (let i in op) {
  157. if (op[i].type === "radio") {
  158. if (op[i].path === path){
  159. f = false; break
  160. }
  161. // Note: Replace the old choice.
  162. if ($(op[i].path).attr("name") === $_.attr("name")) {
  163. op[i].path = path
  164. f = false; break
  165. }
  166. }
  167. }
  168. if (f) op.push({ path, label, type: "radio" })
  169. ls.op = op
  170. })
  171. $c.one("click.WIMF", function() {
  172. let $_ = $(this)
  173. let path = $_.path(), label
  174. if ($_.is("label")) {
  175. label = path
  176. $_ = $_.forWhat()
  177. path = $_.path()
  178. }
  179. if (! $_.is("[type=checkbox]")) return
  180.  
  181. let f = true; for (let i in op)
  182. if (op[i].type === "checkbox" && op[i].path === path){
  183. f = false; break
  184. }
  185. if (f) op.push({ path, label, type: "checkbox" })
  186. ls.op = op
  187. })
  188.  
  189. if (typeof hl === "function") for (let $i of $A) hl($i)
  190. }
  191.  
  192. function shortcut() {
  193. let t_pk
  194. const pk = []
  195. pk.last = () => pk[pk.length - 1]
  196.  
  197. const $w = $(unsafeWindow), sc = ts.sc,
  198. sc_rm = () => {
  199. for (let i in sc) sc[i].m = 0
  200. },
  201. ct = () => {
  202. clearTimeout(t_pk)
  203. pk.splice(0)
  204. pk.sdk = false
  205. t_pk = null
  206. sc_rm()
  207. },
  208. st = () => {
  209. clearTimeout(t_pk)
  210. t_pk = setTimeout(ct, 800)
  211. }
  212.  
  213. for (let i in sc) sc[i] = sc[i].split("&").map(i => i === "" ? sc.leader[0] : i)
  214. const c_k = {
  215. toggle: () => $(".WIMF").melt("fadeio", 1.5),
  216. mark: UI.action.mark,
  217. fill: UI.action.fill,
  218. rset: UI.action.rset,
  219. conf: UI.action.conf,
  220. info: UI.action.info
  221. }
  222.  
  223. ct()
  224. $w.one("keydown.WIMF", e => {
  225. st(); let ck = "", sdk = false
  226. for (let dk of [ "alt", "ctrl", "shift", "meta" ]) {
  227. if (e[dk + "Key"]) {
  228. ck += dk = dk[0].toUpperCase() + dk.slice(1)
  229. if (e.key === dk || e.key === "Control") {
  230. sdk = true; break
  231. }
  232. ck += "-"
  233. }
  234. }
  235. if (! sdk) ck += e.key.toLowerCase()
  236.  
  237. if (pk.sdk && ck.includes(pk.last())) {
  238. pk.pop()
  239. }
  240. pk.sdk = sdk
  241. pk.push(ck)
  242.  
  243. for (let i in sc) {
  244. const k = sc[i]
  245. if (k.m === k.length) continue
  246. if (k[k.m] === ck) {
  247. if (++k.m === k.length) {
  248. if (i !== "leader") ct()
  249. if (c_k[i]) c_k[i]()
  250. }
  251. }
  252. else if (pk.sdk && k[k.m].includes(ck)) ;
  253. else k.m = 0
  254. }
  255. })
  256. }
  257.  
  258. const UI = {}
  259. UI.meta = {
  260. author: "ForkKILLET",
  261. slogan: "管理你的表单,不让他们走丢",
  262. aboutCompetition: `
  263. <p>华东师大二附中“创意·创新·创造”大赛 <br/>
  264. <i>-- 刘怀轩 东昌南校 初三2
  265. </p>`,
  266.  
  267. mainButton: (name, emoji) => `
  268. <span class="WIMF-button" name="${name}">${emoji}</span>
  269. `,
  270. html: `
  271. <div class="WIMF">
  272. <div class="WIMF-main">
  273. <b class="WIMF-title">WhereIsMyForm</b>
  274. #{mainButton | mark 标记 | 🔍}
  275. #{mainButton | fill 填充 | 📃}
  276. #{mainButton | rset 清存 | 🗑️}
  277. #{mainButton | conf 设置 | ⚙️}
  278. #{mainButton | info 关于 | ℹ️}
  279. #{mainButton | quit 退出 | ❌}
  280. </div>
  281. <div class="WIMF-text"></div>
  282. <div class="WIMF-task"></div>
  283. </div>
  284. `,
  285. testURL: "https://www.wjx.cn/newsurveys.aspx",
  286. info: `
  287. <b class="WIMF-title">Infomation</b> <br/>
  288.  
  289. <p>#{slogan} <br/>
  290. <i>-- #{author}</i>
  291. </p> <br/> <br/>
  292.  
  293. #{aboutCompetition} <br/> <br/>
  294.  
  295. <p>可用的测试页面:</p>
  296. <a href="#{testURL}">#{testURL}</a>
  297. `,
  298. confInput: (zone, name, hint) => `
  299. ${name[0].toUpperCase() + name.slice(1)} ${hint}
  300. <input type="text" name="${zone}_${name}"/>
  301. `,
  302. confApply: (zone) => `<button data-zone="${zone}">OK</button>`,
  303. conf: `
  304. <b class="WIMF-title">Configuration</b> <br/>
  305.  
  306. <p>
  307. <b>Shortcuts 快捷键</b> <br/>
  308. #{confInput | sc | leader | 引导}
  309. #{confInput | sc | toggle | 开关浮窗}
  310. #{confInput | sc | mark | 标记}
  311. #{confInput | sc | fill | 填充}
  312. #{confInput | sc | rset | 清存}
  313. #{confInput | sc | conf | 设置}
  314. #{confInput | sc | info | 关于}
  315. #{confApply | sc}
  316. </p>
  317. `,
  318. styl: `
  319. /* :: Animation */
  320.  
  321. @keyframes melting-sudden {
  322. 0%, 70% { opacity: 1; }
  323. 100% { opacity: 0; }
  324. }
  325. @keyframes melting-fadeout {
  326. 0% { opacity: 1; }
  327. 100% { opacity: 0; }
  328. }
  329. @keyframes melting-fadein {
  330. 0% { opacity: 0; }
  331. 100% { opacity: 1; }
  332. }
  333.  
  334. /* :: Skeleton */
  335.  
  336. .WIMF {
  337. position: fixed;
  338. z-index: 1919810;
  339. user-select: none;
  340.  
  341. opacity: 1;
  342. transition: top 1s, right 1s;
  343. transform: scale(.9);
  344. }
  345. .WIMF, .WIMF * {
  346. box-sizing: content-box;
  347. }
  348.  
  349. .WIMF-main, .WIMF-text, .WIMF-task p {
  350. width: 100px;
  351.  
  352. padding: 0 3px 0 4.5px;
  353. border-radius: 12px;
  354. font-size: 12px;
  355. background-color: #fff;
  356. box-shadow: 0 0 4px #aaa;
  357. }
  358. .WIMF-main {
  359. position: absolute;
  360. top: 0;
  361. right: 0;
  362. height: 80px;
  363. }
  364.  
  365. .WIMF-task {
  366. position: absolute;
  367. top: 0;
  368. right: 115px;
  369. }
  370.  
  371. /* :: Modification */
  372.  
  373. .WIMF-mark {
  374. background-color: #ffff81;
  375. }
  376. .WIMF-title {
  377. display: block;
  378. text-align: center;
  379. }
  380.  
  381. /* :: Cell */
  382.  
  383. .WIMF-main::after { /* Note: A cover. */
  384. position: absolute;
  385. width: 100%;
  386. height: 100%;
  387. top: 0;
  388. left: 0;
  389. pointer-events: none;
  390.  
  391. content: "";
  392. border-radius: 12px;
  393. background-color: black;
  394.  
  395. opacity: 0;
  396. transition: opacity .8s;
  397. }
  398. .WIMF-main.dragging::after {
  399. opacity: .5;
  400. }
  401.  
  402. .WIMF-button {
  403. display: inline-block;
  404. width: 17px;
  405. height: 17px;
  406.  
  407. padding: 2px 3px 3px 3px;
  408. margin: 3px;
  409.  
  410. border-radius: 7px;
  411. font-size: 12px;
  412. text-align: center;
  413. box-shadow: 0 0 3px #bbb;
  414.  
  415. background-color: #fff;
  416. transition: background-color .8s;
  417. }
  418. .WIMF-button:hover, .WIMF-button.active {
  419. background-color: #bbb;
  420. }
  421. .WIMF-button:hover::before {
  422. position: absolute;
  423. right: 114px;
  424. width: 75px;
  425.  
  426. content: attr(name);
  427. padding: 0 3px;
  428.  
  429. font-size: 14px;
  430. border-radius: 4px;
  431. background-color: #fff;
  432. box-shadow: 0 0 4px #aaa;
  433. }
  434.  
  435. .WIMF-text {
  436. position: absolute;
  437. display: none;
  438. top: 85px;
  439. right: 0;
  440. height: 300px;
  441.  
  442. overflow: -moz-scrollbars-none;
  443. overflow-y: scroll;
  444. -ms-overflow-style: none;
  445. }
  446. .WIMF-text::-webkit-scrollbar {
  447. display: none;
  448. }
  449. .WIMF-text a {
  450. overflow-wrap: anywhere;
  451. }
  452.  
  453. .WIMF-text input {
  454. width: 95px;
  455. margin: 3px 0;
  456.  
  457. border: none;
  458. border-radius: 3px;
  459. outline: none;
  460. box-shadow: 0 0 3px #aaa;
  461. }
  462.  
  463. .WIMF-text button {
  464. margin: 3px 0;
  465. padding: 0 5px;
  466.  
  467. border: none;
  468. border-radius: 3px;
  469. outline: none;
  470.  
  471. box-shadow: 0 0 3px #aaa;
  472. background-color: #fff;
  473. transition: background-color .8s;
  474. }
  475. .WIMF-text button:hover {
  476. background-color: #bbb;
  477. }
  478.  
  479. .WIMF-task p {
  480. margin-bottom: 3px;
  481. background-color: #9f9;
  482. }
  483. `
  484. }
  485. UI.M = new Proxy(UI.meta, {
  486. get: (t, n) => t[n].replace(/#{(.*?)}/g, (_, s) => {
  487. const [ k, ...a ] = s.split(/ *\| */), m = t[k]
  488. if (a.length && typeof m === "function") return m(...a)
  489. return m
  490. })
  491. })
  492.  
  493. UI.$btn = n => $(`.WIMF-button[name^=${n}]`)
  494. UI.action = {
  495. mark() {
  496. const $b = UI.$btn("mark")
  497. if ($b.is(".active")) {
  498. $(".WIMF-mark").removeClass("WIMF-mark")
  499. UI.task("表单高亮已取消。", "Form highlight is canceled.")
  500. }
  501. else {
  502. scan({
  503. hl: $i => $i.addClass("WIMF-mark")
  504. })
  505. UI.task("表单已高亮。", "Forms are highlighted.")
  506. }
  507. $b.toggleClass("active")
  508. },
  509. fill() {
  510. let c = 0; for (let o of ls.op) {
  511. const $i = $(o.path)
  512. if (! $i.length) Throw("Form path not found")
  513. switch (o.type) {
  514. case "text":
  515. $i.val(o.val)
  516. break
  517. case "radio":
  518. case "checkbox":
  519. // Hack: HTMLElement:.click is stabler than $.click sometimes.
  520. // If user clicks <label> instead of <input>, we also do that.
  521. if (o.label) $(o.label)[0].click()
  522. else $i[0].click()
  523. break
  524. default:
  525. Throw("Unknown form type.")
  526. }
  527. c++
  528. }
  529. UI.task(`已填充 ${c} 个表单项。`, `${c} form field(s) is filled.`)
  530. },
  531. rset() {
  532. ls.op = []
  533. UI.task("保存的表单已清除。", "Saved form is reset.")
  534. },
  535. conf() {
  536. UI.text.show("conf")
  537.  
  538. const $A = $(".WIMF-text button")
  539. for (let i = 0; i < $A.length; i++) {
  540. const $b = $($A[i]),
  541. zone = $b.data("zone"),
  542. $t = $b.prevAll(`input[type=text][name^=${zone}_]`),
  543. c_b = {
  544. sc: shortcut
  545. }
  546.  
  547. function map(it) {
  548. for (let j = $t.length - 1; j >= 0; j--) {
  549. const $e = $($t[j]), sp = $e.attr("name").replace("_", ".")
  550. it($e, sp)
  551. }
  552. }
  553. map(($_, sp) => $_.val(ts._[sp]))
  554. $b.on("click", () => {
  555. map(($_, sp) => ts._[sp] = $_.val())
  556. if (c_b[zone]) c_b[zone]()
  557. UI.task(`设置块 ${zone} 已应用。`, `Configuration zone ${zone} is applied.`)
  558. })
  559. }
  560. },
  561. info() {
  562. UI.text.show("info")
  563. },
  564. quit() {
  565. $(".WIMF").melt("fadeout", 1.5, true)
  566. },
  567. back() {
  568. $(".WIMF-text").hide()
  569. UI.$btn("back").attr("name", "quit 退出")
  570. UI.hideText()
  571. }
  572. }
  573. UI.text = {
  574. hide: () => UI.$btn(UI.textActive).removeClass("active"),
  575. show: (n) => {
  576. UI.text.hide()
  577. $(".WIMF-text").show().html(UI.M[n])
  578. UI.$btn(n).addClass("active")
  579. UI.textActive = n
  580. UI.$btn("quit").attr("name", "back 返回")
  581. }
  582. }
  583. UI.task = (m) => $(`<p>${m}</p>`).prependTo($(".WIMF-task")).melt("sudden", 3, true)
  584. UI.move = (t, r) => {
  585. if (t != null) ts.top = Math.max(t, 0)
  586. if (r != null) ts.right = Math.max(r, 0)
  587. $(".WIMF").css("top", ts.top + "px").css("right", ts.right + "px")
  588. }
  589. UI.init = () => {
  590. GM_addStyle(UI.M.styl)
  591. $("body").after(UI.M.html)
  592. UI.move()
  593.  
  594. const $m = $(".WIMF-main"), $w = $(unsafeWindow)
  595.  
  596. $(".WIMF-button").on("click", function() {
  597. UI.action[$(this).attr("name").split(" ")[0]]()
  598. })
  599.  
  600. $m.on("mousedown", e => {
  601. const { clientX: x0, clientY: y0 } = e
  602.  
  603. $w.on("mouseup", finish)
  604.  
  605. let c = false
  606. const t_f = setTimeout(finish, 1800),
  607. t_c = setTimeout(() => {
  608. c = true
  609. $m.addClass("dragging")
  610. }, 200) // Note: Differentiate from clickings.
  611.  
  612. function finish(f) {
  613. clearTimeout(t_f); clearTimeout(t_c)
  614. if (c && f) {
  615. const { clientX: x1, clientY: y1 } = f,
  616. dx = x1 - x0, dy = y1 - y0
  617. UI.move(ts.top + dy, ts.right - dx)
  618. }
  619. if (c) $m.removeClass("dragging").off("mousemove")
  620. $w.off("mouseup")
  621. }
  622. })
  623. }
  624.  
  625. $(function init() {
  626. ls({
  627. op: []
  628. })
  629. ts({
  630. top: 0,
  631. right: 0,
  632. sc: {
  633. leader: "Alt-w",
  634. toggle: "&q",
  635. mark: "&m",
  636. fill: "&f",
  637. rset: "&r",
  638. conf: "&c",
  639. info: "&i"
  640. }
  641. })
  642.  
  643. UI.init()
  644. scan()
  645. shortcut()
  646. })
  647.  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址