create-prisma-php-app 1.13.2 → 1.13.4
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.
|
@@ -5,3 +5,42 @@ function redirect(string $url): void
|
|
|
5
5
|
header("Location: $url");
|
|
6
6
|
exit;
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
function isAjaxRequest()
|
|
10
|
+
{
|
|
11
|
+
$isAjax = false;
|
|
12
|
+
|
|
13
|
+
// Check for standard AJAX header
|
|
14
|
+
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
|
|
15
|
+
$isAjax = true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check for HTMX request header
|
|
19
|
+
if (!empty($_SERVER['HTTP_HX_REQUEST'])) {
|
|
20
|
+
$isAjax = true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check for common AJAX content types
|
|
24
|
+
if (!empty($_SERVER['CONTENT_TYPE'])) {
|
|
25
|
+
$ajaxContentTypes = [
|
|
26
|
+
'application/json',
|
|
27
|
+
'application/x-www-form-urlencoded',
|
|
28
|
+
'multipart/form-data',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
foreach ($ajaxContentTypes as $contentType) {
|
|
32
|
+
if (strpos($_SERVER['CONTENT_TYPE'], $contentType) !== false) {
|
|
33
|
+
$isAjax = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check for common AJAX request methods
|
|
40
|
+
$ajaxMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
|
41
|
+
if (in_array(strtoupper($_SERVER['REQUEST_METHOD']), $ajaxMethods)) {
|
|
42
|
+
$isAjax = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return $isAjax;
|
|
46
|
+
}
|
|
@@ -22,6 +22,8 @@ $isDelete = $requestMethod === 'DELETE';
|
|
|
22
22
|
$isPatch = $requestMethod === 'PATCH';
|
|
23
23
|
$isHead = $requestMethod === 'HEAD';
|
|
24
24
|
$isOptions = $requestMethod === 'OPTIONS';
|
|
25
|
+
$isAjax = isAjaxRequest();
|
|
26
|
+
$isHtmx = !empty($_SERVER['HTTP_HX_REQUEST']) && strtolower($_SERVER['HTTP_HX_REQUEST']) === 'true';
|
|
25
27
|
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
|
26
28
|
$requestedWith = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? '';
|
|
27
29
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
|
|
@@ -189,6 +189,11 @@ class FormHandler
|
|
|
189
189
|
public function validateField($field, $rules)
|
|
190
190
|
{
|
|
191
191
|
$value = Validator::validateString($this->data[$field] ?? null);
|
|
192
|
+
|
|
193
|
+
if (!isset($rules['required']) && empty($value)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
192
197
|
foreach ($rules as $rule => $options) {
|
|
193
198
|
$ruleValue = $options;
|
|
194
199
|
$customMessage = null;
|
|
@@ -415,177 +420,190 @@ class FormHandler
|
|
|
415
420
|
?>
|
|
416
421
|
|
|
417
422
|
<script>
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
init() {
|
|
426
|
-
this.dataRulesElements.forEach(fieldElement => {
|
|
427
|
-
this.initializeFieldFromDOM(fieldElement);
|
|
428
|
-
});
|
|
429
|
-
}
|
|
423
|
+
if (typeof FormHandler === 'undefined') {
|
|
424
|
+
class FormHandler {
|
|
425
|
+
constructor() {
|
|
426
|
+
this.errors = [];
|
|
427
|
+
this.dataRulesElements = document.querySelectorAll('[data-rules]');
|
|
428
|
+
this.init();
|
|
429
|
+
}
|
|
430
430
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
431
|
+
init() {
|
|
432
|
+
this.dataRulesElements.forEach(fieldElement => {
|
|
433
|
+
this.initializeFieldFromDOM(fieldElement);
|
|
434
|
+
});
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
|
|
438
|
-
|
|
437
|
+
initializeFieldFromDOM(fieldElement) {
|
|
438
|
+
if (!fieldElement) return;
|
|
439
439
|
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
this.watch(target);
|
|
440
|
+
const fieldName = fieldElement.name;
|
|
441
|
+
const rules = JSON.parse(fieldElement.getAttribute('data-rules') || '{}');
|
|
443
442
|
|
|
444
|
-
const errors = this.validateField(
|
|
445
|
-
const errorContainer = document.getElementById(`fh-error-${
|
|
443
|
+
const errors = this.validateField(fieldElement, fieldElement.value, rules);
|
|
444
|
+
const errorContainer = document.getElementById(`fh-error-${fieldElement.name}`);
|
|
446
445
|
if (errorContainer) {
|
|
447
|
-
errorContainer.
|
|
446
|
+
if (errorContainer.dataset.errorMessage) {
|
|
447
|
+
errorContainer.textContent = errors.join(', ');
|
|
448
|
+
}
|
|
448
449
|
}
|
|
449
|
-
};
|
|
450
450
|
|
|
451
|
-
|
|
452
|
-
|
|
451
|
+
const immediateObserver = (e) => {
|
|
452
|
+
const target = e.target;
|
|
453
|
+
this.watch(target);
|
|
454
|
+
|
|
455
|
+
const errors = this.validateField(target, target.value, rules);
|
|
456
|
+
const errorContainer = document.getElementById(`fh-error-${target.name}`);
|
|
457
|
+
if (errorContainer) {
|
|
458
|
+
errorContainer.textContent = errors.join(', ');
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
fieldElement.addEventListener('input', immediateObserver);
|
|
463
|
+
}
|
|
453
464
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
465
|
+
updateElementDisplay(displayElement, field) {
|
|
466
|
+
const tagName = field.tagName.toUpperCase();
|
|
467
|
+
if (tagName === 'INPUT' || tagName === 'TEXTAREA') {
|
|
468
|
+
if (displayElement.tagName === 'INPUT' || displayElement.tagName === 'TEXTAREA') {
|
|
469
|
+
displayElement.value = field.value;
|
|
470
|
+
} else {
|
|
471
|
+
displayElement.dataset.watchValue = field.value;
|
|
472
|
+
displayElement.textContent = field.value;
|
|
473
|
+
}
|
|
459
474
|
} else {
|
|
460
|
-
displayElement.
|
|
461
|
-
displayElement.textContent = field.value;
|
|
475
|
+
displayElement.textContent = field.textContent;
|
|
462
476
|
}
|
|
463
|
-
} else {
|
|
464
|
-
displayElement.textContent = field.textContent;
|
|
465
477
|
}
|
|
466
|
-
}
|
|
467
478
|
|
|
468
|
-
|
|
469
|
-
|
|
479
|
+
watch(field) {
|
|
480
|
+
if (!field) return;
|
|
470
481
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
482
|
+
const watchElement = document.getElementById(`fh-watch-${field.name}`);
|
|
483
|
+
if (watchElement) {
|
|
484
|
+
this.updateElementDisplay(watchElement, field);
|
|
485
|
+
}
|
|
474
486
|
}
|
|
475
|
-
}
|
|
476
487
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
488
|
+
clearErrors() {
|
|
489
|
+
const errorElements = document.querySelectorAll('[id^="fh-error-"]');
|
|
490
|
+
errorElements.forEach(element => {
|
|
491
|
+
element.textContent = '';
|
|
492
|
+
});
|
|
482
493
|
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
getErrors(field) {
|
|
487
|
-
if (field) {
|
|
488
|
-
return document.getElementById(`fh-error-${field}`).textContent;
|
|
489
|
-
} else {
|
|
490
|
-
return this.errors;
|
|
494
|
+
this.errors = [];
|
|
491
495
|
}
|
|
492
|
-
}
|
|
493
496
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
let ruleValue = options;
|
|
500
|
-
let customMessage = null;
|
|
501
|
-
|
|
502
|
-
if (typeof options === 'object') {
|
|
503
|
-
ruleValue = options.value;
|
|
504
|
-
customMessage = options.message || null;
|
|
497
|
+
getErrors(field) {
|
|
498
|
+
if (field) {
|
|
499
|
+
return document.getElementById(`fh-error-${field}`).textContent;
|
|
500
|
+
} else {
|
|
501
|
+
return this.errors;
|
|
505
502
|
}
|
|
503
|
+
}
|
|
506
504
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
505
|
+
validateField(field, value, rules) {
|
|
506
|
+
if (!rules) return [];
|
|
507
|
+
this.errors = [];
|
|
508
|
+
|
|
509
|
+
for (const [rule, options] of Object.entries(rules)) {
|
|
510
|
+
let ruleValue = options;
|
|
511
|
+
let customMessage = null;
|
|
512
|
+
|
|
513
|
+
if (typeof options === 'object') {
|
|
514
|
+
ruleValue = options.value;
|
|
515
|
+
customMessage = options.message || null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
switch (rule) {
|
|
519
|
+
case 'text':
|
|
520
|
+
case 'search':
|
|
521
|
+
if (typeof value !== 'string') {
|
|
522
|
+
this.errors.push(customMessage || 'Must be a string.');
|
|
523
|
+
}
|
|
524
|
+
break;
|
|
525
|
+
case 'email':
|
|
526
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
527
|
+
this.errors.push(customMessage || 'Invalid email format.');
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
case 'number':
|
|
531
|
+
if (isNaN(value)) {
|
|
532
|
+
this.errors.push(customMessage || 'Must be a number.');
|
|
533
|
+
}
|
|
534
|
+
break;
|
|
535
|
+
case 'date':
|
|
536
|
+
if (isNaN(Date.parse(value))) {
|
|
537
|
+
this.errors.push(customMessage || 'Invalid date format.');
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
case 'range':
|
|
541
|
+
const [min, max] = ruleValue;
|
|
542
|
+
if (isNaN(value) || value < min || value > max) {
|
|
543
|
+
this.errors.push(customMessage || `Must be between ${min} and ${max}.`);
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
case 'url':
|
|
547
|
+
try {
|
|
548
|
+
new URL(value);
|
|
549
|
+
} catch (e) {
|
|
550
|
+
this.errors.push(customMessage || 'Invalid URL format.');
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
case 'required':
|
|
554
|
+
if (!value) {
|
|
555
|
+
this.errors.push(customMessage || 'This field is required.');
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
case 'min':
|
|
559
|
+
if (Number(value) < ruleValue) {
|
|
560
|
+
this.errors.push(customMessage || `Must be at least ${ruleValue}.`);
|
|
561
|
+
}
|
|
562
|
+
break;
|
|
563
|
+
case 'max':
|
|
564
|
+
if (Number(value) > ruleValue) {
|
|
565
|
+
this.errors.push(customMessage || `Must be at most ${ruleValue}.`);
|
|
566
|
+
}
|
|
567
|
+
break;
|
|
568
|
+
case 'minLength':
|
|
569
|
+
if (value.length < ruleValue) {
|
|
570
|
+
this.errors.push(customMessage || `Must be at least ${ruleValue} characters.`);
|
|
571
|
+
}
|
|
572
|
+
break;
|
|
573
|
+
case 'maxLength':
|
|
574
|
+
if (value.length > ruleValue) {
|
|
575
|
+
this.errors.push(customMessage || `Must be at most ${ruleValue} characters.`);
|
|
576
|
+
}
|
|
577
|
+
break;
|
|
578
|
+
case 'pattern':
|
|
579
|
+
if (!new RegExp(ruleValue).test(value)) {
|
|
580
|
+
this.errors.push(customMessage || 'Invalid format.');
|
|
581
|
+
}
|
|
582
|
+
break;
|
|
583
|
+
case 'accept':
|
|
584
|
+
if (!ruleValue.split(',').includes(value)) {
|
|
585
|
+
this.errors.push(customMessage || 'Invalid file format.');
|
|
586
|
+
}
|
|
587
|
+
break;
|
|
588
|
+
default:
|
|
589
|
+
// Optionally handle unknown rules or log them
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
580
592
|
}
|
|
581
|
-
}
|
|
582
593
|
|
|
583
|
-
|
|
594
|
+
return this.errors;
|
|
595
|
+
}
|
|
584
596
|
}
|
|
585
|
-
}
|
|
586
597
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
598
|
+
let formHandler = FormHandler ? new FormHandler() : null;
|
|
599
|
+
// Initialize FormHandler on initial page load
|
|
600
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
601
|
+
formHandler = new FormHandler();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Reinitialize FormHandler when HTMX swaps content
|
|
605
|
+
document.body.addEventListener('htmx:afterOnLoad', function() {
|
|
606
|
+
formHandler = new FormHandler();
|
|
607
|
+
});
|
|
608
|
+
}
|
|
591
609
|
</script>
|