Skip to content

Commit 9aa6b7f

Browse files
committed
widget: fix #227 (0.1.44)
1 parent 3df3202 commit 9aa6b7f

5 files changed

Lines changed: 67 additions & 3 deletions

File tree

widget/src/cap.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ interface CapConfig {
5656
"data-cap-i18n-verified-aria-label"?: string;
5757
"data-cap-i18n-error-aria-label"?: string;
5858
"data-cap-i18n-wasm-disabled"?: string;
59+
"data-cap-i18n-required-label"?: string;
5960
"data-cap-troubleshooting-url"?: string;
61+
required?: boolean | "";
6062
onsolve?: string;
6163
onprogress?: string;
6264
onreset?: string;

widget/src/cap.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

widget/src/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cap.js/widget",
3-
"version": "0.1.43",
3+
"version": "0.1.44",
44
"description": "Client-side widget for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.",
55
"keywords": [
66
"algorithm",

widget/src/src/cap.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,25 @@
210210
display: none;
211211
}
212212

213+
.captcha.invalid {
214+
animation: cap-shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97);
215+
border-color: var(--cap-invalid-border-color, #f55b50);
216+
box-shadow: 0 0 0 3px var(--cap-invalid-ring-color, rgba(245, 91, 80, 0.2));
217+
}
218+
219+
@keyframes cap-shake {
220+
10%, 90% { transform: translateX(-1px); }
221+
20%, 80% { transform: translateX(2px); }
222+
30%, 50%, 70% { transform: translateX(-4px); }
223+
40%, 60% { transform: translateX(4px); }
224+
}
225+
226+
@media (prefers-reduced-motion: reduce) {
227+
.captcha.invalid {
228+
animation: none !important;
229+
}
230+
}
231+
213232
.captcha[disabled] {
214233
cursor: not-allowed;
215234
}

widget/src/src/cap.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@
297297
}
298298

299299
class CapWidget extends HTMLElement {
300+
static formAssociated = true;
300301
#resetTimer = null;
301302
#workersCount = navigator.hardwareConcurrency || 8;
302303
token = null;
@@ -305,6 +306,7 @@
305306
#host;
306307
#solving = false;
307308
#eventHandlers;
309+
#internals;
308310

309311
#speculative = null;
310312
#speculativeTimer = null;
@@ -564,6 +566,7 @@
564566
"onerror",
565567
"data-cap-worker-count",
566568
"data-cap-i18n-initial-state",
569+
"required",
567570
];
568571
}
569572

@@ -580,6 +583,23 @@
580583
this.boundHandleSolve = this.handleSolve.bind(this);
581584
this.boundHandleError = this.handleError.bind(this);
582585
this.boundHandleReset = this.handleReset.bind(this);
586+
587+
try {
588+
this.#internals = this.attachInternals();
589+
} catch {}
590+
}
591+
592+
#updateValidity() {
593+
if (!this.#internals?.setValidity) return;
594+
if (this.hasAttribute("required") && !this.token) {
595+
this.#internals.setValidity(
596+
{ valueMissing: true },
597+
this.getI18nText("required-label", "Please verify you're human"),
598+
this.#div || this,
599+
);
600+
} else {
601+
this.#internals.setValidity({});
602+
}
583603
}
584604

585605
initialize() {
@@ -624,6 +644,10 @@
624644
) {
625645
this.animateLabel(this.getI18nText("initial-state", "Verify you're human"));
626646
}
647+
648+
if (name === "required") {
649+
this.#updateValidity();
650+
}
627651
}
628652

629653
async connectedCallback() {
@@ -642,8 +666,24 @@
642666
this.#host.innerHTML = `<input type="hidden" name="${fieldName}">`;
643667

644668
this.#attachInteractionListeners();
669+
this.#updateValidity();
670+
671+
this.addEventListener("invalid", this.#handleInvalid);
645672
}
646673

674+
#handleInvalid = () => {
675+
if (!this.#div) return;
676+
try {
677+
this.scrollIntoView({ behavior: "smooth", block: "center" });
678+
} catch {
679+
this.scrollIntoView();
680+
}
681+
this.#div.classList.remove("invalid");
682+
void this.#div.offsetWidth;
683+
this.#div.classList.add("invalid");
684+
setTimeout(() => this.#div?.classList.remove("invalid"), 1500);
685+
};
686+
647687
async solve() {
648688
if (this.#solving) {
649689
return;
@@ -1142,6 +1182,8 @@
11421182
handleSolve(event) {
11431183
this.updateUI("done", this.getI18nText("solved-label", "You're a human"), true);
11441184
this.executeAttributeCode("onsolve", event);
1185+
this.#internals?.setValidity?.({});
1186+
this.#div?.classList.remove("invalid");
11451187
}
11461188

11471189
handleError(event) {
@@ -1154,6 +1196,7 @@
11541196
handleReset(event) {
11551197
this.updateUI("", this.getI18nText("initial-state", "I'm a human"));
11561198
this.executeAttributeCode("onreset", event);
1199+
this.#updateValidity();
11571200
}
11581201

11591202
executeAttributeCode(attributeName, event) {
@@ -1188,8 +1231,8 @@
11881231
clearTimeout(this.#resetTimer);
11891232
this.#resetTimer = null;
11901233
}
1191-
this.dispatchEvent("reset");
11921234
this.token = null;
1235+
this.dispatchEvent("reset");
11931236
const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
11941237
if (this.querySelector(`input[name='${fieldName}']`)) {
11951238
this.querySelector(`input[name='${fieldName}']`).value = "";

0 commit comments

Comments
 (0)