create-prisma-php-app 1.11.21 → 1.11.23

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.
@@ -0,0 +1,2 @@
1
+ node_modules/
2
+ vendor/
@@ -0,0 +1,40 @@
1
+ # Use an official PHP image with the version you need
2
+ FROM php:8.1-apache
3
+
4
+ # Install system dependencies for Composer and PHP extensions
5
+ RUN apt-get update && apt-get install -y \
6
+ git \
7
+ unzip \
8
+ libzip-dev \
9
+ zip \
10
+ && docker-php-ext-install pdo_mysql zip
11
+
12
+ # Enable Apache mods
13
+ RUN a2enmod rewrite headers
14
+
15
+ # Install Composer globally
16
+ COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
17
+
18
+ # Set the working directory in the container
19
+ WORKDIR /var/www/html
20
+
21
+ # Copy the application's composer.json and lock file
22
+ COPY composer.json composer.lock ./
23
+
24
+ # Install PHP dependencies
25
+ RUN composer install --no-scripts --no-autoloader
26
+
27
+ # Copy the rest of the application
28
+ COPY . .
29
+
30
+ # Finish composer
31
+ RUN composer dump-autoload --optimize
32
+
33
+ # Apache config
34
+ COPY ./apache.conf /etc/apache2/sites-available/000-default.conf
35
+
36
+ # Expose port 80 to access the container
37
+ EXPOSE 80
38
+
39
+ # Command to run when starting the container
40
+ CMD ["apache2-foreground"]
@@ -0,0 +1,12 @@
1
+ <VirtualHost *:80>
2
+ ServerAdmin webmaster@localhost
3
+ DocumentRoot /var/www/html
4
+ ErrorLog ${APACHE_LOG_DIR}/error.log
5
+ CustomLog ${APACHE_LOG_DIR}/access.log combined
6
+
7
+ <Directory "/var/www/html">
8
+ Options Indexes FollowSymLinks
9
+ AllowOverride All
10
+ Require all granted
11
+ </Directory>
12
+ </VirtualHost>
@@ -25,9 +25,17 @@ function determineContentToInclude()
25
25
  writeRoutes();
26
26
  AuthMiddleware::handle($uri);
27
27
 
28
- $isDirectAccessToPrivateRoute = preg_match('/^_/', $uri);
28
+ $isDirectAccessToPrivateRoute = preg_match('/\/_/', $uri);
29
29
  if ($isDirectAccessToPrivateRoute) {
30
- return ['path' => $includePath, 'layouts' => $layoutsToInclude, 'uri' => $uri];
30
+ $sameSiteFetch = false;
31
+ $serverFetchSite = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? '';
32
+ if (isset($serverFetchSite) && $serverFetchSite === 'same-origin') {
33
+ $sameSiteFetch = true;
34
+ }
35
+
36
+ if (!$sameSiteFetch) {
37
+ return ['path' => $includePath, 'layouts' => $layoutsToInclude, 'uri' => $uri];
38
+ }
31
39
  }
32
40
 
33
41
  if ($uri) {
@@ -0,0 +1,19 @@
1
+ version: "3.8"
2
+ services:
3
+ web:
4
+ build:
5
+ context: .
6
+ dockerfile: Dockerfile
7
+ ports:
8
+ - "80:80"
9
+ volumes:
10
+ - .:/var/www/html
11
+ depends_on:
12
+ - db
13
+ db:
14
+ image: mysql:5.7
15
+ environment:
16
+ MYSQL_ROOT_PASSWORD: prisma_php
17
+ MYSQL_DATABASE: prisma_php
18
+ ports:
19
+ - "3306:3306"
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{execSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+"\\htdocs\\".length).replace(/\\/g,"\\\\"),n=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let i=`http://localhost/${n}`;i=i.endsWith("/")?i.slice(0,-1):i;const c=i.replace(/(?<!:)(\/\/+)/g,"/"),r=n.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${r.startsWith("/")?r.substring(1):r}/`}}}function configureBrowserSyncCommand(e){const s=path.join(e,"settings","bs-config.cjs");return fs.writeFileSync(s,'const { createProxyMiddleware } = require("http-proxy-middleware");\nconst fs = require("fs");\n\nconst jsonData = fs.readFileSync("prisma-php.json", "utf8");\nconst config = JSON.parse(jsonData);\n\nmodule.exports = {\n proxy: "http://localhost:3000",\n middleware: [\n (req, res, next) => {\n res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");\n res.setHeader("Pragma", "no-cache");\n res.setHeader("Expires", "0");\n next();\n },\n createProxyMiddleware({\n target: config.bsTarget,\n changeOrigin: true,\n pathRewrite: config.bsPathRewrite,\n }),\n ],\n files: "src/**/*.*",\n notify: false,\n open: false,\n ghostMode: false,\n};',"utf8"),"browser-sync start --config settings/bs-config.cjs"}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const n=JSON.parse(fs.readFileSync(t,"utf8")),i=configureBrowserSyncCommand(e);n.scripts=Object.assign(Object.assign({},n.scripts),{projectName:"node settings/project-name.cjs"});let c=[];s.tailwindcss&&(n.scripts=Object.assign(Object.assign({},n.scripts),{tailwind:"postcss ./src/app/css/tailwind.css -o ./src/app/css/styles.css --watch"}),c.push("tailwind")),s.websocket&&(n.scripts=Object.assign(Object.assign({},n.scripts),{websocket:"node ./settings/restart-websocket.cjs"}),c.push("websocket"));const r=Object.assign({},n.scripts);r["browser-sync"]=i,r.dev=c.length>0?`npm-run-all --parallel projectName browser-sync ${c.join(" ")}`:"npm-run-all --parallel projectName browser-sync",n.scripts=r,n.type="module",s.prisma&&(n.prisma={seed:"node prisma/seed.js"}),fs.writeFileSync(t,JSON.stringify(n,null,2))}async function updateComposerJson(e,s){const t=path.join(e,"composer.json");if(checkExcludeFiles(t))return;let n;if(fs.existsSync(t)){{const e=fs.readFileSync(t,"utf8");n=JSON.parse(e)}s.websocket&&(n.require=Object.assign(Object.assign({},n.require),{"cboden/ratchet":"^0.4.4"})),s.prisma&&(n.require=Object.assign(Object.assign({},n.require),{"ramsey/uuid":"5.x-dev","hidehalo/nanoid-php":"1.x-dev"})),fs.writeFileSync(t,JSON.stringify(n,null,2))}}async function updateIndexJsForWebSocket(e,s){if(!s.websocket)return;const t=path.join(e,"src","app","js","index.js");if(checkExcludeFiles(t))return;let n=fs.readFileSync(t,"utf8");n+='\n// WebSocket initialization\nconst ws = new WebSocket("ws://localhost:8080");\n',fs.writeFileSync(t,n,"utf8")}async function createUpdateGitignoreFile(e,s){const t=path.join(e,".gitignore");if(checkExcludeFiles(t))return;let n="";s.forEach((e=>{n.includes(e)||(n+=`\n${e}`)})),n=n.trimStart(),fs.writeFileSync(t,n)}function copyRecursiveSync(e,s,t){var n;const i=fs.existsSync(e),c=i&&fs.statSync(e);if(i&&c&&c.isDirectory()){const i=s.toLowerCase();if(!t.websocket&&i.includes("src\\lib\\websocket"))return;if(!t.prisma&&i.includes("src\\lib\\prisma"))return;const c=s.replace(/\\/g,"/");if(null===(n=null==updateAnswer?void 0:updateAnswer.excludeFilePath)||void 0===n?void 0:n.includes(c))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach((n=>{copyRecursiveSync(path.join(e,n),path.join(s,n),t)}))}else{if(checkExcludeFiles(s))return;if(!t.tailwindcss&&(s.includes("tailwind.css")||s.includes("styles.css")))return;if(!t.websocket&&(s.includes("restart-websocket.cjs")||s.includes("restart-websocket.bat")))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,t){s.forEach((({srcDir:s,destDir:n})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,n),t)}))}function createOrUpdateTailwindConfig(e){const s=path.join(e,"tailwind.config.js");if(checkExcludeFiles(s))return;let t=fs.readFileSync(s,"utf8");const n=["./src/app/**/*.{html,js,php}"].map((e=>` "${e}"`)).join(",\n");t=t.replace(/content: \[\],/g,`content: [\n${n}\n],`),fs.writeFileSync(s,t,{flag:"w"})}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,"export default {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n cssnano: {},\n },\n};",{flag:"w"})}function modifyLayoutPHP(e,s){const t=path.join(e,"src","app","layout.php");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8");const n='\n <link href="<?php echo $baseUrl; ?>css/index.css" rel="stylesheet">\n <script src="<?php echo $baseUrl; ?>js/index.js"><\/script>\n <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">',i=s?` <link href="<?php echo $baseUrl; ?>css/styles.css" rel="stylesheet"> ${n}`:` <script src="https://cdn.tailwindcss.com"><\/script> ${n}`;e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){}}async function createOrUpdateEnvFile(e,s){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){var s,t;return!!(null==updateAnswer?void 0:updateAnswer.isUpdate)&&(null!==(t=null===(s=null==updateAnswer?void 0:updateAnswer.excludeFilePath)||void 0===s?void 0:s.includes(e.replace(/\\/g,"/")))&&void 0!==t&&t)}async function createDirectoryStructure(e,s){const t=[{src:"/bootstrap.php",dest:"/bootstrap.php"},{src:"/.htaccess",dest:"/.htaccess"},{src:"/../composer.json",dest:"/composer.json"}];(null==updateAnswer?void 0:updateAnswer.isUpdate)&&(t.push({src:"/tsconfig.json",dest:"/tsconfig.json"}),updateAnswer.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"},{src:"/tailwind.config.js",dest:"/tailwind.config.js"}));const n=[{srcDir:"/settings",destDir:"/settings"},{srcDir:"/src",destDir:"/src"}];s.prisma&&n.push({srcDir:"/prisma",destDir:"/prisma"}),t.forEach((({src:s,dest:t})=>{const n=path.join(__dirname,s),i=path.join(e,t);if(checkExcludeFiles(i))return;const c=fs.readFileSync(n,"utf8");fs.writeFileSync(i,c,{flag:"w"})})),await executeCopy(e,n,s),await updatePackageJson(e,s),await updateComposerJson(e,s),await updateIndexJsForWebSocket(e,s),s.tailwindcss?(createOrUpdateTailwindConfig(e),modifyLayoutPHP(e,!0),modifyPostcssConfig(e)):modifyLayoutPHP(e,!1);const i='# Prisma PHP Auth Secret Key For development only - Change this in production\nAUTH_SECRET=uxsjXVPHN038DEYls2Kw0QUgBcXKUyrjv416nIFWPY4= \n \n# PHPMailer\n# SMTP_HOST=smtp.gmail.com or your SMTP host\n# SMTP_USERNAME=john.doe@gmail.com or your SMTP username\n# SMTP_PASSWORD=123456\n# SMTP_PORT=587 for TLS, 465 for SSL or your SMTP port\n# SMTP_ENCRYPTION=ssl or tls\n# MAIL_FROM=john.doe@gmail.com\n# MAIL_FROM_NAME="John Doe"';if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${i}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,i);await createUpdateGitignoreFile(e,["vendor",".env","node_modules"])}async function getAnswer(e={}){var s,t,n,i;const c=[];e.projectName||c.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!0,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!0,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!0,active:"Yes",inactive:"No"});const r=c,a=()=>{process.exit(0)};try{const c=await prompts(r,{onCancel:a});return 0===Object.keys(c).length?null:{projectName:c.projectName?String(c.projectName).trim().replace(/ /g,"-"):null!==(s=e.projectName)&&void 0!==s?s:"my-app",tailwindcss:null!==(t=c.tailwindcss)&&void 0!==t?t:e.tailwindcss,websocket:null!==(n=c.websocket)&&void 0!==n?n:e.websocket,prisma:null!==(i=c.prisma)&&void 0!==i?i:e.prisma}}catch(e){return null}}async function installDependencies(e,s,t=!1){fs.existsSync(path.join(e,"package.json"))||execSync("npm init -y",{stdio:"inherit",cwd:e}),s.forEach((e=>{}));const n=`npm install ${t?"--save-dev":""} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}async function uninstallDependencies(e,s,t=!1){s.forEach((e=>{}));const n=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise(((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,(e=>{let n="";e.on("data",(e=>n+=e)),e.on("end",(()=>{try{const e=JSON.parse(n);s(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}}))})).on("error",(e=>t(e)))}))}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};async function main(){var e,s,t,n,i,c;try{const r=process.argv.slice(2);let a=r[0],o=null;if(a){const c={projectName:a,tailwindcss:r.includes("--tailwindcss"),websocket:r.includes("--websocket"),prisma:r.includes("--prisma")};if(o=await getAnswer(c),null===o)return;const l=process.cwd(),p=path.join(l,"prisma-php.json"),d=readJsonFile(p);let u=[];null===(e=d.excludeFiles)||void 0===e||e.map((e=>{const s=path.join(l,e);fs.existsSync(s)&&u.push(s.replace(/\\/g,"/"))})),updateAnswer={projectName:a,tailwindcss:null!==(s=null==o?void 0:o.tailwindcss)&&void 0!==s&&s,websocket:null!==(t=null==o?void 0:o.websocket)&&void 0!==t&&t,prisma:null!==(n=null==o?void 0:o.prisma)&&void 0!==n&&n,isUpdate:!0,excludeFiles:null!==(i=d.excludeFiles)&&void 0!==i?i:[],excludeFilePath:null!=u?u:[],filePath:l}}else o=await getAnswer();if(null===o)return;execSync("npm install -g create-prisma-php-app",{stdio:"inherit"}),execSync("npm install -g browser-sync",{stdio:"inherit"}),a||fs.mkdirSync(o.projectName);const l=process.cwd();let p=a?l:path.join(l,o.projectName);a||process.chdir(o.projectName);const d=["typescript","@types/node","ts-node","http-proxy-middleware@^3.0.0","npm-run-all"];o.tailwindcss&&d.push("tailwindcss","autoprefixer","postcss","postcss-cli","cssnano"),o.websocket&&d.push("chokidar-cli"),o.prisma&&d.push("prisma","@prisma/client"),await installDependencies(p,d,!0),a||execSync("npx tsc --init",{stdio:"inherit"}),o.tailwindcss&&execSync("npx tailwindcss init -p",{stdio:"inherit"}),o.prisma&&(fs.existsSync(path.join(p,"prisma"))||execSync("npx prisma init",{stdio:"inherit"})),await createDirectoryStructure(p,o);const u=path.join(p,"public");if(fs.existsSync(u)||fs.mkdirSync(u),null==updateAnswer?void 0:updateAnswer.isUpdate){const e=[];if(!updateAnswer.tailwindcss){["postcss.config.js","tailwind.config.js"].forEach((e=>{const s=path.join(p,e);fs.existsSync(s)&&fs.unlinkSync(s)})),e.push("tailwindcss","autoprefixer","postcss","postcss-cli","cssnano")}updateAnswer.websocket||e.push("chokidar-cli"),updateAnswer.prisma||e.push("prisma","@prisma/client"),e.length>0&&await uninstallDependencies(p,e,!0)}const h=await fetchPackageVersion("create-prisma-php-app"),m=p.replace(/\\/g,"\\"),f=bsConfigUrls(m),w=o.prisma?"src/Lib/Prisma/Classes":"",y={projectName:o.projectName,projectRootPath:m,phpEnvironment:"XAMPP",phpRootPathExe:"C:\\xampp\\php\\php.exe",phpGenerateClassPath:w,bsTarget:f.bsTarget,bsPathRewrite:f.bsPathRewrite,tailwindcss:o.tailwindcss,websocket:o.websocket,prisma:o.prisma,version:h,excludeFiles:null!==(c=null==updateAnswer?void 0:updateAnswer.excludeFiles)&&void 0!==c?c:[]};fs.writeFileSync(path.join(p,"prisma-php.json"),JSON.stringify(y,null,2),{flag:"w"}),(null==updateAnswer?void 0:updateAnswer.isUpdate)?execSync("C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar update",{stdio:"inherit"}):execSync("C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar install",{stdio:"inherit"})}catch(e){process.exit(1)}}main();
2
+ import{execSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+"\\htdocs\\".length).replace(/\\/g,"\\\\"),n=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let i=`http://localhost/${n}`;i=i.endsWith("/")?i.slice(0,-1):i;const c=i.replace(/(?<!:)(\/\/+)/g,"/"),r=n.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${r.startsWith("/")?r.substring(1):r}/`}}}function configureBrowserSyncCommand(e){const s=path.join(e,"settings","bs-config.cjs");return fs.writeFileSync(s,'const { createProxyMiddleware } = require("http-proxy-middleware");\nconst fs = require("fs");\n\nconst jsonData = fs.readFileSync("prisma-php.json", "utf8");\nconst config = JSON.parse(jsonData);\n\nmodule.exports = {\n proxy: "http://localhost:3000",\n middleware: [\n (req, res, next) => {\n res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");\n res.setHeader("Pragma", "no-cache");\n res.setHeader("Expires", "0");\n next();\n },\n createProxyMiddleware({\n target: config.bsTarget,\n changeOrigin: true,\n pathRewrite: config.bsPathRewrite,\n }),\n ],\n files: "src/**/*.*",\n notify: false,\n open: false,\n ghostMode: false,\n};',"utf8"),"browser-sync start --config settings/bs-config.cjs"}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const n=JSON.parse(fs.readFileSync(t,"utf8")),i=configureBrowserSyncCommand(e);n.scripts=Object.assign(Object.assign({},n.scripts),{projectName:"node settings/project-name.cjs"});let c=[];s.tailwindcss&&(n.scripts=Object.assign(Object.assign({},n.scripts),{tailwind:"postcss ./src/app/css/tailwind.css -o ./src/app/css/styles.css --watch"}),c.push("tailwind")),s.websocket&&(n.scripts=Object.assign(Object.assign({},n.scripts),{websocket:"node ./settings/restart-websocket.cjs"}),c.push("websocket")),s.docker&&(n.scripts=Object.assign(Object.assign({},n.scripts),{docker:"docker-compose up"}),c.push("docker"));const r=Object.assign({},n.scripts);r["browser-sync"]=i,r.dev=c.length>0?`npm-run-all --parallel projectName browser-sync ${c.join(" ")}`:"npm-run-all --parallel projectName browser-sync",n.scripts=r,n.type="module",s.prisma&&(n.prisma={seed:"node prisma/seed.js"}),fs.writeFileSync(t,JSON.stringify(n,null,2))}async function updateComposerJson(e,s){const t=path.join(e,"composer.json");if(checkExcludeFiles(t))return;let n;if(fs.existsSync(t)){{const e=fs.readFileSync(t,"utf8");n=JSON.parse(e)}s.websocket&&(n.require=Object.assign(Object.assign({},n.require),{"cboden/ratchet":"^0.4.4"})),s.prisma&&(n.require=Object.assign(Object.assign({},n.require),{"ramsey/uuid":"5.x-dev","hidehalo/nanoid-php":"1.x-dev"})),fs.writeFileSync(t,JSON.stringify(n,null,2))}}async function updateIndexJsForWebSocket(e,s){if(!s.websocket)return;const t=path.join(e,"src","app","js","index.js");if(checkExcludeFiles(t))return;let n=fs.readFileSync(t,"utf8");n+='\n// WebSocket initialization\nconst ws = new WebSocket("ws://localhost:8080");\n',fs.writeFileSync(t,n,"utf8")}async function createUpdateGitignoreFile(e,s){const t=path.join(e,".gitignore");if(checkExcludeFiles(t))return;let n="";s.forEach((e=>{n.includes(e)||(n+=`\n${e}`)})),n=n.trimStart(),fs.writeFileSync(t,n)}function copyRecursiveSync(e,s,t){var n;const i=fs.existsSync(e),c=i&&fs.statSync(e);if(i&&c&&c.isDirectory()){const i=s.toLowerCase();if(!t.websocket&&i.includes("src\\lib\\websocket"))return;if(!t.prisma&&i.includes("src\\lib\\prisma"))return;const c=s.replace(/\\/g,"/");if(null===(n=null==updateAnswer?void 0:updateAnswer.excludeFilePath)||void 0===n?void 0:n.includes(c))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach((n=>{copyRecursiveSync(path.join(e,n),path.join(s,n),t)}))}else{if(checkExcludeFiles(s))return;if(!t.tailwindcss&&(s.includes("tailwind.css")||s.includes("styles.css")))return;if(!t.websocket&&(s.includes("restart-websocket.cjs")||s.includes("restart-websocket.bat")))return;if(!t.docker&&(s.includes(".dockerignore")||s.includes("docker-compose.yml")||s.includes("Dockerfile")||s.includes("apache.conf")))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,t){s.forEach((({srcDir:s,destDir:n})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,n),t)}))}function createOrUpdateTailwindConfig(e){const s=path.join(e,"tailwind.config.js");if(checkExcludeFiles(s))return;let t=fs.readFileSync(s,"utf8");const n=["./src/app/**/*.{html,js,php}"].map((e=>` "${e}"`)).join(",\n");t=t.replace(/content: \[\],/g,`content: [\n${n}\n],`),fs.writeFileSync(s,t,{flag:"w"})}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,"export default {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n cssnano: {},\n },\n};",{flag:"w"})}function modifyLayoutPHP(e,s){const t=path.join(e,"src","app","layout.php");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8");const n='\n <link href="<?php echo $baseUrl; ?>css/index.css" rel="stylesheet">\n <script src="<?php echo $baseUrl; ?>js/index.js"><\/script>\n <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">',i=s?` <link href="<?php echo $baseUrl; ?>css/styles.css" rel="stylesheet"> ${n}`:` <script src="https://cdn.tailwindcss.com"><\/script> ${n}`;e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){}}async function createOrUpdateEnvFile(e,s){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){var s,t;return!!(null==updateAnswer?void 0:updateAnswer.isUpdate)&&(null!==(t=null===(s=null==updateAnswer?void 0:updateAnswer.excludeFilePath)||void 0===s?void 0:s.includes(e.replace(/\\/g,"/")))&&void 0!==t&&t)}async function createDirectoryStructure(e,s){const t=[{src:"/bootstrap.php",dest:"/bootstrap.php"},{src:"/.htaccess",dest:"/.htaccess"},{src:"/../composer.json",dest:"/composer.json"}];(null==updateAnswer?void 0:updateAnswer.isUpdate)&&(t.push({src:"/tsconfig.json",dest:"/tsconfig.json"}),updateAnswer.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"},{src:"/tailwind.config.js",dest:"/tailwind.config.js"}));const n=[{srcDir:"/settings",destDir:"/settings"},{srcDir:"/src",destDir:"/src"}];s.prisma&&n.push({srcDir:"/prisma",destDir:"/prisma"}),s.docker&&n.push({srcDir:"/.dockerignore",destDir:"/.dockerignore"},{srcDir:"/docker-compose.yml",destDir:"/docker-compose.yml"},{srcDir:"/Dockerfile",destDir:"/Dockerfile"},{srcDir:"/apache.conf",destDir:"/apache.conf"}),t.forEach((({src:s,dest:t})=>{const n=path.join(__dirname,s),i=path.join(e,t);if(checkExcludeFiles(i))return;const c=fs.readFileSync(n,"utf8");fs.writeFileSync(i,c,{flag:"w"})})),await executeCopy(e,n,s),await updatePackageJson(e,s),await updateComposerJson(e,s),await updateIndexJsForWebSocket(e,s),s.tailwindcss?(createOrUpdateTailwindConfig(e),modifyLayoutPHP(e,!0),modifyPostcssConfig(e)):modifyLayoutPHP(e,!1);const i='# Prisma PHP Auth Secret Key For development only - Change this in production\nAUTH_SECRET=uxsjXVPHN038DEYls2Kw0QUgBcXKUyrjv416nIFWPY4= \n \n# PHPMailer\n# SMTP_HOST=smtp.gmail.com or your SMTP host\n# SMTP_USERNAME=john.doe@gmail.com or your SMTP username\n# SMTP_PASSWORD=123456\n# SMTP_PORT=587 for TLS, 465 for SSL or your SMTP port\n# SMTP_ENCRYPTION=ssl or tls\n# MAIL_FROM=john.doe@gmail.com\n# MAIL_FROM_NAME="John Doe"';if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${i}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,i);await createUpdateGitignoreFile(e,["vendor",".env","node_modules"])}async function getAnswer(e={}){var s,t,n,i,c;const r=[];e.projectName||r.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.tailwindcss||r.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!0,active:"Yes",inactive:"No"}),e.websocket||r.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!0,active:"Yes",inactive:"No"}),e.prisma||r.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!0,active:"Yes",inactive:"No"}),e.docker||r.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"});const o=r;if(0===o.length&&e.projectName)return e;const a=()=>{process.exit(0)};try{const r=await prompts(o,{onCancel:a});return 0===Object.keys(r).length?null:{projectName:r.projectName?String(r.projectName).trim().replace(/ /g,"-"):null!==(s=e.projectName)&&void 0!==s?s:"my-app",tailwindcss:null!==(t=r.tailwindcss)&&void 0!==t?t:e.tailwindcss,websocket:null!==(n=r.websocket)&&void 0!==n?n:e.websocket,prisma:null!==(i=r.prisma)&&void 0!==i?i:e.prisma,docker:null!==(c=r.docker)&&void 0!==c?c:e.docker}}catch(e){return null}}async function installDependencies(e,s,t=!1){fs.existsSync(path.join(e,"package.json"))||execSync("npm init -y",{stdio:"inherit",cwd:e}),s.forEach((e=>{}));const n=`npm install ${t?"--save-dev":""} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}async function uninstallDependencies(e,s,t=!1){s.forEach((e=>{}));const n=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise(((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,(e=>{let n="";e.on("data",(e=>n+=e)),e.on("end",(()=>{try{const e=JSON.parse(n);s(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}}))})).on("error",(e=>t(e)))}))}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};async function main(){var e,s,t,n,i,c,r;try{const o=process.argv.slice(2);let a=o[0],l=null;if(a){const r={projectName:a,tailwindcss:o.includes("--tailwindcss"),websocket:o.includes("--websocket"),prisma:o.includes("--prisma"),docker:o.includes("--docker")};if(l=await getAnswer(r),null===l)return;const p=process.cwd(),d=path.join(p,"prisma-php.json"),u=readJsonFile(d);let h=[];null===(e=u.excludeFiles)||void 0===e||e.map((e=>{const s=path.join(p,e);fs.existsSync(s)&&h.push(s.replace(/\\/g,"/"))})),updateAnswer={projectName:a,tailwindcss:null!==(s=null==l?void 0:l.tailwindcss)&&void 0!==s&&s,websocket:null!==(t=null==l?void 0:l.websocket)&&void 0!==t&&t,prisma:null!==(n=null==l?void 0:l.prisma)&&void 0!==n&&n,docker:null!==(i=null==l?void 0:l.docker)&&void 0!==i&&i,isUpdate:!0,excludeFiles:null!==(c=u.excludeFiles)&&void 0!==c?c:[],excludeFilePath:null!=h?h:[],filePath:p}}else l=await getAnswer();if(null===l)return;execSync("npm uninstall -g create-prisma-php-app",{stdio:"inherit"}),execSync("npm install -g create-prisma-php-app",{stdio:"inherit"}),execSync("npm uninstall -g browser-sync",{stdio:"inherit"}),execSync("npm install -g browser-sync",{stdio:"inherit"}),a||fs.mkdirSync(l.projectName);const p=process.cwd();let d=a?p:path.join(p,l.projectName);a||process.chdir(l.projectName);const u=["typescript","@types/node","ts-node","http-proxy-middleware@^3.0.0","npm-run-all"];l.tailwindcss&&u.push("tailwindcss","autoprefixer","postcss","postcss-cli","cssnano"),l.websocket&&u.push("chokidar-cli"),l.prisma&&u.push("prisma","@prisma/client"),await installDependencies(d,u,!0),a||execSync("npx tsc --init",{stdio:"inherit"}),l.tailwindcss&&execSync("npx tailwindcss init -p",{stdio:"inherit"}),l.prisma&&(fs.existsSync(path.join(d,"prisma"))||execSync("npx prisma init",{stdio:"inherit"})),await createDirectoryStructure(d,l);const h=path.join(d,"public");if(fs.existsSync(h)||fs.mkdirSync(h),null==updateAnswer?void 0:updateAnswer.isUpdate){const e=[];if(!updateAnswer.tailwindcss){["postcss.config.js","tailwind.config.js"].forEach((e=>{const s=path.join(d,e);fs.existsSync(s)&&fs.unlinkSync(s)})),e.push("tailwindcss","autoprefixer","postcss","postcss-cli","cssnano")}if(updateAnswer.websocket||e.push("chokidar-cli"),updateAnswer.prisma||e.push("prisma","@prisma/client"),!updateAnswer.docker){[".dockerignore","docker-compose.yml","Dockerfile","apache.conf"].forEach((e=>{const s=path.join(d,e);fs.existsSync(s)&&fs.unlinkSync(s)}))}e.length>0&&await uninstallDependencies(d,e,!0)}const m=await fetchPackageVersion("create-prisma-php-app"),f=d.replace(/\\/g,"\\"),w=bsConfigUrls(f),y=l.prisma?"src/Lib/Prisma/Classes":"",g={projectName:l.projectName,projectRootPath:f,phpEnvironment:"XAMPP",phpRootPathExe:"C:\\xampp\\php\\php.exe",phpGenerateClassPath:y,bsTarget:w.bsTarget,bsPathRewrite:w.bsPathRewrite,tailwindcss:l.tailwindcss,websocket:l.websocket,prisma:l.prisma,docker:l.docker,version:m,excludeFiles:null!==(r=null==updateAnswer?void 0:updateAnswer.excludeFiles)&&void 0!==r?r:[]};fs.writeFileSync(path.join(d,"prisma-php.json"),JSON.stringify(g,null,2),{flag:"w"}),(null==updateAnswer?void 0:updateAnswer.isUpdate)?execSync("C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar update",{stdio:"inherit"}):execSync("C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar install",{stdio:"inherit"})}catch(e){process.exit(1)}}main();
@@ -35,6 +35,7 @@ const main = async () => {
35
35
  if (localSettings.tailwindcss) commandArgs.push("--tailwindcss");
36
36
  if (localSettings.websocket) commandArgs.push("--websocket");
37
37
  if (localSettings.prisma) commandArgs.push("--prisma");
38
+ if (localSettings.docker) commandArgs.push("--docker");
38
39
  console.log("Executing command...\n");
39
40
  await executeCommand("npx", [
40
41
  "create-prisma-php-app@latest",
@@ -91,23 +91,77 @@ class FormHandler
91
91
  /**
92
92
  * Retrieves the validation errors from the form state.
93
93
  *
94
- * @param string|null $field The name of the field to get errors for. If null, all errors are returned.
95
- * @return mixed If a field name is provided, returns the error message for that field or an empty string if no error.
94
+ * This function provides error messages for individual fields or returns all form errors if no specific field is requested.
95
+ * Error messages are wrapped in HTML `<span>` tags with unique IDs to facilitate identification and styling.
96
+ *
97
+ * - If a field name is provided:
98
+ * - Returns the error message for the specific field as a span element with a unique `id` attribute.
99
+ * - If no error message is available for the field, returns an empty span element.
100
+ * - If no field name is provided:
101
+ * - Returns an associative array of all error messages, each wrapped in a span element with a unique `id` attribute.
102
+ * - If no errors are found, returns an empty array.
103
+ * - If the form has not been validated yet:
104
+ * - Returns an empty string if a specific field name is provided.
105
+ * - Returns an empty array if no field name is provided.
106
+ *
107
+ * @param string|null $field The name of the field to retrieve errors for. If null, returns all errors.
108
+ * - Must be a valid string or `null`.
109
+ * - If provided, the resulting span element will have a unique `id` attribute prefixed with "fh-error-".
110
+ *
111
+ * @param string $class (optional) Additional classes to assign to the error `<span>` element.
112
+ * - Defaults to an empty string.
113
+ *
114
+ * @return mixed If a field name is provided, returns the error message wrapped in a span or an empty string if no error.
96
115
  * If no field name is provided, returns an associative array of all errors or an empty array if no errors.
97
116
  * If the form has not been validated yet, returns an empty string.
117
+ *
118
+ * @example
119
+ * Example usage to get a specific field's error message with a custom class:
120
+ * echo $form->getErrors('email', 'form-error');
121
+ * This will generate: "<span class='form-error' id='fh-error-email'>Error message here</span>"
122
+ *
123
+ * Example usage to get all error messages:
124
+ * print_r($form->getErrors());
125
+ * This will generate an associative array like:
126
+ * [
127
+ * 'email' => "<span class='form-error' id='fh-error-email'>Invalid email</span>",
128
+ * 'username' => "<span class='form-error' id='fh-error-username'>Username too short</span>"
129
+ * ]
98
130
  */
99
131
  public function getErrors(string $field = null): mixed
100
132
  {
133
+ $wrapError = function (string $field, string $message) {
134
+ return "id='fh-error-$field' data-error-message='$message'";
135
+ };
136
+
137
+ $field = Validator::validateString($field);
101
138
  $state = $this->stateManager->getState(self::FORM_INPUT_ERRORS);
102
139
 
103
140
  if ($this->validated && $state) {
104
141
  if ($field) {
105
- return $state['errors'][$field] ?? '';
142
+ $errorState = $state['errors'] ?? [];
143
+ return $wrapError($field, $errorState[$field] ?? '');
106
144
  }
107
- return $state['errors'] ?? [];
145
+
146
+ $errors = $state['errors'] ?? [];
147
+ foreach ($errors as $fieldName => $message) {
148
+ $errors[$fieldName] = $wrapError($fieldName, $message);
149
+ }
150
+
151
+ return $errors;
108
152
  }
109
153
 
110
- return '';
154
+ if ($field) {
155
+ $fieldData = $this->data[$field] ?? '';
156
+ return $wrapError($field, $fieldData);
157
+ }
158
+
159
+ return [];
160
+ }
161
+
162
+ public function clearErrors()
163
+ {
164
+ $this->stateManager->resetState(self::FORM_INPUT_ERRORS, true);
111
165
  }
112
166
 
113
167
  /**
@@ -211,9 +265,16 @@ class FormHandler
211
265
  public function register($fieldName, $rules = []): string
212
266
  {
213
267
  $value = Validator::validateString($this->data[$fieldName] ?? '');
214
- $attributes = "name=\"$fieldName\" value=\"$value\"";
215
268
 
216
- if (!array_intersect(array_keys($rules), ['text', 'email', 'password', 'number', 'date', 'color', 'range', 'tel', 'url', 'search', 'time', 'datetime-local', 'month', 'week'])) {
269
+ $isTypeButton = array_key_exists('button', $rules);
270
+ $attributes = "";
271
+ if ($isTypeButton) {
272
+ $attributes = "id='fh-$fieldName' name='$fieldName' data-rules='" . json_encode($rules) . "'";
273
+ } else {
274
+ $attributes = "id='fh-$fieldName' name='$fieldName' value='$value' data-rules='" . json_encode($rules) . "'";
275
+ }
276
+
277
+ if (!array_intersect(array_keys($rules), ['text', 'email', 'password', 'number', 'date', 'color', 'range', 'tel', 'url', 'search', 'time', 'datetime-local', 'month', 'week', 'file', 'submit', 'checkbox', 'radio', 'hidden', 'button', 'reset'])) {
217
278
  $rules['text'] = ['value' => true];
218
279
  }
219
280
 
@@ -225,17 +286,38 @@ class FormHandler
225
286
  $inputField[$fieldName] = [
226
287
  'value' => $value,
227
288
  'attributes' => $attributes,
228
- 'rules' => $rules
289
+ 'rules' => $rules,
229
290
  ];
230
291
  $this->stateManager->setState([self::FORM_INPUT_REGISTER => $inputField], true);
231
292
 
232
293
  return $attributes;
233
294
  }
234
295
 
296
+ /**
297
+ * Retrieves the registered form fields.
298
+ *
299
+ * @return array An associative array of registered form fields.
300
+ *
301
+ * @example
302
+ * $form->getRegisteredFields();
303
+ * This will return an array of registered form fields.
304
+ */
305
+ public function getRegisteredFields(): array
306
+ {
307
+ return $this->stateManager->getState(self::FORM_INPUT_REGISTER) ?? [];
308
+ }
309
+
235
310
  private function parseRule($rule, $ruleValue)
236
311
  {
237
312
  $attribute = '';
238
- $ruleParam = is_array($ruleValue) ? $ruleValue['value'] : $ruleValue;
313
+ $ruleParam = $ruleValue;
314
+ $requestName = null;
315
+ // $ruleParam = is_array($ruleValue) ? $ruleValue['value'] : $ruleValue;
316
+
317
+ if (is_array($ruleValue)) {
318
+ $ruleParam = $ruleValue['value'];
319
+ $requestName = $ruleValue['name'] ?? null;
320
+ }
239
321
 
240
322
  switch ($rule) {
241
323
  case 'text':
@@ -253,24 +335,30 @@ class FormHandler
253
335
  case 'month':
254
336
  case 'week':
255
337
  case 'file':
256
- $attribute .= " type=\"$rule\"";
338
+ case 'submit':
339
+ case "checkbox":
340
+ case "radio":
341
+ case "hidden":
342
+ case "button":
343
+ case "reset":
344
+ $attribute .= " type='$rule'";
257
345
  break;
258
346
  case 'required':
259
347
  $attribute .= " required";
260
348
  break;
261
349
  case 'min':
262
350
  case 'max':
263
- $attribute .= " $rule=\"$ruleParam\"";
351
+ $attribute .= " $rule='$ruleParam'";
264
352
  break;
265
353
  case 'minLength':
266
354
  case 'maxLength':
267
- $attribute .= " $rule=\"{$ruleParam}\"";
355
+ $attribute .= " $rule='$ruleParam'";
268
356
  break;
269
357
  case 'pattern':
270
- $attribute .= " pattern=\"$ruleParam\"";
358
+ $attribute .= " pattern='$ruleParam'";
271
359
  break;
272
360
  case 'autocomplete':
273
- $attribute .= " autocomplete=\"$ruleParam\"";
361
+ $attribute .= " autocomplete='$ruleParam'";
274
362
  break;
275
363
  case 'readonly':
276
364
  $attribute .= " readonly";
@@ -279,7 +367,7 @@ class FormHandler
279
367
  $attribute .= " disabled";
280
368
  break;
281
369
  case 'placeholder':
282
- $attribute .= " placeholder=\"$ruleParam\"";
370
+ $attribute .= " placeholder='$ruleParam'";
283
371
  break;
284
372
  case 'autofocus':
285
373
  $attribute .= " autofocus";
@@ -288,16 +376,37 @@ class FormHandler
288
376
  $attribute .= " multiple";
289
377
  break;
290
378
  case 'accept':
291
- $attribute .= " accept=\"$ruleParam\"";
379
+ $attribute .= " accept='$ruleParam'";
292
380
  break;
293
381
  case 'size':
294
- $attribute .= " size=\"$ruleParam\"";
382
+ $attribute .= " size='$ruleParam'";
295
383
  break;
296
384
  case 'step':
297
- $attribute .= " step=\"$ruleParam\"";
385
+ $attribute .= " step='$ruleParam'";
298
386
  break;
299
387
  case 'list':
300
- $attribute .= " list=\"$ruleParam\"";
388
+ $attribute .= " list='$ruleParam'";
389
+ break;
390
+ case 'create':
391
+ $attribute .= " data-url='$ruleParam' data-request-name='$requestName' data-request-type='create' data-type='register'";
392
+ break;
393
+ case 'read':
394
+ $attribute .= " data-url='$ruleParam' data-request-name='$requestName' data-request-type='read' data-type='register'";
395
+ break;
396
+ case 'update':
397
+ $attribute .= " data-url='$ruleParam' data-request-name='$requestName' data-request-type='update' data-type='register'";
398
+ break;
399
+ case 'delete':
400
+ $attribute .= " data-url='$ruleParam' data-request-name='$requestName' data-request-type='delete' data-type='register'";
401
+ break;
402
+ case 'event':
403
+ $attribute .= " data-event='$ruleParam' data-type='register'";
404
+ break;
405
+ case 'debounce':
406
+ $attribute .= " data-debounce='$ruleParam' data-type='register'";
407
+ break;
408
+ case 'templateConnect':
409
+ $attribute .= " data-template-connect='$ruleParam' data-type='register'";
301
410
  break;
302
411
  default:
303
412
  // Optionally handle unknown rules or log them
@@ -305,4 +414,730 @@ class FormHandler
305
414
  }
306
415
  return $attribute;
307
416
  }
417
+
418
+ /**
419
+ * Creates a watch element for a form field.
420
+ *
421
+ * This function returns an HTML string for a watch element with a unique `id` attribute,
422
+ * useful for monitoring changes in the value of a form field.
423
+ *
424
+ * @param string $field The name of the field to create a watch element for.
425
+ *
426
+ * @return string An HTML string representing the watch element. The element will have a unique `id` attribute prefixed with "fh-watch-" and suffixed by the field name, and it will include `data-watch-value` and `data-type` attributes.
427
+ *
428
+ * @example
429
+ * Example usage to create a watch element for a "username" field:
430
+ *
431
+ * echo $form->watch('username');
432
+ * Output: "<div id='fh-watch-username' data-watch-value='{value}' data-type='watch'></div>"
433
+ */
434
+ public function watch(string $field)
435
+ {
436
+ $field = Validator::validateString($field);
437
+ $fieldData = $this->data[$field] ?? '';
438
+ return "id='fh-watch-$field' data-watch-value='$fieldData' data-type='watch'";
439
+ }
440
+
441
+ /**
442
+ * Creates a template element for a form field.
443
+ *
444
+ * This function returns an HTML string for a template element with specified data attributes,
445
+ * useful for creating dynamic content that can be cloned and inserted into the DOM.
446
+ *
447
+ * @param array $params An associative array of parameters.
448
+ * - 'field' (optional): The name of the field to create a template for. If not provided, a generic template is created.
449
+ * - 'readOnLoad' (optional): A flag indicating whether the template should include the `data-read-on-load` attribute. Defaults to `true`.
450
+ * - 'listen' (optional): A string specifying any event the template should listen to. Defaults to an empty string.
451
+ * - 'noCache' (optional): A flag indicating whether the template should include the `data-no-cache` attribute. Defaults to `false`.
452
+ *
453
+ * @return string An HTML string with data attributes for the template. If a field name is provided,
454
+ * it includes the `data-template-field` attribute. Otherwise, it includes the `data-template-general` attribute.
455
+ *
456
+ * @example
457
+ * Example usage to create a template element for a "user" field:
458
+ *
459
+ * $params = [
460
+ * 'field' => 'user',
461
+ * 'readOnLoad' => true,
462
+ * 'listen' => 'exampleEvent',
463
+ * 'noCache' => true
464
+ * ];
465
+ * echo $form->template($params);
466
+ * Output: "data-template-field='user' data-read-on-load='1' data-type='template' data-listen='exampleEvent' data-no-cache='1'"
467
+ *
468
+ * Example usage to create a generic template element:
469
+ *
470
+ * $params = [
471
+ * 'readOnLoad' => true,
472
+ * 'listen' => 'exampleEvent',
473
+ * 'noCache' => false
474
+ * ];
475
+ * echo $form->template($params);
476
+ * Output: "data-template-general='true' data-read-on-load='1' data-type='template' data-listen='exampleEvent' data-no-cache='0'"
477
+ */
478
+ public function template(array $params = []): string
479
+ {
480
+ $field = isset($params['field']) ? Validator::validateString($params['field']) : null;
481
+ $readOnLoad = isset($params['readOnLoad']) ? Validator::validateBoolean($params['readOnLoad']) : true;
482
+ $listen = isset($params['listen']) ? Validator::validateString($params['listen']) : '';
483
+ $noCache = isset($params['noCache']) ? Validator::validateBoolean($params['noCache']) : false;
484
+ if ($field) {
485
+ $templateData = "data-template-field='$field' data-read-on-load='$readOnLoad' data-type='template' data-listen='$listen' data-no-cache='$noCache'";
486
+ } else {
487
+ $templateData = "data-template-general='true' data-read-on-load='$readOnLoad' data-type='template' data-listen='$listen' data-no-cache='$noCache'";
488
+ }
489
+
490
+ return $templateData;
491
+ }
492
+
493
+ /**
494
+ * Creates a template placeholder for a form field.
495
+ *
496
+ * This function returns a `data-template-placeholder` attribute with the field name as the value,
497
+ *
498
+ * useful for dynamically populating template elements with field data.
499
+ *
500
+ * @param string $field The name of the field to create a placeholder for.
501
+ *
502
+ * @return string A `data-template-placeholder` attribute with the field name as the value.
503
+ *
504
+ * @example
505
+ * Example usage to create a placeholder for a "name" field:
506
+ * echo $form->templatePlaceholder('name');
507
+ * This will generate: "data-template-placeholder='name'"
508
+ */
509
+ public function templatePlaceholder(string $field)
510
+ {
511
+ $field = Validator::validateString($field);
512
+ return "data-template-placeholder='$field' data-type='template-placeholder'";
513
+ }
514
+
515
+ /**
516
+ * Generates a string containing data attributes for a button element based on input parameters.
517
+ *
518
+ * This function accepts an associative array where keys are attribute names (without the 'data-' prefix)
519
+ * and values are the attribute values. It will only add an attribute if the associated value is not empty.
520
+ *
521
+ * @param array $attributes Associative array with the following keys and their corresponding values:
522
+ * - 'info': Information for the `data-info` attribute.
523
+ * - 'event': Event-handler pairs formatted as a string for the `data-event` attribute.
524
+ * - 'formAction': Actions or operations associated with a form for the `data-form` attribute.
525
+ * - 'connect': Additional connections related to the button for the `data-connect` attribute.
526
+ *
527
+ * @return string A string containing the concatenated `data-*` attributes, ready for inclusion in an HTML tag.
528
+ *
529
+ * @throws InvalidArgumentException If any value in the array does not pass validation.
530
+ */
531
+ public function event(array $attributes)
532
+ {
533
+ $dataAttributes = [];
534
+ $possibleAttributes = ['info', 'event', 'form', 'connect'];
535
+
536
+ foreach ($possibleAttributes as $attr) {
537
+ if (isset($attributes[$attr]) && $attributes[$attr] !== '') {
538
+ $validatedValue = Validator::validateString($attributes[$attr]);
539
+ $dataAttributes[] = "data-$attr='$validatedValue' data-type='event'";
540
+ }
541
+ }
542
+
543
+ return implode(' ', $dataAttributes);
544
+ }
545
+
546
+ public function templateConnect(string $field)
547
+ {
548
+ $field = Validator::validateString($field);
549
+ return "data-template-connect='$field' data-type='template-connect'";
550
+ }
551
+
552
+ public function form(string $field, ...$options)
553
+ {
554
+ $field = Validator::validateString($field);
555
+ $attributes = "id='fh-form-$field' method='post'";
556
+ $optionsAttributes = json_encode($options);
557
+ $attributes .= " data-options='$optionsAttributes' data-type='form'";
558
+ return $attributes;
559
+ }
308
560
  }
561
+
562
+ ?>
563
+
564
+ <script>
565
+ class FormHandler {
566
+ constructor() {
567
+ this.errors = [];
568
+ this.requestCache = new Map();
569
+ this.dataRulesElements = document.querySelectorAll('[data-rules]');
570
+ this.dataFormElements = document.querySelectorAll('[data-type="form"]');
571
+ this.init();
572
+ }
573
+
574
+ init() {
575
+ this.dataRulesElements.forEach(fieldElement => {
576
+ this.initializeFieldFromDOM(fieldElement);
577
+ });
578
+
579
+ this.dataFormElements.forEach(formElement => {
580
+ this.initializeFormFromDOM(formElement);
581
+ });
582
+
583
+ this.event();
584
+ }
585
+
586
+ initializeFieldFromDOM(fieldElement) {
587
+ if (!fieldElement) {
588
+ // console.error('Element not found for field:', fieldElement);
589
+ return;
590
+ }
591
+
592
+ const fieldName = fieldElement.name;
593
+ const dataUrl = fieldElement.getAttribute('data-url');
594
+ const rules = JSON.parse(fieldElement.getAttribute('data-rules') || '{}');
595
+ const event = fieldElement.getAttribute('data-event');
596
+
597
+ let debounceTime = fieldElement.getAttribute('data-debounce');
598
+ if (!debounceTime || debounceTime.length < 1) {
599
+ debounceTime = 300;
600
+ }
601
+
602
+ const debouncedInputHandler = debounce(async () => {
603
+ if (dataUrl) {
604
+ if (fieldElement.getAttribute('data-type') === 'register') {
605
+ if (fieldElement.getAttribute('data-request-type') === 'read') {
606
+ await this.read(dataUrl, fieldElement).catch(error => {
607
+ console.error("Read failed for field", fieldName, error);
608
+ });
609
+ }
610
+ }
611
+ }
612
+ }, debounceTime);
613
+
614
+ const immediateObserver = (e) => {
615
+ const target = e.target;
616
+ this.watch(target);
617
+
618
+ const errors = this.validateField(target, target.value, rules);
619
+ const errorContainer = document.getElementById(`fh-error-${target.name}`);
620
+ if (errorContainer) {
621
+ errorContainer.textContent = errors.join(', ');
622
+ }
623
+ };
624
+
625
+ fieldElement.addEventListener('input', debouncedInputHandler);
626
+ fieldElement.addEventListener('input', immediateObserver);
627
+
628
+ if (dataUrl) {
629
+ if (this.isDataTypeExists(fieldElement, 'register')) {
630
+ if (fieldElement.getAttribute('data-request-type') === 'read') {
631
+ this.read(dataUrl, fieldElement).catch(error => {
632
+ console.error("Initial read failed for field", fieldName, error);
633
+ });
634
+ }
635
+ }
636
+ }
637
+ }
638
+
639
+ initializeFormFromDOM(formElement) {
640
+ if (!formElement) return;
641
+
642
+ formElement.addEventListener('submit', async (e) => {
643
+ e.preventDefault();
644
+
645
+ const formData = new FormData(formElement);
646
+ const formFields = Object.fromEntries(formData.entries());
647
+
648
+ const optionsAttribute = formElement.getAttribute('data-options');
649
+ if (!optionsAttribute) {
650
+ console.error("Missing 'data-options' attribute.");
651
+ return;
652
+ }
653
+
654
+ let formOptions;
655
+ try {
656
+ formOptions = JSON.parse(optionsAttribute);
657
+ } catch (parseError) {
658
+ console.error("Failed to parse 'data-options'.", parseError);
659
+ return;
660
+ }
661
+
662
+ const formAction = formElement.dataset.action;
663
+ const url = formOptions[0]?.[formAction];
664
+ const readFieldName = formOptions[0]?.['read'];
665
+ const field = document.getElementById(`fh-${readFieldName.name}`);
666
+ const finishFunctionName = formOptions[0]?.['finishFunction'];
667
+
668
+ if (!url) {
669
+ console.error(`Invalid form action: ${formAction}`);
670
+ return;
671
+ }
672
+
673
+ const handleAction = async (actionFunc, errorMsg) => {
674
+ try {
675
+ await actionFunc(url, formFields);
676
+ } catch (error) {
677
+ console.error(errorMsg, error);
678
+ }
679
+ };
680
+
681
+ switch (formAction) {
682
+ case 'create':
683
+ await handleAction(this.create.bind(this), "Create failed");
684
+ break;
685
+ case 'update':
686
+ await handleAction(this.update.bind(this), "Update failed");
687
+ break;
688
+ case 'delete':
689
+ await handleAction(this.delete.bind(this), "Delete failed");
690
+ break;
691
+ default:
692
+ console.error(`Unknown action: ${formAction}`);
693
+ break;
694
+ }
695
+
696
+ if (field) {
697
+ try {
698
+ await this.read(field.dataset.url, field);
699
+ } catch (error) {
700
+ console.error("Read failed", error);
701
+ }
702
+ } else {
703
+ if (readFieldName) {
704
+ try {
705
+ await this.read(readFieldName.url, {
706
+ name: readFieldName,
707
+ value: ''
708
+ });
709
+ } catch (error) {
710
+ console.error("Read failed", error);
711
+ }
712
+ }
713
+ }
714
+
715
+ if (finishFunctionName && typeof window[finishFunctionName] === 'function') {
716
+ window[finishFunctionName]();
717
+ }
718
+ });
719
+ }
720
+
721
+ isDataTypeExists(element, dataType) {
722
+ return this.getDataTypes(element).includes(dataType);
723
+ }
724
+
725
+ getDataTypes(element) {
726
+ const dataTypes = element.getAttribute('data-type');
727
+ if (dataTypes) {
728
+ return dataTypes.split(',').map(val => val.trim());
729
+ } else {
730
+ return [];
731
+ }
732
+ }
733
+
734
+ updateElementDisplay(displayElement, field) {
735
+ const tagName = field.tagName.toUpperCase();
736
+ if (tagName === 'INPUT' || tagName === 'TEXTAREA') {
737
+ if (displayElement.tagName === 'INPUT' || displayElement.tagName === 'TEXTAREA') {
738
+ displayElement.value = field.value;
739
+ } else {
740
+ displayElement.dataset.watchValue = field.value;
741
+ displayElement.textContent = field.value;
742
+ }
743
+ } else {
744
+ displayElement.textContent = field.textContent;
745
+ }
746
+ }
747
+
748
+ watch(field) {
749
+ if (!field) return;
750
+
751
+ const watchElement = document.getElementById(`fh-watch-${field.name}`);
752
+ if (watchElement) {
753
+ this.updateElementDisplay(watchElement, field);
754
+ }
755
+ }
756
+
757
+ clearErrors() {
758
+ const errorElements = document.querySelectorAll('[id^="fh-error-"]');
759
+ errorElements.forEach(element => {
760
+ element.textContent = '';
761
+ });
762
+
763
+ this.errors = [];
764
+ }
765
+
766
+ getErrors(field) {
767
+ if (field) {
768
+ return document.getElementById(`fh-error-${field}`).textContent;
769
+ } else {
770
+ return this.errors;
771
+ }
772
+ }
773
+
774
+ event() {
775
+ const eventElements = document.querySelectorAll('[data-event]');
776
+ eventElements.forEach(element => {
777
+ const eventAttr = element.getAttribute('data-event');
778
+ if (eventAttr) {
779
+ const events = eventAttr.split(';');
780
+ events.forEach(eventFunctionPair => {
781
+ const [eventType, functionName] = eventFunctionPair.split(',').map(val => val.trim());
782
+
783
+ if (eventType && functionName && typeof window[functionName] === 'function') {
784
+ element.addEventListener(eventType, window[functionName]);
785
+
786
+ element.addEventListener(eventType, (e) => {
787
+ const eventForm = element.getAttribute('data-form');
788
+ if (eventForm) {
789
+ const formAttributes = eventForm.split(',');
790
+ const formElement = document.getElementById(`fh-form-${formAttributes[0]}`);
791
+ const formAction = formAttributes[1];
792
+
793
+ if (!formElement) return;
794
+
795
+ if (['create', 'read', 'update', 'delete'].includes(formAction)) {
796
+ formElement.dataset.action = formAction;
797
+ }
798
+
799
+ if (formAction === 'create') {
800
+ const templateElements = document.querySelectorAll('[data-template-connect]');
801
+ templateElements.forEach(templateElement => {
802
+ if (templateElement) {
803
+ this.updateElementDisplay(templateElement, {
804
+ tagName: templateElement.tagName,
805
+ value: '',
806
+ textContent: ''
807
+ });
808
+ }
809
+ });
810
+ }
811
+ }
812
+ });
813
+ } else {
814
+ console.error(`Invalid event or function: ${eventFunctionPair}`);
815
+ }
816
+ });
817
+ }
818
+ });
819
+ }
820
+
821
+ getTemplateForField(field) {
822
+ let templates = [];
823
+ const typeTemplate = document.querySelectorAll('[data-type="template"]');
824
+
825
+ if (typeTemplate.length > 0) {
826
+ templates = Array.from(typeTemplate).filter(template => {
827
+ const templateField = template.getAttribute('data-template-field');
828
+ const generalTemplate = template.getAttribute('data-template-general');
829
+ const listen = template.getAttribute('data-listen');
830
+ if (templateField === field.name) {
831
+ return true;
832
+ }
833
+
834
+ const templateListener = document.querySelector(`[data-template-field="${listen}"]`);
835
+ if (templateListener && templateListener.getAttribute('data-listen') !== '') {
836
+ return true;
837
+ }
838
+
839
+ if (listen === 'read') {
840
+ return true;
841
+ }
842
+
843
+ if (generalTemplate === 'true') {
844
+ return true;
845
+ }
846
+
847
+ return false;
848
+ });
849
+ }
850
+ return templates;
851
+ }
852
+
853
+ processDataItems(items, template, tbody) {
854
+ items.forEach((item) => {
855
+ const clone = document.importNode(template.content, true);
856
+
857
+ Object.keys(item).forEach(key => {
858
+ const placeholderElement = clone.querySelector(`[data-template-placeholder="${key}"]`);
859
+ if (placeholderElement) {
860
+ placeholderElement.textContent = item[key];
861
+ }
862
+ });
863
+
864
+ const dataInfoElements = clone.querySelectorAll('[data-info], [data-event], [data-connect]');
865
+ dataInfoElements.forEach(element => {
866
+ this.attachEventHandlers(element, item);
867
+ });
868
+
869
+ tbody.appendChild(clone);
870
+ });
871
+ }
872
+
873
+ attachEventHandlers(element, item) {
874
+ const infoKeys = element.getAttribute('data-info');
875
+ if (infoKeys) {
876
+ if (infoKeys === 'all') {
877
+ const keys = Object.keys(item);
878
+ keys.forEach(key => {
879
+ if (item[key] !== undefined) {
880
+ element.dataset[key] = item[key];
881
+ }
882
+ });
883
+ } else {
884
+ const keys = infoKeys.split(',').map(key => key.trim());
885
+ keys.forEach(key => {
886
+ if (item[key] !== undefined) {
887
+ element.dataset[key] = item[key];
888
+ }
889
+ });
890
+ }
891
+ }
892
+
893
+ const eventAttr = element.getAttribute('data-event');
894
+ if (eventAttr) {
895
+ const events = eventAttr.split(';');
896
+ events.forEach(eventFunctionPair => {
897
+ const [eventType, functionName] = eventFunctionPair.split(',').map(val => val.trim());
898
+
899
+ if (eventType && functionName && typeof window[functionName] === 'function') {
900
+ element.addEventListener(eventType, window[functionName]);
901
+
902
+ element.addEventListener(eventType, (e) => {
903
+ this.handleDataConnectAndFormActions(element, item);
904
+ });
905
+ } else {
906
+ console.error(`Invalid event or function: ${eventFunctionPair}`);
907
+ }
908
+ });
909
+ }
910
+ }
911
+
912
+ handleDataConnectAndFormActions(element, item) {
913
+ const eventForm = element.getAttribute('data-form');
914
+ if (eventForm) {
915
+ const formAttributes = eventForm.split(',');
916
+ const formElement = document.getElementById(`fh-form-${formAttributes[0]}`);
917
+ const formAction = formAttributes[1];
918
+
919
+ if (!formElement) return;
920
+
921
+ if (['create', 'read', 'update', 'delete'].includes(formAction)) {
922
+ formElement.dataset.action = formAction;
923
+ }
924
+ }
925
+
926
+ const templateConnectValues = element.getAttribute('data-connect');
927
+ if (templateConnectValues) {
928
+ const connectValues = templateConnectValues.split(',').map(value => value.trim());
929
+ connectValues.forEach(connectValue => {
930
+ const templateElements = document.querySelectorAll(`[data-template-connect="${connectValue}"]`);
931
+ templateElements.forEach(templateElement => {
932
+ this.updateElementDisplay(templateElement, {
933
+ tagName: templateElement.tagName,
934
+ value: item[connectValue],
935
+ textContent: item[connectValue]
936
+ });
937
+ });
938
+ });
939
+ }
940
+ }
941
+
942
+ updateTemplates(templates, response, field) {
943
+ const items = Array.isArray(response) ? response : [response];
944
+ templates.forEach(template => {
945
+ let readOnLoad = template.getAttribute('data-read-on-load');
946
+ let noCache = template.getAttribute('data-no-cache');
947
+ if (readOnLoad === '' && (field.value === undefined || field.value.length === 0)) {
948
+ const tbody = template.parentNode;
949
+ tbody.innerHTML = '';
950
+ tbody.appendChild(template);
951
+ return;
952
+ }
953
+
954
+ const tbody = template.parentNode;
955
+ tbody.innerHTML = '';
956
+ tbody.appendChild(template);
957
+
958
+ this.processDataItems(items, template, tbody);
959
+
960
+ if (noCache !== '') {
961
+ this.requestCache.delete(cacheKey);
962
+ }
963
+ });
964
+ }
965
+
966
+ clearCache() {
967
+ this.requestCache.clear();
968
+ }
969
+
970
+ async create(url, data) {
971
+ if (!url || !data) return;
972
+
973
+ try {
974
+ const response = await api.post(url, data);
975
+ this.clearCache();
976
+ } catch (error) {
977
+ console.error("Create failed", error);
978
+ }
979
+ }
980
+
981
+ async read(url, field) {
982
+ if (!url || !field) return;
983
+
984
+ let fieldDataName = '';
985
+ if (field instanceof HTMLElement) {
986
+ const requestName = field.getAttribute('data-request-name');
987
+ fieldDataName = requestName && requestName.length > 0 ? requestName : field.name;
988
+ } else {
989
+ fieldDataName = field.name ?? '';
990
+ }
991
+
992
+ if (!fieldDataName) return;
993
+
994
+ const data = {
995
+ [fieldDataName]: field.value
996
+ };
997
+
998
+ let templates = this.getTemplateForField(field);
999
+ if (templates.length === 0) return;
1000
+
1001
+ const cacheKey = `${url}-${JSON.stringify(data)}`;
1002
+ if (this.requestCache.has(cacheKey)) {
1003
+ return this.requestCache.get(cacheKey)
1004
+ .then(response => {
1005
+ this.updateTemplates(templates, response, field);
1006
+ })
1007
+ .catch(error => {
1008
+ console.error("🚀 ~ FormHandler ~ read ~ error from cache:", error);
1009
+ });
1010
+ }
1011
+
1012
+ const requestPromise = api.post(url, data);
1013
+ this.requestCache.set(cacheKey, requestPromise);
1014
+
1015
+ try {
1016
+ const response = await requestPromise;
1017
+ this.updateTemplates(templates, response, field, cacheKey);
1018
+ } catch (error) {
1019
+ console.error("🚀 ~ FormHandler ~ read ~ error from request:", error);
1020
+ this.requestCache.delete(cacheKey);
1021
+ }
1022
+ }
1023
+
1024
+ async update(url, data) {
1025
+ if (!url || !data) return;
1026
+
1027
+ try {
1028
+ const response = await api.put(url, data);
1029
+ this.clearCache();
1030
+ } catch (error) {
1031
+ console.error("Update failed", error);
1032
+ }
1033
+ }
1034
+
1035
+ async delete(url, data) {
1036
+ if (!url || !data) return;
1037
+
1038
+ try {
1039
+ const response = await api.delete(url, data);
1040
+ this.clearCache();
1041
+ } catch (error) {
1042
+ console.error("Delete failed", error);
1043
+ }
1044
+ }
1045
+
1046
+ validateField(field, value, rules) {
1047
+ if (!rules) return [];
1048
+ this.errors = [];
1049
+
1050
+ for (const [rule, options] of Object.entries(rules)) {
1051
+ let ruleValue = options;
1052
+ let customMessage = null;
1053
+
1054
+ if (typeof options === 'object') {
1055
+ ruleValue = options.value;
1056
+ customMessage = options.message || null;
1057
+ }
1058
+
1059
+ switch (rule) {
1060
+ case 'text':
1061
+ case 'search':
1062
+ if (typeof value !== 'string') {
1063
+ this.errors.push(customMessage || 'Must be a string.');
1064
+ }
1065
+ break;
1066
+ case 'email':
1067
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
1068
+ this.errors.push(customMessage || 'Invalid email format.');
1069
+ }
1070
+ break;
1071
+ case 'number':
1072
+ if (isNaN(value)) {
1073
+ this.errors.push(customMessage || 'Must be a number.');
1074
+ }
1075
+ break;
1076
+ case 'date':
1077
+ if (isNaN(Date.parse(value))) {
1078
+ this.errors.push(customMessage || 'Invalid date format.');
1079
+ }
1080
+ break;
1081
+ case 'range':
1082
+ const [min, max] = ruleValue;
1083
+ if (isNaN(value) || value < min || value > max) {
1084
+ this.errors.push(customMessage || `Must be between ${min} and ${max}.`);
1085
+ }
1086
+ break;
1087
+ case 'url':
1088
+ try {
1089
+ new URL(value);
1090
+ } catch (e) {
1091
+ this.errors.push(customMessage || 'Invalid URL format.');
1092
+ }
1093
+ break;
1094
+ case 'required':
1095
+ if (!value) {
1096
+ this.errors.push(customMessage || 'This field is required.');
1097
+ }
1098
+ break;
1099
+ case 'min':
1100
+ if (Number(value) < ruleValue) {
1101
+ this.errors.push(customMessage || `Must be at least ${ruleValue}.`);
1102
+ }
1103
+ break;
1104
+ case 'max':
1105
+ if (Number(value) > ruleValue) {
1106
+ this.errors.push(customMessage || `Must be at most ${ruleValue}.`);
1107
+ }
1108
+ break;
1109
+ case 'minLength':
1110
+ if (value.length < ruleValue) {
1111
+ this.errors.push(customMessage || `Must be at least ${ruleValue} characters.`);
1112
+ }
1113
+ break;
1114
+ case 'maxLength':
1115
+ if (value.length > ruleValue) {
1116
+ this.errors.push(customMessage || `Must be at most ${ruleValue} characters.`);
1117
+ }
1118
+ break;
1119
+ case 'pattern':
1120
+ if (!new RegExp(ruleValue).test(value)) {
1121
+ this.errors.push(customMessage || 'Invalid format.');
1122
+ }
1123
+ break;
1124
+ case 'accept':
1125
+ if (!ruleValue.split(',').includes(value)) {
1126
+ this.errors.push(customMessage || 'Invalid file format.');
1127
+ }
1128
+ break;
1129
+ default:
1130
+ // Optionally handle unknown rules or log them
1131
+ break;
1132
+ }
1133
+ }
1134
+
1135
+ return this.errors;
1136
+ }
1137
+ }
1138
+
1139
+ let formHandler = null;
1140
+ document.addEventListener('DOMContentLoaded', function() {
1141
+ formHandler = new FormHandler();
1142
+ });
1143
+ </script>
@@ -1,22 +1,31 @@
1
1
  /**
2
2
  * Debounces a function to limit the rate at which it is called.
3
3
  *
4
+ * The debounced function will postpone its execution until after the specified wait time
5
+ * has elapsed since the last time it was invoked. If `immediate` is `true`, the function
6
+ * will be called at the beginning of the wait period instead of at the end.
7
+ *
4
8
  * @param {Function} func - The function to debounce.
5
- * @param {number} wait - The number of milliseconds to wait before invoking the function.
6
- * @param {boolean} immediate - Whether to invoke the function immediately on the leading edge.
7
- * @returns {Function} - The debounced function.
9
+ * @param {number} [wait=300] - The number of milliseconds to wait before invoking the function.
10
+ * @param {boolean} [immediate=false] - If `true`, the function is invoked immediately on the leading edge.
11
+ * @returns {Function} - Returns the debounced version of the original function.
8
12
  */
9
- function debounce(func, wait, immediate) {
13
+ function debounce(func, wait = 300, immediate = false) {
10
14
  let timeout;
15
+
11
16
  return function () {
12
- const context = this,
13
- args = arguments;
17
+ const context = this;
18
+ const args = arguments;
14
19
  clearTimeout(timeout);
20
+
15
21
  timeout = setTimeout(() => {
16
22
  timeout = null;
17
23
  if (!immediate) func.apply(context, args);
18
24
  }, wait);
19
- if (immediate && !timeout) func.apply(context, args);
25
+
26
+ if (immediate && !timeout) {
27
+ func.apply(context, args);
28
+ }
20
29
  };
21
30
  }
22
31
 
@@ -9,6 +9,7 @@
9
9
  <link rel="shortcut icon" href="<?php echo $baseUrl; ?>favicon.ico" type="image/x-icon">
10
10
  <script>
11
11
  const baseUrl = '<?php echo $baseUrl; ?>';
12
+ const pathname = '<?php echo $pathname; ?>';
12
13
  </script>
13
14
  </head>
14
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-prisma-php-app",
3
- "version": "1.11.21",
3
+ "version": "1.11.23",
4
4
  "description": "Prisma-PHP: A Revolutionary Library Bridging PHP with Prisma ORM",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",