diff options
Diffstat (limited to 'static')
| -rw-r--r-- | static/script/captcha.js | 143 | ||||
| -rw-r--r-- | static/style/captcha.css | 76 |
2 files changed, 219 insertions, 0 deletions
diff --git a/static/script/captcha.js b/static/script/captcha.js new file mode 100644 index 0000000..78dcb3d --- /dev/null +++ b/static/script/captcha.js @@ -0,0 +1,143 @@ +textXform = { + "a": "\u{10900}", + "b": "\u{10901}", + "c": "\u{10902}", + "d": "\u{10903}", + "e": "\u{10904}", + "f": "\u{10905}", + "g": "\u{10906}", + "h": "\u{10907}", + "i": "\u{10908}", + "j": "\u{10909}", + "k": "\u{1090A}", + "l": "\u{1090B}", + "m": "\u{1090C}", + "n": "\u{1090D}", + "o": "\u{1090E}", + "p": "\u{1090F}", + "q": "\u{10910}", + "r": "\u{10911}", + "s": "\u{10912}", + "t": "\u{10913}", + "u": "\u{10914}", + "v": "\u{10915}", + ".": "\u{1091F}", +} + +class DroseraCaptcha extends HTMLElement { + constructor() { + super(); + } + onSquareClick(event) { + const square = event.target; + if (square.dataset.active === undefined) { + square.dataset.active = "1"; + } else if (square.dataset.active == "1") { + square.dataset.active = "2"; + } else if (square.dataset.active == "2") { + square.dataset.active = "3"; + } else { + delete square.dataset.active; + } + event.preventDefault(); + } + onSquareReset(event) { + const square = event.target; + delete square.dataset.active; + event.preventDefault(); + } + onSquareContext(event) { + event.preventDefault(); + } + onResetCaptcha(event) { + const grid = event.target.parentNode.getElementsByClassName("dsc-inner")[0]; + for (const child of grid.childNodes) { + if (!child.disabled) { + delete child.dataset.active; + } + } + } + onSubmitCaptcha(event) { + const grid = event.target.parentNode.getElementsByClassName("dsc-inner")[0]; + const output = grid.childNodes.values().map(square => square.dataset.active ?? "0").join(""); + const params = new URLSearchParams({ + size: this.size, + label: this.label, + lock: this.lock, + answer: output + }) + console.log("/captcha?" + params.toString()) + const request = new Request("/captcha?" + params.toString()); + this.outer.disabled = true; + window.fetch(request).then(response => { + if (response.ok) { + this.outer.disabled = false; + this.outer.dataset.solved = ""; + if (this.reload) { + location.reload(); + } + } else { + this.outer.disabled = false; + } + }) + } + connectedCallback() { + const shadow = this.attachShadow({ mode: "open" }); + + const linkElem = document.createElement("link"); + linkElem.setAttribute("rel", "stylesheet"); + linkElem.setAttribute("href", "/static/style/captcha.css"); + shadow.appendChild(linkElem); + + const outer = document.createElement("fieldset"); + outer.className = "dsc-outer"; + if (this.dataset.solved !== undefined) { + outer.dataset.solved = ""; + } + this.reload = this.dataset.reload !== undefined; + + const resetBtn = document.createElement("button"); + resetBtn.innerText = "Reset" + resetBtn.addEventListener("click", this.onResetCaptcha); + const submitBtn = document.createElement("button"); + submitBtn.className = "dsc-submit"; + submitBtn.innerText = "Submit" + submitBtn.addEventListener("click", this.onSubmitCaptcha.bind(this)); + + const inner = document.createElement("div"); + inner.style.gridTemplateColumns = `repeat(${this.dataset.size}, 1fr)` + inner.className = "dsc-inner"; + for (var alp = 0; alp < this.dataset.size; alp++) { + for (var bet = 0; bet < this.dataset.size; bet++) { + const square = document.createElement("button"); + square.tabIndex = 0; + square.addEventListener("click", this.onSquareClick); + square.addEventListener("auxclick", this.onSquareReset); + square.addEventListener("contextmenu", this.onSquareContext); + const label = document.createElement("span"); + const shift = alp * this.dataset.size + bet; + label.innerText = textXform[this.dataset.label.at(shift)]; + const lock = this.dataset.lock.at(shift); + if (lock != ".") { + square.dataset.active = lock; + square.disabled = true; + } + square.appendChild(label); + inner.appendChild(square); + } + } + + this.size = this.dataset.size; + this.label = this.dataset.label; + this.lock = this.dataset.lock; + this.outer = outer; + this.inner = inner; + + outer.appendChild(resetBtn); + outer.appendChild(inner); + outer.appendChild(submitBtn); + shadow.appendChild(outer); + } +} + +customElements.define("drosera-captcha", DroseraCaptcha);
\ No newline at end of file diff --git a/static/style/captcha.css b/static/style/captcha.css new file mode 100644 index 0000000..fe7c5d3 --- /dev/null +++ b/static/style/captcha.css @@ -0,0 +1,76 @@ +.dsc-outer { + --active-1: oklch(75% 0.15 195.032); + --active-2: oklch(75% 0.15 328.437); + --active-3: oklch(75% 0.15 109.374); + transform: rotate(-45deg) scale(0.7); + width: fit-content; + height: fit-content; + margin: auto; + display: grid; + gap: 0.5em; + grid-template-columns: auto auto; + padding: 0.5em; + border: none; + + &[data-solved] { + outline: 2px solid var(--active-1); + } + + >button { + font-size: 1.5em; + + &.dsc-submit { + writing-mode: sideways-rl; + } + } +} + +button { + border: 2px outset #000; + background-color: #eee; + + &:active { + border: 2px inset #000; + } +} + +.dsc-inner { + grid-column-start: 1; + border: 2px inset #000; + display: grid; + gap: 0.5em; + padding: 0.5em; + + >button { + height: 2em; + width: 2em; + aspect-ratio: 1 / 1; + display: flex; + justify-content: center; + align-items: center; + font-size: 2em; + + >span { + transform: rotate(45deg); + pointer-events: none; + } + + &:disabled { + border: 2px solid #000; + outline: 1px solid #000; + color: #000; + } + + &[data-active="1"] { + background-color: var(--active-1); + } + + &[data-active="2"] { + background-color: var(--active-2); + } + + &[data-active="3"] { + background-color: var(--active-3); + } + } +}
\ No newline at end of file |