create-prisma-php-app 1.11.22 → 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.
- package/dist/.dockerignore +2 -0
- package/dist/Dockerfile +40 -0
- package/dist/apache.conf +12 -0
- package/dist/bootstrap.php +10 -2
- package/dist/docker-compose.yml +19 -0
- package/dist/index.js +1 -1
- package/dist/prisma-php.js +1 -0
- package/dist/src/Lib/FormHandler.php +854 -19
- package/dist/src/app/js/index.js +16 -7
- package/dist/src/app/layout.php +1 -0
- package/package.json +1 -1
package/dist/Dockerfile
ADDED
|
@@ -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"]
|
package/dist/apache.conf
ADDED
|
@@ -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>
|
package/dist/bootstrap.php
CHANGED
|
@@ -25,9 +25,17 @@ function determineContentToInclude()
|
|
|
25
25
|
writeRoutes();
|
|
26
26
|
AuthMiddleware::handle($uri);
|
|
27
27
|
|
|
28
|
-
$isDirectAccessToPrivateRoute = preg_match('
|
|
28
|
+
$isDirectAccessToPrivateRoute = preg_match('/\/_/', $uri);
|
|
29
29
|
if ($isDirectAccessToPrivateRoute) {
|
|
30
|
-
|
|
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;if(0===r.length&&e.projectName)return e;const 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 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(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();
|
package/dist/prisma-php.js
CHANGED
|
@@ -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
|
-
*
|
|
95
|
-
*
|
|
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
|
-
|
|
142
|
+
$errorState = $state['errors'] ?? [];
|
|
143
|
+
return $wrapError($field, $errorState[$field] ?? '');
|
|
106
144
|
}
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
351
|
+
$attribute .= " $rule='$ruleParam'";
|
|
264
352
|
break;
|
|
265
353
|
case 'minLength':
|
|
266
354
|
case 'maxLength':
|
|
267
|
-
$attribute .= " $rule
|
|
355
|
+
$attribute .= " $rule='$ruleParam'";
|
|
268
356
|
break;
|
|
269
357
|
case 'pattern':
|
|
270
|
-
$attribute .= " pattern
|
|
358
|
+
$attribute .= " pattern='$ruleParam'";
|
|
271
359
|
break;
|
|
272
360
|
case 'autocomplete':
|
|
273
|
-
$attribute .= " autocomplete
|
|
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
|
|
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
|
|
379
|
+
$attribute .= " accept='$ruleParam'";
|
|
292
380
|
break;
|
|
293
381
|
case 'size':
|
|
294
|
-
$attribute .= " size
|
|
382
|
+
$attribute .= " size='$ruleParam'";
|
|
295
383
|
break;
|
|
296
384
|
case 'step':
|
|
297
|
-
$attribute .= " step
|
|
385
|
+
$attribute .= " step='$ruleParam'";
|
|
298
386
|
break;
|
|
299
387
|
case 'list':
|
|
300
|
-
$attribute .= " list
|
|
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>
|
package/dist/src/app/js/index.js
CHANGED
|
@@ -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 -
|
|
7
|
-
* @returns {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
|
-
|
|
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
|
-
|
|
25
|
+
|
|
26
|
+
if (immediate && !timeout) {
|
|
27
|
+
func.apply(context, args);
|
|
28
|
+
}
|
|
20
29
|
};
|
|
21
30
|
}
|
|
22
31
|
|
package/dist/src/app/layout.php
CHANGED