jdzcaptcha 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/cli/jpack.js +3 -0
  4. package/config/jpack.js +129 -0
  5. package/config/jpack.template +5 -0
  6. package/config/jpack.wrapper.js +16 -0
  7. package/dist/assets/jdzcaptcha/placeholder.png +0 -0
  8. package/dist/assets/jdzcaptcha/streamline/light/icon-1.png +0 -0
  9. package/dist/assets/jdzcaptcha/streamline/light/icon-10.png +0 -0
  10. package/dist/assets/jdzcaptcha/streamline/light/icon-11.png +0 -0
  11. package/dist/assets/jdzcaptcha/streamline/light/icon-12.png +0 -0
  12. package/dist/assets/jdzcaptcha/streamline/light/icon-13.png +0 -0
  13. package/dist/assets/jdzcaptcha/streamline/light/icon-14.png +0 -0
  14. package/dist/assets/jdzcaptcha/streamline/light/icon-15.png +0 -0
  15. package/dist/assets/jdzcaptcha/streamline/light/icon-16.png +0 -0
  16. package/dist/assets/jdzcaptcha/streamline/light/icon-17.png +0 -0
  17. package/dist/assets/jdzcaptcha/streamline/light/icon-18.png +0 -0
  18. package/dist/assets/jdzcaptcha/streamline/light/icon-19.png +0 -0
  19. package/dist/assets/jdzcaptcha/streamline/light/icon-2.png +0 -0
  20. package/dist/assets/jdzcaptcha/streamline/light/icon-20.png +0 -0
  21. package/dist/assets/jdzcaptcha/streamline/light/icon-21.png +0 -0
  22. package/dist/assets/jdzcaptcha/streamline/light/icon-22.png +0 -0
  23. package/dist/assets/jdzcaptcha/streamline/light/icon-23.png +0 -0
  24. package/dist/assets/jdzcaptcha/streamline/light/icon-24.png +0 -0
  25. package/dist/assets/jdzcaptcha/streamline/light/icon-25.png +0 -0
  26. package/dist/assets/jdzcaptcha/streamline/light/icon-26.png +0 -0
  27. package/dist/assets/jdzcaptcha/streamline/light/icon-27.png +0 -0
  28. package/dist/assets/jdzcaptcha/streamline/light/icon-28.png +0 -0
  29. package/dist/assets/jdzcaptcha/streamline/light/icon-29.png +0 -0
  30. package/dist/assets/jdzcaptcha/streamline/light/icon-3.png +0 -0
  31. package/dist/assets/jdzcaptcha/streamline/light/icon-30.png +0 -0
  32. package/dist/assets/jdzcaptcha/streamline/light/icon-31.png +0 -0
  33. package/dist/assets/jdzcaptcha/streamline/light/icon-32.png +0 -0
  34. package/dist/assets/jdzcaptcha/streamline/light/icon-33.png +0 -0
  35. package/dist/assets/jdzcaptcha/streamline/light/icon-34.png +0 -0
  36. package/dist/assets/jdzcaptcha/streamline/light/icon-35.png +0 -0
  37. package/dist/assets/jdzcaptcha/streamline/light/icon-36.png +0 -0
  38. package/dist/assets/jdzcaptcha/streamline/light/icon-37.png +0 -0
  39. package/dist/assets/jdzcaptcha/streamline/light/icon-38.png +0 -0
  40. package/dist/assets/jdzcaptcha/streamline/light/icon-39.png +0 -0
  41. package/dist/assets/jdzcaptcha/streamline/light/icon-4.png +0 -0
  42. package/dist/assets/jdzcaptcha/streamline/light/icon-40.png +0 -0
  43. package/dist/assets/jdzcaptcha/streamline/light/icon-41.png +0 -0
  44. package/dist/assets/jdzcaptcha/streamline/light/icon-42.png +0 -0
  45. package/dist/assets/jdzcaptcha/streamline/light/icon-43.png +0 -0
  46. package/dist/assets/jdzcaptcha/streamline/light/icon-44.png +0 -0
  47. package/dist/assets/jdzcaptcha/streamline/light/icon-45.png +0 -0
  48. package/dist/assets/jdzcaptcha/streamline/light/icon-46.png +0 -0
  49. package/dist/assets/jdzcaptcha/streamline/light/icon-47.png +0 -0
  50. package/dist/assets/jdzcaptcha/streamline/light/icon-48.png +0 -0
  51. package/dist/assets/jdzcaptcha/streamline/light/icon-49.png +0 -0
  52. package/dist/assets/jdzcaptcha/streamline/light/icon-5.png +0 -0
  53. package/dist/assets/jdzcaptcha/streamline/light/icon-50.png +0 -0
  54. package/dist/assets/jdzcaptcha/streamline/light/icon-6.png +0 -0
  55. package/dist/assets/jdzcaptcha/streamline/light/icon-7.png +0 -0
  56. package/dist/assets/jdzcaptcha/streamline/light/icon-8.png +0 -0
  57. package/dist/assets/jdzcaptcha/streamline/light/icon-9.png +0 -0
  58. package/dist/public/css/jdzcaptcha.min.css +1 -0
  59. package/dist/public/js/jdzcaptcha.min.js +2 -0
  60. package/lib/iconsets/streamline/Icons by Streamline.txt +5 -0
  61. package/lib/iconsets/streamline/light/icon-1.png +0 -0
  62. package/lib/iconsets/streamline/light/icon-10.png +0 -0
  63. package/lib/iconsets/streamline/light/icon-11.png +0 -0
  64. package/lib/iconsets/streamline/light/icon-12.png +0 -0
  65. package/lib/iconsets/streamline/light/icon-13.png +0 -0
  66. package/lib/iconsets/streamline/light/icon-14.png +0 -0
  67. package/lib/iconsets/streamline/light/icon-15.png +0 -0
  68. package/lib/iconsets/streamline/light/icon-16.png +0 -0
  69. package/lib/iconsets/streamline/light/icon-17.png +0 -0
  70. package/lib/iconsets/streamline/light/icon-18.png +0 -0
  71. package/lib/iconsets/streamline/light/icon-19.png +0 -0
  72. package/lib/iconsets/streamline/light/icon-2.png +0 -0
  73. package/lib/iconsets/streamline/light/icon-20.png +0 -0
  74. package/lib/iconsets/streamline/light/icon-21.png +0 -0
  75. package/lib/iconsets/streamline/light/icon-22.png +0 -0
  76. package/lib/iconsets/streamline/light/icon-23.png +0 -0
  77. package/lib/iconsets/streamline/light/icon-24.png +0 -0
  78. package/lib/iconsets/streamline/light/icon-25.png +0 -0
  79. package/lib/iconsets/streamline/light/icon-26.png +0 -0
  80. package/lib/iconsets/streamline/light/icon-27.png +0 -0
  81. package/lib/iconsets/streamline/light/icon-28.png +0 -0
  82. package/lib/iconsets/streamline/light/icon-29.png +0 -0
  83. package/lib/iconsets/streamline/light/icon-3.png +0 -0
  84. package/lib/iconsets/streamline/light/icon-30.png +0 -0
  85. package/lib/iconsets/streamline/light/icon-31.png +0 -0
  86. package/lib/iconsets/streamline/light/icon-32.png +0 -0
  87. package/lib/iconsets/streamline/light/icon-33.png +0 -0
  88. package/lib/iconsets/streamline/light/icon-34.png +0 -0
  89. package/lib/iconsets/streamline/light/icon-35.png +0 -0
  90. package/lib/iconsets/streamline/light/icon-36.png +0 -0
  91. package/lib/iconsets/streamline/light/icon-37.png +0 -0
  92. package/lib/iconsets/streamline/light/icon-38.png +0 -0
  93. package/lib/iconsets/streamline/light/icon-39.png +0 -0
  94. package/lib/iconsets/streamline/light/icon-4.png +0 -0
  95. package/lib/iconsets/streamline/light/icon-40.png +0 -0
  96. package/lib/iconsets/streamline/light/icon-41.png +0 -0
  97. package/lib/iconsets/streamline/light/icon-42.png +0 -0
  98. package/lib/iconsets/streamline/light/icon-43.png +0 -0
  99. package/lib/iconsets/streamline/light/icon-44.png +0 -0
  100. package/lib/iconsets/streamline/light/icon-45.png +0 -0
  101. package/lib/iconsets/streamline/light/icon-46.png +0 -0
  102. package/lib/iconsets/streamline/light/icon-47.png +0 -0
  103. package/lib/iconsets/streamline/light/icon-48.png +0 -0
  104. package/lib/iconsets/streamline/light/icon-49.png +0 -0
  105. package/lib/iconsets/streamline/light/icon-5.png +0 -0
  106. package/lib/iconsets/streamline/light/icon-50.png +0 -0
  107. package/lib/iconsets/streamline/light/icon-6.png +0 -0
  108. package/lib/iconsets/streamline/light/icon-7.png +0 -0
  109. package/lib/iconsets/streamline/light/icon-8.png +0 -0
  110. package/lib/iconsets/streamline/light/icon-9.png +0 -0
  111. package/lib/index.less +5 -0
  112. package/lib/js/captcha.js +182 -0
  113. package/lib/js/constants.js +61 -0
  114. package/lib/js/fetch.js +51 -0
  115. package/lib/js/ui.js +117 -0
  116. package/lib/js/utils.js +159 -0
  117. package/lib/js/widget.js +624 -0
  118. package/lib/less/animations.less +45 -0
  119. package/lib/less/structure.less +259 -0
  120. package/lib/less/variables.less +2 -0
  121. package/lib/less/variants/dark.less +62 -0
  122. package/lib/less/variants/light.less +66 -0
  123. package/lib/placeholder.png +0 -0
  124. package/package.json +37 -0
@@ -0,0 +1,624 @@
1
+ import { CSS, defaults } from './constants.js';
2
+ import { Utils } from './utils.js';
3
+ import { UI } from './ui.js';
4
+ import Fetch from './fetch.js';
5
+
6
+ export class Widget {
7
+ /**
8
+ * Creates a new Widget instance.
9
+ * @param {HTMLElement} $element The DOM element to generate the widget into.
10
+ * @param {Object} options An object containing the configuration options for the widget.
11
+ */
12
+ constructor($element, options) {
13
+ // ignore the element if it is not a valid DOM element
14
+ if (!$element) {
15
+ Utils.warn('Element is not a valid DOM element.');
16
+ return null;
17
+ }
18
+
19
+ // Ensure the `path` option is set
20
+ if (!options.path) {
21
+ this.error('The option "path" has not been set.');
22
+ return null;
23
+ }
24
+
25
+ // ignore the element if it is not a valid DOM element
26
+ if ($element.dataset.jdzcId) {
27
+ Utils.warn('The widget is already initialized.');
28
+ return null;
29
+ }
30
+
31
+ this.id = this.generateCaptchaId();
32
+ this.$element = $element;
33
+ this.$element.dataset.jdzcId = this.id;
34
+ this.$iconHolder = null;
35
+ this.token = null;
36
+ this.startedInitialization = false;
37
+ this.invalidateTimeoutId = null;
38
+ this.captchaImageWidth = 0;
39
+ this.generated = false; // Tracks if the captcha is fully generated
40
+ this.generatedInTime = 0;
41
+ this.hovering = false; // Tracks if the user is hovering over the selection area
42
+ this.submitting = false; // Tracks if the captcha is currently submitting
43
+ this.options = options;
44
+
45
+ this.generate();
46
+ }
47
+
48
+ /**
49
+ * Generates the widget and sets up event listeners.
50
+ */
51
+ generate() {
52
+ if (this.generated) {
53
+ Utils.warn('The widget ' + this.id + ' has already been generated.');
54
+ return;
55
+ }
56
+
57
+ // Get the CSRF token from the closest form, if available
58
+ const $form = this.$element.closest('form');
59
+ if ($form) {
60
+ const $tokenInput = $form.querySelector(`input[name = "jdzc[${this.options.fields.token}]"]`);
61
+ this.token = $tokenInput ? $tokenInput.value : null;
62
+ }
63
+
64
+ // Throw an exception if the token is not found
65
+ if (!this.token) {
66
+ Utils.error('CSRF token is missing or invalid for widget[' + this.id + ']. Ensure the form contains a valid input field for the token.');
67
+ return;
68
+ }
69
+
70
+ this.$element.setAttribute('data-theme', this.options.theme);
71
+ this.$element.setAttribute('data-series', this.options.series);
72
+ this.$element.classList.add(`jdzc-theme-${this.options.theme}`);
73
+
74
+ // Apply the custom font family, if set
75
+ if (this.options.fontFamily) {
76
+ this.$element.style.fontFamily = this.options.fontFamily;
77
+ }
78
+
79
+ // If not initialized yet, show the 'initial' captcha holder and wait for click
80
+ if (!this.startedInitialization && this.options.security.enableInitialMessage) {
81
+ this.startedInitialization = true;
82
+
83
+ this.$element.classList.add(CSS.init);
84
+ this.$element.classList.remove(CSS.error, CSS.success);
85
+ this.$element.innerHTML = UI.buildCaptchaInitialHolder(this.options);
86
+
87
+ // Wait for user click to start loading
88
+ this.$element.addEventListener('click', () => {
89
+ this.$element.classList.remove(CSS.init);
90
+ this.generate();
91
+ }, { once: true });
92
+
93
+ return;
94
+ }
95
+
96
+ // Build the captcha if it hasn't been built yet
97
+ if (!this.generated) {
98
+ this.$element.innerHTML = UI.buildCaptchaHolder(this.options, this.id);
99
+ }
100
+
101
+ // Assign the icon holder
102
+ this.$iconHolder = this.$element.querySelector(`.${CSS.boxB}`);
103
+
104
+ // Add the loading spinner
105
+ UI.addLoadingSpinner(this.$iconHolder);
106
+
107
+ // If the loadingAnimationDelay has been set and is not 0, add the loading delay
108
+ if (this.options.security.loadingAnimationDelay && this.options.security.loadingAnimationDelay > 0 && !this.options.security.enableInitialMessage) {
109
+ setTimeout(() => this.load(), this.options.security.loadingAnimationDelay);
110
+ } else {
111
+ this.load();
112
+ }
113
+
114
+ // Register event listener for the selection area
115
+ const $selectionArea = this.$element.querySelector(`.${CSS.selection}`);
116
+ if ($selectionArea) {
117
+ $selectionArea.addEventListener('click', (event) => {
118
+ const rect = $selectionArea.getBoundingClientRect();
119
+ const xPos = event.clientX - rect.left;
120
+ const yPos = event.clientY - rect.top;
121
+
122
+ // Call submitIconSelection with the calculated coordinates
123
+ this.submitIconSelection(xPos, yPos);
124
+ });
125
+ }
126
+
127
+ this.generated = true; // Set generated to true after successful generation
128
+ }
129
+
130
+ /**
131
+ * Loads the captcha data for the widget.
132
+ */
133
+ load() {
134
+ if (this.generated) {
135
+ return; // Prevent loading if already generated
136
+ }
137
+
138
+ const requestPayload = Utils.createPayload({
139
+ i: this.id,
140
+ a: 1,
141
+ t: (this.$element.getAttribute('data-series') || 'streamline') + '/' + (this.$element.getAttribute('data-theme') || 'light'),
142
+ tk: this.token,
143
+ });
144
+
145
+ Fetch({
146
+ url: this.options.path,
147
+ type: 'POST',
148
+ headers: this.createHeaders(this.token),
149
+ data: { payload: requestPayload },
150
+ success: (data) => {
151
+ if (data && typeof data === 'string' && Utils.isBase64(data)) {
152
+ const result = JSON.parse(atob(data));
153
+
154
+ if (result.error) {
155
+ UI.processCaptchaRequestError(result.error, result.data);
156
+ return;
157
+ }
158
+
159
+ // Create the Base64 payload.
160
+ const imageRequestPayload = Utils.createPayload({ i: this.id, tk: this.token });
161
+ const urlParamSeparator = this.options.path.indexOf('?') > -1 ? '&' : '?';
162
+
163
+ // Load the captcha image.
164
+ const $iconsHolder = this.$iconHolder.querySelector(`.${CSS.icons}`);
165
+ $iconsHolder.style.backgroundImage = `url(${this.options.path}${urlParamSeparator}payload=${imageRequestPayload})`;
166
+
167
+ UI.removeLoadingSpinnerOnImageLoad($iconsHolder, () => UI.removeLoadingSpinner(this.$iconHolder));
168
+
169
+ // Add the selection area to the captcha holder.
170
+ $iconsHolder.parentNode.insertAdjacentHTML('beforeend', `<div class="${CSS.selection}"><i></i></div>`);
171
+ const selectionCursor = this.$iconHolder.querySelector(`.${CSS.selection} > i`);
172
+
173
+ // Register the events.
174
+ this.registerSelectionEvents();
175
+
176
+ // Trigger the 'init' event if not already generated.
177
+ if (!this.generated) {
178
+ Utils.trigger(this.$element, 'jdzc.init', { captchaId: this.id, options: this.options });
179
+ }
180
+
181
+ // Determine the width of the image.
182
+ const modalSelection = this.$iconHolder.querySelector(`.${CSS.selection}`);
183
+ this.captchaImageWidth = Utils.width(modalSelection);
184
+
185
+ // Set the building timestamp.
186
+ this.generatedInTime = new Date();
187
+ this.generated = true;
188
+
189
+ // Start the invalidation timer and save the timer identifier.
190
+ this.invalidateTimeoutId = setTimeout(() => this.invalidateSession(true), this.options.security.invalidateTime);
191
+
192
+ return;
193
+ }
194
+
195
+ this.setCaptchaError('The JdzCaptcha could not be loaded.', 'Invalid data was returned by the captcha back-end service. Make sure JdzCaptcha is installed/configured properly.');
196
+ },
197
+ error: () => this.showIncorrectIconMessage(
198
+ this.options.messages.incorrect.title, // Top message
199
+ this.options.messages.incorrect.subtitle, // Bottom message
200
+ true // Reset the captcha
201
+ ),
202
+ });
203
+
204
+ this.generated = true; // Set generated to true after successful loading
205
+ }
206
+
207
+ /**
208
+ * Resets the widget.
209
+ */
210
+ reset() {
211
+ Utils.clearInvalidationTimeout(this.invalidateTimeoutId);
212
+ this.startedInitialization = false;
213
+ this.generated = false;
214
+ Utils.trigger(this.$element, 'jdzc.reset', { captchaId: this.id });
215
+ this.generate();
216
+ }
217
+
218
+ /**
219
+ * Invalidates the captcha session.
220
+ * @param {boolean} invalidateServer Whether to invalidate the session on the server side.
221
+ */
222
+ invalidateSession(invalidateServer = true) {
223
+ // Reset the captcha state
224
+ this.generated = false;
225
+ this.startedInitialization = false;
226
+
227
+ // If server-side invalidation is required
228
+ if (invalidateServer) {
229
+ const payload = Utils.createPayload({ i: this.id, a: 3, tk: this.token });
230
+
231
+ Fetch({
232
+ url: this.options.path,
233
+ type: 'POST',
234
+ headers: this.createHeaders(this.token),
235
+ data: { payload },
236
+ success: () => {
237
+ // Trigger the 'invalidated' event
238
+ Utils.trigger(this.$element, 'jdzc.invalidated', { captchaId: this.id });
239
+
240
+ // Reset the captcha holder
241
+ this.resetCaptchaHolder();
242
+ },
243
+ error: () => {
244
+ // Handle error during server-side invalidation
245
+ this.setCaptchaError('The JdzCaptcha could not be reset.', 'Invalid data was returned by the captcha back-end service. Make sure JdzCaptcha is installed/configured properly.');
246
+ },
247
+ });
248
+ } else {
249
+ // Reset the captcha holder directly if no server-side invalidation is required
250
+ this.resetCaptchaHolder();
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Submits the icon selection made by the user to the server for validation.
256
+ * @param {number} xPos The clicked X position.
257
+ * @param {number} yPos The clicked Y position.
258
+ */
259
+ submitIconSelection(xPos, yPos) {
260
+ if (this.submitting) {
261
+ return; // Prevent duplicate submissions
262
+ }
263
+
264
+ if (xPos !== undefined && yPos !== undefined) {
265
+ this.submitting = true; // Set submitting to true
266
+
267
+ // Stop the reset timeout.
268
+ Utils.clearInvalidationTimeout(this.invalidateTimeoutId);
269
+
270
+ // Round the clicked position.
271
+ xPos = Math.round(xPos);
272
+ yPos = Math.round(yPos);
273
+
274
+ // Update the form fields with the captcha data.
275
+ const $selectionField = this.$element.querySelector(`input[name = "jdzc[${this.options.fields.selection}]"]`);
276
+ const $idField = this.$element.querySelector(`input[name = "jdzc[${this.options.fields.id}]"]`);
277
+ if ($selectionField) {
278
+ $selectionField.setAttribute('value', [xPos, yPos, this.captchaImageWidth].join(','));
279
+ }
280
+ if ($idField) {
281
+ $idField.setAttribute('value', this.id);
282
+ }
283
+
284
+ // Hide the mouse cursor.
285
+ const $selectionCursor = this.$iconHolder.querySelector(`.${CSS.selection} > i`);
286
+ if ($selectionCursor) {
287
+ $selectionCursor.style.display = 'none';
288
+ }
289
+
290
+ // Create the Base64 payload.
291
+ const requestPayload = Utils.createPayload({
292
+ i: this.id,
293
+ x: xPos,
294
+ y: yPos,
295
+ w: this.captchaImageWidth,
296
+ a: 2,
297
+ tk: this.token,
298
+ });
299
+
300
+ // Perform the request.
301
+ Fetch({
302
+ url: this.options.path,
303
+ type: 'POST',
304
+ headers: this.createHeaders(this.token),
305
+ data: { payload: requestPayload },
306
+ success: (data) => {
307
+ this.submitting = false; // Reset submitting to false
308
+ if (data.success === true) {
309
+ this.showCompletionMessage();
310
+ } else {
311
+ this.showIncorrectIconMessage();
312
+ }
313
+ },
314
+ error: () => {
315
+ this.submitting = false; // Reset submitting to false
316
+ this.setCaptchaError('The JdzCaptcha selection could not be submitted.', 'Invalid data was returned by the captcha back-end service. Make sure JdzCaptcha is installed/configured properly.');
317
+ },
318
+ });
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Displays the success message when the correct icon is selected.
324
+ */
325
+ showCompletionMessage() {
326
+ // Add the success state and remove the error state
327
+ this.$element.classList.add(CSS.success);
328
+ this.$element.classList.remove(CSS.error);
329
+
330
+ // Display the success message
331
+ this.$iconHolder.innerHTML = UI.buildValidSelectionMessage(this.options);
332
+
333
+ // Unregister the selection events
334
+ this.unregisterSelectionEvents();
335
+
336
+ // Trigger the 'success' event
337
+ Utils.trigger(this.$element, 'jdzc.success', { captchaId: this.id });
338
+
339
+ // Reset the captcha after a delay
340
+ setTimeout(() => this.reset(), this.options.security.selectionResetDelay);
341
+ }
342
+
343
+ /**
344
+ * Displays the error message when the incorrect icon is selected.
345
+ */
346
+ showIncorrectIconMessage() {
347
+ this.$element.classList.add(CSS.error);
348
+ this.$element.classList.remove(CSS.success);
349
+ this.$iconHolder.innerHTML = UI.buildInvalidSelectionMessage(this.options);
350
+
351
+ // Trigger the 'error' event.
352
+ Utils.trigger(this.$element, 'jdzc.error', { captchaId: this.id });
353
+
354
+ // Reset the captcha after a delay.
355
+ setTimeout(() => this.reset(), this.options.security.selectionResetDelay);
356
+ }
357
+
358
+ /**
359
+ * Logs a serious error that prevents the plugin from initializing and updates the captcha state.
360
+ * @param {string} displayError The error message to display in the captcha holder element.
361
+ * @param {string} [consoleError] The error message to display in the developer console. If not provided, `displayError` will be used.
362
+ * @param {boolean} triggerEvent Whether to trigger the custom 'error' event.
363
+ */
364
+ setCaptchaError(displayError, consoleError = '', triggerEvent = true) {
365
+ const DEBUG_MODE = window.JdzCaptcha && window.JdzCaptcha.debugMode;
366
+
367
+ // Determine the error messages to display.
368
+ const topMessage = DEBUG_MODE ? 'JdzCaptcha error' : this.messages.incorrect.title;
369
+ const bottomMessage = DEBUG_MODE ? displayError : this.messages.incorrect.subtitle;
370
+ const errorReset = !DEBUG_MODE;
371
+
372
+ // Display the error message in the captcha holder.
373
+ this.showIncorrectIconMessage(topMessage, bottomMessage, errorReset);
374
+
375
+ // Log the error to the console.
376
+ Utils.error(consoleError || displayError);
377
+
378
+ // Trigger the custom 'error' event if required.
379
+ if (triggerEvent) {
380
+ Utils.trigger(this.$element, 'jdzc.error', { captchaId: this.id });
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Processes the error data which was received from the server while requesting the captcha data. Actions
386
+ * might be performed based on the given error code or error data.
387
+ * @param code The error code.
388
+ * @param data The payload of the error.
389
+ */
390
+ processCaptchaRequestError(code, data) {
391
+ code = parseInt(code);
392
+
393
+ switch (code) {
394
+ case 1: // Too many incorrect selections, timeout.
395
+ this.showIncorrectIconMessage(this.options.messages.timeout.title, this.options.messages.timeout.subtitle, false);
396
+
397
+ // Remove the header from the captcha.
398
+ const captchaHeader = this.$element.querySelector(`.${CSS.boxH}`);
399
+ captchaHeader.parentNode.removeChild(captchaHeader);
400
+
401
+ // Trigger: timeout
402
+ Utils.trigger(this.$element, 'jdzc.timeout', { captchaId: this.id });
403
+
404
+ // Reset the captcha to the init holder.
405
+ setTimeout(() => this.invalidateSession(false), data);
406
+ break;
407
+
408
+ case 2: // No CSRF token found while validating.
409
+ this.setCaptchaError('The captcha token is missing or is incorrect.', 'A server request was made without including a captcha token, however this option is enabled.');
410
+ break;
411
+
412
+ default: // Any other error.
413
+ this.setCaptchaError('An unexpected error occurred.', 'An unexpected error occurred while JdzCaptcha performed an action.');
414
+ break;
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Changes the captcha state to the 'error' state.
420
+ * @param {string} [topMessage] The title message of the error state.
421
+ * @param {string} [bottomMessage] The subtitle message of the error state.
422
+ * @param {boolean} [reset=true] Whether the captcha should reinitialize automatically after some time.
423
+ */
424
+ showIncorrectIconMessage(topMessage = null, bottomMessage = null, reset = true) {
425
+ topMessage = topMessage || this.options.messages.incorrect.title;
426
+ bottomMessage = bottomMessage || this.options.messages.incorrect.subtitle;
427
+
428
+ // Remove opacity styles
429
+ this.$iconHolder.classList.remove(CSS.opacity);
430
+ this.$element.classList.remove(CSS.opacity);
431
+
432
+ // Unregister the selection events
433
+ this.unregisterSelectionEvents();
434
+
435
+ // Add the error state and display the error message
436
+ this.$element.classList.add(CSS.error);
437
+ this.$iconHolder.innerHTML = UI.buildErrorMessage(topMessage, bottomMessage);
438
+
439
+ // Mark the captcha as 'not submitting'
440
+ this.submitting = false;
441
+
442
+ // Trigger the 'error' event
443
+ Utils.trigger(this.$element, 'jdzc.error', { captchaId: this.id });
444
+
445
+ // Handle timeout or reset the captcha
446
+ if (reset) {
447
+ setTimeout(() => this.reset(), this.options.security.selectionResetDelay);
448
+ } else {
449
+ // Trigger a timeout event if reset is disabled
450
+ Utils.trigger(this.$element, 'jdzc.timeout', { captchaId: this.id });
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Resets the state of the captcha holder element.
456
+ * The error state will be removed, hidden input fields will be cleared, and the captcha will be reinitialized.
457
+ */
458
+ resetCaptchaHolder() {
459
+ // Remove the error state
460
+ this.$element.classList.remove(CSS.error);
461
+
462
+ // Clear the selection input field
463
+ const $selectionField = this.$element.querySelector(`input[name = "jdzc[${this.options.fields.selection}]"]`);
464
+ if ($selectionField) {
465
+ $selectionField.setAttribute('value', null);
466
+ }
467
+
468
+ // Reset the captcha body
469
+ Utils.empty(this.$iconHolder);
470
+ this.$iconHolder.insertAdjacentHTML('beforeend', `<div class="${CSS.icons}"></div>`);
471
+
472
+ // Reload the captcha
473
+ this.generate();
474
+
475
+ // Trigger the 'refreshed' event
476
+ Utils.trigger(this.$element, 'jdzc.refreshed', { captchaId: this.id });
477
+ }
478
+
479
+ /**
480
+ * Registers events linked to the captcha selection area element.
481
+ */
482
+ registerSelectionEvents() {
483
+ const $captchaSelection = this.$element.querySelector(`.${CSS.selection}`);
484
+
485
+ // Ensure the element and its cached listeners do not exist.
486
+ if (!$captchaSelection || $captchaSelection._jdzc_listeners) return;
487
+
488
+ const handlers = {
489
+ click: this.mouseClickEvent.bind(this),
490
+ mousemove: this.mouseMoveEvent.bind(this),
491
+ mouseenter: this.mouseEnterEvent.bind(this),
492
+ mouseleave: this.mouseLeaveEvent.bind(this),
493
+ };
494
+
495
+ // Cache the listeners for later removal.
496
+ $captchaSelection._jdzc_listeners = handlers;
497
+
498
+ // Register the events.
499
+ Object.entries(handlers).forEach(([event, handler]) => {
500
+ $captchaSelection.addEventListener(event, handler);
501
+ });
502
+ }
503
+
504
+ /**
505
+ * Unregisters all event listeners linked to the captcha selection area element.
506
+ */
507
+ unregisterSelectionEvents() {
508
+ const $captchaSelection = this.$element.querySelector(`.${CSS.selection}`);
509
+
510
+ // Ensure the element and its cached listeners exist.
511
+ if (!$captchaSelection || !$captchaSelection._jdzc_listeners) return;
512
+
513
+ // Unregister each cached event listener.
514
+ Object.entries($captchaSelection._jdzc_listeners).forEach(([event, handler]) => {
515
+ $captchaSelection.removeEventListener(event, handler);
516
+ });
517
+
518
+ // Clear the cached listeners to free memory.
519
+ delete $captchaSelection._jdzc_listeners;
520
+ }
521
+
522
+ /**
523
+ * Moves the custom cursor to the current location of the actual cursor.
524
+ * @param {MouseEvent} event The mouse move event.
525
+ */
526
+ moveCustomCursor(event) {
527
+ if (!event.currentTarget) {
528
+ return;
529
+ }
530
+
531
+ // Calculate the clicked X and Y position.
532
+ const rect = event.currentTarget.getBoundingClientRect();
533
+ const xPos = Math.round(event.clientX - rect.left);
534
+ const yPos = Math.round(event.clientY - rect.top);
535
+
536
+ // Apply the style position to the cursor.
537
+ const $selectionCursor = this.$iconHolder.querySelector(`.${CSS.selection} > i`);
538
+ if ($selectionCursor) {
539
+ $selectionCursor.style.left = `${xPos - 8}px`;
540
+ $selectionCursor.style.top = `${yPos - 7}px`;
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Handles the user's click on the captcha selection area.
546
+ * @param {MouseEvent} event The mouse click event.
547
+ */
548
+ mouseClickEvent(event) {
549
+ if (!this.generated || this.submitting) {
550
+ return; // Prevent clicking if the captcha is not ready or already submitting
551
+ }
552
+
553
+ if (new Date() - this.generatedInTime <= this.options.security.clickDelay) {
554
+ return; // Only allow a user to click after a set click delay
555
+ }
556
+
557
+ const rect = event.currentTarget.getBoundingClientRect();
558
+ const xPos = event.clientX - rect.left;
559
+ const yPos = event.clientY - rect.top;
560
+
561
+ if (!xPos || !yPos) {
562
+ return; // Invalid click
563
+ }
564
+
565
+ this.submitIconSelection(xPos, yPos);
566
+ }
567
+
568
+ /**
569
+ * Updates the position of the custom cursor as the user moves the mouse.
570
+ * @param {MouseEvent} event The mouse move event.
571
+ */
572
+ mouseMoveEvent(event) {
573
+ if (!this.hovering || this.submitting || !this.generated) {
574
+ return; // Prevent cursor movement if not hovering, submitting, or generated
575
+ }
576
+
577
+ this.moveCustomCursor(event);
578
+ }
579
+
580
+ /**
581
+ * Handles the mouse entering the selection area.
582
+ * @param {MouseEvent} event The mouse enter event.
583
+ */
584
+ mouseEnterEvent(event) {
585
+ const $selectionCursor = this.$iconHolder.querySelector(`.${CSS.selection} > i`);
586
+ if ($selectionCursor) {
587
+ $selectionCursor.style.display = 'inline'; // Show the cursor
588
+ }
589
+
590
+ this.hovering = true; // Set hovering to true
591
+ this.moveCustomCursor(event); // Update cursor position
592
+ }
593
+
594
+ /**
595
+ * Handles the mouse leaving the selection area.
596
+ */
597
+ mouseLeaveEvent() {
598
+ const $selectionCursor = this.$iconHolder.querySelector(`.${CSS.selection} > i`);
599
+ if ($selectionCursor) {
600
+ $selectionCursor.style.display = 'none'; // Hide the cursor
601
+ }
602
+
603
+ this.hovering = false; // Set hovering to false
604
+ }
605
+
606
+ /**
607
+ * Creates the custom header object which should be included in every AJAX request.
608
+ * @param {string} [token] The captcha session token, possibly empty.
609
+ * @returns {Object} The header object.
610
+ */
611
+ createHeaders(token) {
612
+ return token ? { 'X-JdzCaptcha-Token': token } : {};
613
+ }
614
+
615
+
616
+ /**
617
+ * Generates a random captcha identifier.
618
+ * @returns {number} The widget identifier.
619
+ */
620
+ generateCaptchaId() {
621
+ const maxNumber = 10 ** 13 - 1; // Maximum 13-digit number
622
+ return Math.floor(Math.random() * maxNumber);
623
+ }
624
+ };
@@ -0,0 +1,45 @@
1
+ @-webkit-keyframes jdzc-breath {
2
+ 0% {
3
+ transform: scale(1) translateZ(0);
4
+ border-color: @jdz-color;
5
+ }
6
+ 25% {
7
+ transform: scale(0.8) translateZ(0);
8
+ border-color: @jdz-color-light;
9
+ }
10
+ 50% {
11
+ transform: scale(1) translateZ(0);
12
+ border-color: @jdz-color;
13
+ }
14
+ 75% {
15
+ transform: scale(0.8) translateZ(0);
16
+ border-color: @jdz-color-light;
17
+ }
18
+ 100% {
19
+ transform: scale(1) translateZ(0);
20
+ border-color: @jdz-color;
21
+ }
22
+ }
23
+
24
+ @keyframes jdzc-breath {
25
+ 0% {
26
+ transform: scale(1) translateZ(0);
27
+ border-color: @jdz-color;
28
+ }
29
+ 25% {
30
+ transform: scale(0.8) translateZ(0);
31
+ border-color: @jdz-color-light;
32
+ }
33
+ 50% {
34
+ transform: scale(1) translateZ(0);
35
+ border-color: @jdz-color;
36
+ }
37
+ 75% {
38
+ transform: scale(0.8) translateZ(0);
39
+ border-color: @jdz-color-light;
40
+ }
41
+ 100% {
42
+ transform: scale(1) translateZ(0);
43
+ border-color: @jdz-color;
44
+ }
45
+ }