From 275c0d74cd68677622a5431505c5c870c473063d Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 29 Mar 2026 21:40:15 +0000
Subject: [PATCH] =Seems to be working, huzzah! Added some changes for on-this-page nav
---
base/_setup.php | 1
assets/js/concise/navigation.js | 2
assets/js/min/navigation.min.js | 2
inc/blocks/SummaryBlock.php | 28
inc/managers/SEO/render/SchemaOutput.php | 124 +
inc/managers/Cache.php | 2
jvb.php | 8
inc/helpers/ui.php | 12
inc/registrar/Terms.php | 2
inc/managers/DashboardManager.php | 30
inc/rest/routes/ContentRoutes.php | 258 ++-
inc/managers/CRUDManager.php | 9
assets/js/concise/UtilityFunctions.js | 3
inc/blocks/CustomBlocks.php | 12
assets/js/min/utility.min.js | 2
assets/js/concise/CRUD.js | 66
inc/registrar/config/seo/Schema.php | 52
JVBase.php | 6
inc/managers/LoginManager.php | 3
inc/integrations/Square.php | 7
inc/managers/queue/executors/ContentExecutor.php | 287 +++-
inc/ui/CRUDSkeleton.php | 6
inc/rest/routes/ShopRoutes.php | 4
inc/rest/routes/NewsRoutes.php | 1
inc/admin/SEOAdmin.php | 50
assets/js/min/page-nav.min.js | 2
inc/integrations/Umami.php | 8
inc/meta/Field.php | 16
assets/js/concise/UploadManager.js | 16
inc/registrar/Registrar.php | 47
assets/js/min/crud.min.js | 2
inc/helpers/renderFields.php | 1
inc/meta/Meta.php | 640 ++++-------
inc/meta/Storage.php | 18
inc/registrar/Posts.php | 2
assets/js/concise/on-this-page.js | 222 ---
inc/rest/routes/UploadRoutes.php | 21
inc/meta/Sanitizer.php | 10
inc/registrar/Fields.php | 2
inc/integrations/GoogleMyBusiness.php | 4
base/seo.php | 320 +++++
inc/registrar/fields/Field.php | 10
assets/css/nav.min.css | 2
inc/managers/queue/executors/UploadExecutor.php | 5
assets/js/min/uploader.min.js | 2
inc/utility/Image.php | 13
src/fields/render.php | 2
inc/rest/routes/SettingsRoutes.php | 3
assets/js/concise/DataStore.js | 10
inc/managers/queue/executors/ContentTermExecutor.php | 7
inc/managers/SEO/render/Traits/ThingSchema.php | 2
inc/rest/Rest.php | 192 +++
assets/js/min/dataStore.min.js | 2
inc/meta/MetaOld.php | 701 ++++++++++++
inc/helpers/terms.php | 25
55 files changed, 2,257 insertions(+), 1,027 deletions(-)
diff --git a/JVBase.php b/JVBase.php
index c5f383f..255ff5f 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -50,6 +50,7 @@
use JVBase\rest\routes\AdminRoutes;
use JVBase\rest\routes\IntegrationsRoutes;
use JVBase\utility\Features;
+use JVBase\base\SchemaHelper;
if (!defined('ABSPATH')) {
exit;
@@ -101,6 +102,7 @@
'admin' => new AdminPages(),
'seoAdmin' => new SEOAdmin(),
'seo' => new SchemaOutput(),
+ 'schemaHelper' => new SchemaHelper(),
// 'uploads' => new UploadManager(),
'userTerms' => new UserTermsManager(),
'email' => new EmailManager(),
@@ -381,4 +383,8 @@
{
return $this->customBlocks??false;
}
+ public function schemaHelper():SchemaHelper
+ {
+ return $this->managers['schemaHelper'];
+ }
}
diff --git a/assets/css/nav.min.css b/assets/css/nav.min.css
index 07c23b2..910f637 100644
--- a/assets/css/nav.min.css
+++ b/assets/css/nav.min.css
@@ -1 +1 @@
-nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;font-family:var(--heading);padding:0;margin:0}nav li{display:flex;align-items:center;height:max(var(--btn),max-content);width:100%;max-inline-size:none;padding:0}nav a,nav button{display:flex;text-decoration:none;align-items:center;justify-content:center;height:var(--btn);width:100%;white-space:nowrap;text-transform:uppercase;transition:var(--trans-color)}nav a{height:var(--btn);padding:var(--padding)}nav button{justify-content:center;aspect-ratio:1;padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon{transform:rotate(0);transition:transform var(--trans-base)}.has-submenu.open>button .icon{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max(100%,max-content);background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:all var(--trans-t) var(--trans-fn);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:2px solid var(--action-0);outline-offset:2px}nav.always{--dir:column;--wrap:nowrap;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{--justify:flex-end;width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px)}nav.always>ul{--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;position:relative;right:-300vw;width:100vw;max-height:100%;padding:1rem 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{flex-wrap:wrap;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;max-width:calc(100% - var(--btn));text-align:center}nav.always .has-submenu{display:flex}nav.always .has-submenu>a{flex:1}nav.always .has-submenu>button{flex:0 0 var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;height:max-content;top:0;width:100%;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>button{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>button .icon-list,nav.always>button .icon-x{display:none}nav.always.open>button .icon-x,nav.always>button .icon-list{display:block;width:32px;height:32px}@media (min-width:768px){nav.always>ul{padding-top:var(--btn)}}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-7)}#breadcrumbs ol{height:max-content;--wrap:wrap!important;--justify:flex-start!important}#breadcrumbs li{width:max-content}#breadcrumbs a{height:var(--chip)}#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}#breadcrumbs li:last-of-type::after{display:none}#breadcrumbs :is(a,span){padding:0 .125rem;color:var(--contrast);text-transform:none}#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed.bottom{position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed.bottom li{flex:1;justify-content:center}nav.fixed.bottom a{gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed.bottom a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));max-width:none;padding:0 .5rem;background-color:rgba(var(--base-rgb),var(--op-4));color:var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-6)}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}body:has(.additional-actionsbutton) nav.on-this-page{width:calc(100vw - var(--btn_) - 1rem)}.on-this-page ul{width:100%;gap:0}.on-this-page li{justify-content:center}.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}.on-this-page a{height:var(--chip);padding:0}nav.letters li{height:var(--chip);max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}nav.letters,nav.letters ul{height:var(--chipchip)}@media (min-width:768px){nav.letters,nav.letters ul{height:var(--chip)}nav.letters ul{--wrap:nowrap}nav.letters li{max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:flex-start;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}.index ul{width:max-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--trans-base)}.index li.active,.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;align-items:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}.index.open ul{--dir:column;--justify:flex-end;height:100%;width:100%}.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}.index.open a{justify-content:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed{height:max-content;--wrap:wrap;--gap:0 .25rem}nav.condensed ul{min-height:var(--chip_);height:max-content;--justify:center;--wrap:wrap}.condensed li{width:max-content;min-height:var(--chip)}.condensed li+li::before{content:'·';padding:0 .25em}.condensed a{height:max-content;min-height:var(--chip);font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}.condensed a:focus{border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials,.always ul.socials a,.always ul.socials li{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{position:fixed;bottom:var(--btn);left:var(--btnbtn);right:var(--btnbtn);padding-bottom:2px;z-index:var(--z-6);touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button{aspect-ratio:unset}nav.tabs button.active{cursor:default}nav.tabs button.active:hover{background-color:var(--base-100);color:var(--contrast)}nav.tabs button h2{--wrap:nowrap;margin:0;font-size:var(--txt-x-small)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content.active{padding:1rem 0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}:where(body>header,.wp-site-blocks>header){--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}nav.term-navigation:has([hidden]){display:none}.dashboard-nav{--justify:flex-start;width:100%}nav.filters{--dir:row;--justify:flex-start;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}
\ No newline at end of file
+nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;font-family:var(--heading);padding:0;margin:0}nav li{display:flex;align-items:center;height:max(var(--btn),max-content);width:100%;max-inline-size:none;padding:0}nav a,nav button{display:flex;text-decoration:none;align-items:center;justify-content:center;height:var(--btn);width:100%;white-space:nowrap;text-transform:uppercase;transition:var(--trans-color)}nav a{height:var(--btn);padding:var(--padding)}nav button{justify-content:center;aspect-ratio:1;padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon{transform:rotate(0);transition:transform var(--trans-base)}.has-submenu.open>button .icon{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max(100%,max-content);background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:all var(--trans-t) var(--trans-fn);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:2px solid var(--action-0);outline-offset:2px}nav.always{--dir:column;--wrap:nowrap;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{--justify:flex-end;width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px)}nav.always>ul{--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;position:relative;right:-300vw;width:100vw;max-height:100%;padding:1rem 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{flex-wrap:wrap;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;max-width:calc(100% - var(--btn));text-align:center}nav.always .has-submenu{display:flex}nav.always .has-submenu>a{flex:1}nav.always .has-submenu>button{flex:0 0 var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;height:max-content;top:0;width:100%;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>button{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>button .icon-list,nav.always>button .icon-x{display:none}nav.always.open>button .icon-x,nav.always>button .icon-list{display:block;width:32px;height:32px}@media (min-width:768px){nav.always>ul{padding-top:var(--btn)}}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-7)}#breadcrumbs ol{height:max-content;--wrap:wrap!important;--justify:flex-start!important}#breadcrumbs li{width:max-content}#breadcrumbs a{height:var(--chip)}#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}#breadcrumbs li:last-of-type::after{display:none}#breadcrumbs :is(a,span){padding:0 .125rem;color:var(--contrast);text-transform:none}#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed.bottom{position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed.bottom li{flex:1;justify-content:center}nav.fixed.bottom a{gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed.bottom a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));max-width:none;padding:0 .5rem;background-color:rgba(var(--base-rgb),var(--op-4));color:var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-6)}.on-this-page a,.on-this-page li{width:100%;height:100%}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}body:has(.additional-actionsbutton) nav.on-this-page{width:calc(100vw - var(--btn_) - 1rem)}.on-this-page button{order:3;padding:0 1rem;width:max-content;aspect-ratio:unset;height:var(--btn)}.on-this-page.open button{order:0}.on-this-page ul{width:100%;gap:0}.on-this-page li{justify-content:center}.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}.on-this-page a{padding:0}.on-this-page #back-to-top span{display:none}.on-this-page.open #back-to-top span{display:block}nav.letters li{height:var(--chip);max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}nav.letters,nav.letters ul{height:var(--chipchip)}@media (min-width:768px){nav.letters,nav.letters ul{height:var(--chip)}nav.letters ul{--wrap:nowrap}nav.letters li{max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:space-between;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}.index ul{width:100%}.index li{flex-shrink:0;transform:scaleX(0);max-width:0;overflow:hidden}.index li.active,.index li.adj{transform:scaleX(1);width:calc(100% - var(--btn_));flex-shrink:1;max-width:none}.index li:first-of-type{flex-shrink:1;transform:scaleX(1);order:9999;width:var(--btn);height:var(--btn);max-width:none}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;align-items:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}.index.open ul{--dir:column;--justify:flex-end;height:100%;width:100%}.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}.index.open a{justify-content:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed{height:max-content;--wrap:wrap;--gap:0 .25rem}nav.condensed ul{min-height:var(--chip_);height:max-content;--justify:center;--wrap:wrap}.condensed li{width:max-content;min-height:var(--chip)}.condensed li+li::before{content:'·';padding:0 .25em}.condensed a{height:max-content;min-height:var(--chip);font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}.condensed a:focus{border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials,.always ul.socials a,.always ul.socials li{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{position:fixed;bottom:var(--btn);left:var(--btnbtn);right:var(--btnbtn);padding-bottom:2px;z-index:var(--z-6);touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button{aspect-ratio:unset}nav.tabs button.active{cursor:default}nav.tabs button.active:hover{background-color:var(--base-100);color:var(--contrast)}nav.tabs button h2{--wrap:nowrap;margin:0;font-size:var(--txt-x-small)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content.active{padding:1rem 0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}:where(body>header,.wp-site-blocks>header){--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}nav.term-navigation:has([hidden]){display:none}.dashboard-nav{--justify:flex-start;width:100%}nav.filters{--dir:row;--justify:flex-start;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}
\ No newline at end of file
diff --git a/assets/js/concise/CRUD.js b/assets/js/concise/CRUD.js
index a4e1573..3a61fd5 100644
--- a/assets/js/concise/CRUD.js
+++ b/assets/js/concise/CRUD.js
@@ -52,8 +52,9 @@
if (refs.trash) refs.trash.dataset.id = data.id;
};
const imageSetup = function(el, refs, data) {
- if (data?.fields?.post_thumbnail) {
- const thumbnail = data.images[data.fields.post_thumbnail] ?? {};
+ let hasThumbnail = data?.fields?.post_thumbnail || data?.fields?.thumbnail;
+ if (hasThumbnail) {
+ const thumbnail = data.images[hasThumbnail] ?? {};
refs.img.src = thumbnail.medium??'';
refs.img.alt = thumbnail.alt??data.fields.post_title??'';
}
@@ -516,31 +517,64 @@
}
this.store.clearCache();
}
+
if (event === 'operation-status'
&& data.status === 'completed'
&& data.type === 'content_update') {
+
this.store.clearCache();
- // Check for result data (from ContentExecutor)
- if (!data.result || !data.result.posts) {
- console.warn('Content update completed but no result.posts', data);
+ if (!data.result || !data.result.success || !data.result.errors)
+ {
+ console.warn('Content update completed but no results', data);
return;
}
- // Get successfully processed post IDs
- const successfulIds = Object.keys(data.result.posts);
-
- if (successfulIds.length === 0) {
+ if (Object.keys(data.result.success).length > 0) {
+ this.checkCompletedChanges(Object.entries(data.result.success));
+ }
+ if (Object.keys(data.result.errors).length > 0) {
+ this.checkFailedChanges(Object.entries(data.result.errors));
return;
}
- // Clear from both persistent and in-memory storage
- this.changesStore.deleteMany(successfulIds);
- successfulIds.forEach(id => this.changes.delete(id));
+ if (Object.keys(data.result.success).length === 0) {
+ this.changesStore.delete(id);
+ this.store.clearCache();
+ }
}
});
}
+ checkCompletedChanges(items) {
+ for (let [id, data] of items) {
+
+ let stored = this.changesStore.get(id);
+ if (!stored) continue;
+
+ for (let [field, value] of Object.entries(data)) {
+ if (Object.hasOwn(stored, field)) {
+ let changes = window.getDifferences.map(stored[field], value);
+
+ if (!changes) {
+ delete stored[field];
+ }
+
+ }
+ }
+
+ //It'll have the id and the content still
+ if (Object.values(stored).length === 2) {
+ this.changesStore.delete(id);
+ this.store.clearCache();
+ } else {
+ this.changesStore.save(stored);
+ }
+ }
+ }
+ checkFailedChanges(items) {
+ //TODO do something.
+ }
initSettings() {
this.defaults = {
@@ -1220,7 +1254,13 @@
this.activeItem = item.id;
this.ui.modals.edit.modal.dataset.itemId = itemID;
this.ui.modals.edit.modal.dataset.content = this.content;
- this.ui.modals.edit.h2.textContent = `Editing ${item.fields.post_title === '' ? this.singular : item.fields.post_title}`;
+ let title;
+ if (Object.hasOwn(item.fields, 'post_title')) {
+ title = item.fields.post_title;
+ } else if (Object.hasOwn(item.fields, 'name')) {
+ title = item.fields.name;
+ }
+ this.ui.modals.edit.h2.textContent = `Editing ${title === '' ? this.singular : title}`;
this.ui.modals.edit.form.dataset.formId = `edit-${itemID}`;
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 7948f97..b01b645 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -547,6 +547,12 @@
headers,
signal: controller.signal
});
+ if (!response.ok) {
+ // Access the error details from the response body
+ const errorBody = await response.text();
+ // Throw a new error with a descriptive message
+ throw new Error(`HTTP error! status: ${response.status}, message: ${errorBody}`);
+ }
if (response.status === 304) {
// 304 means "Not Modified" - use cached data if available
@@ -581,7 +587,6 @@
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
-
const data = await response.json();
await this.processFetchedData(name, data, cacheKey, response);
@@ -597,7 +602,8 @@
const isAbortError = error?.name === 'AbortError';
if (!isAbortError) {
- console.error(`Fetch error for store "${name}":`, error);
+ console.error(`Fetch error for store "${name}":`, error.message);
+ console.dir(error);
this.notify(name, 'fetch-error', { error });
throw error;
}
diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index b2f7c7c..e38109e 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -1353,8 +1353,20 @@
let fieldId = uploads[0].field;
let field = document.querySelector(`[data-uploader="${fieldId}"]`);
if (!field) {
- console.log('No field found for '+fieldId);
- return;
+ if ('crudManager' in window && fieldId.startsWith(window.crudManager.content)) {
+ let [content, itemId, fieldName] = fieldId.split('_');
+ if (parseInt(itemId) > 0) {
+ window.crudManager.openEditModal(itemId);
+ field = document.querySelector(`[data-uploader="${fieldId}"]`);
+ } else {
+ console.log('No field found for '+fieldId);
+ return;
+ }
+ } else {
+ console.log('No field found for '+fieldId);
+ return;
+ }
+
}
let fieldData = this.fields.get(fieldId);
if (fieldData.groupUI.container) {
diff --git a/assets/js/concise/UtilityFunctions.js b/assets/js/concise/UtilityFunctions.js
index 8272c31..efbf22f 100644
--- a/assets/js/concise/UtilityFunctions.js
+++ b/assets/js/concise/UtilityFunctions.js
@@ -895,6 +895,9 @@
return ui;
}
+window.sleep = async function (ms = 50) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+};
class DebouncedActions {
constructor() {
diff --git a/assets/js/concise/navigation.js b/assets/js/concise/navigation.js
index cc2648d..73667cf 100644
--- a/assets/js/concise/navigation.js
+++ b/assets/js/concise/navigation.js
@@ -57,6 +57,7 @@
return;
}
if (this.openNav && e.target.closest(`#${this.openNav}`) === null) {
+ console.log('Closing nav', this.openNav);
this.toggleNav(false, this.openNav);
}
@@ -162,5 +163,6 @@
}
document.addEventListener('DOMContentLoaded', function() {
+
window.jvbNav = new Navigation();
});
diff --git a/assets/js/concise/on-this-page.js b/assets/js/concise/on-this-page.js
index cd22d25..81c95fb 100644
--- a/assets/js/concise/on-this-page.js
+++ b/assets/js/concise/on-this-page.js
@@ -1,178 +1,64 @@
-class OnThisPage extends window.UIHandler {
- constructor() {
- super();
+class OnThisPage {
+ constructor() {
+ this.initElements();
+ this.initListeners();
+ }
- // Initialize state tracking
- this.navOpen = false;
+ initElements() {
+ this.selectors = {
+ nav: 'nav.on-this-page',
+ toggle: 'button.toggle',
+ icon: 'button.toggle .icon'
+ }
+ this.ui = window.uiFromSelectors(this.selectors);
- // Bind methods
- this.toggleNav = this.toggleNav.bind(this);
+ let items = this.ui.nav.querySelectorAll('li a');
+ this.ui.items = {};
+ this.ui.sections = {};
+ this.selectors.items = [];
- // Bind elements first
- this.bindElements();
+ for (let item of items) {
+ let id = item.getAttribute('href');
+ this.ui.items[id.replace('#', '')] = item.closest('li');
+ this.selectors.items.push(id);
+ this.ui.sections[id.replace('#', '')] = document.querySelector(id);
+ }
+ this.selectors.items = this.selectors.items.join(',');
+ }
+ initListeners() {
+ this.ui.toggle.addEventListener('click', () => {
+ let icon = this.ui.nav.classList().contains('open') ? 'icon-x-square' : 'icon-plus-square';
+ console.log('Changing icon to: '+icon);
+ this.ui.icon.className = 'icon '+icon;
+ });
- if (this.elements.nav) {
- if(this.elements.toggle){
- // Bind click directly
- this.elements.toggle.addEventListener('click', this.toggleNav);
+ this.observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
- // Bind UIHandler events for escape and outside clicks
- this.bindEvents();
- }
+ let index = Object.keys(this.ui.items).indexOf(entry.target.id);
+ let i = 0;
+ for (let item of Object.values(this.ui.items)) {
+ item.classList.toggle('active', index === i);
+ // item.classList.toggle('adj', i === index +1 || i === index -1);
+ i++;
+ }
+ }
+ });
+ },
+ {
+ rootMargin: '-50% 0px -50% 0px',
+ threshold: 0
+ }
+ );
-
- // Set up section observer
- this.setupSectionObserver();
- }
- }
-
- bindElements() {
- const nav = document.querySelector('nav.on-this-page');
-
- if (!nav) return;
- this.elements = {
- nav,
- links: nav.querySelectorAll('a'),
- sections: Array.from(nav.querySelectorAll('a'))
- .map(a => {
- const id = a.getAttribute('href');
- return document.querySelector(id);
- })
- .filter(Boolean)
- };
- if(nav.querySelector('button.toggle')){
- this.elements.toggle = nav.querySelector('button.toggle');
- }
- }
-
- // Override to prevent UIHandler's component event binding
- bindComponentEvents() {
- // Intentionally empty
- }
-
- toggleNav(event) {
- event?.preventDefault();
- event?.stopPropagation();
-
- const { nav, toggle } = this.elements;
- if (!nav || !toggle) return;
-
- // Toggle state
- this.navOpen = !this.navOpen;
-
- // Update DOM
- if (this.navOpen) {
- nav.classList.add('open');
- toggle.setAttribute('aria-label', 'Hide Index');
- toggle.setAttribute('aria-expanded', 'true');
- this.bindLinkHandlers();
- } else {
- nav.classList.remove('open');
- toggle.setAttribute('aria-label', 'Show Index');
- toggle.setAttribute('aria-expanded', 'false');
- this.cleanupLinkHandlers();
- }
- }
-
- bindLinkHandlers() {
- const { links } = this.elements;
- links?.forEach(link => {
- link._boundHandler = () => {
- this.navOpen = false;
- this.elements.nav.classList.remove('open');
- this.elements.toggle.setAttribute('aria-label', 'Show Index');
- this.elements.toggle.setAttribute('aria-expanded', 'false');
- this.cleanupLinkHandlers();
- };
- link.addEventListener('click', link._boundHandler);
- });
- }
-
- cleanupLinkHandlers() {
- const { links } = this.elements;
- links?.forEach(link => {
- if (link._boundHandler) {
- link.removeEventListener('click', link._boundHandler);
- delete link._boundHandler;
- }
- });
- }
-
- setupSectionObserver() {
- const { sections } = this.elements;
-
- if (!sections?.length) return;
-
- this.initializeObserver(
- 'sections',
- sections,
- {
- rootMargin: '-50% 0% -50% 0%',
- threshold: 0
- },
- (entries) => {
- entries.forEach(entry => {
- if (!entry.isIntersecting) return;
-
- const id = entry.target.id;
- const link = this.elements.nav?.querySelector(`a[href="#${id}"]`);
- if (link) {
- this.updateActiveClasses(link);
- }
- });
- }
- );
- }
-
- updateActiveClasses(activeLink) {
- const listItem = activeLink.closest('li');
- if (!listItem) return;
-
- // Remove existing active and adjacent classes
- const allItems = this.elements.nav.querySelectorAll('li');
- allItems.forEach(item => {
- item.classList.remove('active', 'adj');
- });
-
- // Add new classes
- listItem.classList.add('active');
-
- // Add adjacent classes
- if (listItem.previousElementSibling) {
- listItem.previousElementSibling.classList.add('adj');
- }
- if (listItem.nextElementSibling) {
- listItem.nextElementSibling.classList.add('adj');
- }
- }
-
- // Use local state for component active check
- isComponentActive(componentKey) {
- if (componentKey === 'nav') {
- return this.navOpen;
- }
- return super.isComponentActive(componentKey);
- }
-
- // UIHandler event handlers
- handleOutsideClick(event) {
- if (this.navOpen && !this.elements.nav.contains(event.target)) {
- this.toggleNav(event);
- }
- }
-
- handleEscapeKey(event) {
- if (event.key === 'Escape' && this.navOpen) {
- this.toggleNav(event);
- event.preventDefault();
- }
- }
-
- cleanup() {
- this.cleanupLinkHandlers();
- super.cleanup();
- }
+ for (let section of Object.values(this.ui.sections)) {
+ console.log('Observing section: ', section);
+ this.observer.observe(section);
+ }
+ }
}
// Initialize
diff --git a/assets/js/min/crud.min.js b/assets/js/min/crud.min.js
index a10477c..25efdb9 100644
--- a/assets/js/min/crud.min.js
+++ b/assets/js/min/crud.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector(".crud[data-content]:not([data-ignore])"),this.container&&(this.content=this.container.dataset.content,this.endpoint=this.container.dataset.endpoint??"content",this.singular=this.container.dataset.singular,this.plural=this.container.dataset.plural,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.populate=window.jvbPopulate,this.cache=new window.jvbCache(this.content),this.activeItem=null,this.isTimeline=!1,this.isPopulating=!1,this.changes=new Map,this.items=new Map,this.init())}init(){this.initElements(),this.initListeners(),this.defineTemplates();let e=this.initSettings();this.initStore(e),this.checkHideFilters(),this.initIntegrations(),this.initUploader(),this.initModals()}defineTemplates(){const e=window.jvbTemplates,t=this,i=(e,i,s)=>{e.dataset.itemId=s.id;let a=i.checkbox.closest(".preview");window.prefixInput(i.checkbox,`select-${s.id}`,a,!0),i.checkbox.value=s.id,i.checkbox.checked=t.selected.has(parseInt(s.id)),i.selectLabel&&(i.selectLabel.htmlFor=`select-${s.id}`),i.edit&&(i.edit.dataset.id=s.id),i.trash&&(i.trash.dataset.id=s.id)},s=function(e,t,i){if(i?.fields?.post_thumbnail){const e=i.images[i.fields.post_thumbnail]??{};t.img.src=e.medium??"",t.img.alt=e.alt??i.fields.post_title??""}};e.define("gridView",{refs:{img:"img",checkbox:".select-item",selectLabel:"label.select-item-label",edit:'[data-action="edit"]',trash:'[data-action="trash"]'},setup({el:e,refs:t,manyRefs:a,data:l}){i(e,t,l),s(0,t,l)}}),e.define("listView",{refs:{img:"img",checkbox:".select-item",selectLabel:"label.select-item-label",edit:'[data-action="edit"]',trash:'[data-action="trash"]'},manyRefs:{attrs:"[data-attr]",fields:"[data-field]"},setup({el:e,refs:t,manyRefs:a,data:l}){i(e,t,l),s(0,t,l),a?.attrs?.forEach((e=>{const t=l[e.dataset.attr];t&&""!==t?e.textContent=t:e.remove()})),a?.fields?.forEach((e=>{const t=l.fields?.[e.dataset.field];t&&""!==t?"DIV"===e.tagName?e.innerHTML=t:e.textContent=t:e.remove()}))}});let a={};this.isTimeline&&(a.sharedRow="tr.shared",a.point="tr.timeline-point"),e.define("tableView",{refs:{checkbox:".select-item",selectLabel:"label.select-item-label",...a},manyRefs:{inputs:"input,select,textarea",status:'input[name="post_status"]',selectors:'[data-type="selector"]',fields:"[data-field]"},setup({el:e,refs:s,manyRefs:a,data:l}){if(i(e,s,l),a?.inputs?.forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${l.id}-`,t)})),a?.status?.forEach((e=>{e.value===l.status&&(e.checked=!0)})),t.isTimeline)s.sharedRow&&(s.sharedRow.querySelectorAll("input,select,textarea").forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${l.id}-`,t)})),t.populate.populate(s.sharedRow,l),s.sharedRow.querySelectorAll('input[name="post_status"]').forEach((e=>{e.value===l.status&&(e.checked=!0)}))),s.point&&l.fields?.timeline&&(Object.entries(l.fields.timeline).forEach((([i,a],n)=>{const o=s.point.cloneNode(!0);o.dataset.index=`${n}`,o.dataset.itemId=a.id,o.querySelectorAll("input,select,textarea").forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${a.id}-`,t)})),t.populate.populate(o,{fields:a,images:l.images,taxonomies:l.taxonomies});const d=l.images?.[a.post_thumbnail];d&&o.querySelector(".field.upload")?.setAttribute("title",d["image-title"]??""),e.insertBefore(o,s.point)})),s.point.remove());else if(void 0!==t.ui.table.form?.dataset.edit)a?.inputs?.forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${l.id}-`,t)})),a?.status?.forEach((e=>{e.value===l.status&&(e.checked=!0)})),t.populate.populate(e,l);else{const e=Object.hasOwn(l,"fields")?l.fields:l;a?.fields?.forEach((t=>{if(Object.hasOwn(e,t.dataset.field)&&""!==e[t.dataset.field]){let i=e[t.dataset.field],s=e.children[0];s&&(s.textContent="date"===t.dataset.field?window.formatTimeAgo(i):i)}}))}a?.selectors?.forEach((e=>e.setAttribute("data-lazy","")))}}),e.define("emptyState"),e.define("bulkItem",{refs:{checkbox:"input",img:"img",label:"label"},setup({el:e,refs:t,manyRefs:i,data:s}){t.checkbox&&(t.checkbox.id=`bulk_${s.id}`,t.checkbox.value=s.id,t.checkbox.checked=!0,t.checkbox.name="selected[]");let a=s?.images[s?.fields?.post_thumnbail]??{};t.img&&Object.keys(a).length>0&&(t.img.src=a.medium??"",t.img.alt=a.alt??""),t.label&&(t.label.title=item.fields.post_title)}}),e.define("trashOptions"),e.define("notTrashOptions"),e.define("contentTable")}initElements(){this.allowedFilters=["status","orderby","order","search","date-filter","dateFrom","dateTo"],this.selectors={buttons:{create:".create-item",clearFilters:'[data-action="clear-filters"]'},views:{grid:'input[data-view="grid"]',list:'input[data-view="list"]',table:'input[data-view="table"]'},modals:{create:{modal:"dialog.create",form:"dialog.create form",h2:"dialog.create h2"},edit:{modal:"dialog.edit",form:"dialog.edit form",h2:"dialog.edit h2"},bulkEdit:{modal:"dialog.bulkEdit",selected:"dialog.bulkEdit .selected",h2:"dialog.bulkEdit h2 span",form:"dialog.bulkEdit form"},date:{modal:"dialog.date-range",start:"dialog.date-range .date-start",end:"dialog.date-range .date-end",month:"dialog.date-range .month-select"}},grid:`.${this.content}.item-grid`,table:{nav:"#vertical",form:"form.table",table:"form.table table",body:"form.table body",head:"form.table thead",foot:"form.table tfoot",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{action:".bulk-action-select",count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"},filters:{container:"details.all-filters",search:'.all-filters input[type="search"]',status:{all:'[name="status"]#all',publish:'[name="status"]#publish',draft:'[name="status"]#draft',trash:'[name="status"]#trash'},orderby:{date:'[name="orderby"]#date',alphabetical:'[name="orderby"]#alphabetical'},order:{asc:'[name="order"][value="asc"]',desc:'[name="order"][value="desc"]'},date:'[data-filter="date"]'},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.selectors);const e=document.querySelectorAll('[data-filter="taxonomies"]');e.length>0&&(this.ui.filters.taxonomies={},e.forEach((e=>{const t=e.dataset.taxonomy;this.ui.filters.taxonomies[t]=e,this.allowedFilters.push(`tax_${t}`)}))),this.isTimeline=!!document.querySelector("[data-timeline]")}initUploader(){this.ui.uploader&&(window.jvbUploads.scanFields(this.ui.uploader),window.jvbUploads.subscribe(((e,t)=>{if("sent-to-queue"===e&&t===this.ui.uploader.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearCache()})),"sent-to-queue"===e&&t.field){const e=t.field.config.name,i=t.field.config.itemID;i&&e&&this.changes.has(i)&&delete this.changes.get(i)[e]}})))}initModals(){this.modals={};for(let[e,t]of Object.entries(this.ui.modals))t.modal&&(this.modals[e]=new window.jvbModal(t.modal),this.modals[e].subscribe(((t,i)=>{if("modal-close"===t){const t=this.ui.modals[e].form.dataset.formId;t&&this.forms.clearForm(t),this.resetForm(this.ui.modals[e].form),"date"===e&&this.handleCustomDateSelection(),["edit","bulkEdit","create"].includes(e)&&window.debouncer.timeouts.has(`save-${this.content}`)&&this.scheduleSave(0)}})))}initStore(e){let t={...this.defaults,...e};const i=window.jvbStore.register(this.content,[{storeName:this.content,keyPath:"id",endpoint:this.endpoint??"content",headers:{"X-Action-Nonce":window.auth.getNonce("dash")},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:t,ignore:["content","user"],TTL:36e5,showLoading:!0},{storeName:"changes",keyPath:"id"}]);this.changesStore=i.changes,this.store=i[this.content],this.store.subscribe(((e,t)=>{if("data-loaded"===e)this.render(),this.selectionHandler.collectItems()})),this.changesStore.subscribe(((e,t)=>{if("data-ready"===e){let e=this.changesStore.getAll();e.length>0&&(e.forEach((e=>{this.changes.set(e.id,e)})),this.savePosts("",!1).then((()=>{})))}}))}initIntegrations(){this.selected=new Set,this.selectionHandler=new window.jvbHandleSelection(this.container,{selectAll:{checkbox:"#select-all",label:".bulk-select label",span:".bulk-select label span"},wrapper:{wrapper:".wrap"},item:{idAttribute:"itemId"}}),this.selectionHandler.subscribe(((e,t)=>{this.selected=new Set([...t.selectedItems].map((e=>parseInt(e)))),this.ui.bulk.control.hidden=0===this.selected.size,this.ui.bulk.count.hidden=0===this.selected.size,this.ui.bulk.count.textContent=`${this.selected.size} ${this.plural} selected`})),this.forms=window.jvbForm,window.jvbUploads&&window.jvbUploads.subscribe(((e,t)=>{"groups_uploaded"===e&&t.content===this.content&&this.handleGroupsUploaded(t)})),this.queue.subscribe(((e,t)=>{if(["image_upload","video_upload","document_upload"].includes(t.type)&&"operation-status"===e&&"completed"===t.status&&this.store.clearCache(),"operation-status"===e&&"completed"===t.status&&"uploads/groups"===t.endpoint&&(t.result&&t.result.group_mappings&&this.handleGroupMappings(t.result.group_mappings),this.store.clearCache()),"operation-status"===e&&"completed"===t.status&&"content_update"===t.type){if(this.store.clearCache(),!t.result||!t.result.posts)return void console.warn("Content update completed but no result.posts",t);const e=Object.keys(t.result.posts);if(0===e.length)return;this.changesStore.deleteMany(e),e.forEach((e=>this.changes.delete(e)))}}))}initSettings(){this.defaults={content:this.content,user:window.auth.getUser(),page:1,status:"all",orderby:"date",order:"desc",search:""};let e={},t=this.container.dataset.view??"grid";this.view=this.cache.get("view")??t,this.view!==t&&(this.ui.views[this.view].checked=!0),this.status=this.cache.get("status")??this.defaults.status,this.status!==this.defaults.status&&(this.ui.filters.status[this.status].checked=!0,e.status=this.status),this.orderby=this.cache.get("orderby")??this.defaults.orderby,this.orderby!==this.defaults.orderby&&(this.ui.filters.orderby[this.orderby].checked=!0,e.orderBy=this.orderby),this.order=this.cache.get("order")??this.defaults.order,this.order!==this.defaults.order&&(this.ui.filters.order[this.order].checked=!0,e.order=this.order),this.ui.filters.taxonomies&&Object.entries(this.ui.filters.taxonomies).forEach((([t,i])=>{const s=`tax_${t}`,a=this.cache.get(s);a&&(i.value=a,e[s]=a)}));let i=this.cache.get("tabNav")??"horizontal";this.ui.table.nav&&"vertical"===i&&(this.ui.table.nav.checked=!0);let s={showFilters:{element:this.ui.filters.container,default:"closed"},showUploader:{element:this.ui.uploader,default:"open"}};for(let[e,t]of Object.entries(s))if(t.element){let i=this.cache.get(e)??t.default;t.element.open="open"===i,t.element.addEventListener("toggle",(()=>{this.cache.set(e,t.element.open?"open":"closed")}))}return e}initListeners(){this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleModalSubmit.bind(this),document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler),this.ui.filters.search&&this.ui.filters.search.addEventListener("input",this.inputHandler);for(let[e,t]of Object.entries(this.ui.modals))t.form&&t.form.addEventListener("submit",this.submitHandler)}handleModalSubmit(e){e.preventDefault();const t=e.target.closest("dialog");if(!t)return;if(t.classList.contains("create"))return void this.handleCreateSubmit(t);this.plural;this.scheduleSave(0)}async handleCreateSubmit(e){e.dataset.itemId;this.changes.size>0&&(this.cancelBackup(),await this.handleBackup());const t=await this.changesStore.getAll();if(0===t.length)return;let i={};t.forEach((e=>{const{id:t,...s}=e;i[t]=s}));let s=this.queue.addToQueue({endpoint:this.endpoint,headers:{"X-Action-Nonce":window.auth.getNonce("dash")},data:{posts:i},popup:`Creating your new ${this.singular}`,title:`Creating your new ${this.singular}`});if(!s)return;const a=e.querySelectorAll("[data-upload-field]");for(const e of a){const t=e.dataset.uploader;if(!t)continue;0!==window.jvbUploads.stores.uploads.filterByIndex({field:t}).length&&await window.jvbUploads.queueUploads("uploads",t,s)}}handleChange(e){const t=e.target.closest("[data-item-id]"),i=e.target.matches("[data-filter]"),s=e.target.matches(".bulk-action-select"),a=e.target.matches("[data-view]");if(t||i||s||a)if(this.isPopulating||!t||e.target.closest("[data-ignore], .select-item")){if(a)return this.items.clear(),void this.handleViewChange(e.target);if(s)this.handleBulkAction(e.target);else if(i)this.handleFilterChange(e.target);else if("table"===this.view){if(e.target.matches("details.multi-select"))return void this.toggleColumn(e.target.id,e.target.checked);e.target.matches(this.selectors.table.nav)&&(this.tabNav=e.target.checked,this.cache.set("tabNav",e.target.checked?"vertical":"horizontal"))}}else this.handleItemUpdate(e)}handleBulkAction(e){if(e.value.startsWith("tax-")){const t=e.options[e.selectedIndex],i=t.dataset.taxonomy,s=t.dataset.single,a=t.dataset.plural;return window.jvbSelector.openEmpty(i,s,a,(e=>this.handleBulkTaxonomy(e))),void(e.value="")}switch(e.value){case"edit":this.openBulkEditModal();break;case"publish":case"trash":case"delete":this.setBulkStatus(e.value);break;case"draft":case"restore":this.setBulkStatus("draft")}}handleBulkTaxonomy(e){e.termIds.length&&this.selected.size&&(this.selected.forEach((t=>{const i=this.store.get(t);if(!i)return;const s=(i.taxonomies?.[e.taxonomy]||[]).map((e=>e.id)),a=[...new Set([...s,...e.termIds])];this.updateItem(t,e.taxonomy,a)})),this.savePosts(`Adding ${e.terms.length} ${e.taxonomy} to ${this.selected.size} ${this.plural}...`).then((()=>{})),this.selectionHandler.clearSelection())}handleItemUpdate(e){let t=window.targetCheck(e,"[data-item-id]");if(!t)return;const i=e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');let s,a;if(i)s=i.dataset.field,a=this.forms.getFieldValue(i);else{let t=e.target.closest("[data-field]");s=t.dataset.field,a=this.forms.getFieldValue(e.target)}t.dataset.itemId.split(",").forEach((e=>{this.updateItem(e,s,a)}))}updateItem(e,t,i){if(this.isPopulating)return;const s=this.store.get(e);if(s){const a=s.fields?.[t]??s[t];if(null===window.getDifferences.map(a,i)){if(this.changes.has(e)){delete this.changes.get(e)[t];0===Object.keys(this.changes.get(e)).filter((e=>"id"!==e&&"content"!==e)).length&&(this.changes.delete(e),this.changesStore.delete(e))}return}}this.changes.has(e)||this.changes.set(e,{id:e,content:this.content}),this.changes.get(e)[t]=i,this.scheduleBackup(),"number"!=typeof e&&String(e).includes("group")||this.scheduleSave()}scheduleBackup(){window.debouncer.schedule(`changes-${this.content}`,(async()=>{this.changes.size>0&&await this.handleBackup()}),2e3)}cancelBackup(){window.debouncer.cancel(`changes-${this.content}`)}async handleBackup(){const e=Array.from(this.changes.values());this.changes.clear();const t=e.map((e=>e.id)),i=await Promise.all(t.map((e=>this.changesStore.get(e)))),s=e.map(((e,t)=>i[t]?window.deepMerge(i[t],e):e));await this.changesStore.saveMany(s)}scheduleSave(e=1e4){window.debouncer.schedule(`save-${this.content}`,(async()=>{this.changes.size>0&&(this.cancelBackup(),await this.handleBackup()),await this.savePosts("",!1)}),e)}handleFilterChange(e){let t=e.dataset.filter;return"date"===t&&"custom"===e.value?(e.value="",void this.modals.date.handleOpen()):"date"===t&&""!==e.value?(this.setFilter("date-filter",e.value),this.deleteFilter("dateFrom"),this.deleteFilter("dateTo"),void this.checkHideFilters()):("taxonomies"===t&&(t=`tax_${e.dataset.taxonomy}`),void this.setFilter(t,e.value))}checkHideFilters(){const e=this.store.filters,t=Object.entries(e).some((([e,t])=>!["content","user","page"].includes(e)&&(this.defaults[e]!==t&&""!==t&&null!==t)));this.ui.buttons.clearFilters.hidden=!t}clearAllFilters(){let e=this.store.filters;this.store.clearFilters();for(let[t,i]of Object.entries(e))this.cache.remove(t),this.deleteFilter(t,i);this.a11y.announce("All filters cleared")}handleCustomDateSelection(){if(this.ui.modals.date.month&&this.ui.modals.date.month.value){const[e,t]=this.ui.modals.date.month.value.split("-"),i=`${e}-${t}-01`,s=new Date(e,parseInt(t),0).getDate(),a=`${e}-${t}-${String(s).padStart(2,"0")}`;this.setFilter("dateFrom",i),this.setFilter("dateTo",a),this.deleteFilter("date-filter"),this.ui.modals.date.month.value=""}else this.ui.modals.date.start&&this.ui.modals.date.start.value&&this.ui.modals.date.end&&this.ui.modals.date.end.value&&(this.setFilter("dateFrom",this.ui.modals.date.start.value),this.setFilter("dateTo",this.ui.modals.date.end.value),this.deleteFilter("date-filter"),this.ui.modals.date.start.value="",this.ui.modals.date.end.value="");this.checkHideFilters()}handleViewChange(e){this.view=e.dataset.view,this.cache.set("view",this.view),this.render()}handleClick(e){if(e.target.matches(".clear-search"))return void this.deleteFilter("search","");const t=e.target.closest("[data-action]");return t?(e.preventDefault(),void this.handleActionButton(t)):e.target.matches(".apply-date-filter")?(this.handleCustomDateSelection(),void this.modals.date.handleClose()):void(e.target.matches(this.selectors.buttons.create)&&this.openCreateModal())}openCreateModal(){this.forms.registerForm(this.ui.modals.create.form,{cache:!1}),this.ui.modals.create.modal.dataset.itemId=window.generateID("new"),this.modals.create.handleOpen()}handleActionButton(e){const t=e.dataset.id;switch(e.dataset.action){case"edit":this.openEditModal(t);break;case"delete":confirm("Delete this item? This cannot be undone")&&(this.updateItem(t,"post_status","delete"),window.fade(e.closest(".item"),!1),this.savePosts(`Permanently deleting ${this.singular}...`).then((()=>{})),this.store.delete(t));break;case"trash":"trash"===this.status?confirm("Delete this item? This cannot be undone")&&(this.updateItem(t,"post_status","delete"),window.fade(e.closest(".item"),!1),this.savePosts(`Permanently deleting ${this.singular}...`).then((()=>{})),this.store.delete(t)):(this.updateItem(t,"post_status","trash"),window.fade(e.closest(".item"),!1),this.savePosts(`Sending ${this.singular} to trash...`).then((()=>{})));break;case"bulk-edit":this.selected.size>0&&this.openBulkEditModal();break;case"bulk-delete":this.handleBulkDelete();break;case"refresh":this.store.clearCache(),this.store.fetch();break;case"clear-filters":this.clearAllFilters()}}handleBulkDelete(){let e="trash"===this.status;if(this.selected.size>0&&confirm(`${e?"Permanently delete":"Send"} ${this.selected.size} ${1===this.selected.size?this.singular:this.plural}${e?"":"to trash"}?`)){this.selected.forEach((t=>{this.store.delete(t),this.updateItem(t,"post_status",e?"delete":"trash")}));let t=e?`Permanently deleting ${this.selected.size} ${1===this.selected.size?this.singular:this.plural}`:`Sending ${this.selected.size} ${1===this.selected.size?this.singular:this.plural} to trash`;this.savePosts(t).then((()=>{})),this.selectionHandler.clearSelection()}}handleInput(e){e.preventDefault(),e.stopPropagation();let t=e.target.value.trim(),i=`${this.content}-search`;0!==t.length?window.debouncer.schedule(i,(()=>{this.a11y.announce(`Searching for "${t}"...`),this.store.setFilters({search:t,page:1})}),300):this.deleteFilter("search","")}handleKeys(e){if(this.tabNav&&"Tab"===e.key){e.preventDefault();const t=e.target.closest("[data-field]"),i=e.target.closest("tr");if(!t||!i)return;const s=t.dataset.field,a=e.shiftKey;let l=this.findNextEditableRow(i,a);l||(l=this.wrapToRow(i,a)),l&&this.focusFieldInRow(l,s,a)}}findNextEditableRow(e,t=!1){let i=t?e.previousElementSibling:e.nextElementSibling;for(;i&&!this.isEditableRow(i);)i=t?i.previousElementSibling:i.nextElementSibling;return i}wrapToRow(e,t=!1){if(this.isTimeline){const i=e.closest("tbody");if(!i)return null;const s=Array.from(i.querySelectorAll("tr")).filter((e=>this.isEditableRow(e)));return t?s[s.length-1]:s[0]}{if(!this.ui.table.body)return null;const e=Array.from(this.ui.table.body.querySelectorAll("tr")).filter((e=>this.isEditableRow(e)));return t?e[e.length-1]:e[0]}}isEditableRow(e){return!e.closest("thead")&&!e.closest("tfoot")&&(this.isTimeline?e.classList.contains("shared")||e.classList.contains("timeline-point"):!!e.dataset.itemId)}focusFieldInRow(e,t,i=!1){const s=e.querySelector(`[data-field="${t}"]`);if(!s)return;const a=this.findFocusableInput(s);if(a){a.focus(),a.select&&"text"===a.type&&a.select();const e=i?"next":"previous";this.a11y?.announce(`Moved to ${t} in ${e} row`)}}findFocusableInput(e){const t=['input:not([type="hidden"]):not([disabled])',"textarea:not([disabled])","select:not([disabled])","button:not([disabled])"];for(const i of t){const t=e.querySelector(i);if(t)return t}return null}openEditModal(e){let t=this.store.get(parseInt(e));t&&(this.activeItem=t.id,this.ui.modals.edit.modal.dataset.itemId=e,this.ui.modals.edit.modal.dataset.content=this.content,this.ui.modals.edit.h2.textContent=`Editing ${""===t.fields.post_title?this.singular:t.fields.post_title}`,this.ui.modals.edit.form.dataset.formId=`edit-${e}`,this.forms.registerForm(this.ui.modals.edit.form,{cache:!1,autoUpload:!0}),this.isPopulating=!0,this.populate.populate(this.ui.modals.edit.form,t),requestAnimationFrame((()=>{requestAnimationFrame((()=>{this.isPopulating=!1}))})),this.modals.edit.handleOpen())}openBulkEditModal(){window.removeChildren(this.ui.modals.bulkEdit.selected),this.ui.modals.edit.form.reset(),window.chunkIt(this.selected,(t=>{let i=this.store.get(parseInt(t));if(i)return e.push(i.id),window.jvbTemplates.create("bulkItem",i)}),(e=>this.ui.modals.bulkEdit.selected.append(e))).then((()=>{}));let e=Array.from(this.selected).map((e=>this.store.get(parseInt(e)))).filter(Boolean);this.ui.modals.bulkEdit.modal.dataset.itemId=e.join(","),this.ui.modals.bulkEdit.h2&&(this.ui.modals.bulkEdit.h2.textContent=this.selected.size),this.modals.bulkEdit.handleOpen(),this.forms.registerForm(this.ui.modals.bulkEdit.form,{cache:!1}),this.isPopulating=!0,this.populate.populate(this.ui.modals.edit.form,item),requestAnimationFrame((()=>{requestAnimationFrame((()=>{this.isPopulating=!1}))}))}async savePosts(e="",t=!1){this.changes.size>0&&(this.cancelBackup(),await this.handleBackup());let i=await this.changesStore.getAll();if(0===i.length)return;if(i=this.validateChanges(i),0===i.length)return;""===e&&(e=`Saving ${i.length} ${1===i.length?this.singular:this.plural}`);let s={},a=[];i.forEach((e=>{let t=e.id;const{id:i,...l}=e;s[t]=l,e.post_status&&this.shouldRemoveItemUI(e.post_status)&&a.push(t)})),a.length>0&&this.removeItems(a);let l={endpoint:this.endpoint,headers:{"X-Action-Nonce":window.auth.getNonce("dash")},data:{posts:s},delay:t,popup:"Saving changes",title:e};this.queue.addToQueue(l)}validateChanges(e){return e.reduce(((e,t)=>{const{id:i,content:s,...a}=t,l=this.store.get(i);if(!l)return e.push(t),e;const n={id:i,content:s};let o=!1;for(const[e,t]of Object.entries(a)){const i=l.fields?.[e]??l[e];null!==window.getDifferences.map(i,t)&&(n[e]=t,o=!0)}return o?e.push(n):(this.changes.delete(i),this.changesStore.delete(i)),e}),[])}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,i=[];if(this.selected.forEach((t=>{i.push(t),this.updateItem(t,"post_status",e)})),"delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";this.shouldRemoveItemUI(e)&&this.removeItems(i),this.selectionHandler.clearSelection(),this.savePosts(`${t} ${i.length} ${1===i.length?this.singular:this.plural}...`).then((()=>{}))}render(){const e=this.store.getFiltered();if(0!==e.length){switch(this.view){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e).then((()=>{}));break;case"list":this.renderList(e)}this.updateUI()}else this.renderEmpty()}updateUI(){if(this.ui.bulk.action){let e=!1,t=this.ui.bulk.action.querySelector('[value="edit"]'),i=this.status;"trash"===i&&t?(window.removeChildren(this.ui.bulk.action),e=window.jvbTemplates.create("trashOptions")):"trash"===i||t||(window.removeChildren(this.ui.bulk.action),e=window.jvbTemplates.create("notTrashOptions")),e&&e.querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulk.action.append(e)})),this.ui.bulk.action.value=""}this.selected.size>0&&this.selectionHandler.updateSelectionUI()}renderEmpty(){this.toggleTable(!1),window.removeChildren(this.ui.grid);const e=window.jvbTemplates.create("emptyState");e&&(this.ui.grid.append(e),this.a11y.announceItems(0,!1,!1))}toggleTable(e=!0){if(this.ui.table.selectedColumns&&(this.ui.table.selectedColumns.hidden=!e),e&&!this.ui.table.form){let e=window.jvbTemplates.create("contentTable");this.container.append(e),this.ui.table=window.uiFromSelectors(this.selectors.table),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.form&&(this.ui.table.form.hidden=!e,e||this.forms.clearForm(this.ui.table.form.dataset.formId),this.ui.table.body&&window.removeChildren(this.ui.table.body)),this.keyHandler=this.handleKeys.bind(this),e?document.addEventListener("keydown",this.keyHandler):document.removeEventListener("keydown",this.keyHandler)}renderGrid(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view"),window.chunkIt(e,(e=>this.renderGridItem(e)),(e=>this.ui.grid.append(e))).then((()=>{}))}renderList(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),window.chunkIt(e,(e=>this.renderListItem(e)),(e=>this.ui.grid.append(e))).then((()=>{}))}async renderTable(e){this.toggleTable(),window.removeChildren(this.ui.grid),await window.chunkIt(e,(e=>this.renderTableItem(e)),(e=>{this.ui.table.body?this.ui.table.body.append(e):this.ui.table.table.insertBefore(e,this.ui.table.foot)}),5),requestAnimationFrame((()=>{window.jvbSelector?.scanExistingFields(this.ui.table.table)}))}renderGridItem(e){let t=window.jvbTemplates.create("gridView",e);return this.items.set(e.id,t),t}renderListItem(e){let t=window.jvbTemplates.create("listView",e);return this.items.set(e.id,t),t}renderTableItem(e){let t=window.jvbTemplates.create("tableView",e);return this.items.set(e.id,t),t}toggleColumn(e,t){this.ui.table.table.querySelectorAll(`.${e}`).forEach((e=>{e.hidden=!t}))}handleGroupsUploaded(e){const{posts:t,fieldId:i}=e;let s=window.jvbUploads,a=(s.fields.get(i),[]);t.forEach((e=>{const t={id:e.groupId,title:e.fields.post_title||`New ${this.singular}`,status:"draft",date:(new Date).toISOString(),modified:(new Date).toISOString(),thumbnail:null,icon:this.content,taxonomies:{},fields:e.fields,images:{}};e.images.forEach(((e,i)=>{let a=e.upload_id;0===i&&(t.fields.post_thumbnail=e);let l=s.stores.uploads.get(a);l&&(t.images[a]={"image-alt-text":"","image-caption":"","image-title":l.fields.originalName,medium:s.createPreviewUrl(s.formatFile(l))})})),a.push(t)})),this.store.saveMany(a).then((()=>this.render())),this.a11y.announce(`${t.length} ${1===t.length?this.singular:this.plural} created. Waiting for server confirmation...`)}handleGroupMappings(e){for(const[t,i]of Object.entries(e)){let e={};this.changes.has(t)&&(e=this.changes.get(t),this.changes.delete(t));let s=this.changesStore.get(t)??{};(e.size>0||s.size>0)&&(e=window.deepMerge(s,e),this.changes.set(i,e),this.scheduleBackup())}}shouldRemoveItemUI(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.store.filters.status}removeItems(e){e.forEach((e=>{if(this.items.has(e)){let t=this.items.get(e);t&&window.fade(t,!1)}}))}setFilters(e){for(let[t,i]of Object.entries(e)){if(!this.allowedFilters.includes(t)){delete e[t];continue}this.cache.set(t,i);let s=this.findFilterEl(t);this.setElValue(s,i)}this.store.setFilters(e)}setFilter(e,t){if(!this.allowedFilters.includes(e))return;this.cache.set(e,t),"status"===e&&(this.status=t),"orderby"===e&&(this.orderby=t),"order"===e&&(this.order=t);let i=this.findFilterEl(e,t);this.setElValue(i,t),this.store.setFilter(e,t)}deleteFilter(e,t){if(!this.allowedFilters.includes(e))return;if(Object.hasOwn(this.defaults,e))return void this.setFilter(e,this.defaults[e]);let i=this.findFilterEl(e,t);this.setElValue(i,!1),this.cache.remove(e),this.setFilter(e,"")}setElValue(e,t){if(e){if(!t)return["SELECT","TEXTAREA"].includes(e.tagName)&&(e.value=""),["text","search"].includes(e.type)&&(e.value=""),void("radio"===e.type&&(e.checked=!1));["SELECT","TEXTAREA"].includes(e.tagName)&&(e.value=t),["text","search"].includes(e.type)&&(e.value=t),"radio"===e.type&&(e.checked=!0)}}findFilterEl(e,t){if(["date-filter","dateFrom","dateTo"].includes(e)){switch(e){case"date-filter":e="month";break;case"dateFrom":e="start";break;case"dateTo":e="end"}return this.ui.modals.date[e]}if(e.includes("tax_")){const t=e.replace("tax_",""),i=this.ui.filters.taxonomies?.[t];return i||(console.warn("Taxonomy filter element not found:",t),null)}if(!Object.hasOwn(this.ui.filters,e))return console.warn("Filter el not found: ",e),!1;let i=this.ui.filters[e];if("object"==typeof i){if(!Object.hasOwn(this.ui.filters[e],t))return!1;i=this.ui.filters[e][t]}return i}resetForm(e){e.querySelectorAll('input[type="hidden"], input[type="text"], input[type="number"], input[type="email"], input[type="url"], textarea').forEach((e=>{e.value=""})),e.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach((e=>{e.checked=!1})),e.querySelectorAll("select").forEach((e=>{e.selectedIndex=0})),e.querySelectorAll(".selected-items").forEach((e=>{window.removeChildren(e)})),e.querySelectorAll(".item-grid.preview").forEach((e=>{window.removeChildren(e)}))}destroy(){window.debouncer.cancel(`changes-${this.content}`),this.changes.size>0&&(this.changesStore.saveMany(this.changes).then((()=>{})),this.changes.clear()),this.timelineSortables&&(this.timelineSortables.forEach((e=>e.destroy())),this.timelineSortables=[]);for(let[e,t]of Object.entries(this.ui.modals))t.form&&t.form.removeEventListener("submit",this.submitHandler);document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),this.ui.filters.search&&this.ui.filters.search.removeEventListener("input",this.handleInput)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{if("auth-loaded"===t){let t=document.querySelector("[data-content]");t&&!Object.hasOwn(t.dataset,"ignore")&&(window.crudManager=new e({content:t.dataset.content}))}}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector(".crud[data-content]:not([data-ignore])"),this.container&&(this.content=this.container.dataset.content,this.endpoint=this.container.dataset.endpoint??"content",this.singular=this.container.dataset.singular,this.plural=this.container.dataset.plural,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.populate=window.jvbPopulate,this.cache=new window.jvbCache(this.content),this.activeItem=null,this.isTimeline=!1,this.isPopulating=!1,this.changes=new Map,this.items=new Map,this.init())}init(){this.initElements(),this.initListeners(),this.defineTemplates();let e=this.initSettings();this.initStore(e),this.checkHideFilters(),this.initIntegrations(),this.initUploader(),this.initModals()}defineTemplates(){const e=window.jvbTemplates,t=this,s=(e,s,i)=>{e.dataset.itemId=i.id;let a=s.checkbox.closest(".preview");window.prefixInput(s.checkbox,`select-${i.id}`,a,!0),s.checkbox.value=i.id,s.checkbox.checked=t.selected.has(parseInt(i.id)),s.selectLabel&&(s.selectLabel.htmlFor=`select-${i.id}`),s.edit&&(s.edit.dataset.id=i.id),s.trash&&(s.trash.dataset.id=i.id)},i=function(e,t,s){let i=s?.fields?.post_thumbnail||s?.fields?.thumbnail;if(i){const e=s.images[i]??{};t.img.src=e.medium??"",t.img.alt=e.alt??s.fields.post_title??""}};e.define("gridView",{refs:{img:"img",checkbox:".select-item",selectLabel:"label.select-item-label",edit:'[data-action="edit"]',trash:'[data-action="trash"]'},setup({el:e,refs:t,manyRefs:a,data:l}){s(e,t,l),i(0,t,l)}}),e.define("listView",{refs:{img:"img",checkbox:".select-item",selectLabel:"label.select-item-label",edit:'[data-action="edit"]',trash:'[data-action="trash"]'},manyRefs:{attrs:"[data-attr]",fields:"[data-field]"},setup({el:e,refs:t,manyRefs:a,data:l}){s(e,t,l),i(0,t,l),a?.attrs?.forEach((e=>{const t=l[e.dataset.attr];t&&""!==t?e.textContent=t:e.remove()})),a?.fields?.forEach((e=>{const t=l.fields?.[e.dataset.field];t&&""!==t?"DIV"===e.tagName?e.innerHTML=t:e.textContent=t:e.remove()}))}});let a={};this.isTimeline&&(a.sharedRow="tr.shared",a.point="tr.timeline-point"),e.define("tableView",{refs:{checkbox:".select-item",selectLabel:"label.select-item-label",...a},manyRefs:{inputs:"input,select,textarea",status:'input[name="post_status"]',selectors:'[data-type="selector"]',fields:"[data-field]"},setup({el:e,refs:i,manyRefs:a,data:l}){if(s(e,i,l),a?.inputs?.forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${l.id}-`,t)})),a?.status?.forEach((e=>{e.value===l.status&&(e.checked=!0)})),t.isTimeline)i.sharedRow&&(i.sharedRow.querySelectorAll("input,select,textarea").forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${l.id}-`,t)})),t.populate.populate(i.sharedRow,l),i.sharedRow.querySelectorAll('input[name="post_status"]').forEach((e=>{e.value===l.status&&(e.checked=!0)}))),i.point&&l.fields?.timeline&&(Object.entries(l.fields.timeline).forEach((([s,a],n)=>{const o=i.point.cloneNode(!0);o.dataset.index=`${n}`,o.dataset.itemId=a.id,o.querySelectorAll("input,select,textarea").forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${a.id}-`,t)})),t.populate.populate(o,{fields:a,images:l.images,taxonomies:l.taxonomies});const r=l.images?.[a.post_thumbnail];r&&o.querySelector(".field.upload")?.setAttribute("title",r["image-title"]??""),e.insertBefore(o,i.point)})),i.point.remove());else if(void 0!==t.ui.table.form?.dataset.edit)a?.inputs?.forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${l.id}-`,t)})),a?.status?.forEach((e=>{e.value===l.status&&(e.checked=!0)})),t.populate.populate(e,l);else{const e=Object.hasOwn(l,"fields")?l.fields:l;a?.fields?.forEach((t=>{if(Object.hasOwn(e,t.dataset.field)&&""!==e[t.dataset.field]){let s=e[t.dataset.field],i=e.children[0];i&&(i.textContent="date"===t.dataset.field?window.formatTimeAgo(s):s)}}))}a?.selectors?.forEach((e=>e.setAttribute("data-lazy","")))}}),e.define("emptyState"),e.define("bulkItem",{refs:{checkbox:"input",img:"img",label:"label"},setup({el:e,refs:t,manyRefs:s,data:i}){t.checkbox&&(t.checkbox.id=`bulk_${i.id}`,t.checkbox.value=i.id,t.checkbox.checked=!0,t.checkbox.name="selected[]");let a=i?.images[i?.fields?.post_thumnbail]??{};t.img&&Object.keys(a).length>0&&(t.img.src=a.medium??"",t.img.alt=a.alt??""),t.label&&(t.label.title=item.fields.post_title)}}),e.define("trashOptions"),e.define("notTrashOptions"),e.define("contentTable")}initElements(){this.allowedFilters=["status","orderby","order","search","date-filter","dateFrom","dateTo"],this.selectors={buttons:{create:".create-item",clearFilters:'[data-action="clear-filters"]'},views:{grid:'input[data-view="grid"]',list:'input[data-view="list"]',table:'input[data-view="table"]'},modals:{create:{modal:"dialog.create",form:"dialog.create form",h2:"dialog.create h2"},edit:{modal:"dialog.edit",form:"dialog.edit form",h2:"dialog.edit h2"},bulkEdit:{modal:"dialog.bulkEdit",selected:"dialog.bulkEdit .selected",h2:"dialog.bulkEdit h2 span",form:"dialog.bulkEdit form"},date:{modal:"dialog.date-range",start:"dialog.date-range .date-start",end:"dialog.date-range .date-end",month:"dialog.date-range .month-select"}},grid:`.${this.content}.item-grid`,table:{nav:"#vertical",form:"form.table",table:"form.table table",body:"form.table body",head:"form.table thead",foot:"form.table tfoot",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{action:".bulk-action-select",count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"},filters:{container:"details.all-filters",search:'.all-filters input[type="search"]',status:{all:'[name="status"]#all',publish:'[name="status"]#publish',draft:'[name="status"]#draft',trash:'[name="status"]#trash'},orderby:{date:'[name="orderby"]#date',alphabetical:'[name="orderby"]#alphabetical'},order:{asc:'[name="order"][value="asc"]',desc:'[name="order"][value="desc"]'},date:'[data-filter="date"]'},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.selectors);const e=document.querySelectorAll('[data-filter="taxonomies"]');e.length>0&&(this.ui.filters.taxonomies={},e.forEach((e=>{const t=e.dataset.taxonomy;this.ui.filters.taxonomies[t]=e,this.allowedFilters.push(`tax_${t}`)}))),this.isTimeline=!!document.querySelector("[data-timeline]")}initUploader(){this.ui.uploader&&(window.jvbUploads.scanFields(this.ui.uploader),window.jvbUploads.subscribe(((e,t)=>{if("sent-to-queue"===e&&t===this.ui.uploader.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearCache()})),"sent-to-queue"===e&&t.field){const e=t.field.config.name,s=t.field.config.itemID;s&&e&&this.changes.has(s)&&delete this.changes.get(s)[e]}})))}initModals(){this.modals={};for(let[e,t]of Object.entries(this.ui.modals))t.modal&&(this.modals[e]=new window.jvbModal(t.modal),this.modals[e].subscribe(((t,s)=>{if("modal-close"===t){const t=this.ui.modals[e].form.dataset.formId;t&&this.forms.clearForm(t),this.resetForm(this.ui.modals[e].form),"date"===e&&this.handleCustomDateSelection(),["edit","bulkEdit","create"].includes(e)&&window.debouncer.timeouts.has(`save-${this.content}`)&&this.scheduleSave(0)}})))}initStore(e){let t={...this.defaults,...e};const s=window.jvbStore.register(this.content,[{storeName:this.content,keyPath:"id",endpoint:this.endpoint??"content",headers:{"X-Action-Nonce":window.auth.getNonce("dash")},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:t,ignore:["content","user"],TTL:36e5,showLoading:!0},{storeName:"changes",keyPath:"id"}]);this.changesStore=s.changes,this.store=s[this.content],this.store.subscribe(((e,t)=>{if("data-loaded"===e)this.render(),this.selectionHandler.collectItems()})),this.changesStore.subscribe(((e,t)=>{if("data-ready"===e){let e=this.changesStore.getAll();e.length>0&&(e.forEach((e=>{this.changes.set(e.id,e)})),this.savePosts("",!1).then((()=>{})))}}))}initIntegrations(){this.selected=new Set,this.selectionHandler=new window.jvbHandleSelection(this.container,{selectAll:{checkbox:"#select-all",label:".bulk-select label",span:".bulk-select label span"},wrapper:{wrapper:".wrap"},item:{idAttribute:"itemId"}}),this.selectionHandler.subscribe(((e,t)=>{this.selected=new Set([...t.selectedItems].map((e=>parseInt(e)))),this.ui.bulk.control.hidden=0===this.selected.size,this.ui.bulk.count.hidden=0===this.selected.size,this.ui.bulk.count.textContent=`${this.selected.size} ${this.plural} selected`})),this.forms=window.jvbForm,window.jvbUploads&&window.jvbUploads.subscribe(((e,t)=>{"groups_uploaded"===e&&t.content===this.content&&this.handleGroupsUploaded(t)})),this.queue.subscribe(((e,t)=>{if(["image_upload","video_upload","document_upload"].includes(t.type)&&"operation-status"===e&&"completed"===t.status&&this.store.clearCache(),"operation-status"===e&&"completed"===t.status&&"uploads/groups"===t.endpoint&&(t.result&&t.result.group_mappings&&this.handleGroupMappings(t.result.group_mappings),this.store.clearCache()),"operation-status"===e&&"completed"===t.status&&"content_update"===t.type){if(this.store.clearCache(),!t.result||!t.result.success||!t.result.errors)return void console.warn("Content update completed but no results",t);if(Object.keys(t.result.success).length>0&&this.checkCompletedChanges(Object.entries(t.result.success)),Object.keys(t.result.errors).length>0)return void this.checkFailedChanges(Object.entries(t.result.errors));0===Object.keys(t.result.success).length&&(this.changesStore.delete(id),this.store.clearCache())}}))}checkCompletedChanges(e){for(let[t,s]of e){let e=this.changesStore.get(t);if(e){for(let[t,i]of Object.entries(s))if(Object.hasOwn(e,t)){window.getDifferences.map(e[t],i)||delete e[t]}2===Object.values(e).length?(this.changesStore.delete(t),this.store.clearCache()):this.changesStore.save(e)}}}checkFailedChanges(e){}initSettings(){this.defaults={content:this.content,user:window.auth.getUser(),page:1,status:"all",orderby:"date",order:"desc",search:""};let e={},t=this.container.dataset.view??"grid";this.view=this.cache.get("view")??t,this.view!==t&&(this.ui.views[this.view].checked=!0),this.status=this.cache.get("status")??this.defaults.status,this.status!==this.defaults.status&&(this.ui.filters.status[this.status].checked=!0,e.status=this.status),this.orderby=this.cache.get("orderby")??this.defaults.orderby,this.orderby!==this.defaults.orderby&&(this.ui.filters.orderby[this.orderby].checked=!0,e.orderBy=this.orderby),this.order=this.cache.get("order")??this.defaults.order,this.order!==this.defaults.order&&(this.ui.filters.order[this.order].checked=!0,e.order=this.order),this.ui.filters.taxonomies&&Object.entries(this.ui.filters.taxonomies).forEach((([t,s])=>{const i=`tax_${t}`,a=this.cache.get(i);a&&(s.value=a,e[i]=a)}));let s=this.cache.get("tabNav")??"horizontal";this.ui.table.nav&&"vertical"===s&&(this.ui.table.nav.checked=!0);let i={showFilters:{element:this.ui.filters.container,default:"closed"},showUploader:{element:this.ui.uploader,default:"open"}};for(let[e,t]of Object.entries(i))if(t.element){let s=this.cache.get(e)??t.default;t.element.open="open"===s,t.element.addEventListener("toggle",(()=>{this.cache.set(e,t.element.open?"open":"closed")}))}return e}initListeners(){this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleModalSubmit.bind(this),document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler),this.ui.filters.search&&this.ui.filters.search.addEventListener("input",this.inputHandler);for(let[e,t]of Object.entries(this.ui.modals))t.form&&t.form.addEventListener("submit",this.submitHandler)}handleModalSubmit(e){e.preventDefault();const t=e.target.closest("dialog");if(!t)return;if(t.classList.contains("create"))return void this.handleCreateSubmit(t);this.plural;this.scheduleSave(0)}async handleCreateSubmit(e){e.dataset.itemId;this.changes.size>0&&(this.cancelBackup(),await this.handleBackup());const t=await this.changesStore.getAll();if(0===t.length)return;let s={};t.forEach((e=>{const{id:t,...i}=e;s[t]=i}));let i=this.queue.addToQueue({endpoint:this.endpoint,headers:{"X-Action-Nonce":window.auth.getNonce("dash")},data:{posts:s},popup:`Creating your new ${this.singular}`,title:`Creating your new ${this.singular}`});if(!i)return;const a=e.querySelectorAll("[data-upload-field]");for(const e of a){const t=e.dataset.uploader;if(!t)continue;0!==window.jvbUploads.stores.uploads.filterByIndex({field:t}).length&&await window.jvbUploads.queueUploads("uploads",t,i)}}handleChange(e){const t=e.target.closest("[data-item-id]"),s=e.target.matches("[data-filter]"),i=e.target.matches(".bulk-action-select"),a=e.target.matches("[data-view]");if(t||s||i||a)if(this.isPopulating||!t||e.target.closest("[data-ignore], .select-item")){if(a)return this.items.clear(),void this.handleViewChange(e.target);if(i)this.handleBulkAction(e.target);else if(s)this.handleFilterChange(e.target);else if("table"===this.view){if(e.target.matches("details.multi-select"))return void this.toggleColumn(e.target.id,e.target.checked);e.target.matches(this.selectors.table.nav)&&(this.tabNav=e.target.checked,this.cache.set("tabNav",e.target.checked?"vertical":"horizontal"))}}else this.handleItemUpdate(e)}handleBulkAction(e){if(e.value.startsWith("tax-")){const t=e.options[e.selectedIndex],s=t.dataset.taxonomy,i=t.dataset.single,a=t.dataset.plural;return window.jvbSelector.openEmpty(s,i,a,(e=>this.handleBulkTaxonomy(e))),void(e.value="")}switch(e.value){case"edit":this.openBulkEditModal();break;case"publish":case"trash":case"delete":this.setBulkStatus(e.value);break;case"draft":case"restore":this.setBulkStatus("draft")}}handleBulkTaxonomy(e){e.termIds.length&&this.selected.size&&(this.selected.forEach((t=>{const s=this.store.get(t);if(!s)return;const i=(s.taxonomies?.[e.taxonomy]||[]).map((e=>e.id)),a=[...new Set([...i,...e.termIds])];this.updateItem(t,e.taxonomy,a)})),this.savePosts(`Adding ${e.terms.length} ${e.taxonomy} to ${this.selected.size} ${this.plural}...`).then((()=>{})),this.selectionHandler.clearSelection())}handleItemUpdate(e){let t=window.targetCheck(e,"[data-item-id]");if(!t)return;const s=e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');let i,a;if(s)i=s.dataset.field,a=this.forms.getFieldValue(s);else{let t=e.target.closest("[data-field]");i=t.dataset.field,a=this.forms.getFieldValue(e.target)}t.dataset.itemId.split(",").forEach((e=>{this.updateItem(e,i,a)}))}updateItem(e,t,s){if(this.isPopulating)return;const i=this.store.get(e);if(i){const a=i.fields?.[t]??i[t];if(null===window.getDifferences.map(a,s)){if(this.changes.has(e)){delete this.changes.get(e)[t];0===Object.keys(this.changes.get(e)).filter((e=>"id"!==e&&"content"!==e)).length&&(this.changes.delete(e),this.changesStore.delete(e))}return}}this.changes.has(e)||this.changes.set(e,{id:e,content:this.content}),this.changes.get(e)[t]=s,this.scheduleBackup(),"number"!=typeof e&&String(e).includes("group")||this.scheduleSave()}scheduleBackup(){window.debouncer.schedule(`changes-${this.content}`,(async()=>{this.changes.size>0&&await this.handleBackup()}),2e3)}cancelBackup(){window.debouncer.cancel(`changes-${this.content}`)}async handleBackup(){const e=Array.from(this.changes.values());this.changes.clear();const t=e.map((e=>e.id)),s=await Promise.all(t.map((e=>this.changesStore.get(e)))),i=e.map(((e,t)=>s[t]?window.deepMerge(s[t],e):e));await this.changesStore.saveMany(i)}scheduleSave(e=1e4){window.debouncer.schedule(`save-${this.content}`,(async()=>{this.changes.size>0&&(this.cancelBackup(),await this.handleBackup()),await this.savePosts("",!1)}),e)}handleFilterChange(e){let t=e.dataset.filter;return"date"===t&&"custom"===e.value?(e.value="",void this.modals.date.handleOpen()):"date"===t&&""!==e.value?(this.setFilter("date-filter",e.value),this.deleteFilter("dateFrom"),this.deleteFilter("dateTo"),void this.checkHideFilters()):("taxonomies"===t&&(t=`tax_${e.dataset.taxonomy}`),void this.setFilter(t,e.value))}checkHideFilters(){const e=this.store.filters,t=Object.entries(e).some((([e,t])=>!["content","user","page"].includes(e)&&(this.defaults[e]!==t&&""!==t&&null!==t)));this.ui.buttons.clearFilters.hidden=!t}clearAllFilters(){let e=this.store.filters;this.store.clearFilters();for(let[t,s]of Object.entries(e))this.cache.remove(t),this.deleteFilter(t,s);this.a11y.announce("All filters cleared")}handleCustomDateSelection(){if(this.ui.modals.date.month&&this.ui.modals.date.month.value){const[e,t]=this.ui.modals.date.month.value.split("-"),s=`${e}-${t}-01`,i=new Date(e,parseInt(t),0).getDate(),a=`${e}-${t}-${String(i).padStart(2,"0")}`;this.setFilter("dateFrom",s),this.setFilter("dateTo",a),this.deleteFilter("date-filter"),this.ui.modals.date.month.value=""}else this.ui.modals.date.start&&this.ui.modals.date.start.value&&this.ui.modals.date.end&&this.ui.modals.date.end.value&&(this.setFilter("dateFrom",this.ui.modals.date.start.value),this.setFilter("dateTo",this.ui.modals.date.end.value),this.deleteFilter("date-filter"),this.ui.modals.date.start.value="",this.ui.modals.date.end.value="");this.checkHideFilters()}handleViewChange(e){this.view=e.dataset.view,this.cache.set("view",this.view),this.render()}handleClick(e){if(e.target.matches(".clear-search"))return void this.deleteFilter("search","");const t=e.target.closest("[data-action]");return t?(e.preventDefault(),void this.handleActionButton(t)):e.target.matches(".apply-date-filter")?(this.handleCustomDateSelection(),void this.modals.date.handleClose()):void(e.target.matches(this.selectors.buttons.create)&&this.openCreateModal())}openCreateModal(){this.forms.registerForm(this.ui.modals.create.form,{cache:!1}),this.ui.modals.create.modal.dataset.itemId=window.generateID("new"),this.modals.create.handleOpen()}handleActionButton(e){const t=e.dataset.id;switch(e.dataset.action){case"edit":this.openEditModal(t);break;case"delete":confirm("Delete this item? This cannot be undone")&&(this.updateItem(t,"post_status","delete"),window.fade(e.closest(".item"),!1),this.savePosts(`Permanently deleting ${this.singular}...`).then((()=>{})),this.store.delete(t));break;case"trash":"trash"===this.status?confirm("Delete this item? This cannot be undone")&&(this.updateItem(t,"post_status","delete"),window.fade(e.closest(".item"),!1),this.savePosts(`Permanently deleting ${this.singular}...`).then((()=>{})),this.store.delete(t)):(this.updateItem(t,"post_status","trash"),window.fade(e.closest(".item"),!1),this.savePosts(`Sending ${this.singular} to trash...`).then((()=>{})));break;case"bulk-edit":this.selected.size>0&&this.openBulkEditModal();break;case"bulk-delete":this.handleBulkDelete();break;case"refresh":this.store.clearCache(),this.store.fetch();break;case"clear-filters":this.clearAllFilters()}}handleBulkDelete(){let e="trash"===this.status;if(this.selected.size>0&&confirm(`${e?"Permanently delete":"Send"} ${this.selected.size} ${1===this.selected.size?this.singular:this.plural}${e?"":"to trash"}?`)){this.selected.forEach((t=>{this.store.delete(t),this.updateItem(t,"post_status",e?"delete":"trash")}));let t=e?`Permanently deleting ${this.selected.size} ${1===this.selected.size?this.singular:this.plural}`:`Sending ${this.selected.size} ${1===this.selected.size?this.singular:this.plural} to trash`;this.savePosts(t).then((()=>{})),this.selectionHandler.clearSelection()}}handleInput(e){e.preventDefault(),e.stopPropagation();let t=e.target.value.trim(),s=`${this.content}-search`;0!==t.length?window.debouncer.schedule(s,(()=>{this.a11y.announce(`Searching for "${t}"...`),this.store.setFilters({search:t,page:1})}),300):this.deleteFilter("search","")}handleKeys(e){if(this.tabNav&&"Tab"===e.key){e.preventDefault();const t=e.target.closest("[data-field]"),s=e.target.closest("tr");if(!t||!s)return;const i=t.dataset.field,a=e.shiftKey;let l=this.findNextEditableRow(s,a);l||(l=this.wrapToRow(s,a)),l&&this.focusFieldInRow(l,i,a)}}findNextEditableRow(e,t=!1){let s=t?e.previousElementSibling:e.nextElementSibling;for(;s&&!this.isEditableRow(s);)s=t?s.previousElementSibling:s.nextElementSibling;return s}wrapToRow(e,t=!1){if(this.isTimeline){const s=e.closest("tbody");if(!s)return null;const i=Array.from(s.querySelectorAll("tr")).filter((e=>this.isEditableRow(e)));return t?i[i.length-1]:i[0]}{if(!this.ui.table.body)return null;const e=Array.from(this.ui.table.body.querySelectorAll("tr")).filter((e=>this.isEditableRow(e)));return t?e[e.length-1]:e[0]}}isEditableRow(e){return!e.closest("thead")&&!e.closest("tfoot")&&(this.isTimeline?e.classList.contains("shared")||e.classList.contains("timeline-point"):!!e.dataset.itemId)}focusFieldInRow(e,t,s=!1){const i=e.querySelector(`[data-field="${t}"]`);if(!i)return;const a=this.findFocusableInput(i);if(a){a.focus(),a.select&&"text"===a.type&&a.select();const e=s?"next":"previous";this.a11y?.announce(`Moved to ${t} in ${e} row`)}}findFocusableInput(e){const t=['input:not([type="hidden"]):not([disabled])',"textarea:not([disabled])","select:not([disabled])","button:not([disabled])"];for(const s of t){const t=e.querySelector(s);if(t)return t}return null}openEditModal(e){let t,s=this.store.get(parseInt(e));s&&(this.activeItem=s.id,this.ui.modals.edit.modal.dataset.itemId=e,this.ui.modals.edit.modal.dataset.content=this.content,Object.hasOwn(s.fields,"post_title")?t=s.fields.post_title:Object.hasOwn(s.fields,"name")&&(t=s.fields.name),this.ui.modals.edit.h2.textContent=`Editing ${""===t?this.singular:t}`,this.ui.modals.edit.form.dataset.formId=`edit-${e}`,this.forms.registerForm(this.ui.modals.edit.form,{cache:!1,autoUpload:!0}),this.isPopulating=!0,this.populate.populate(this.ui.modals.edit.form,s),requestAnimationFrame((()=>{requestAnimationFrame((()=>{this.isPopulating=!1}))})),this.modals.edit.handleOpen())}openBulkEditModal(){window.removeChildren(this.ui.modals.bulkEdit.selected),this.ui.modals.edit.form.reset(),window.chunkIt(this.selected,(t=>{let s=this.store.get(parseInt(t));if(s)return e.push(s.id),window.jvbTemplates.create("bulkItem",s)}),(e=>this.ui.modals.bulkEdit.selected.append(e))).then((()=>{}));let e=Array.from(this.selected).map((e=>this.store.get(parseInt(e)))).filter(Boolean);this.ui.modals.bulkEdit.modal.dataset.itemId=e.join(","),this.ui.modals.bulkEdit.h2&&(this.ui.modals.bulkEdit.h2.textContent=this.selected.size),this.modals.bulkEdit.handleOpen(),this.forms.registerForm(this.ui.modals.bulkEdit.form,{cache:!1}),this.isPopulating=!0,this.populate.populate(this.ui.modals.edit.form,item),requestAnimationFrame((()=>{requestAnimationFrame((()=>{this.isPopulating=!1}))}))}async savePosts(e="",t=!1){this.changes.size>0&&(this.cancelBackup(),await this.handleBackup());let s=await this.changesStore.getAll();if(0===s.length)return;if(s=this.validateChanges(s),0===s.length)return;""===e&&(e=`Saving ${s.length} ${1===s.length?this.singular:this.plural}`);let i={},a=[];s.forEach((e=>{let t=e.id;const{id:s,...l}=e;i[t]=l,e.post_status&&this.shouldRemoveItemUI(e.post_status)&&a.push(t)})),a.length>0&&this.removeItems(a);let l={endpoint:this.endpoint,headers:{"X-Action-Nonce":window.auth.getNonce("dash")},data:{posts:i},delay:t,popup:"Saving changes",title:e};this.queue.addToQueue(l)}validateChanges(e){return e.reduce(((e,t)=>{const{id:s,content:i,...a}=t,l=this.store.get(s);if(!l)return e.push(t),e;const n={id:s,content:i};let o=!1;for(const[e,t]of Object.entries(a)){const s=l.fields?.[e]??l[e];null!==window.getDifferences.map(s,t)&&(n[e]=t,o=!0)}return o?e.push(n):(this.changes.delete(s),this.changesStore.delete(s)),e}),[])}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,s=[];if(this.selected.forEach((t=>{s.push(t),this.updateItem(t,"post_status",e)})),"delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";this.shouldRemoveItemUI(e)&&this.removeItems(s),this.selectionHandler.clearSelection(),this.savePosts(`${t} ${s.length} ${1===s.length?this.singular:this.plural}...`).then((()=>{}))}render(){const e=this.store.getFiltered();if(0!==e.length){switch(this.view){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e).then((()=>{}));break;case"list":this.renderList(e)}this.updateUI()}else this.renderEmpty()}updateUI(){if(this.ui.bulk.action){let e=!1,t=this.ui.bulk.action.querySelector('[value="edit"]'),s=this.status;"trash"===s&&t?(window.removeChildren(this.ui.bulk.action),e=window.jvbTemplates.create("trashOptions")):"trash"===s||t||(window.removeChildren(this.ui.bulk.action),e=window.jvbTemplates.create("notTrashOptions")),e&&e.querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulk.action.append(e)})),this.ui.bulk.action.value=""}this.selected.size>0&&this.selectionHandler.updateSelectionUI()}renderEmpty(){this.toggleTable(!1),window.removeChildren(this.ui.grid);const e=window.jvbTemplates.create("emptyState");e&&(this.ui.grid.append(e),this.a11y.announceItems(0,!1,!1))}toggleTable(e=!0){if(this.ui.table.selectedColumns&&(this.ui.table.selectedColumns.hidden=!e),e&&!this.ui.table.form){let e=window.jvbTemplates.create("contentTable");this.container.append(e),this.ui.table=window.uiFromSelectors(this.selectors.table),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.form&&(this.ui.table.form.hidden=!e,e||this.forms.clearForm(this.ui.table.form.dataset.formId),this.ui.table.body&&window.removeChildren(this.ui.table.body)),this.keyHandler=this.handleKeys.bind(this),e?document.addEventListener("keydown",this.keyHandler):document.removeEventListener("keydown",this.keyHandler)}renderGrid(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view"),window.chunkIt(e,(e=>this.renderGridItem(e)),(e=>this.ui.grid.append(e))).then((()=>{}))}renderList(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),window.chunkIt(e,(e=>this.renderListItem(e)),(e=>this.ui.grid.append(e))).then((()=>{}))}async renderTable(e){this.toggleTable(),window.removeChildren(this.ui.grid),await window.chunkIt(e,(e=>this.renderTableItem(e)),(e=>{this.ui.table.body?this.ui.table.body.append(e):this.ui.table.table.insertBefore(e,this.ui.table.foot)}),5),requestAnimationFrame((()=>{window.jvbSelector?.scanExistingFields(this.ui.table.table)}))}renderGridItem(e){let t=window.jvbTemplates.create("gridView",e);return this.items.set(e.id,t),t}renderListItem(e){let t=window.jvbTemplates.create("listView",e);return this.items.set(e.id,t),t}renderTableItem(e){let t=window.jvbTemplates.create("tableView",e);return this.items.set(e.id,t),t}toggleColumn(e,t){this.ui.table.table.querySelectorAll(`.${e}`).forEach((e=>{e.hidden=!t}))}handleGroupsUploaded(e){const{posts:t,fieldId:s}=e;let i=window.jvbUploads,a=(i.fields.get(s),[]);t.forEach((e=>{const t={id:e.groupId,title:e.fields.post_title||`New ${this.singular}`,status:"draft",date:(new Date).toISOString(),modified:(new Date).toISOString(),thumbnail:null,icon:this.content,taxonomies:{},fields:e.fields,images:{}};e.images.forEach(((e,s)=>{let a=e.upload_id;0===s&&(t.fields.post_thumbnail=e);let l=i.stores.uploads.get(a);l&&(t.images[a]={"image-alt-text":"","image-caption":"","image-title":l.fields.originalName,medium:i.createPreviewUrl(i.formatFile(l))})})),a.push(t)})),this.store.saveMany(a).then((()=>this.render())),this.a11y.announce(`${t.length} ${1===t.length?this.singular:this.plural} created. Waiting for server confirmation...`)}handleGroupMappings(e){for(const[t,s]of Object.entries(e)){let e={};this.changes.has(t)&&(e=this.changes.get(t),this.changes.delete(t));let i=this.changesStore.get(t)??{};(e.size>0||i.size>0)&&(e=window.deepMerge(i,e),this.changes.set(s,e),this.scheduleBackup())}}shouldRemoveItemUI(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.store.filters.status}removeItems(e){e.forEach((e=>{if(this.items.has(e)){let t=this.items.get(e);t&&window.fade(t,!1)}}))}setFilters(e){for(let[t,s]of Object.entries(e)){if(!this.allowedFilters.includes(t)){delete e[t];continue}this.cache.set(t,s);let i=this.findFilterEl(t);this.setElValue(i,s)}this.store.setFilters(e)}setFilter(e,t){if(!this.allowedFilters.includes(e))return;this.cache.set(e,t),"status"===e&&(this.status=t),"orderby"===e&&(this.orderby=t),"order"===e&&(this.order=t);let s=this.findFilterEl(e,t);this.setElValue(s,t),this.store.setFilter(e,t)}deleteFilter(e,t){if(!this.allowedFilters.includes(e))return;if(Object.hasOwn(this.defaults,e))return void this.setFilter(e,this.defaults[e]);let s=this.findFilterEl(e,t);this.setElValue(s,!1),this.cache.remove(e),this.setFilter(e,"")}setElValue(e,t){if(e){if(!t)return["SELECT","TEXTAREA"].includes(e.tagName)&&(e.value=""),["text","search"].includes(e.type)&&(e.value=""),void("radio"===e.type&&(e.checked=!1));["SELECT","TEXTAREA"].includes(e.tagName)&&(e.value=t),["text","search"].includes(e.type)&&(e.value=t),"radio"===e.type&&(e.checked=!0)}}findFilterEl(e,t){if(["date-filter","dateFrom","dateTo"].includes(e)){switch(e){case"date-filter":e="month";break;case"dateFrom":e="start";break;case"dateTo":e="end"}return this.ui.modals.date[e]}if(e.includes("tax_")){const t=e.replace("tax_",""),s=this.ui.filters.taxonomies?.[t];return s||(console.warn("Taxonomy filter element not found:",t),null)}if(!Object.hasOwn(this.ui.filters,e))return console.warn("Filter el not found: ",e),!1;let s=this.ui.filters[e];if("object"==typeof s){if(!Object.hasOwn(this.ui.filters[e],t))return!1;s=this.ui.filters[e][t]}return s}resetForm(e){e.querySelectorAll('input[type="hidden"], input[type="text"], input[type="number"], input[type="email"], input[type="url"], textarea').forEach((e=>{e.value=""})),e.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach((e=>{e.checked=!1})),e.querySelectorAll("select").forEach((e=>{e.selectedIndex=0})),e.querySelectorAll(".selected-items").forEach((e=>{window.removeChildren(e)})),e.querySelectorAll(".item-grid.preview").forEach((e=>{window.removeChildren(e)}))}destroy(){window.debouncer.cancel(`changes-${this.content}`),this.changes.size>0&&(this.changesStore.saveMany(this.changes).then((()=>{})),this.changes.clear()),this.timelineSortables&&(this.timelineSortables.forEach((e=>e.destroy())),this.timelineSortables=[]);for(let[e,t]of Object.entries(this.ui.modals))t.form&&t.form.removeEventListener("submit",this.submitHandler);document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),this.ui.filters.search&&this.ui.filters.search.removeEventListener("input",this.handleInput)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{if("auth-loaded"===t){let t=document.querySelector("[data-content]");t&&!Object.hasOwn(t.dataset,"ignore")&&(window.crudManager=new e({content:t.dataset.content}))}}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/dataStore.min.js b/assets/js/min/dataStore.min.js
index 0ee0509..b0061ff 100644
--- a/assets/js/min/dataStore.min.js
+++ b/assets/js/min/dataStore.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.25){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`${jvbBase.base}${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},ignore:[],required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.ignoreFilters=new Set(["search","page","per_page","orderby","order","context","source",...i.config.ignore]),i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),saveMany:t=>this.saveMany(e,t),delete:t=>this.delete(e,t),deleteMany:t=>this.deleteMany(e,t),get:t=>this.get(e,t),getMany:t=>this.getMany(e,t),getAll:()=>this.getAll(e),getAllByIndex:(t,s)=>this.getAllByIndex(e,t,s),filterByIndex:t=>this.filterByIndex(e,t),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach((e=>t.append(s,e))):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{i.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}i.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const r=this.stores.get(e);return r?.db&&r.db.objectStoreNames.contains(t)?new Promise((e=>{const i=r.db.transaction([t],"readonly").objectStore(t).getAll();i.onsuccess=t=>{const r=t.target.result||[];r.forEach(s),e(r)},i.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,r){const i=this.stores.get(e);return i?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=i.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>{const e=o.error||new Error("Transaction failed with unknown error");a(e)};try{h=r(c,o)}catch(e){a(e||new Error("Callback failed with unknown error"))}}))):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL)){let t=r.items.map((t=>this.get(e,t)));return this.notify(e,"data-loaded",{cached:!0,items:t??[]}),r}t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&r&&(r.etag&&(a["If-None-Match"]=r.etag),r.lastModified&&(a["If-Modified-Since"]=r.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(i,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){if(!("AbortError"===t?.name))throw console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t}),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s,r){const i=this.stores.get(e),a=(t.items||[]).filter((e=>e&&"object"==typeof e)),o=[];i.db&&a.length>0&&await this.withTransaction(e,i.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const r=this._saveItem(e,s);o.push(r),t.put(r.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,i.config.keyPath))),timestamp:Date.now(),endpoint:i.config.endpoint,filters:{...i.filters},etag:r.headers.get("ETag"),lastModified:r.headers.get("Last-Modified"),has_more:t.has_more||!1};i.cache.set(s,n),i.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),i.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}};for(let[t,s]of Object.entries(i.filters))"string"==typeof s&&s.includes(",")&&this.createSplitCacheEntries(e,a,t,i.filters,r);o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}createSplitCacheEntries(e,t,s,r,i){const a=this.stores.get(e);r[s].split(",").map((e=>e.trim())).forEach((t=>{let o={};o[s]=t;const n={...r,[s]:t},c=this.generateCacheKey(n);if(a.cache.has(c))return;let h=this.filterByIndex(e,o).map((e=>this.getItemKey(e,a.config.keyPath)));const l={key:c,items:h,timestamp:Date.now(),endpoint:a.config.endpoint,filters:n,etag:i.headers.get("Etag"),lastModified:i.headers.get("Last-Modified"),has_more:20===h.length};a.cache.set(c,l),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.put(l)}))}))}_saveItem(e,t){const s=this.stores.get(e),r=this.processForStorage(t,s.config.validateData);if(!r.valid)throw new Error(`Non-serializable data: ${r.error}`);const i=r.data,a=this.getItemKey(i,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:i,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),r=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(r.processed)})),this.notify(e,"item-saved",{item:r.item,key:r.key,previousItem:r.previousItem}),r.key}async saveMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Map?Array.from(t.values()):Array.isArray(t)?t:Object.values(t);if(0===r.length)return[];const i=[];return r.forEach((t=>{const s=this._saveItem(e,t);i.push(s)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{i.forEach((t=>{e.put(t.processed)}))})),this.notify(e,"items-saved",{count:i.length,keys:i.map((e=>e.key))}),i.map((e=>e.key))}processForStorage(e,t=!0,s="root"){if(null===e)return{valid:!0,data:null};if(void 0===e)return t?{valid:!1,error:`Undefined value at ${s}`}:{valid:!0,data:void 0};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:void 0};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:void 0};if(e instanceof FormData)return{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0===e.data&&null!==a||(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:void 0}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}async deleteMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===r.length?[]:(r.forEach((e=>{s.data.delete(e)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{r.forEach((t=>{e.delete(t)}))})),this.notify(e,"items-deleted",{count:r.length,ids:r}),r)}get(e,t){return this.stores.get(e).data.get(t)}getMany(e,t,s=!0){const r=this.stores.get(e);if(!r)return[];const i=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===i.length?[]:s?i.reduce(((e,t)=>{const s=r.data.get(t);return s&&e.push(s),e}),[]):i.map((e=>r.data.get(e)??null))}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter((e=>!(!e||"object"!=typeof e)&&Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r?.items){const e=r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]);return this.applyOrdering(e,t)}const i=Array.from(t.data.values()),a=t.filters.search?.toLowerCase().trim()||"",o=[];t.filters.taxonomy&&"object"==typeof t.filters.taxonomy&&Object.entries(t.filters.taxonomy).forEach((([e,t])=>{const s=Array.isArray(t)?t:[t];o.push((t=>{if(!t.taxonomies||!t.taxonomies[e])return!1;const r=Object.keys(t.taxonomies[e]).map((e=>parseInt(e)));return s.some((e=>r.includes(parseInt(e))))}))}));for(const[e,s]of Object.entries(t.filters))if("taxonomy"!==e&&!t.ignoreFilters.has(e)&&null!=s&&""!==s&&"all"!==s)if("string"==typeof s&&s.includes(",")){const t=s.split(",").map((e=>e.trim()));o.push((s=>t.includes(String(s[e]))))}else o.push((t=>String(t[e])===String(s)));const n=i.filter((e=>{for(const t of o)if(!t(e))return!1;return!(a&&!this.searchObject(e,a))}));return this.applyOrdering(n,t)}applyOrdering(e,t){if(Array.isArray(e)||(e=Array.from(e)),0===e.length)return e;const s=t.filters.orderby||"date",r=(t.filters.order||"desc").toLowerCase();return["random","rand"].includes(s)||["random","rand"].includes(r)?this.shuffle(e):(e.sort(((e,t)=>{let i,a;switch(s){case"alphabetical":case"title":i=(e.title||e.name||"").toLowerCase(),a=(t.title||t.name||"").toLowerCase();break;case"modified":i=new Date(e.modified||e.date||0),a=new Date(t.modified||t.date||0);break;default:i=new Date(e.date||e.modified||0),a=new Date(t.date||t.modified||0)}return i<a?"asc"===r?-1:1:i>a?"asc"===r?1:-1:0})),e)}shuffle(e){const t=e.slice();for(let e=t.length-1;e>0;e--){const s=Math.floor(Math.random()*(e+1));[t[e],t[s]]=[t[s],t[e]]}return t}searchObject(e,t){if(!e||"object"!=typeof e)return"string"==typeof e&&e.toLowerCase().includes(t);for(const s of Object.values(e))if(null!=s)if("object"!=typeof s){if("string"==typeof s&&s.toLowerCase().includes(t))return!0}else if(this.searchObject(s,t))return!0;return!1}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t});const a=await this.shouldFetchWithFilters(e,t,i);if(r.config.endpoint&&a)await this.fetch(e);else{const t=this.getFiltered(e);this.notify(e,"data-loaded",{cached:!0,items:t})}}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if(!1===r.lastResponse.has_more){if(Object.entries(t).every((([e,t])=>(r.ignoreFilters.has(e),!0))))return!1}if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e]))||Object.keys(s.filters).some((e=>!(e in t)&&t!==s.config.filters)))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.25){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`${jvbBase.base}${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},ignore:[],required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.ignoreFilters=new Set(["search","page","per_page","orderby","order","context","source",...i.config.ignore]),i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),saveMany:t=>this.saveMany(e,t),delete:t=>this.delete(e,t),deleteMany:t=>this.deleteMany(e,t),get:t=>this.get(e,t),getMany:t=>this.getMany(e,t),getAll:()=>this.getAll(e),getAllByIndex:(t,s)=>this.getAllByIndex(e,t,s),filterByIndex:t=>this.filterByIndex(e,t),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach((e=>t.append(s,e))):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{i.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}i.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const r=this.stores.get(e);return r?.db&&r.db.objectStoreNames.contains(t)?new Promise((e=>{const i=r.db.transaction([t],"readonly").objectStore(t).getAll();i.onsuccess=t=>{const r=t.target.result||[];r.forEach(s),e(r)},i.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,r){const i=this.stores.get(e);return i?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=i.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>{const e=o.error||new Error("Transaction failed with unknown error");a(e)};try{h=r(c,o)}catch(e){a(e||new Error("Callback failed with unknown error"))}}))):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL)){let t=r.items.map((t=>this.get(e,t)));return this.notify(e,"data-loaded",{cached:!0,items:t??[]}),r}t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&r&&(r.etag&&(a["If-None-Match"]=r.etag),r.lastModified&&(a["If-Modified-Since"]=r.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(i,{method:"GET",headers:a,signal:o.signal});if(!n.ok){const e=await n.text();throw new Error(`HTTP error! status: ${n.status}, message: ${e}`)}if(304===n.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){if(!("AbortError"===t?.name))throw console.error(`Fetch error for store "${e}":`,t.message),console.dir(t),this.notify(e,"fetch-error",{error:t}),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s,r){const i=this.stores.get(e),a=(t.items||[]).filter((e=>e&&"object"==typeof e)),o=[];i.db&&a.length>0&&await this.withTransaction(e,i.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const r=this._saveItem(e,s);o.push(r),t.put(r.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,i.config.keyPath))),timestamp:Date.now(),endpoint:i.config.endpoint,filters:{...i.filters},etag:r.headers.get("ETag"),lastModified:r.headers.get("Last-Modified"),has_more:t.has_more||!1};i.cache.set(s,n),i.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),i.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}};for(let[t,s]of Object.entries(i.filters))"string"==typeof s&&s.includes(",")&&this.createSplitCacheEntries(e,a,t,i.filters,r);o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}createSplitCacheEntries(e,t,s,r,i){const a=this.stores.get(e);r[s].split(",").map((e=>e.trim())).forEach((t=>{let o={};o[s]=t;const n={...r,[s]:t},c=this.generateCacheKey(n);if(a.cache.has(c))return;let h=this.filterByIndex(e,o).map((e=>this.getItemKey(e,a.config.keyPath)));const l={key:c,items:h,timestamp:Date.now(),endpoint:a.config.endpoint,filters:n,etag:i.headers.get("Etag"),lastModified:i.headers.get("Last-Modified"),has_more:20===h.length};a.cache.set(c,l),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.put(l)}))}))}_saveItem(e,t){const s=this.stores.get(e),r=this.processForStorage(t,s.config.validateData);if(!r.valid)throw new Error(`Non-serializable data: ${r.error}`);const i=r.data,a=this.getItemKey(i,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:i,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),r=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(r.processed)})),this.notify(e,"item-saved",{item:r.item,key:r.key,previousItem:r.previousItem}),r.key}async saveMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Map?Array.from(t.values()):Array.isArray(t)?t:Object.values(t);if(0===r.length)return[];const i=[];return r.forEach((t=>{const s=this._saveItem(e,t);i.push(s)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{i.forEach((t=>{e.put(t.processed)}))})),this.notify(e,"items-saved",{count:i.length,keys:i.map((e=>e.key))}),i.map((e=>e.key))}processForStorage(e,t=!0,s="root"){if(null===e)return{valid:!0,data:null};if(void 0===e)return t?{valid:!1,error:`Undefined value at ${s}`}:{valid:!0,data:void 0};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:void 0};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:void 0};if(e instanceof FormData)return{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0===e.data&&null!==a||(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:void 0}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}async deleteMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===r.length?[]:(r.forEach((e=>{s.data.delete(e)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{r.forEach((t=>{e.delete(t)}))})),this.notify(e,"items-deleted",{count:r.length,ids:r}),r)}get(e,t){return this.stores.get(e).data.get(t)}getMany(e,t,s=!0){const r=this.stores.get(e);if(!r)return[];const i=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===i.length?[]:s?i.reduce(((e,t)=>{const s=r.data.get(t);return s&&e.push(s),e}),[]):i.map((e=>r.data.get(e)??null))}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter((e=>!(!e||"object"!=typeof e)&&Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r?.items){const e=r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]);return this.applyOrdering(e,t)}const i=Array.from(t.data.values()),a=t.filters.search?.toLowerCase().trim()||"",o=[];t.filters.taxonomy&&"object"==typeof t.filters.taxonomy&&Object.entries(t.filters.taxonomy).forEach((([e,t])=>{const s=Array.isArray(t)?t:[t];o.push((t=>{if(!t.taxonomies||!t.taxonomies[e])return!1;const r=Object.keys(t.taxonomies[e]).map((e=>parseInt(e)));return s.some((e=>r.includes(parseInt(e))))}))}));for(const[e,s]of Object.entries(t.filters))if("taxonomy"!==e&&!t.ignoreFilters.has(e)&&null!=s&&""!==s&&"all"!==s)if("string"==typeof s&&s.includes(",")){const t=s.split(",").map((e=>e.trim()));o.push((s=>t.includes(String(s[e]))))}else o.push((t=>String(t[e])===String(s)));const n=i.filter((e=>{for(const t of o)if(!t(e))return!1;return!(a&&!this.searchObject(e,a))}));return this.applyOrdering(n,t)}applyOrdering(e,t){if(Array.isArray(e)||(e=Array.from(e)),0===e.length)return e;const s=t.filters.orderby||"date",r=(t.filters.order||"desc").toLowerCase();return["random","rand"].includes(s)||["random","rand"].includes(r)?this.shuffle(e):(e.sort(((e,t)=>{let i,a;switch(s){case"alphabetical":case"title":i=(e.title||e.name||"").toLowerCase(),a=(t.title||t.name||"").toLowerCase();break;case"modified":i=new Date(e.modified||e.date||0),a=new Date(t.modified||t.date||0);break;default:i=new Date(e.date||e.modified||0),a=new Date(t.date||t.modified||0)}return i<a?"asc"===r?-1:1:i>a?"asc"===r?1:-1:0})),e)}shuffle(e){const t=e.slice();for(let e=t.length-1;e>0;e--){const s=Math.floor(Math.random()*(e+1));[t[e],t[s]]=[t[s],t[e]]}return t}searchObject(e,t){if(!e||"object"!=typeof e)return"string"==typeof e&&e.toLowerCase().includes(t);for(const s of Object.values(e))if(null!=s)if("object"!=typeof s){if("string"==typeof s&&s.toLowerCase().includes(t))return!0}else if(this.searchObject(s,t))return!0;return!1}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t});const a=await this.shouldFetchWithFilters(e,t,i);if(r.config.endpoint&&a)await this.fetch(e);else{const t=this.getFiltered(e);this.notify(e,"data-loaded",{cached:!0,items:t})}}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if(!1===r.lastResponse.has_more){if(Object.entries(t).every((([e,t])=>(r.ignoreFilters.has(e),!0))))return!1}if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e]))||Object.keys(s.filters).some((e=>!(e in t)&&t!==s.config.filters)))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/navigation.min.js b/assets/js/min/navigation.min.js
index 282d0da..e3c382b 100644
--- a/assets/js/min/navigation.min.js
+++ b/assets/js/min/navigation.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach((e=>{let t=e.id;""===t&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.hoverOnListener),e.addEventListener("mouseleave",this.hoverOffListener));let[s,n,i]=[e.querySelectorAll("nav .toggle"),e.querySelectorAll(".has-submenu"),e.querySelectorAll(".toggle:not(.main)")],a={nav:e,toggles:s,submenus:n,submenuToggles:i};this.navs.set(t,a),this.counter++}))}initListeners(){this.clickListener=this.handleClick.bind(this),this.escapeListener=this.handleEscape.bind(this),this.hoverOnListener=this.handleHoverOn.bind(this),this.hoverOffListener=this.handleHoverOff.bind(this),document.addEventListener("click",this.clickListener)}handleClick(e){if(0===this.navs.size)return;this.openNav&&null===e.target.closest(`#${this.openNav}`)&&this.toggleNav(!1,this.openNav);let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav");this.toggleNav(!e.classList.contains("open"),e.id)}let s=e.target.closest('[data-action="toggle-submenu"], .has-submenu .a');if(s){let e=s.closest("li");this.toggleSubmenu(!e.classList.contains("open"),e)}}handleHoverOn(e){let t=e.target.closest("nav");t&&this.toggleNav(!0,t.id);let s=e.target.closest(".has-submenu");s&&this.toggleSubmenu(!0,s)}handleHoverOff(e){let t=e.target.closest("nav");t&&this.toggleNav(!1,t.id)}handleEscape(e){this.openNav&&"Escape"===e.key&&this.toggleNav(!1,this.openNav)}toggleNav(e,t){let s=this.navs.get(t);s&&(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.escapeListener)):(this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.escapeListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach((e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}))),s.nav.ariaExpanded=e,s.nav.classList.toggle("open",e),s.ariaHidden=!e,e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus())}toggleSubmenu(e,t){let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];t.classList.toggle("open",e),t.ariaHidden=!e,s.ariaExpanded=e,e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbNav=new e}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach((e=>{let t=e.id;""===t&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.hoverOnListener),e.addEventListener("mouseleave",this.hoverOffListener));let[s,n,i]=[e.querySelectorAll("nav .toggle"),e.querySelectorAll(".has-submenu"),e.querySelectorAll(".toggle:not(.main)")],a={nav:e,toggles:s,submenus:n,submenuToggles:i};this.navs.set(t,a),this.counter++}))}initListeners(){this.clickListener=this.handleClick.bind(this),this.escapeListener=this.handleEscape.bind(this),this.hoverOnListener=this.handleHoverOn.bind(this),this.hoverOffListener=this.handleHoverOff.bind(this),document.addEventListener("click",this.clickListener)}handleClick(e){if(0===this.navs.size)return;this.openNav&&null===e.target.closest(`#${this.openNav}`)&&(console.log("Closing nav",this.openNav),this.toggleNav(!1,this.openNav));let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav");this.toggleNav(!e.classList.contains("open"),e.id)}let s=e.target.closest('[data-action="toggle-submenu"], .has-submenu .a');if(s){let e=s.closest("li");this.toggleSubmenu(!e.classList.contains("open"),e)}}handleHoverOn(e){let t=e.target.closest("nav");t&&this.toggleNav(!0,t.id);let s=e.target.closest(".has-submenu");s&&this.toggleSubmenu(!0,s)}handleHoverOff(e){let t=e.target.closest("nav");t&&this.toggleNav(!1,t.id)}handleEscape(e){this.openNav&&"Escape"===e.key&&this.toggleNav(!1,this.openNav)}toggleNav(e,t){let s=this.navs.get(t);s&&(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.escapeListener)):(this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.escapeListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach((e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}))),s.nav.ariaExpanded=e,s.nav.classList.toggle("open",e),s.ariaHidden=!e,e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus())}toggleSubmenu(e,t){let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];t.classList.toggle("open",e),t.ariaHidden=!e,s.ariaExpanded=e,e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbNav=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/page-nav.min.js b/assets/js/min/page-nav.min.js
index b7b6fe2..1d087be 100644
--- a/assets/js/min/page-nav.min.js
+++ b/assets/js/min/page-nav.min.js
@@ -1 +1 @@
-(()=>{class e extends window.UIHandler{constructor(){super(),this.navOpen=!1,this.toggleNav=this.toggleNav.bind(this),this.bindElements(),this.elements.nav&&(this.elements.toggle&&(this.elements.toggle.addEventListener("click",this.toggleNav),this.bindEvents()),this.setupSectionObserver())}bindElements(){const e=document.querySelector("nav.on-this-page");e&&(this.elements={nav:e,links:e.querySelectorAll("a"),sections:Array.from(e.querySelectorAll("a")).map((e=>{const t=e.getAttribute("href");return document.querySelector(t)})).filter(Boolean)},e.querySelector("button.toggle")&&(this.elements.toggle=e.querySelector("button.toggle")))}bindComponentEvents(){}toggleNav(e){e?.preventDefault(),e?.stopPropagation();const{nav:t,toggle:n}=this.elements;t&&n&&(this.navOpen=!this.navOpen,this.navOpen?(t.classList.add("open"),n.setAttribute("aria-label","Hide Index"),n.setAttribute("aria-expanded","true"),this.bindLinkHandlers()):(t.classList.remove("open"),n.setAttribute("aria-label","Show Index"),n.setAttribute("aria-expanded","false"),this.cleanupLinkHandlers()))}bindLinkHandlers(){const{links:e}=this.elements;e?.forEach((e=>{e._boundHandler=()=>{this.navOpen=!1,this.elements.nav.classList.remove("open"),this.elements.toggle.setAttribute("aria-label","Show Index"),this.elements.toggle.setAttribute("aria-expanded","false"),this.cleanupLinkHandlers()},e.addEventListener("click",e._boundHandler)}))}cleanupLinkHandlers(){const{links:e}=this.elements;e?.forEach((e=>{e._boundHandler&&(e.removeEventListener("click",e._boundHandler),delete e._boundHandler)}))}setupSectionObserver(){const{sections:e}=this.elements;e?.length&&this.initializeObserver("sections",e,{rootMargin:"-50% 0% -50% 0%",threshold:0},(e=>{e.forEach((e=>{if(!e.isIntersecting)return;const t=e.target.id,n=this.elements.nav?.querySelector(`a[href="#${t}"]`);n&&this.updateActiveClasses(n)}))}))}updateActiveClasses(e){const t=e.closest("li");if(!t)return;this.elements.nav.querySelectorAll("li").forEach((e=>{e.classList.remove("active","adj")})),t.classList.add("active"),t.previousElementSibling&&t.previousElementSibling.classList.add("adj"),t.nextElementSibling&&t.nextElementSibling.classList.add("adj")}isComponentActive(e){return"nav"===e?this.navOpen:super.isComponentActive(e)}handleOutsideClick(e){this.navOpen&&!this.elements.nav.contains(e.target)&&this.toggleNav(e)}handleEscapeKey(e){"Escape"===e.key&&this.navOpen&&(this.toggleNav(e),e.preventDefault())}cleanup(){this.cleanupLinkHandlers(),super.cleanup()}}document.addEventListener("DOMContentLoaded",(()=>{document.querySelector("nav.on-this-page")&&(window.onThisPage=new e)}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.initElements(),this.initListeners()}initElements(){this.selectors={nav:"nav.on-this-page",toggle:"button.toggle",icon:"button.toggle .icon"},this.ui=window.uiFromSelectors(this.selectors);let e=this.ui.nav.querySelectorAll("li a");this.ui.items={},this.ui.sections={},this.selectors.items=[];for(let t of e){let e=t.getAttribute("href");this.ui.items[e.replace("#","")]=t.closest("li"),this.selectors.items.push(e),this.ui.sections[e.replace("#","")]=document.querySelector(e)}this.selectors.items=this.selectors.items.join(",")}initListeners(){this.ui.toggle.addEventListener("click",(()=>{let e=this.ui.nav.classList().contains("open")?"icon-x-square":"icon-plus-square";console.log("Changing icon to: "+e),this.ui.icon.className="icon "+e})),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{if(e.isIntersecting){let t=Object.keys(this.ui.items).indexOf(e.target.id),s=0;for(let e of Object.values(this.ui.items))e.classList.toggle("active",t===s),s++}}))}),{rootMargin:"-50% 0px -50% 0px",threshold:0});for(let e of Object.values(this.ui.sections))console.log("Observing section: ",e),this.observer.observe(e)}}document.addEventListener("DOMContentLoaded",(()=>{document.querySelector("nav.on-this-page")&&(window.onThisPage=new e)}))})();
\ No newline at end of file
diff --git a/assets/js/min/uploader.min.js b/assets/js/min/uploader.min.js
index 651676a..0515707 100644
--- a/assets/js/min/uploader.min.js
+++ b/assets/js/min/uploader.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.queue=window.jvbQueue,this.error=window.jvbError,this.templates=window.jvbTemplates,this.subscribers=new Set,this.initStores(),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.sortables=new Map,this.changes=new Map,this.previewUrls=new Set,this.initElements(),this.initListeners(),this.defineTemplates()}defineTemplates(){const e=this.templates,t=this;e.define("uploadItem",{refs:{select:'[name="select-item"]',featured:'[name="featured"]',img:"img",video:"video",file:"label > span",details:"details",alt:'[name="image-alt-text"]',title:'[name="image-title"]',description:'[name="image-caption"]'},manyRefs:{inputs:"input, select, textarea"},setup({el:e,refs:s,manyRefs:i,data:r}){let a,o,l,d=!1;switch(Object.hasOwn(r,"file")?(e.dataset.uploadId=r.uploadId,a=t.getSubtypeFromMime(r.file.type)||"image",o="document"!==a&&t.createPreviewUrl(r.file),d=o,l=r.file.name||""):(e.dataset.id=r.id,a=t.getSubtypeFromURL(r.medium??r.src),o=r.medium??r.src,l=r["image-alt-text"]??""),e.dataset.subtype=a,s.featured&&(s.featured.value=r.uploadId),a){case"image":s.img&&(s.img.src=o,s.img.alt=l,d&&(s.img.dataset.previewUrl=d)),s.video&&s.video.remove(),s.file&&s.file.remove();break;case"video":s.video&&(s.video.src=o,s.video.alt=l,d&&(s.video.dataset.previewUrl=d)),s.img&&s.img.remove(),s.file&&s.file.remove();break;case"document":if(s.preview){let e=r.file.name.split(".").pop()?.toLowerCase()??"",t={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},i=window.getIcon(t[e]??"file");s.preview.innerText=r.file.name??r.title,s.preview.prepend(i)}s.img&&s.img.remove(),s.video&&s.video.remove()}if(s.details&&(Object.hasOwn(r,"field")&&Object.hasOwn(r.field,"config")&&Object.hasOwn(r.field.config,"showMeta")&&!r.field.config.showMeta?s.details.remove():(Object.hasOwn(r,"id")?s.details.dataset.attachmentId=r.id:Object.hasOwn(r,"uploadId")&&(s.details.dataset.uploadId=r.uploadId),s.details.setAttribute("data-ignore",""),"image"!==a&&s.alt?s.alt.closest(".field")?.remove():Object.hasOwn(r,"image-alt-text")&&s.alt&&(s.alt.value=r["image-alt-text"]),(Object.hasOwn(r,"title")||Object.hasOwn(r,"file"))&&s.title&&(s.title.value=r.title||r.file.name),Object.hasOwn(r,"image-caption")&&s.description&&(s.description.value=r["image-caption"]))),e.draggable="single"!==e.dataset.mode,i.inputs)for(let t of i.inputs){let s=t.closest("[data-field]")??e;window.prefixInput(t,`${r.id??r.uploadId}-`,s)}}}),e.define("imageGroup",{refs:{selectAll:"[data-select-all]",fields:".fields",details:"details",grid:".item-grid"},setup({el:t,refs:s,manyRefs:i,data:r}){if(t.dataset.groupId=r.groupId,s.selectAll){let e=s.selectAll.closest(".field");window.prefixInput(s.selectAll,`select-all-${r.groupId}`,e,!0)}let a=e.create("groupMetadata",{groupId:r.groupId});a?s.fields.append(a):s.details.remove(),s.grid&&(s.grid.dataset.groupId=r.groupId)}}),e.define("groupMetadata",{manyRefs:{inputs:"input,textarea,select"},setup({el:e,refs:t,manyRefs:s,data:i}){t.inputs&&t.inputs.forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${i.groupId}-`,t)}))}}),e.define("restoreNotification",{refs:{details:".details",wrap:".wrap"},setup({el:t,refs:s,manyRefs:i,data:r}){if(s.details){let e=r.bySource.size>1?` across ${r.bySource.size} pages`:"",t=r.pendingUploads.length>1?"uploads":"upload";s.details.textContent=`${r.pendingUploads.length} ${t} can be recovered${e}`}if(!s.wrap)return void console.warn("No wrap element in template");let a=1;for(const[t,i]of r.bySource){let r={index:a,isCurrent:t===window.location.href,src:t,uploads:i};s.wrap.append(e.create("restoreField",r)),a++}}}),e.define("restoreField",{refs:{h3:"h3",a:"h3 a",grid:".item-grid"},async setup({el:e,refs:s,manyRefs:i,data:r}){let a=t.registerField(e,!1,!1,`recovery_${r.index}`);r.isCurrent?(e.open=!0,s.a?.remove(),s.h3&&(s.h3.textContent="From this page:")):s.a&&(s.a.href=r.src,s.a.title="Navigate to page and restore",s.a.textContent=r.src);let o=[...new Set(r.uploads.map((e=>e.group??"preview")))];for(let e of o){let i="preview"===e||t.stores.groups.get(e);if(!i)continue;let o=await t.createGroupElement(e,a),l=o.querySelector(".item-grid"),d=r.uploads.filter((t=>t.group===("preview"===e)?null:e));for(const[e,t]of Object.entries(i.fields??{})){let s=o.querySelector(`input[name*="${e}"]`);s&&(s.value=t)}for(let e of d){let s=await t.createUpload(e.id,t.formatFile(e),a);l.append(s)}s.grid.append(o)}}})}initStores(){const{uploads:e,groups:t}=window.jvbStore.register("uploads",[{storeName:"uploads",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"status",keyPath:"status"},{name:"group",keyPath:"group"},{name:"src",keyPath:"src"}]},{storeName:"groups",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"src",keyPath:"src"}]}]);this.stores={uploads:e,groups:t,ready:[]},this.stores.uploads.subscribe(this.handleStores.bind(this,"uploads")),this.stores.groups.subscribe(this.handleStores.bind(this,"groups")),this.queue.subscribe(((e,t)=>{if(("operation-status"===e||"cancel-operation"===e)&&["image_upload","video_upload","document_upload"].includes(t.type)){let s=[];if(t.data)if(t.data instanceof FormData){const e=this.stores.uploads.formDataToObject(t.data);s=e.upload_ids||[]}else s=t.data.upload_ids||[];if(0===s.length&&t.result&&t.result.upload_ids&&(s=t.result.upload_ids),!s||0===s.length)return void console.warn("[UploadManager] No upload_ids found for operation:",{id:t.id,type:t.type,status:t.status,hasData:!!t.data,hasResult:!!t.result});if("cancel-operation"===e)return this.handleOperationCancelled(s);this.setBulkUpload(s,"status",t.status).then((()=>{console.log(`[UploadManager] Updated ${s.length} uploads to status: ${t.status}`)})),"completed"===t.status&&("process_upload_groups"===t.type?(s.forEach((e=>{this.setBulkUpload([e],"serverProcessed",!0).then((()=>{}))})),t.result&&t.result.created_posts&&console.log("[UploadManager] Created posts:",t.result.created_posts),setTimeout((()=>{s.forEach((e=>{this.removeUpload(e).then((()=>{}))}))}),2e3)):s.forEach((e=>{this.removeUpload(e).then((()=>{}))}))),"failed"!==t.status&&"failed_permanent"!==t.status||console.error("[UploadManager] Operation failed:",{id:t.id,type:t.type,uploadIds:s,error:t.error_message})}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery().then((()=>{})))}initWorker(){this.worker=null,this.workerState={worker:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:3e3,maxConcurrent:3,restartAfterTimeout:!0}}}initElements(){this.selectors={fields:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-wrapper",preview:".preview-wrap",grid:".item-grid.preview",progress:{progress:".file-upload-container .progress",fill:".file-upload-container .progress .fill",details:".file-upload-container .progress .details",icon:".file-upload-container .progress .icon"},selectAll:"[data-select-all]",actions:".selection-actions",count:".selected .info",hidden:'input[type="hidden"]'},groups:{container:".group-display",grid:".item-grid.groups",empty:".empty-group",header:".sidebar .header"},group:{item:".upload-group",actions:".selection-actions",selectAll:'[name="select-all-group"]',count:".group-header .info",fields:"details .fields",grid:".item-grid.group",total:".group-content .group-count"},items:{item:".item.upload",checkbox:'[name="select-item"]',featured:'[name="featured"]',image:"img",details:"details",progress:{progress:".progress",fill:".fill",details:".details",icon:".icon"}}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dropHandler=this.handleDrop.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}async setUpload(e,t){const s={...{id:e,attachment:null,group:null,field:null,src:window.location.href,blob:null,status:"local_processing",operationId:null,fields:{}},...t};return Object.preventExtensions(s),await this.stores.uploads.save(s),s}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls.delete(e))}formatFile(e){return e.blob?new File([e.blob],e.fields.originalName||"file",{type:e.fields.type||e.blob.type,lastModified:e.fields.lastModified||Date.now()}):null}handleClick(e){let t=window.targetCheck(e,this.selectors.fields.dropZone);t&&!e.target.matches("input, button, a")&&t.querySelector(this.selectors.fields.input)?.click();const s=window.targetCheck(e,"[data-action]");s&&this.handleAction(s)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(s).then((()=>{}));break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e).then((()=>{}));break;case"upload":this.queueUploads("uploads/groups",s).then((()=>{}));break;case"restore":this.handleRestoreSelected().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":this.handleClearCache().then((()=>{}))}}handleChange(e){let t=this.getFieldIdFromElement(e.target);if(t)if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);s.length>0&&this.processFiles(t,s).then((()=>{}))}else e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]')||("post_group"===this.fields.get(t).config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e));else{e.target.closest("[data-upload-id], [data-attachment-id]")&&this.queueUploadMeta(e)}}handleGroupMetaChange(e){const t=e.dataset.groupId;if(!t)return;const s=e.name;if(!s)return;const i=e.value,r=s.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");window.debouncer.schedule(`group-meta-${t}-${r}`,(async()=>{const e=this.stores.groups.get(t);e&&(e.fields||(e.fields={}),e.fields[r]=i,await this.setGroup(t,e))}),300)}handleDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.fields.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleDragLeave(e){const t=e.target.closest(this.selectors.fields.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.fields.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleDrop(e){const t=e.target.closest(this.selectors.fields.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover"),t.classList.add("uploading");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const i=this.getFieldIdFromElement(t);i&&(this.processFiles(i,s).then((()=>{this.updateHandlerItems(i)})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t,s=null){let i=new FormData;const r=this.fields.get(t);if(!r)return;let a=this.stores.uploads.filterByIndex({field:t});if(0===a.length)return;const[o,l]=["uploads"===e,"uploads/groups"===e];let d,n,u,p,c;i.append("fieldId",r.id),i.append("content",r.config.content),o&&(i.append("mode",r.config.mode),i.append("field_name",r.config.repeaterPath||r.config.name),i.append("fieldId",r.id),i.append("field_type",r.config.type),i.append("subtype",r.config.subtype),i.append("item_id",r.config.itemID),i.append("destination",r.config.destination),s&&i.append("depends_on",s)),l?({posts:d,uploadMap:n,files:u}=this.collectGroups(t)):o&&({uploadMap:n,files:u}=this.collectUploads(t)),l&&i.append("posts",JSON.stringify(d)),u.forEach((e=>{i.append("files[]",e)})),i.append("upload_ids",JSON.stringify(n)),o?(p=`Uploading ${a.length} file${a.length>1?"s":""} to server...`,c=`Uploading ${a.length} file${a.length>1?"s":""}...`):l&&(p=`Creating ${d.length} ${r.config.content}${d.length>1?"s":""} from uploads...`,c=`Creating ${d.length} post${d.length>1?"s":""}...`),await this.setBulkUpload(a,"status","queued");let h=this.sendToQueue(e,i,p,c);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1),this.notify("groups_uploaded",{fieldId:t,posts:d,content:r.config.content})}return h?(r.operationId=h,await this.setBulkUpload(a,"operationId",h),await this.setBulkUpload(a,"status","uploading"),await this.setBulkGroup(t,"operationId",h),this.fields.set(r.id,r),this.notify("sent-to-queue",{field:r,operation:h})):await this.setBulkUpload(a,"status","failed"),h}async sendToQueue(e,t,s="",i="",r=!1){""===i&&(i=s);const a={endpoint:e,method:"POST",data:t,title:s,popup:i,canMerge:r,sendNow:"uploads/groups"===e,headers:{"X-Action-Nonce":window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(a)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=[],i=[],r=[];const a=this.stores.groups.filterByIndex({field:e}).filter((e=>{const t=this.getGroupUploadsInOrder(e);return t.length>0&&t.some((e=>this.formatFile(e)))}));for(const e of a){const t=this.groups.get(e.id)?.element,a=this.collectGroupFieldsFromDOM(t,e.id),o={groupId:e.id,images:[],fields:a},l=this.getGroupUploadsInOrder(e);for(const t of l){const s=this.formatFile(t);if(s){r.push(s);const a={upload_id:t.id,index:i.length},l=this.uploads.get(t.id),d=l?.element?.querySelector(`input[name="${e.id}_featured"]`);d?.checked&&(o.fields.featured=t.id),o.images.push(a),i.push(t.id)}}o.images.length>0&&s.push(o)}const o=t.filter((e=>!e.group));for(const e of o){const t={groupId:window.generateID("group"),images:[],fields:{}},a=this.formatFile(e);if(a){r.push(a);const s={upload_id:e.id,index:i.length};t.images.push(s),i.push(e.id)}t.images.length>0&&s.push(t)}return{posts:s,uploadMap:i,files:r}}getGroupUploadsInOrder(e){return e.uploads&&0!==e.uploads.length?e.uploads.map((e=>this.stores.uploads.get(e))).filter(Boolean):[]}collectGroupFieldsFromDOM(e,t){if(!e)return{};const s={};return e.querySelectorAll("input, textarea, select").forEach((e=>{const i=e.name.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");["featured","select-all"].some((e=>i.includes(e)))||e.value&&(s[i]=e.value)})),s}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],i=[];for(const e of t){const t=this.formatFile(e);t&&(i.push(t),s.push(e.id))}return{uploadMap:s,files:i}}queueUploadMeta(e){let t=e.target.closest("[data-attachment-id]")?.dataset.attachmentId,s=!1;if(!t&&(t=e.target.closest("[data-upload-id]")?.dataset.uploadId,s=!0,!t))return;if(!this.changes.has(t)){let e={};s?e.uploadId=t:e.attachmentId=t,this.changes.set(t,e)}let i=e.target.closest("[data-field]").dataset.field;this.changes.get(t)[i]=e.target.value,this.scheduleSave()}scheduleSave(){window.debouncer.schedule("upload-meta",(async()=>{if(this.changes.size>0){let e={};for(let[t,s]of this.changes.entries())console.log(t,s),e[t]=s;let t={user:window.auth.getUser(),items:e};await this.sendToQueue("uploads/meta",t,"Uploading Meta","Uploading Meta",!0),this.changes.clear()}}),2e3)}scanFields(e,t=!0,s=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t,s)))}registerField(e,t=!0,s=!0,i=null){const r={element:e,id:i||this.determineFieldId(e),config:this.extractFieldConfig(e,t,s),uploads:new Set,operationId:null,groups:[],ui:window.uiFromSelectors(this.selectors.fields,e),groupUI:window.uiFromSelectors(this.selectors.groups,e)};return this.fields.set(r.id,r),e.dataset.uploader=r.id,this.getSelectionHandler(r.id),"single"!==r.config.type&&this.initSortable(r.id),r.id}extractFieldConfig(e,t,s){const i={autoUpload:t,showMeta:s,destination:e.dataset.destination||"meta",content:this.extractFieldContent(e),mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:this.extractFieldItemId(e)??0,maxFiles:parseInt(e.dataset.maxFiles)??25,subType:e.dataset.subtype??"image",repeaterPath:null},r=e.closest("[data-index]"),a=r?.closest("[data-field][data-repeater-id]");return a&&r&&(i.repeaterPath=`${a.dataset.field}:${r.dataset.index}:${i.name}`),i}extractFieldContent(e){return e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||null}extractFieldItemId(e){return e.dataset.itemId||e.closest("dialog")?.dataset.itemId||null}determineFieldId(e){let t=this.extractFieldContent(e);t=null===t?"":t+"_";let s=this.extractFieldItemId(e);s=null===s?"":s+"_";const i=e.dataset.field||"",r=e.closest("[data-index]"),a=r?.closest("[data-field][data-repeater-id]");return a&&r?`${t}${s}${a.dataset.field}_${r.dataset.index}_${i}`:`${t}${s}${i}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,i){const r=this.fields.get(e);r&&window.showProgress(r.ui.progress,t,s,i)}getWorker(){return this.workerState.worker||"undefined"==typeof OffscreenCanvas||(this.workerState.worker=new Worker("worker.js"),this.workerState.worker.onmessage=e=>this.handleWorkerMessage(e),this.workerState.worker.onerror=e=>this.handleWorkerError(e)),this.workerState.worker}handleWorkerMessage(e){const{id:t,blob:s}=e.data,i=this.workerState.tasks.get(t);i&&(clearTimeout(i.timeoutId),i.resolve(s),this.workerState.tasks.delete(t))}handleWorkerError(e){this.workerState.tasks.forEach((t=>{clearTimeout(t.timeoutId),t.reject(e)})),this.workerState.tasks.clear(),this.restartWorker()}restartWorker(){this.workerState.worker&&(this.workerState.worker.terminate(),this.workerState.worker=null),this.workerState.restart.count++}async processImages(e,t=2200,s=2200){const i=[],r=[...e],a=this.workerState.settings.maxConcurrent,o=async()=>{for(;r.length>0;){const e=r.shift(),a=await this.processImage(e.file,t,s);i.push({uploadId:e.uploadId,blob:a})}};return await Promise.all(Array.from({length:Math.min(a,e.length)},(()=>o()))),i}async processImage(e,t=2200,s=2200,i=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),i)}catch(i){return this.resizeImage(e,t,s)}}withTimeout(e,t){return Promise.race([e,new Promise(((e,s)=>setTimeout((()=>s(new Error("Timeout"))),t)))])}async workerImage(e,t=2200,s=2200){const{settings:i,restart:r}=this.workerState;if(r.count>=r.max)throw new Error("Worker max restarts exceeded");const a=await createImageBitmap(e);let{width:o,height:l}=a;if(o>t||l>s){const e=Math.min(t/o,s/l);o=Math.round(o*e),l=Math.round(l*e)}const d=this.getWorker(),n=crypto.randomUUID();return new Promise(((t,s)=>{const r=setTimeout((()=>{this.workerState.tasks.delete(n),i.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),i.timeout);this.workerState.tasks.set(n,{resolve:t,reject:s,timeoutId:r}),d.postMessage({id:n,imageBitmap:a,width:o,height:l,type:e.type,quality:.9},[a])}))}resizeImage(e,t,s){return new Promise((i=>{const r=new Image;r.onload=()=>{URL.revokeObjectURL(r.src);let{width:a,height:o}=r;if(a>t||o>s){const e=Math.min(t/a,s/o);a=Math.round(a*e),o=Math.round(o*e)}const l=document.createElement("canvas");l.width=a,l.height=o,l.getContext("2d").drawImage(r,0,0,a,o),l.toBlob(i,e.type,.9)},r.src=URL.createObjectURL(e)}))}async processFiles(e,t){let s=this.fields.get(e);if(!s)return;s.groupUI.container&&(s.groupUI.container.hidden=!1);const i=t.length;let r=0;this.updateFieldProgress(e,0,i,"Processing files...");const a=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),i=await this.setUpload(s,{id:s,field:e,status:"local_processing",fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),r=await this.createUpload(s,t,e);return this.uploads.set(s,{element:r,ui:window.uiFromSelectors(this.selectors.items,r)}),await this.addToGroup(s,null),{uploadId:s,upload:i,file:t}}))),o=a.filter((e=>e.file.type.startsWith("image/"))),l=a.filter((e=>!e.file.type.startsWith("image/"))),d=await this.processImages(o.map((e=>({file:e.file,uploadId:e.uploadId}))));for(const{uploadId:t,blob:s}of d){const a=o.find((e=>e.uploadId===t));a&&(a.upload.blob=s,a.upload.fields.size=s.size,a.upload.status="queued",await this.setUpload(t,a.upload),r++,this.updateFieldProgress(e,r,i,"Processing files..."))}for(const{uploadId:t,upload:s,file:a}of l)s.blob=a,s.status="queued",await this.setUpload(t,s),r++,this.updateFieldProgress(e,r,i,"Processing files...");this.maybeLockUploads(e),s.config.autoUpload&&"post_group"!==s.config.destination&&await this.queueUploads("uploads",e)}async checkRecovery(){const e=this.stores.uploads.filterByIndex({status:["local_processing","queued","uploading"]}),t=Array.from(this.stores.groups.data.values());for(const e of t){this.stores.uploads.filterByIndex({group:e.id}).length>0||await this.stores.groups.delete(e.id)}if(0===e.length)return;const s=new Map;e.forEach((e=>{const t=e.src||"unknown";s.has(t)||s.set(t,[]),s.get(t).push(e)}));let i={bySource:s,pendingUploads:e};document.body.append(this.templates.create("restoreNotification",i));let r=document.querySelector("dialog.restore-uploads");this.restoreModal=new window.jvbModal(r),this.restoreSelection=new window.jvbHandleSelection(r,{wrapper:{wrapper:".restore-field",id:"selection"},items:".item-grid.restore",selectAll:{bulkControls:".selection-actions",checkbox:"#select-all-restore",count:".selection-count"}}),this.restoreModal.handleOpen()}async handleRestoreSelected(){if(!this.restoreSelection)return;let e=Array.from(this.restoreSelection.selectedItems);0!==e.length&&await this.restoreSelectedUploads(e)}async handleRestoreAll(){if(!this.restoreModal)return;const e=Array.from(this.restoreModal.modal.querySelectorAll(".item.upload")).map((e=>e.dataset.uploadId));await this.restoreSelectedUploads(e)}async restoreSelectedUploads(e){let t=window.location.href,s=Array.from(this.stores.uploads.data.values()).filter((s=>e.includes(s.id)&&s.src===t)),i=[...new Set(s.map((e=>e.group)))].filter(Boolean),r=s[0].field;if(!document.querySelector(`[data-uploader="${r}"]`))return void console.log("No field found for "+r);let a=this.fields.get(r);a.groupUI.container&&(a.groupUI.container.hidden=!1);let o=[];for(let e of i){let t=this.stores.groups.get(e);await this.createGroup(r,e);let i=this.groups.get(e),a=s.filter((t=>t.group===e));if(t&&this.groups.has(e)){let e=t.fields;for(const[t,s]of Object.entries(e)){let e=i.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of a){let s=await this.createUpload(t.id,this.formatFile(t),r);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),o.push(t.id)}}let l=s.filter((e=>!o.includes(e.id)));for(let e of l){let t=await this.createUpload(e.id,this.formatFile(e),r);this.uploads.set(e.id,{element:t,ui:window.uiFromSelectors(this.selectors.items,t)}),await this.addToGroup(e.id,null)}this.cleanupRestore()}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}getStatusText(e){return{received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"}[e]||e}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]??0}async createUpload(e,t,s){let i=this.fields.get(s);if(!i)return null;let r={uploadId:e,file:t,field:i};return this.templates.create("uploadItem",r)}getSubtypeFromURL(e){if(!e||""===e)return"";const t=e.split("?")[0].toLowerCase();return[".webp",".jpg",".jpeg",".png",".gif",".svg"].some((e=>t.endsWith(e)))?"image":[".mp4",".ogg",".mov",".webm",".avi"].some((e=>t.endsWith(e)))?"video":"document"}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}async handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId,i=t.dataset.id;if((s||i)&&confirm("Remove this item?")){if(s)await this.removeUpload(s);else{const s=this.getFieldIdFromElement(e);t.remove(),s&&(this.updateHiddenInput(s),this.maybeLockUploads(s))}this.a11y.announce("Item removed")}}updateHiddenInput(e){const t=this.fields.get(e);if(!t?.ui.hidden)return;const s=Array.from(t.ui.grid?.querySelectorAll(this.selectors.items.item)||[]).map((e=>e.dataset.id||e.dataset.uploadId)).filter(Boolean).join(",");t.ui.hidden.value!==s&&(t.ui.hidden.value=s,t.ui.hidden.dispatchEvent(new Event("change",{bubbles:!0})))}async setBulkUpload(e,t,s){const i=Array.from(e).map((async e=>{if("string"==typeof e&&(e=await this.stores.uploads.get(e)),e)return"status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e)}));await Promise.all(i)}async setUploadStatus(e,t){"string"==typeof e&&(e=await this.stores.uploads.get(e)),e&&e.progress&&window.showProgress(e.progress,this.getStatusProgress(t),100,this.getStatusText(t),this.queue.icons[t]??"")}async removeUpload(e){let t=this.stores.uploads.get(e);if(!t)return;const s=t.field;if(t.group){let s=this.stores.groups.get(t.group);s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length?await this.removeGroup(s.id,!1):await this.stores.groups.save(s)}await this.clearUpload(e),this.updateHiddenInput(s),this.maybeLockUploads(s);let i=this.selectionHandlers.get(s);i&&i.deselect(e),this.a11y.announce("Upload removed")}async clearUpload(e){const t=this.uploads.get(e);if(t&&(this.revokePreviewUrl(t.preview),t.element)){const e=t.element.dataset.previewUrl;this.revokePreviewUrl(e),t.element.remove()}this.uploads.delete(e),await this.stores.uploads.delete(e)}async handleAddToGroup(e){const t=this.selected.get(e);if(!t||0===t.size)return;let s=await this.createGroup(e);s&&(await Promise.all(Array.from(t).map((e=>this.addToGroup(e,s)))),this.selectionHandlers.get(e)?.clearSelection(),this.a11y.announce(`Created group with ${t.size} items`))}async createGroup(e,t=null){let s=this.fields.get(e);if(!s)return;t||(t=window.generateID("group"));const i=this.createGroupElement(t,e);if(!i)return null;const r=s.groupUI.empty;r?.nextSibling?s.groupUI.grid.insertBefore(i,r.nextSibling):s.groupUI.grid.append(i);const a=i.querySelector(".item-grid");a&&(a.dataset.groupId=t,this.createSortable(e,a,t));let o=this.stores.groups.data.has(t)?this.stores.groups.data.get(t):{};return await this.setGroup(t,{...o,id:t,field:e}),t}createGroupElement(e,t=null){let s={groupId:e,fieldId:t},i=this.templates.create("imageGroup",s);return this.groups.set(e,{element:i,ui:window.uiFromSelectors(this.selectors.group,i)}),this.getSelectionHandler(t)?.addWrapper(i),i}async setGroup(e,t){const s={...{id:e,src:window.location.href,uploads:[],operationId:null,field:null,fields:{}},...t};Object.preventExtensions(s),await this.stores.groups.save(s)}async setBulkGroup(e,t,s){let i=this.stores.groups.filterByIndex({field:e});if(0===i.length)return;let r=i.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(r)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),i=this.uploads.get(e);if(!s||!i)return;const r=this.fields.get(s.field);if(!r)return;if(null!==i.element?.parentElement&&(!t&&null===s.group||t===s.group))return void this.handleReorder(s.field,t);if(s.group){const t=this.stores.groups.get(s.group);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.removeGroup(t.id,!1):await this.stores.groups.save(t))}i.ui.checkbox&&(i.ui.checkbox.checked=!1);const a=this.selectionHandlers.get(s.field);if(a&&a.isSelected(e)&&a.deselect(e),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),i.ui.featured&&(i.ui.featured.hidden=!t),t){i.ui.featured&&(i.ui.featured.name=`${t}_featured`);let r=this.stores.groups.get(t);r&&(r.uploads.push(e),s.group=t,await this.stores.groups.save(r))}else s.group=null;let o=t?this.groups.get(t)?.ui.grid:r.ui.grid;o&&(o.append(i.element),t&&await this.handleReorder(s.field,t)),await this.stores.uploads.save(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.group.item);if(!t)return;let s=t.dataset.groupId;if(!confirm("Delete this group? Items will be moved back to the upload area."))return;let i=this.stores.uploads.filterByIndex({group:s});Promise.all(i.map((e=>this.addToGroup(e.id,null)))).then((()=>{this.removeGroup(s,!1).then((()=>{})),this.a11y.announce("Group deleted. Items returned to upload area")}))}async removeGroup(e,t=!0){let s=this.groups.get(e),i=this.stores.groups.get(e);if(!i)return;let r=!0;t&&i.uploads.length>0&&(r=window.confirm("Keep uploads in this group?")),await Promise.all(i.uploads.map((e=>r?this.addToGroup(e,null):this.removeUpload(e))));if(this.fields.get(i.field)){const t=this.getGroupKey(i.field,e),r=this.selectionHandlers.get(t);r?.destroy&&r.destroy(),this.selectionHandlers.get(i.field)?.removeWrapper(s.element);const a=this.sortables.get(t);a?.destroy&&a.destroy(),this.sortables.delete(t)}s?.element&&s.element.remove(),this.groups.delete(e),await this.stores.groups.delete(e),this.a11y.announce("Group removed")}maybeLockUploads(e){let t=this.fields.get(e);if(!t||!t.ui.dropZone)return;let s=this.stores.uploads.filterByIndex({field:e}).length,i=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=i}async handleOperationCancelled(e){0!==e.length&&e.forEach((e=>{this.removeUpload(e)}))}getGroupKey(e,t=null){return t?`${e}_${t}`:`${e}`}getSelectionHandler(e){let t=this.getGroupKey(e);if(!this.selectionHandlers.has(t)){let s=this.fields.get(e);if(!s)return;if("post_group"!==s.config.destination)return;let i=new window.jvbHandleSelection(s.element,{selectAll:{checkbox:this.selectors.fields.selectAll,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions},item:{item:this.selectors.items.item,checkbox:this.selectors.items.checkbox,idAttribute:"uploadId"},wrapper:{wrapper:".preview-wrap, .upload-group",id:"groupId"}});i.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems)})),this.selectionHandlers.set(t,i)}return this.selectionHandlers.get(t)}updateHandlerItems(e){let t=this.getSelectionHandler(e);t&&t.collectItems()}initSortable(e){if(!window.Sortable)return;const t=this.fields.get(e);t&&(!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0),this.createSortable(e,t.ui.grid,null),this.initEmptyGroupDropZone(e))}createSortable(e,t,s){if(!t)return null;const i=this.getGroupKey(e,s);if(this.sortables.has(i))return this.sortables.get(i);const r=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},dragClass:"dragging",ignore:".empty-group",onStart:t=>{const s=t.item,i=s?.dataset.uploadId,r=this.selected.get(e);if(i&&(!r||!r.has(i))){const t=this.selectionHandlers.get(e);t&&t.select(i)}},onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(i,r),r}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),e.stopPropagation(),e.dataTransfer.dropEffect="move",s.classList.add("drag-over")})),s.addEventListener("dragleave",(e=>{s.contains(e.relatedTarget)||s.classList.remove("drag-over")})),s.addEventListener("drop",(async t=>{t.preventDefault(),t.stopPropagation(),s.classList.remove("drag-over");const i=this.selected.get(e);if(!i||0===i.size)return;const r=await this.createGroup(e);r&&(await Promise.all(Array.from(i).map((e=>this.addToGroup(e,r)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,i=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===i.length)return;const r=s.dataset.groupId||null;for(const e of i)await this.addToGroup(e,r);await this.handleReorder(t,r),this.selectionHandlers.get(t)?.clearSelection()}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(s){if(t){let e=Array.from(s.children).filter((e=>e.matches(this.selectors.items.item)&&!e.classList.contains("ghost"))).map((e=>e.dataset.uploadId)).filter((e=>e)),i=this.stores.groups.get(t);i&&(i.uploads=e,this.stores.groups.save(i).then((()=>{})))}else this.updateHiddenInput(e);this.a11y.announce("Items reordered")}else console.log("Couldn't Reorder items...")}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.previewUrls.forEach((e=>{this.revokePreviewUrl(e)})),this.previewUrls.clear()}cleanupAllPreviewUrls(){this.previewUrls.forEach((e=>this.revokePreviewUrl(e))),this.previewUrls.clear()}async handleClearCache(){const e=window.location.href,t=this.stores.uploads.filterByIndex({src:e}),s=this.stores.groups.filterByIndex({src:e});await Promise.all([...t.map((e=>this.clearUpload(e.id))),...s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id))))]),this.restoreModal&&this.cleanupRestore(),this.a11y.announce("Cache cleared for this page")}async getFilesForForm(e){const t=e.querySelectorAll(this.selectors.fields.field),s=[];for(const e of t){const t=this.determineFieldId(e),i=e.dataset.field,r=this.stores.uploads.filterByIndex({field:t});for(const e of r){const t=this.formatFile(e);t&&s.push({file:t,fieldName:i,uploadId:e.id,meta:e.fields||{}})}}return s}async clearFieldFromStores(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all(t.map((e=>this.clearUpload(e.id)))),await Promise.all(s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id)))))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUploads=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.queue=window.jvbQueue,this.error=window.jvbError,this.templates=window.jvbTemplates,this.subscribers=new Set,this.initStores(),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.sortables=new Map,this.changes=new Map,this.previewUrls=new Set,this.initElements(),this.initListeners(),this.defineTemplates()}defineTemplates(){const e=this.templates,t=this;e.define("uploadItem",{refs:{select:'[name="select-item"]',featured:'[name="featured"]',img:"img",video:"video",file:"label > span",details:"details",alt:'[name="image-alt-text"]',title:'[name="image-title"]',description:'[name="image-caption"]'},manyRefs:{inputs:"input, select, textarea"},setup({el:e,refs:s,manyRefs:i,data:r}){let a,o,l,d=!1;switch(Object.hasOwn(r,"file")?(e.dataset.uploadId=r.uploadId,a=t.getSubtypeFromMime(r.file.type)||"image",o="document"!==a&&t.createPreviewUrl(r.file),d=o,l=r.file.name||""):(e.dataset.id=r.id,a=t.getSubtypeFromURL(r.medium??r.src),o=r.medium??r.src,l=r["image-alt-text"]??""),e.dataset.subtype=a,s.featured&&(s.featured.value=r.uploadId),a){case"image":s.img&&(s.img.src=o,s.img.alt=l,d&&(s.img.dataset.previewUrl=d)),s.video&&s.video.remove(),s.file&&s.file.remove();break;case"video":s.video&&(s.video.src=o,s.video.alt=l,d&&(s.video.dataset.previewUrl=d)),s.img&&s.img.remove(),s.file&&s.file.remove();break;case"document":if(s.preview){let e=r.file.name.split(".").pop()?.toLowerCase()??"",t={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},i=window.getIcon(t[e]??"file");s.preview.innerText=r.file.name??r.title,s.preview.prepend(i)}s.img&&s.img.remove(),s.video&&s.video.remove()}if(s.details&&(Object.hasOwn(r,"field")&&Object.hasOwn(r.field,"config")&&Object.hasOwn(r.field.config,"showMeta")&&!r.field.config.showMeta?s.details.remove():(Object.hasOwn(r,"id")?s.details.dataset.attachmentId=r.id:Object.hasOwn(r,"uploadId")&&(s.details.dataset.uploadId=r.uploadId),s.details.setAttribute("data-ignore",""),"image"!==a&&s.alt?s.alt.closest(".field")?.remove():Object.hasOwn(r,"image-alt-text")&&s.alt&&(s.alt.value=r["image-alt-text"]),(Object.hasOwn(r,"title")||Object.hasOwn(r,"file"))&&s.title&&(s.title.value=r.title||r.file.name),Object.hasOwn(r,"image-caption")&&s.description&&(s.description.value=r["image-caption"]))),e.draggable="single"!==e.dataset.mode,i.inputs)for(let t of i.inputs){let s=t.closest("[data-field]")??e;window.prefixInput(t,`${r.id??r.uploadId}-`,s)}}}),e.define("imageGroup",{refs:{selectAll:"[data-select-all]",fields:".fields",details:"details",grid:".item-grid"},setup({el:t,refs:s,manyRefs:i,data:r}){if(t.dataset.groupId=r.groupId,s.selectAll){let e=s.selectAll.closest(".field");window.prefixInput(s.selectAll,`select-all-${r.groupId}`,e,!0)}let a=e.create("groupMetadata",{groupId:r.groupId});a?s.fields.append(a):s.details.remove(),s.grid&&(s.grid.dataset.groupId=r.groupId)}}),e.define("groupMetadata",{manyRefs:{inputs:"input,textarea,select"},setup({el:e,refs:t,manyRefs:s,data:i}){t.inputs&&t.inputs.forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${i.groupId}-`,t)}))}}),e.define("restoreNotification",{refs:{details:".details",wrap:".wrap"},setup({el:t,refs:s,manyRefs:i,data:r}){if(s.details){let e=r.bySource.size>1?` across ${r.bySource.size} pages`:"",t=r.pendingUploads.length>1?"uploads":"upload";s.details.textContent=`${r.pendingUploads.length} ${t} can be recovered${e}`}if(!s.wrap)return void console.warn("No wrap element in template");let a=1;for(const[t,i]of r.bySource){let r={index:a,isCurrent:t===window.location.href,src:t,uploads:i};s.wrap.append(e.create("restoreField",r)),a++}}}),e.define("restoreField",{refs:{h3:"h3",a:"h3 a",grid:".item-grid"},async setup({el:e,refs:s,manyRefs:i,data:r}){let a=t.registerField(e,!1,!1,`recovery_${r.index}`);r.isCurrent?(e.open=!0,s.a?.remove(),s.h3&&(s.h3.textContent="From this page:")):s.a&&(s.a.href=r.src,s.a.title="Navigate to page and restore",s.a.textContent=r.src);let o=[...new Set(r.uploads.map((e=>e.group??"preview")))];for(let e of o){let i="preview"===e||t.stores.groups.get(e);if(!i)continue;let o=await t.createGroupElement(e,a),l=o.querySelector(".item-grid"),d=r.uploads.filter((t=>t.group===("preview"===e)?null:e));for(const[e,t]of Object.entries(i.fields??{})){let s=o.querySelector(`input[name*="${e}"]`);s&&(s.value=t)}for(let e of d){let s=await t.createUpload(e.id,t.formatFile(e),a);l.append(s)}s.grid.append(o)}}})}initStores(){const{uploads:e,groups:t}=window.jvbStore.register("uploads",[{storeName:"uploads",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"status",keyPath:"status"},{name:"group",keyPath:"group"},{name:"src",keyPath:"src"}]},{storeName:"groups",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"src",keyPath:"src"}]}]);this.stores={uploads:e,groups:t,ready:[]},this.stores.uploads.subscribe(this.handleStores.bind(this,"uploads")),this.stores.groups.subscribe(this.handleStores.bind(this,"groups")),this.queue.subscribe(((e,t)=>{if(("operation-status"===e||"cancel-operation"===e)&&["image_upload","video_upload","document_upload"].includes(t.type)){let s=[];if(t.data)if(t.data instanceof FormData){const e=this.stores.uploads.formDataToObject(t.data);s=e.upload_ids||[]}else s=t.data.upload_ids||[];if(0===s.length&&t.result&&t.result.upload_ids&&(s=t.result.upload_ids),!s||0===s.length)return void console.warn("[UploadManager] No upload_ids found for operation:",{id:t.id,type:t.type,status:t.status,hasData:!!t.data,hasResult:!!t.result});if("cancel-operation"===e)return this.handleOperationCancelled(s);this.setBulkUpload(s,"status",t.status).then((()=>{console.log(`[UploadManager] Updated ${s.length} uploads to status: ${t.status}`)})),"completed"===t.status&&("process_upload_groups"===t.type?(s.forEach((e=>{this.setBulkUpload([e],"serverProcessed",!0).then((()=>{}))})),t.result&&t.result.created_posts&&console.log("[UploadManager] Created posts:",t.result.created_posts),setTimeout((()=>{s.forEach((e=>{this.removeUpload(e).then((()=>{}))}))}),2e3)):s.forEach((e=>{this.removeUpload(e).then((()=>{}))}))),"failed"!==t.status&&"failed_permanent"!==t.status||console.error("[UploadManager] Operation failed:",{id:t.id,type:t.type,uploadIds:s,error:t.error_message})}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery().then((()=>{})))}initWorker(){this.worker=null,this.workerState={worker:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:3e3,maxConcurrent:3,restartAfterTimeout:!0}}}initElements(){this.selectors={fields:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-wrapper",preview:".preview-wrap",grid:".item-grid.preview",progress:{progress:".file-upload-container .progress",fill:".file-upload-container .progress .fill",details:".file-upload-container .progress .details",icon:".file-upload-container .progress .icon"},selectAll:"[data-select-all]",actions:".selection-actions",count:".selected .info",hidden:'input[type="hidden"]'},groups:{container:".group-display",grid:".item-grid.groups",empty:".empty-group",header:".sidebar .header"},group:{item:".upload-group",actions:".selection-actions",selectAll:'[name="select-all-group"]',count:".group-header .info",fields:"details .fields",grid:".item-grid.group",total:".group-content .group-count"},items:{item:".item.upload",checkbox:'[name="select-item"]',featured:'[name="featured"]',image:"img",details:"details",progress:{progress:".progress",fill:".fill",details:".details",icon:".icon"}}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dropHandler=this.handleDrop.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}async setUpload(e,t){const s={...{id:e,attachment:null,group:null,field:null,src:window.location.href,blob:null,status:"local_processing",operationId:null,fields:{}},...t};return Object.preventExtensions(s),await this.stores.uploads.save(s),s}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls.delete(e))}formatFile(e){return e.blob?new File([e.blob],e.fields.originalName||"file",{type:e.fields.type||e.blob.type,lastModified:e.fields.lastModified||Date.now()}):null}handleClick(e){let t=window.targetCheck(e,this.selectors.fields.dropZone);t&&!e.target.matches("input, button, a")&&t.querySelector(this.selectors.fields.input)?.click();const s=window.targetCheck(e,"[data-action]");s&&this.handleAction(s)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(s).then((()=>{}));break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e).then((()=>{}));break;case"upload":this.queueUploads("uploads/groups",s).then((()=>{}));break;case"restore":this.handleRestoreSelected().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":this.handleClearCache().then((()=>{}))}}handleChange(e){let t=this.getFieldIdFromElement(e.target);if(t)if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);s.length>0&&this.processFiles(t,s).then((()=>{}))}else e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]')||("post_group"===this.fields.get(t).config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e));else{e.target.closest("[data-upload-id], [data-attachment-id]")&&this.queueUploadMeta(e)}}handleGroupMetaChange(e){const t=e.dataset.groupId;if(!t)return;const s=e.name;if(!s)return;const i=e.value,r=s.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");window.debouncer.schedule(`group-meta-${t}-${r}`,(async()=>{const e=this.stores.groups.get(t);e&&(e.fields||(e.fields={}),e.fields[r]=i,await this.setGroup(t,e))}),300)}handleDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.fields.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleDragLeave(e){const t=e.target.closest(this.selectors.fields.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.fields.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleDrop(e){const t=e.target.closest(this.selectors.fields.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover"),t.classList.add("uploading");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const i=this.getFieldIdFromElement(t);i&&(this.processFiles(i,s).then((()=>{this.updateHandlerItems(i)})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t,s=null){let i=new FormData;const r=this.fields.get(t);if(!r)return;let a=this.stores.uploads.filterByIndex({field:t});if(0===a.length)return;const[o,l]=["uploads"===e,"uploads/groups"===e];let d,n,u,p,c;i.append("fieldId",r.id),i.append("content",r.config.content),o&&(i.append("mode",r.config.mode),i.append("field_name",r.config.repeaterPath||r.config.name),i.append("fieldId",r.id),i.append("field_type",r.config.type),i.append("subtype",r.config.subtype),i.append("item_id",r.config.itemID),i.append("destination",r.config.destination),s&&i.append("depends_on",s)),l?({posts:d,uploadMap:n,files:u}=this.collectGroups(t)):o&&({uploadMap:n,files:u}=this.collectUploads(t)),l&&i.append("posts",JSON.stringify(d)),u.forEach((e=>{i.append("files[]",e)})),i.append("upload_ids",JSON.stringify(n)),o?(p=`Uploading ${a.length} file${a.length>1?"s":""} to server...`,c=`Uploading ${a.length} file${a.length>1?"s":""}...`):l&&(p=`Creating ${d.length} ${r.config.content}${d.length>1?"s":""} from uploads...`,c=`Creating ${d.length} post${d.length>1?"s":""}...`),await this.setBulkUpload(a,"status","queued");let h=this.sendToQueue(e,i,p,c);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1),this.notify("groups_uploaded",{fieldId:t,posts:d,content:r.config.content})}return h?(r.operationId=h,await this.setBulkUpload(a,"operationId",h),await this.setBulkUpload(a,"status","uploading"),await this.setBulkGroup(t,"operationId",h),this.fields.set(r.id,r),this.notify("sent-to-queue",{field:r,operation:h})):await this.setBulkUpload(a,"status","failed"),h}async sendToQueue(e,t,s="",i="",r=!1){""===i&&(i=s);const a={endpoint:e,method:"POST",data:t,title:s,popup:i,canMerge:r,sendNow:"uploads/groups"===e,headers:{"X-Action-Nonce":window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(a)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=[],i=[],r=[];const a=this.stores.groups.filterByIndex({field:e}).filter((e=>{const t=this.getGroupUploadsInOrder(e);return t.length>0&&t.some((e=>this.formatFile(e)))}));for(const e of a){const t=this.groups.get(e.id)?.element,a=this.collectGroupFieldsFromDOM(t,e.id),o={groupId:e.id,images:[],fields:a},l=this.getGroupUploadsInOrder(e);for(const t of l){const s=this.formatFile(t);if(s){r.push(s);const a={upload_id:t.id,index:i.length},l=this.uploads.get(t.id),d=l?.element?.querySelector(`input[name="${e.id}_featured"]`);d?.checked&&(o.fields.featured=t.id),o.images.push(a),i.push(t.id)}}o.images.length>0&&s.push(o)}const o=t.filter((e=>!e.group));for(const e of o){const t={groupId:window.generateID("group"),images:[],fields:{}},a=this.formatFile(e);if(a){r.push(a);const s={upload_id:e.id,index:i.length};t.images.push(s),i.push(e.id)}t.images.length>0&&s.push(t)}return{posts:s,uploadMap:i,files:r}}getGroupUploadsInOrder(e){return e.uploads&&0!==e.uploads.length?e.uploads.map((e=>this.stores.uploads.get(e))).filter(Boolean):[]}collectGroupFieldsFromDOM(e,t){if(!e)return{};const s={};return e.querySelectorAll("input, textarea, select").forEach((e=>{const i=e.name.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");["featured","select-all"].some((e=>i.includes(e)))||e.value&&(s[i]=e.value)})),s}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],i=[];for(const e of t){const t=this.formatFile(e);t&&(i.push(t),s.push(e.id))}return{uploadMap:s,files:i}}queueUploadMeta(e){let t=e.target.closest("[data-attachment-id]")?.dataset.attachmentId,s=!1;if(!t&&(t=e.target.closest("[data-upload-id]")?.dataset.uploadId,s=!0,!t))return;if(!this.changes.has(t)){let e={};s?e.uploadId=t:e.attachmentId=t,this.changes.set(t,e)}let i=e.target.closest("[data-field]").dataset.field;this.changes.get(t)[i]=e.target.value,this.scheduleSave()}scheduleSave(){window.debouncer.schedule("upload-meta",(async()=>{if(this.changes.size>0){let e={};for(let[t,s]of this.changes.entries())console.log(t,s),e[t]=s;let t={user:window.auth.getUser(),items:e};await this.sendToQueue("uploads/meta",t,"Uploading Meta","Uploading Meta",!0),this.changes.clear()}}),2e3)}scanFields(e,t=!0,s=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t,s)))}registerField(e,t=!0,s=!0,i=null){const r={element:e,id:i||this.determineFieldId(e),config:this.extractFieldConfig(e,t,s),uploads:new Set,operationId:null,groups:[],ui:window.uiFromSelectors(this.selectors.fields,e),groupUI:window.uiFromSelectors(this.selectors.groups,e)};return this.fields.set(r.id,r),e.dataset.uploader=r.id,this.getSelectionHandler(r.id),"single"!==r.config.type&&this.initSortable(r.id),r.id}extractFieldConfig(e,t,s){const i={autoUpload:t,showMeta:s,destination:e.dataset.destination||"meta",content:this.extractFieldContent(e),mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:this.extractFieldItemId(e)??0,maxFiles:parseInt(e.dataset.maxFiles)??25,subType:e.dataset.subtype??"image",repeaterPath:null},r=e.closest("[data-index]"),a=r?.closest("[data-field][data-repeater-id]");return a&&r&&(i.repeaterPath=`${a.dataset.field}:${r.dataset.index}:${i.name}`),i}extractFieldContent(e){return e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||null}extractFieldItemId(e){return e.dataset.itemId||e.closest("dialog")?.dataset.itemId||null}determineFieldId(e){let t=this.extractFieldContent(e);t=null===t?"":t+"_";let s=this.extractFieldItemId(e);s=null===s?"":s+"_";const i=e.dataset.field||"",r=e.closest("[data-index]"),a=r?.closest("[data-field][data-repeater-id]");return a&&r?`${t}${s}${a.dataset.field}_${r.dataset.index}_${i}`:`${t}${s}${i}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,i){const r=this.fields.get(e);r&&window.showProgress(r.ui.progress,t,s,i)}getWorker(){return this.workerState.worker||"undefined"==typeof OffscreenCanvas||(this.workerState.worker=new Worker("worker.js"),this.workerState.worker.onmessage=e=>this.handleWorkerMessage(e),this.workerState.worker.onerror=e=>this.handleWorkerError(e)),this.workerState.worker}handleWorkerMessage(e){const{id:t,blob:s}=e.data,i=this.workerState.tasks.get(t);i&&(clearTimeout(i.timeoutId),i.resolve(s),this.workerState.tasks.delete(t))}handleWorkerError(e){this.workerState.tasks.forEach((t=>{clearTimeout(t.timeoutId),t.reject(e)})),this.workerState.tasks.clear(),this.restartWorker()}restartWorker(){this.workerState.worker&&(this.workerState.worker.terminate(),this.workerState.worker=null),this.workerState.restart.count++}async processImages(e,t=2200,s=2200){const i=[],r=[...e],a=this.workerState.settings.maxConcurrent,o=async()=>{for(;r.length>0;){const e=r.shift(),a=await this.processImage(e.file,t,s);i.push({uploadId:e.uploadId,blob:a})}};return await Promise.all(Array.from({length:Math.min(a,e.length)},(()=>o()))),i}async processImage(e,t=2200,s=2200,i=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),i)}catch(i){return this.resizeImage(e,t,s)}}withTimeout(e,t){return Promise.race([e,new Promise(((e,s)=>setTimeout((()=>s(new Error("Timeout"))),t)))])}async workerImage(e,t=2200,s=2200){const{settings:i,restart:r}=this.workerState;if(r.count>=r.max)throw new Error("Worker max restarts exceeded");const a=await createImageBitmap(e);let{width:o,height:l}=a;if(o>t||l>s){const e=Math.min(t/o,s/l);o=Math.round(o*e),l=Math.round(l*e)}const d=this.getWorker(),n=crypto.randomUUID();return new Promise(((t,s)=>{const r=setTimeout((()=>{this.workerState.tasks.delete(n),i.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),i.timeout);this.workerState.tasks.set(n,{resolve:t,reject:s,timeoutId:r}),d.postMessage({id:n,imageBitmap:a,width:o,height:l,type:e.type,quality:.9},[a])}))}resizeImage(e,t,s){return new Promise((i=>{const r=new Image;r.onload=()=>{URL.revokeObjectURL(r.src);let{width:a,height:o}=r;if(a>t||o>s){const e=Math.min(t/a,s/o);a=Math.round(a*e),o=Math.round(o*e)}const l=document.createElement("canvas");l.width=a,l.height=o,l.getContext("2d").drawImage(r,0,0,a,o),l.toBlob(i,e.type,.9)},r.src=URL.createObjectURL(e)}))}async processFiles(e,t){let s=this.fields.get(e);if(!s)return;s.groupUI.container&&(s.groupUI.container.hidden=!1);const i=t.length;let r=0;this.updateFieldProgress(e,0,i,"Processing files...");const a=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),i=await this.setUpload(s,{id:s,field:e,status:"local_processing",fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),r=await this.createUpload(s,t,e);return this.uploads.set(s,{element:r,ui:window.uiFromSelectors(this.selectors.items,r)}),await this.addToGroup(s,null),{uploadId:s,upload:i,file:t}}))),o=a.filter((e=>e.file.type.startsWith("image/"))),l=a.filter((e=>!e.file.type.startsWith("image/"))),d=await this.processImages(o.map((e=>({file:e.file,uploadId:e.uploadId}))));for(const{uploadId:t,blob:s}of d){const a=o.find((e=>e.uploadId===t));a&&(a.upload.blob=s,a.upload.fields.size=s.size,a.upload.status="queued",await this.setUpload(t,a.upload),r++,this.updateFieldProgress(e,r,i,"Processing files..."))}for(const{uploadId:t,upload:s,file:a}of l)s.blob=a,s.status="queued",await this.setUpload(t,s),r++,this.updateFieldProgress(e,r,i,"Processing files...");this.maybeLockUploads(e),s.config.autoUpload&&"post_group"!==s.config.destination&&await this.queueUploads("uploads",e)}async checkRecovery(){const e=this.stores.uploads.filterByIndex({status:["local_processing","queued","uploading"]}),t=Array.from(this.stores.groups.data.values());for(const e of t){this.stores.uploads.filterByIndex({group:e.id}).length>0||await this.stores.groups.delete(e.id)}if(0===e.length)return;const s=new Map;e.forEach((e=>{const t=e.src||"unknown";s.has(t)||s.set(t,[]),s.get(t).push(e)}));let i={bySource:s,pendingUploads:e};document.body.append(this.templates.create("restoreNotification",i));let r=document.querySelector("dialog.restore-uploads");this.restoreModal=new window.jvbModal(r),this.restoreSelection=new window.jvbHandleSelection(r,{wrapper:{wrapper:".restore-field",id:"selection"},items:".item-grid.restore",selectAll:{bulkControls:".selection-actions",checkbox:"#select-all-restore",count:".selection-count"}}),this.restoreModal.handleOpen()}async handleRestoreSelected(){if(!this.restoreSelection)return;let e=Array.from(this.restoreSelection.selectedItems);0!==e.length&&await this.restoreSelectedUploads(e)}async handleRestoreAll(){if(!this.restoreModal)return;const e=Array.from(this.restoreModal.modal.querySelectorAll(".item.upload")).map((e=>e.dataset.uploadId));await this.restoreSelectedUploads(e)}async restoreSelectedUploads(e){let t=window.location.href,s=Array.from(this.stores.uploads.data.values()).filter((s=>e.includes(s.id)&&s.src===t)),i=[...new Set(s.map((e=>e.group)))].filter(Boolean),r=s[0].field,a=document.querySelector(`[data-uploader="${r}"]`);if(!a){if(!("crudManager"in window)||!r.startsWith(window.crudManager.content))return void console.log("No field found for "+r);{let[e,t,s]=r.split("_");if(!(parseInt(t)>0))return void console.log("No field found for "+r);window.crudManager.openEditModal(t),a=document.querySelector(`[data-uploader="${r}"]`)}}let o=this.fields.get(r);o.groupUI.container&&(o.groupUI.container.hidden=!1);let l=[];for(let e of i){let t=this.stores.groups.get(e);await this.createGroup(r,e);let i=this.groups.get(e),a=s.filter((t=>t.group===e));if(t&&this.groups.has(e)){let e=t.fields;for(const[t,s]of Object.entries(e)){let e=i.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of a){let s=await this.createUpload(t.id,this.formatFile(t),r);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),l.push(t.id)}}let d=s.filter((e=>!l.includes(e.id)));for(let e of d){let t=await this.createUpload(e.id,this.formatFile(e),r);this.uploads.set(e.id,{element:t,ui:window.uiFromSelectors(this.selectors.items,t)}),await this.addToGroup(e.id,null)}this.cleanupRestore()}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}getStatusText(e){return{received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"}[e]||e}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]??0}async createUpload(e,t,s){let i=this.fields.get(s);if(!i)return null;let r={uploadId:e,file:t,field:i};return this.templates.create("uploadItem",r)}getSubtypeFromURL(e){if(!e||""===e)return"";const t=e.split("?")[0].toLowerCase();return[".webp",".jpg",".jpeg",".png",".gif",".svg"].some((e=>t.endsWith(e)))?"image":[".mp4",".ogg",".mov",".webm",".avi"].some((e=>t.endsWith(e)))?"video":"document"}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}async handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId,i=t.dataset.id;if((s||i)&&confirm("Remove this item?")){if(s)await this.removeUpload(s);else{const s=this.getFieldIdFromElement(e);t.remove(),s&&(this.updateHiddenInput(s),this.maybeLockUploads(s))}this.a11y.announce("Item removed")}}updateHiddenInput(e){const t=this.fields.get(e);if(!t?.ui.hidden)return;const s=Array.from(t.ui.grid?.querySelectorAll(this.selectors.items.item)||[]).map((e=>e.dataset.id||e.dataset.uploadId)).filter(Boolean).join(",");t.ui.hidden.value!==s&&(t.ui.hidden.value=s,t.ui.hidden.dispatchEvent(new Event("change",{bubbles:!0})))}async setBulkUpload(e,t,s){const i=Array.from(e).map((async e=>{if("string"==typeof e&&(e=await this.stores.uploads.get(e)),e)return"status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e)}));await Promise.all(i)}async setUploadStatus(e,t){"string"==typeof e&&(e=await this.stores.uploads.get(e)),e&&e.progress&&window.showProgress(e.progress,this.getStatusProgress(t),100,this.getStatusText(t),this.queue.icons[t]??"")}async removeUpload(e){let t=this.stores.uploads.get(e);if(!t)return;const s=t.field;if(t.group){let s=this.stores.groups.get(t.group);s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length?await this.removeGroup(s.id,!1):await this.stores.groups.save(s)}await this.clearUpload(e),this.updateHiddenInput(s),this.maybeLockUploads(s);let i=this.selectionHandlers.get(s);i&&i.deselect(e),this.a11y.announce("Upload removed")}async clearUpload(e){const t=this.uploads.get(e);if(t&&(this.revokePreviewUrl(t.preview),t.element)){const e=t.element.dataset.previewUrl;this.revokePreviewUrl(e),t.element.remove()}this.uploads.delete(e),await this.stores.uploads.delete(e)}async handleAddToGroup(e){const t=this.selected.get(e);if(!t||0===t.size)return;let s=await this.createGroup(e);s&&(await Promise.all(Array.from(t).map((e=>this.addToGroup(e,s)))),this.selectionHandlers.get(e)?.clearSelection(),this.a11y.announce(`Created group with ${t.size} items`))}async createGroup(e,t=null){let s=this.fields.get(e);if(!s)return;t||(t=window.generateID("group"));const i=this.createGroupElement(t,e);if(!i)return null;const r=s.groupUI.empty;r?.nextSibling?s.groupUI.grid.insertBefore(i,r.nextSibling):s.groupUI.grid.append(i);const a=i.querySelector(".item-grid");a&&(a.dataset.groupId=t,this.createSortable(e,a,t));let o=this.stores.groups.data.has(t)?this.stores.groups.data.get(t):{};return await this.setGroup(t,{...o,id:t,field:e}),t}createGroupElement(e,t=null){let s={groupId:e,fieldId:t},i=this.templates.create("imageGroup",s);return this.groups.set(e,{element:i,ui:window.uiFromSelectors(this.selectors.group,i)}),this.getSelectionHandler(t)?.addWrapper(i),i}async setGroup(e,t){const s={...{id:e,src:window.location.href,uploads:[],operationId:null,field:null,fields:{}},...t};Object.preventExtensions(s),await this.stores.groups.save(s)}async setBulkGroup(e,t,s){let i=this.stores.groups.filterByIndex({field:e});if(0===i.length)return;let r=i.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(r)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),i=this.uploads.get(e);if(!s||!i)return;const r=this.fields.get(s.field);if(!r)return;if(null!==i.element?.parentElement&&(!t&&null===s.group||t===s.group))return void this.handleReorder(s.field,t);if(s.group){const t=this.stores.groups.get(s.group);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.removeGroup(t.id,!1):await this.stores.groups.save(t))}i.ui.checkbox&&(i.ui.checkbox.checked=!1);const a=this.selectionHandlers.get(s.field);if(a&&a.isSelected(e)&&a.deselect(e),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),i.ui.featured&&(i.ui.featured.hidden=!t),t){i.ui.featured&&(i.ui.featured.name=`${t}_featured`);let r=this.stores.groups.get(t);r&&(r.uploads.push(e),s.group=t,await this.stores.groups.save(r))}else s.group=null;let o=t?this.groups.get(t)?.ui.grid:r.ui.grid;o&&(o.append(i.element),t&&await this.handleReorder(s.field,t)),await this.stores.uploads.save(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.group.item);if(!t)return;let s=t.dataset.groupId;if(!confirm("Delete this group? Items will be moved back to the upload area."))return;let i=this.stores.uploads.filterByIndex({group:s});Promise.all(i.map((e=>this.addToGroup(e.id,null)))).then((()=>{this.removeGroup(s,!1).then((()=>{})),this.a11y.announce("Group deleted. Items returned to upload area")}))}async removeGroup(e,t=!0){let s=this.groups.get(e),i=this.stores.groups.get(e);if(!i)return;let r=!0;t&&i.uploads.length>0&&(r=window.confirm("Keep uploads in this group?")),await Promise.all(i.uploads.map((e=>r?this.addToGroup(e,null):this.removeUpload(e))));if(this.fields.get(i.field)){const t=this.getGroupKey(i.field,e),r=this.selectionHandlers.get(t);r?.destroy&&r.destroy(),this.selectionHandlers.get(i.field)?.removeWrapper(s.element);const a=this.sortables.get(t);a?.destroy&&a.destroy(),this.sortables.delete(t)}s?.element&&s.element.remove(),this.groups.delete(e),await this.stores.groups.delete(e),this.a11y.announce("Group removed")}maybeLockUploads(e){let t=this.fields.get(e);if(!t||!t.ui.dropZone)return;let s=this.stores.uploads.filterByIndex({field:e}).length,i=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=i}async handleOperationCancelled(e){0!==e.length&&e.forEach((e=>{this.removeUpload(e)}))}getGroupKey(e,t=null){return t?`${e}_${t}`:`${e}`}getSelectionHandler(e){let t=this.getGroupKey(e);if(!this.selectionHandlers.has(t)){let s=this.fields.get(e);if(!s)return;if("post_group"!==s.config.destination)return;let i=new window.jvbHandleSelection(s.element,{selectAll:{checkbox:this.selectors.fields.selectAll,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions},item:{item:this.selectors.items.item,checkbox:this.selectors.items.checkbox,idAttribute:"uploadId"},wrapper:{wrapper:".preview-wrap, .upload-group",id:"groupId"}});i.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems)})),this.selectionHandlers.set(t,i)}return this.selectionHandlers.get(t)}updateHandlerItems(e){let t=this.getSelectionHandler(e);t&&t.collectItems()}initSortable(e){if(!window.Sortable)return;const t=this.fields.get(e);t&&(!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0),this.createSortable(e,t.ui.grid,null),this.initEmptyGroupDropZone(e))}createSortable(e,t,s){if(!t)return null;const i=this.getGroupKey(e,s);if(this.sortables.has(i))return this.sortables.get(i);const r=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},dragClass:"dragging",ignore:".empty-group",onStart:t=>{const s=t.item,i=s?.dataset.uploadId,r=this.selected.get(e);if(i&&(!r||!r.has(i))){const t=this.selectionHandlers.get(e);t&&t.select(i)}},onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(i,r),r}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),e.stopPropagation(),e.dataTransfer.dropEffect="move",s.classList.add("drag-over")})),s.addEventListener("dragleave",(e=>{s.contains(e.relatedTarget)||s.classList.remove("drag-over")})),s.addEventListener("drop",(async t=>{t.preventDefault(),t.stopPropagation(),s.classList.remove("drag-over");const i=this.selected.get(e);if(!i||0===i.size)return;const r=await this.createGroup(e);r&&(await Promise.all(Array.from(i).map((e=>this.addToGroup(e,r)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,i=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===i.length)return;const r=s.dataset.groupId||null;for(const e of i)await this.addToGroup(e,r);await this.handleReorder(t,r),this.selectionHandlers.get(t)?.clearSelection()}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(s){if(t){let e=Array.from(s.children).filter((e=>e.matches(this.selectors.items.item)&&!e.classList.contains("ghost"))).map((e=>e.dataset.uploadId)).filter((e=>e)),i=this.stores.groups.get(t);i&&(i.uploads=e,this.stores.groups.save(i).then((()=>{})))}else this.updateHiddenInput(e);this.a11y.announce("Items reordered")}else console.log("Couldn't Reorder items...")}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.previewUrls.forEach((e=>{this.revokePreviewUrl(e)})),this.previewUrls.clear()}cleanupAllPreviewUrls(){this.previewUrls.forEach((e=>this.revokePreviewUrl(e))),this.previewUrls.clear()}async handleClearCache(){const e=window.location.href,t=this.stores.uploads.filterByIndex({src:e}),s=this.stores.groups.filterByIndex({src:e});await Promise.all([...t.map((e=>this.clearUpload(e.id))),...s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id))))]),this.restoreModal&&this.cleanupRestore(),this.a11y.announce("Cache cleared for this page")}async getFilesForForm(e){const t=e.querySelectorAll(this.selectors.fields.field),s=[];for(const e of t){const t=this.determineFieldId(e),i=e.dataset.field,r=this.stores.uploads.filterByIndex({field:t});for(const e of r){const t=this.formatFile(e);t&&s.push({file:t,fieldName:i,uploadId:e.id,meta:e.fields||{}})}}return s}async clearFieldFromStores(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all(t.map((e=>this.clearUpload(e.id)))),await Promise.all(s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id)))))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUploads=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/utility.min.js b/assets/js/min/utility.min.js
index f54bcde..da00b2d 100644
--- a/assets/js/min/utility.min.js
+++ b/assets/js/min/utility.min.js
@@ -1 +1 @@
-(()=>{window.fade=function(e,t=!0){t?e.style.animation="fadeIn var(--transition-base)":(e.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${e.dataset.id??e.id??e.className.replace(" ","-")}`,(()=>{e.remove()}),500))},window.formatTimeAgo=function(e,t="default"){const n=e instanceof Date?e:new Date(e),i=n-new Date,o=i<0,r=Math.floor(Math.abs(i)/1e3),a=Math.floor(r/60),s=Math.floor(a/60),l=Math.floor(s/24);if(0===a)return"Just now";let c="";if(r<10)c="a moment";else if(r<60)c="less than a minute";else if(a<5)c="a few minutes";else if(s<24)c=0===s?`${a} ${1===a?"minute":"minutes"}`:`about ${s} ${1===s?"hour":"hours"}`;else{if(!(l<7)){if("default"===t)return n.toLocaleDateString();const e={Y:n.getFullYear(),y:String(n.getFullYear()).slice(-2),F:n.toLocaleDateString("en-CA",{month:"long"}),M:n.toLocaleDateString("en-CA",{month:"short"}),m:String(n.getMonth()+1).padStart(2,"0"),n:n.getMonth()+1,d:String(n.getDate()).padStart(2,"0"),j:n.getDate(),D:n.toLocaleDateString("en-CA",{weekday:"short"}),l:n.toLocaleDateString("en-CA",{weekday:"long"}),H:String(n.getHours()).padStart(2,"0"),i:String(n.getMinutes()).padStart(2,"0"),s:String(n.getSeconds()).padStart(2,"0"),h:String(n.getHours()%12||12).padStart(2,"0"),g:n.getHours()%12||12,A:n.getHours()>=12?"PM":"AM",a:n.getHours()>=12?"pm":"am"};return t.replace(/[YyFMmnjDlHishgAa]/g,(t=>e[t]))}if(1===l)return o?"yesterday":"tomorrow";c=`about ${l} days`,c=`${l} ${1===l?"day":"days"}`}return o?`${c} ago`:`in ${c}`},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((e=>{const t=Array.from(e.classList);if(t.length>0){const n=e.content.cloneNode(!0).firstElementChild;t.forEach((e=>{window.templates.has(e)||window.templates.set(e,n)}))}}))},window.getTemplate=function(e){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(e)&&window.templates.get(e).cloneNode(!0)};window.jvbTemplates=new class{constructor(){this.templates=new Map,this.definitions=new Map}registerAll(e=document){e.querySelectorAll("template").forEach((e=>{e.classList.forEach((t=>{this.templates.has(t)||this.templates.set(t,e)}))}))}define(e,t={},n=null){this.definitions.set(e,{refs:t.refs||null,manyRefs:t.manyRefs||null,setup:t.setup||null,context:n})}create(e,t={}){const n=this.templates.get(e);if(!n)return console.warn(`[TemplateRegistry] Template "${e}" not found`),null;const i=n.content.cloneNode(!0).firstElementChild;if(!i)return null;const o=this.definitions.get(e),r=o?.refs?this.#e(i,o.refs):{},a=o?.manyRefs?this.#e(i,o.manyRefs,!1):{};return o?.setup?.({el:i,refs:r,manyRefs:a,data:t}),i}#e(e,t,n=!0){const i={};for(const[o,r]of Object.entries(t)){let t,a=!1;"string"==typeof r?t=r:(t=r.selector,a=!!r.required);const s=n?e.querySelector(t):e.querySelectorAll(t);a&&(n&&!s&&console.warn(`[TemplateRegistry] Required ref "${o}" not found: ${t}`),n||0!==s.length||console.warn(`[TemplateRegistry] Required manyRef "${o}" not found: ${t}`)),i[o]=n?s:Array.from(s)}return i}},document.addEventListener("DOMContentLoaded",(()=>{window.jvbTemplates.registerAll()})),window.icon=null,window.getIcon=function(e,t=""){if(void 0===e)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return t=""!==t&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${t.slice(0,2)}`:"",n.classList.add(`icon-${e}${t}`),n},window.formatNumber=function(e){return e.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(e,t="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:t}).format(e)},window.escapeHtml=function(e){return e?("string"==typeof e||e instanceof String||(e=String(e)),e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.removeChildren=function(e){if(0!==e.children.length)for(;e.firstChild;)e.removeChild(e.firstChild)},window.formatDateRange=function(e,t){const n=new Date(e),i=new Date(t);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-CA",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})}`},window.throttle=function(e,t=300){let n;return function(...i){n||(e.apply(this,i),n=!0,setTimeout((()=>n=!1),t))}},window.chunkIt=async function(e,t,n,i=10){const o=[];for(let t=0;t<e.length;t+=i)o.push(e.slice(t,t+i));for(const e of o){const i=document.createDocumentFragment();e.forEach((e=>{const n=t(e);n&&i.append(n)})),n(i),await new Promise((e=>requestAnimationFrame(e)))}},window.prefixInput=function(e,t,n=null,i=!1,o=!1){if(!e)return void console.warn("prefixInput called with null/undefined input");const r=e.id,a=i?t:`${t}${e.name}`;let s=null;s=n?n.querySelector(`label[for="${r}"]`):e.labels&&e.labels.length>0?e.labels[0]:"LABEL"===e.previousElementSibling?.tagName?e.previousElementSibling:"LABEL"===e.nextElementSibling?.tagName?e.nextElementSibling:e.closest("[data-field]")?.querySelector(`label[for="${r}"]`),s&&(s.htmlFor=a),e.id=a,o&&(e.name=a)},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.sanitizeHtml=function(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML},window.generateID=function(e="jvb"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(e,t,n,i="",o=""){const r=t<n;e.progress&&r&&window.fade(e.progress,!0);const a=n>0?t/n*100:0;e.fill&&(e.fill.style.width=`${a}%`),e.details&&(e.details.textContent=i),e.count&&(e.count.textContent=`${t}/${n}`),e.icon&&(e.icon.className=""===o?"icon":"icon icon-"+o),e.progress&&t===n&&window.fade(e.progress,!1)},window.formatDate=function(e){if(!e)return"";const t=new Date(e),n=new Date,i=Math.floor((n-t)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:t.toLocaleDateString()},window.getPluralContent=function(e){return"artwork"===e?"artwork":e+"s"},window.showToast=function(e,t="success",n={}){window.jvbNotifications.showToast(e,t,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(e){return e instanceof Date&&!isNaN(e)||(e=new Date(e)),window.dateFormatter.format(e)},window.typeText=function(e,t,n=50){return new Promise((i=>{e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval);let o=0;e.textContent="",e._typeInterval=setInterval((()=>{o<t.length?(e.textContent+=t.charAt(o),o++):(clearInterval(e._typeInterval),delete e._typeInterval,i())}),n)}))},window.eraseText=function(e,t=10){return new Promise((n=>{e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval);let i=e.textContent,o=i.length;e._eraseInterval=setInterval((()=>{o>0?(o--,e.textContent=i.substring(0,o)):(clearInterval(e._eraseInterval),delete e._eraseInterval,n())}),t)}))},window.typeLoop=function(e,t,n=50,i=10,o=1e3,r=250){const a=e.id||e.dataset.typeKey||`type-${Date.now()}`;e.dataset.typeKey||(e.dataset.typeKey=a),e._stopTyping&&e._stopTyping();let s=!0;const l=function(){s=!1,e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval),e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval)};return e._stopTyping=l,async function(){for(;s&&(await window.typeText(e,t,n),s)&&(await new Promise((e=>setTimeout(e,o))),s)&&(await window.eraseText(e,i),s);)await new Promise((e=>setTimeout(e,r)))}(),l},window.toCamelCase=function(e){return e.replace(/-([a-z])/g,(function(e){return e[1].toUpperCase()}))},window.targetCheck=function(e,t){return Array.isArray(t)&&(t=t.join(",")),"string"==typeof t&&(e.target.closest(t)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(e,t){if(this.isFunction(e)||this.isFunction(t))throw"Invalid argument. Function given, object expected.";if(this.isFile(e)||this.isFile(t)){const n=this.compareFiles(e,t);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===e?t:e}}if(this.isValue(e)||this.isValue(t)){const n=this.compareValues(e,t);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=t;break;case this.VALUE_DELETED:i=this.getEmptyValue(e);break;case this.VALUE_UPDATED:default:i=t}return{type:n,data:i}}let n={},i=!1;for(let o in e)if(!this.isFunction(e[o])){let r;t&&void 0!==t[o]&&(r=t[o]);const a=this.map(e[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(t)for(let o in t)if(!this.isFunction(t[o])&&(void 0===e||void 0===e[o])){const e=this.map(void 0,t[o]);null!==e&&(e.hasOwnProperty("type")&&e.hasOwnProperty("data")?n[o]=e.data:n[o]=e,i=!0)}return i?n:null},getEmptyValue:function(e){return this.isArray(e)?[]:this.isObject(e)?{}:"number"==typeof e?0:"boolean"!=typeof e&&""},compareValues:function(e,t){return e===t||this.isDate(e)&&this.isDate(t)&&e.getTime()===t.getTime()?this.VALUE_UNCHANGED:void 0===e?this.VALUE_CREATED:void 0===t?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)},isDate:function(e){return"[object Date]"===Object.prototype.toString.call(e)},isObject:function(e){return"[object Object]"===Object.prototype.toString.call(e)},isFile:function(e){return e instanceof File},isValue:function(e){return!this.isObject(e)&&!this.isArray(e)},compareFiles:function(e,t){return!this.isFile(e)&&this.isFile(t)?this.VALUE_CREATED:this.isFile(e)&&!this.isFile(t)?this.VALUE_DELETED:this.isFile(e)&&this.isFile(t)?e.name===t.name&&e.size===t.size&&e.type===t.type&&e.lastModified===t.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(e,t){if(null==e)return t;if(null==t)return e;if(this.isFunction(e)||this.isFunction(t))return t;if(this.isFile(e)||this.isFile(t))return t;if(this.isValue(e)||this.isValue(t)||this.isArray(e)||this.isArray(t))return t;if(this.isObject(e)&&this.isObject(t)){let n={};for(let t in e)this.isFunction(e[t])||(n[t]=e[t]);for(let i in t)this.isFunction(t[i])||(void 0!==e[i]?n[i]=this.merge(e[i],t[i]):n[i]=t[i]);return n}return t}},window.deepMerge=function(e,t){return window.getDifferences.merge(e,t)},window.isInt=function(e){return!isNaN(parseInt(e))&&isFinite(e)},window.isNumeric=function(e){return!isNaN(parseFloat(e))&&isFinite(e)},window.uiFromSelectors=function(e,t=null,n=!1){let i={};for(let[o,r]of Object.entries(e))i[o]="object"==typeof r?window.uiFromSelectors(r,t):t?n?t.querySelectorAll(r):t.querySelector(r):n?document.querySelectorAll(r):document.querySelector(r);return i};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(e,t,n=1e3){this.cancel(e),this.timeouts.set(e,setTimeout((()=>{t(),this.timeouts.delete(e)}),n))}cancel(e){this.timeouts.has(e)&&(clearTimeout(this.timeouts.get(e)),this.timeouts.delete(e))}cleanup(){for(let e of this.timeouts.values())clearTimeout(e);this.timeouts.clear()}};document.body;const e=document.documentElement,t=document.querySelector(".scroll-progress .bar");let n=window.scrollY||e.scrollTop||0,i=-1,o=!1,r=0;function a(){r=Math.max(0,e.scrollHeight-window.innerHeight)}function s(e){if(!t)return;const n=r>0?e/r:0,i=Math.max(0,Math.min(1,n));t.style.transform=`scaleX(${i})`}function l(){const t=window.scrollY||e.scrollTop||0;t>n?i=1:t<n&&(i=-1),n=t,document.body.classList.toggle("scroll-up",i<0&&t>0),s(t),o=!1}window.addEventListener("scroll",(()=>{o||(o=!0,requestAnimationFrame(l))}),{passive:!0}),window.addEventListener("resize",(()=>{window.debouncer.schedule("recalc-max-scroll",(()=>{a(),s(window.scrollY||e.scrollTop||0)}),20)})),a(),s(n),window.decodeHTMLEntities=function(e){return window.decodeHelper||(window.decodeHelper=document.createElement("textarea")),window.decodeHelper.innerHTML=e,window.decodeHelper.value}})();
\ No newline at end of file
+(()=>{window.fade=function(e,t=!0){t?e.style.animation="fadeIn var(--transition-base)":(e.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${e.dataset.id??e.id??e.className.replace(" ","-")}`,(()=>{e.remove()}),500))},window.formatTimeAgo=function(e,t="default"){const n=e instanceof Date?e:new Date(e),i=n-new Date,o=i<0,r=Math.floor(Math.abs(i)/1e3),a=Math.floor(r/60),s=Math.floor(a/60),l=Math.floor(s/24);if(0===a)return"Just now";let c="";if(r<10)c="a moment";else if(r<60)c="less than a minute";else if(a<5)c="a few minutes";else if(s<24)c=0===s?`${a} ${1===a?"minute":"minutes"}`:`about ${s} ${1===s?"hour":"hours"}`;else{if(!(l<7)){if("default"===t)return n.toLocaleDateString();const e={Y:n.getFullYear(),y:String(n.getFullYear()).slice(-2),F:n.toLocaleDateString("en-CA",{month:"long"}),M:n.toLocaleDateString("en-CA",{month:"short"}),m:String(n.getMonth()+1).padStart(2,"0"),n:n.getMonth()+1,d:String(n.getDate()).padStart(2,"0"),j:n.getDate(),D:n.toLocaleDateString("en-CA",{weekday:"short"}),l:n.toLocaleDateString("en-CA",{weekday:"long"}),H:String(n.getHours()).padStart(2,"0"),i:String(n.getMinutes()).padStart(2,"0"),s:String(n.getSeconds()).padStart(2,"0"),h:String(n.getHours()%12||12).padStart(2,"0"),g:n.getHours()%12||12,A:n.getHours()>=12?"PM":"AM",a:n.getHours()>=12?"pm":"am"};return t.replace(/[YyFMmnjDlHishgAa]/g,(t=>e[t]))}if(1===l)return o?"yesterday":"tomorrow";c=`about ${l} days`,c=`${l} ${1===l?"day":"days"}`}return o?`${c} ago`:`in ${c}`},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((e=>{const t=Array.from(e.classList);if(t.length>0){const n=e.content.cloneNode(!0).firstElementChild;t.forEach((e=>{window.templates.has(e)||window.templates.set(e,n)}))}}))},window.getTemplate=function(e){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(e)&&window.templates.get(e).cloneNode(!0)};window.jvbTemplates=new class{constructor(){this.templates=new Map,this.definitions=new Map}registerAll(e=document){e.querySelectorAll("template").forEach((e=>{e.classList.forEach((t=>{this.templates.has(t)||this.templates.set(t,e)}))}))}define(e,t={},n=null){this.definitions.set(e,{refs:t.refs||null,manyRefs:t.manyRefs||null,setup:t.setup||null,context:n})}create(e,t={}){const n=this.templates.get(e);if(!n)return console.warn(`[TemplateRegistry] Template "${e}" not found`),null;const i=n.content.cloneNode(!0).firstElementChild;if(!i)return null;const o=this.definitions.get(e),r=o?.refs?this.#e(i,o.refs):{},a=o?.manyRefs?this.#e(i,o.manyRefs,!1):{};return o?.setup?.({el:i,refs:r,manyRefs:a,data:t}),i}#e(e,t,n=!0){const i={};for(const[o,r]of Object.entries(t)){let t,a=!1;"string"==typeof r?t=r:(t=r.selector,a=!!r.required);const s=n?e.querySelector(t):e.querySelectorAll(t);a&&(n&&!s&&console.warn(`[TemplateRegistry] Required ref "${o}" not found: ${t}`),n||0!==s.length||console.warn(`[TemplateRegistry] Required manyRef "${o}" not found: ${t}`)),i[o]=n?s:Array.from(s)}return i}},document.addEventListener("DOMContentLoaded",(()=>{window.jvbTemplates.registerAll()})),window.icon=null,window.getIcon=function(e,t=""){if(void 0===e)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return t=""!==t&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${t.slice(0,2)}`:"",n.classList.add(`icon-${e}${t}`),n},window.formatNumber=function(e){return e.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(e,t="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:t}).format(e)},window.escapeHtml=function(e){return e?("string"==typeof e||e instanceof String||(e=String(e)),e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.removeChildren=function(e){if(0!==e.children.length)for(;e.firstChild;)e.removeChild(e.firstChild)},window.formatDateRange=function(e,t){const n=new Date(e),i=new Date(t);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-CA",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})}`},window.throttle=function(e,t=300){let n;return function(...i){n||(e.apply(this,i),n=!0,setTimeout((()=>n=!1),t))}},window.chunkIt=async function(e,t,n,i=10){const o=[];for(let t=0;t<e.length;t+=i)o.push(e.slice(t,t+i));for(const e of o){const i=document.createDocumentFragment();e.forEach((e=>{const n=t(e);n&&i.append(n)})),n(i),await new Promise((e=>requestAnimationFrame(e)))}},window.prefixInput=function(e,t,n=null,i=!1,o=!1){if(!e)return void console.warn("prefixInput called with null/undefined input");const r=e.id,a=i?t:`${t}${e.name}`;let s=null;s=n?n.querySelector(`label[for="${r}"]`):e.labels&&e.labels.length>0?e.labels[0]:"LABEL"===e.previousElementSibling?.tagName?e.previousElementSibling:"LABEL"===e.nextElementSibling?.tagName?e.nextElementSibling:e.closest("[data-field]")?.querySelector(`label[for="${r}"]`),s&&(s.htmlFor=a),e.id=a,o&&(e.name=a)},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.sanitizeHtml=function(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML},window.generateID=function(e="jvb"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(e,t,n,i="",o=""){const r=t<n;e.progress&&r&&window.fade(e.progress,!0);const a=n>0?t/n*100:0;e.fill&&(e.fill.style.width=`${a}%`),e.details&&(e.details.textContent=i),e.count&&(e.count.textContent=`${t}/${n}`),e.icon&&(e.icon.className=""===o?"icon":"icon icon-"+o),e.progress&&t===n&&window.fade(e.progress,!1)},window.formatDate=function(e){if(!e)return"";const t=new Date(e),n=new Date,i=Math.floor((n-t)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:t.toLocaleDateString()},window.getPluralContent=function(e){return"artwork"===e?"artwork":e+"s"},window.showToast=function(e,t="success",n={}){window.jvbNotifications.showToast(e,t,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(e){return e instanceof Date&&!isNaN(e)||(e=new Date(e)),window.dateFormatter.format(e)},window.typeText=function(e,t,n=50){return new Promise((i=>{e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval);let o=0;e.textContent="",e._typeInterval=setInterval((()=>{o<t.length?(e.textContent+=t.charAt(o),o++):(clearInterval(e._typeInterval),delete e._typeInterval,i())}),n)}))},window.eraseText=function(e,t=10){return new Promise((n=>{e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval);let i=e.textContent,o=i.length;e._eraseInterval=setInterval((()=>{o>0?(o--,e.textContent=i.substring(0,o)):(clearInterval(e._eraseInterval),delete e._eraseInterval,n())}),t)}))},window.typeLoop=function(e,t,n=50,i=10,o=1e3,r=250){const a=e.id||e.dataset.typeKey||`type-${Date.now()}`;e.dataset.typeKey||(e.dataset.typeKey=a),e._stopTyping&&e._stopTyping();let s=!0;const l=function(){s=!1,e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval),e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval)};return e._stopTyping=l,async function(){for(;s&&(await window.typeText(e,t,n),s)&&(await new Promise((e=>setTimeout(e,o))),s)&&(await window.eraseText(e,i),s);)await new Promise((e=>setTimeout(e,r)))}(),l},window.toCamelCase=function(e){return e.replace(/-([a-z])/g,(function(e){return e[1].toUpperCase()}))},window.targetCheck=function(e,t){return Array.isArray(t)&&(t=t.join(",")),"string"==typeof t&&(e.target.closest(t)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(e,t){if(this.isFunction(e)||this.isFunction(t))throw"Invalid argument. Function given, object expected.";if(this.isFile(e)||this.isFile(t)){const n=this.compareFiles(e,t);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===e?t:e}}if(this.isValue(e)||this.isValue(t)){const n=this.compareValues(e,t);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=t;break;case this.VALUE_DELETED:i=this.getEmptyValue(e);break;case this.VALUE_UPDATED:default:i=t}return{type:n,data:i}}let n={},i=!1;for(let o in e)if(!this.isFunction(e[o])){let r;t&&void 0!==t[o]&&(r=t[o]);const a=this.map(e[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(t)for(let o in t)if(!this.isFunction(t[o])&&(void 0===e||void 0===e[o])){const e=this.map(void 0,t[o]);null!==e&&(e.hasOwnProperty("type")&&e.hasOwnProperty("data")?n[o]=e.data:n[o]=e,i=!0)}return i?n:null},getEmptyValue:function(e){return this.isArray(e)?[]:this.isObject(e)?{}:"number"==typeof e?0:"boolean"!=typeof e&&""},compareValues:function(e,t){return e===t||this.isDate(e)&&this.isDate(t)&&e.getTime()===t.getTime()?this.VALUE_UNCHANGED:void 0===e?this.VALUE_CREATED:void 0===t?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)},isDate:function(e){return"[object Date]"===Object.prototype.toString.call(e)},isObject:function(e){return"[object Object]"===Object.prototype.toString.call(e)},isFile:function(e){return e instanceof File},isValue:function(e){return!this.isObject(e)&&!this.isArray(e)},compareFiles:function(e,t){return!this.isFile(e)&&this.isFile(t)?this.VALUE_CREATED:this.isFile(e)&&!this.isFile(t)?this.VALUE_DELETED:this.isFile(e)&&this.isFile(t)?e.name===t.name&&e.size===t.size&&e.type===t.type&&e.lastModified===t.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(e,t){if(null==e)return t;if(null==t)return e;if(this.isFunction(e)||this.isFunction(t))return t;if(this.isFile(e)||this.isFile(t))return t;if(this.isValue(e)||this.isValue(t)||this.isArray(e)||this.isArray(t))return t;if(this.isObject(e)&&this.isObject(t)){let n={};for(let t in e)this.isFunction(e[t])||(n[t]=e[t]);for(let i in t)this.isFunction(t[i])||(void 0!==e[i]?n[i]=this.merge(e[i],t[i]):n[i]=t[i]);return n}return t}},window.deepMerge=function(e,t){return window.getDifferences.merge(e,t)},window.isInt=function(e){return!isNaN(parseInt(e))&&isFinite(e)},window.isNumeric=function(e){return!isNaN(parseFloat(e))&&isFinite(e)},window.uiFromSelectors=function(e,t=null,n=!1){let i={};for(let[o,r]of Object.entries(e))i[o]="object"==typeof r?window.uiFromSelectors(r,t):t?n?t.querySelectorAll(r):t.querySelector(r):n?document.querySelectorAll(r):document.querySelector(r);return i},window.sleep=async function(e=50){return new Promise((t=>setTimeout(t,e)))};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(e,t,n=1e3){this.cancel(e),this.timeouts.set(e,setTimeout((()=>{t(),this.timeouts.delete(e)}),n))}cancel(e){this.timeouts.has(e)&&(clearTimeout(this.timeouts.get(e)),this.timeouts.delete(e))}cleanup(){for(let e of this.timeouts.values())clearTimeout(e);this.timeouts.clear()}};document.body;const e=document.documentElement,t=document.querySelector(".scroll-progress .bar");let n=window.scrollY||e.scrollTop||0,i=-1,o=!1,r=0;function a(){r=Math.max(0,e.scrollHeight-window.innerHeight)}function s(e){if(!t)return;const n=r>0?e/r:0,i=Math.max(0,Math.min(1,n));t.style.transform=`scaleX(${i})`}function l(){const t=window.scrollY||e.scrollTop||0;t>n?i=1:t<n&&(i=-1),n=t,document.body.classList.toggle("scroll-up",i<0&&t>0),s(t),o=!1}window.addEventListener("scroll",(()=>{o||(o=!0,requestAnimationFrame(l))}),{passive:!0}),window.addEventListener("resize",(()=>{window.debouncer.schedule("recalc-max-scroll",(()=>{a(),s(window.scrollY||e.scrollTop||0)}),20)})),a(),s(n),window.decodeHTMLEntities=function(e){return window.decodeHelper||(window.decodeHelper=document.createElement("textarea")),window.decodeHelper.innerHTML=e,window.decodeHelper.value}})();
\ No newline at end of file
diff --git a/base/_setup.php b/base/_setup.php
index 9e46047..9a9a9ba 100644
--- a/base/_setup.php
+++ b/base/_setup.php
@@ -8,6 +8,7 @@
require(JVB_DIR.'/base/login.php');
require(JVB_DIR.'/base/membership.php');
require(JVB_DIR.'/base/options.php');
+require(JVB_DIR.'/base/seo.php');
$base = apply_filters('jvb_base', 'jvb_');
$base = (str_ends_with($base, '_')) ? $base : $base.'_';
diff --git a/base/seo.php b/base/seo.php
index bd248f9..79c66d8 100644
--- a/base/seo.php
+++ b/base/seo.php
@@ -1,4 +1,286 @@
<?php
+namespace JVBase\base;
+use JVBase\meta\Meta;
+use JVBase\registrar\config\seo\Resolver;
+use JVBase\registrar\Registrar;
+
+class SchemaHelper
+{
+ protected static array $allowedTypes;
+ protected static array $allowedFormats = ['schema', 'archive', 'meta', 'reference'];
+ protected static array $schemas = [];
+ protected static array $metas = [];
+ protected static array $archives = [];
+ protected static array $references = [];
+ public function __construct()
+ {
+ self::$allowedTypes = array_merge(['website', 'organization'], Registrar::getRegistered());
+ }
+ public static function checkType(string $type, string $reference = '[SchemaHelper] Invalid type'):string|false
+ {
+ $type = strtolower($type);
+ if (!in_array($type, self::$allowedTypes)) {
+ error_log($reference.': '.$type);
+ return false;
+ }
+ return $type;
+ }
+ public static function checkFormat(string $format, string $reference = '[SchemaHelper] Invalid format'):string|false
+ {
+ $format = strtolower($format);
+ if (!in_array($format, self::$allowedFormats)) {
+ error_log($reference.': '.$format);
+ return false;
+ }
+ return $format;
+ }
+ public static function getConfig(string $type, string $format):array
+ {
+ $reference = '[SchemaHelper]::getConfig';
+ $type = self::checkType($type, $reference);
+ $format = self::checkFormat($format, $reference);
+ if (!$type || !$format) {
+ return [];
+ }
+ return match($format) {
+ 'schema' => self::schema($type),
+ 'archive' => self::archive($type),
+ 'meta' => self::meta($type),
+ 'reference' => self::reference($type),
+ };
+ }
+ public static function schema(string $type): array
+ {
+ $type = self::checkType($type, '[SchemaHelper]::schema');
+ if (!$type) {
+ return [];
+ }
+
+ if (!array_key_exists($type, self::$schemas)) {
+ self::$schemas[$type] = get_option(BASE.ucfirst($type).'Schema', self::getDefault($type, 'schema'));
+ }
+ return self::$schemas[$type];
+ }
+ public static function meta(string $type): array
+ {
+ $type = self::checkType($type, '[SchemaHelper]::meta');
+ if (!$type) {
+ return [];
+ }
+ if (!array_key_exists($type, self::$metas)) {
+ self::$metas[$type] = get_option(BASE.ucfirst($type).'Meta', self::getDefault($type, 'meta'));
+ }
+ return self::$metas[$type];
+ }
+ public static function archive(string $type): array
+ {
+ $type = self::checkType($type, '[SchemaHelper]::archive');
+ error_log('[SchemaHelper]::archive type: '.print_r($type, true));
+ if (!$type) {
+ return [];
+ }
+
+ if (!array_key_exists($type, self::$archives)) {
+ self::$archives[$type] = get_option(BASE.ucfirst($type).'Archive', self::getDefault($type, 'archive'));
+ }
+ return self::$archives[$type];
+ }
+ public static function reference(string $type): array
+ {
+ $type = self::checkType($type, '[SchemaHelper]::reference');
+ if (!$type) {
+ return [];
+ }
+ if (!array_key_exists($type, self::$references)) {
+ self::$references[$type] = get_option(BASE.ucfirst($type).'Reference', self::getDefault($type, 'reference'));
+ }
+ return self::$references[$type];
+ }
+
+ public static function getDefault(string $type, string $format):array
+ {
+ $reference = '[SchemaHelper]::getDefault';
+ $type = self::checkType($type, $reference);
+ $format = self::checkFormat($format, $reference);
+ if (!$type || !$format) {
+ return [];
+ }
+
+ $defaults = match ($format) {
+ 'schema' => match ($type) {
+ 'website' => [
+ 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebSite',
+ 'name' => get_bloginfo('name'),
+ 'url' => get_home_url(),
+ 'id' => get_home_url() . '#website',
+ 'description' => get_bloginfo('description'),
+ 'inLanguage' => 'en-CA'
+ ],
+ default => []
+ },
+ 'archive' => [
+ 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage',
+ ],
+ default => [],
+ };
+ return apply_filters(BASE.ucfirst($type).ucfirst($format).'Default', $defaults);
+ }
+
+ public static function updateHistory(string $type, string $format, array $newest):bool
+ {
+ $reference = '[SchemaHelper]::updateHistory';
+ $type = self::checkType($type, $reference);
+ $format = self::checkFormat($format, $reference);
+ if (!$type || !$format) {
+ return false;
+ }
+
+ $historyOption = BASE.ucfirst($type).ucfirst($format).'History';
+ $history = get_option($historyOption, []);
+ array_unshift($history, $newest);
+ if (count($history) > 5) {
+ array_pop($history);
+ }
+ return update_option($historyOption, $history);
+ }
+
+ public static function update(string $type, string $format, array $config, ?Meta $meta = null):bool
+ {
+ $reference = '[SchemaHelper]::update';
+ $type = self::checkType($type, $reference);
+ $format = self::checkFormat($format, $reference);
+ if (!$type || !$format) {
+ return false;
+ }
+ $method = 'update'.ucfirst($type);
+ return self::$method($config, $meta);
+ }
+ public static function updateSchema(string $type, array $config):bool
+ {
+ $reference = '[SchemaHelper]::updateSchema';
+ $type = self::checkType($type, $reference);
+ if (!$type) {
+ return false;
+ }
+ if (!class_exists($config['type'])){
+ error_log('[SchemaHelper]::updateSchema Config must be a valid schema type: '.$config['type']);
+ return false;
+ }
+ if (!in_array($type, self::$allowedTypes)) {
+ error_log('[SchemaHelper]::updateSchema Config must have a schema type');
+ }
+
+ return self::updateClassConfig($type, 'schema', $config);
+ }
+
+ public static function updateClassConfig(string $type, string $format, array $config):bool
+ {
+ $reference = '[SchemaHelper]::updateClassConfig';
+ $type = self::checkType($type, $reference);
+ if (!$type) {
+ return false;
+ }
+ if (!class_exists($config['type'])){
+ error_log($reference.' Config must be a valid schema type: '.$config['type']);
+ return false;
+ }
+ if (!in_array($type, self::$allowedTypes)) {
+ error_log($reference.' Config must have a schema type');
+ }
+ //Merge stored config with updates
+ $stored = self::schema($type);
+ $update = array_merge_recursive($stored, $config);
+
+ //Validate Properties
+ $className = $update['type'];
+ unset($update['type']);
+ foreach ($update as $property => $value) {
+ if (!property_exists($className, $property)) {
+ error_log($reference.' invalid property attempted: '.$property.', with value: '.print_r($value, true).' for class: '.$className);
+ unset($update[$property]);
+ }
+ }
+ $update['type'] = $className;
+
+ //Add changes to history (keeps last 5 changes)
+ self::updateHistory($type, $format, $update);
+ self::$schemas[$type] = $update;
+ return update_option(BASE.ucfirst($type).ucfirst($format), $update);
+ }
+
+ public static function updateMeta(string $type, array $config):bool
+ {
+ $type = self::checkType($type, '[SchemaHelper]::updateMeta');
+ $allowed = array_filter($config, function($key) {
+ $allowed = in_array($key, ['name', 'description']);
+ if (!$allowed) {
+ error_log('[SchemaHelper]::updateMeta invalid property attempted: '.$key);
+ }
+ return $allowed;
+ });
+ if (empty($allowed)) {
+ error_log('[SchemaHelper]::updateMeta Name or Description must be set');
+ return false;
+ }
+ $config = array_map('sanitize_text_field', $config);
+ self::updateHistory($type, 'meta', $config);
+ self::$metas[$type] = $config;
+ return update_option(BASE.ucfirst($type).'Meta', $config);
+ }
+
+ public static function updateArchive(string $type, array $config):bool
+ {
+ $reference = '[SchemaHelper]::updateArchive';
+ $type = self::checkType($type, $reference);
+ if (!$type) {
+ return false;
+ }
+ if (!class_exists($config['type'])){
+ error_log('[SchemaHelper]::updateSchema Config must be a valid schema type: '.$config['type']);
+ return false;
+ }
+ if (!in_array($type, self::$allowedTypes)) {
+ error_log('[SchemaHelper]::updateSchema Config must have a schema type');
+ }
+
+ return self::updateClassConfig($type, 'schema', $config);
+ }
+
+ public static function classFromConfig(array $config, ?Meta $meta = null):mixed
+ {
+ if (!array_key_exists('type', $config)) {
+ error_log('[SchemaHelper]::classFromConfig No class defined in config: '.print_r($config, true));
+ return false;
+ }
+ $className = $config['type'];
+ unset($config['type']);
+ $class = new $className();
+
+ foreach ($config as $property => $value) {
+ if (is_array($value)) {
+ $value = self::classFromConfig($value, $meta);
+ }
+ $method = 'set'.ucfirst($property);
+ if (!method_exists($class, $method)) {
+ error_log('[SchemaHelper]::classFromConfig - method: '.$method.' does not exist in class: '.$className);
+ continue;
+ }
+ if (is_string($value) && str_contains($value, '{{')) {
+ $value = Resolver::resolveForSchema($property, $value, $config, $meta);
+ }
+ if (!empty($value)) {
+ $class->$method($value);
+ }
+ }
+ return $class;
+ }
+}
+
+
+
+
+
+
/**
* JVB_SCHEMA: Site-wide schema configuration
*
@@ -9,44 +291,6 @@
* - attribution: Developer/maintainer info
*/
-use JVBase\managers\SEO\SchemaBuilder;
-
-$schema = apply_filters('jvb_schema', []);
-$registry = SchemaBuilder::getInstance();
-$checked = [];
-foreach ($schema as $key => $config) {
-
- if (array_key_exists('type', $config)) {
- $type = $config['type'];
- } elseif ($key === 'website') {
- $type = 'WebSite';
- }
- $exists = !is_null($registry->getTypeDefinition($type));
- if (!$exists) {
-// error_log('[JVB_SCHEMA] No definitions for: '.print_r($type, true));
- continue;
- }
- $allowed = $registry->getFieldsForType($type);
- $filtered = array_filter($config, function ($item) use ($allowed) {
- return in_array($item, $allowed);
- }, ARRAY_FILTER_USE_KEY);
-
- if (empty($filtered)) {
-// error_log('[JVB_SCHEMA] No valid filters for '.$type.'.');
- continue;
- }
- $removed = array_filter($config, function ($item) use ($allowed) {
- return !in_array($item, $allowed);
- }, ARRAY_FILTER_USE_KEY);
-
- if (!empty($removed)) {
-// error_log('[JVB_SCHEMA] Invalid fields detected for '.$type.': '.print_r($removed, true));
- }
- $checked[$key] = $filtered;
-}
-
-define('JVB_SCHEMA', $checked);
-
/**
JVB_CONTENT['artwork'] = [
diff --git a/inc/admin/SEOAdmin.php b/inc/admin/SEOAdmin.php
index 6c01e01..ec664d3 100644
--- a/inc/admin/SEOAdmin.php
+++ b/inc/admin/SEOAdmin.php
@@ -88,7 +88,9 @@
'sponsor',
'containsInPlace',
'containsPlace',
- 'openingHours'
+ 'openingHours',
+ 'id',
+ 'ignore',
];
protected array $hints = [
@@ -114,7 +116,8 @@
protected function setChecks():void
{
$checks = [
- 'website'
+ 'website',
+ 'organization'
];
$this->checks = array_merge($checks, Registrar::getRegistered());
}
@@ -287,14 +290,14 @@
}
public function renderFieldsFor(string $class, array $stored):void
{
- $fields = $this->getFieldsFor($class);
+ $fields = $this->getFieldsForClass($class);
$instance = new $class();
foreach ($fields as $property => $value) {
$this->renderProperty($property, $stored[$property]??null, $instance);
}
}
- public function getFieldsFor(string $class):array
+ public function getFieldsForClass(string $class):array
{
if (!class_exists($class)) {
error_log('Class not found: '.$class);
@@ -331,7 +334,7 @@
}
-
+ $_POST['type'] = $type;
$result = $this->saveFields($action, $type, $_POST);
@@ -344,6 +347,7 @@
public function saveFields(string $action, string $class, array $data):array
{
+ $action = strtolower($action);
if (!in_array($action, $this->checks)) {
error_log('[SEOAdmin]Action is not allowed: '.$action);
return [
@@ -352,39 +356,13 @@
];
}
- $allowed = $this->getFieldsFor($class);
- if (empty($allowed)) {
- return [
- 'jvb_notice' => 'error',
- 'jvb_message' => 'Could not get fields from class'
- ];
- }
-
- $checked = array_filter($data, function ($item) use ($allowed) {
- return array_key_exists($item, $allowed);
- }, ARRAY_FILTER_USE_KEY);
-
- $stored = get_option(BASE.ucfirst($action).'Schema', []);
- $updates = [];
- foreach ($checked as $property => $value) {
- $sanitized = Sanitizer::sanitize($value, $this->buildConfig($property));
- if (!array_key_exists($property, $stored) || $stored[$property] !== $sanitized)
- $updates[$property] = $sanitized;
- }
- if (!empty($updates)) {
- $history = get_option(BASE.ucfirst($action).'SchemaHistory', []);
- array_unshift($history, $stored);
- if (count($history) > 5){
- array_pop($history);
- }
- update_option(BASE.ucfirst($action).'SchemaHistory', $history);
-
- $update = array_merge($stored, $updates);
- update_option(BASE.ucfirst($action).'Schema', $update);
- }
- return [
+ $success = JVB()->schemaHelper()->updateSchema($action, $data);
+ return $success ? [
'jvb_notice' => 'success',
'jvb_message' => 'Saved changes successfully'
+ ] : [
+ 'jvb_notice' => 'error',
+ 'jvb_message' => 'Something went wrong...'
];
}
}
diff --git a/inc/blocks/CustomBlocks.php b/inc/blocks/CustomBlocks.php
index ffb46b8..f0c0f9d 100644
--- a/inc/blocks/CustomBlocks.php
+++ b/inc/blocks/CustomBlocks.php
@@ -18,7 +18,7 @@
{
$this->cache = Cache::for('blocks', WEEK_IN_SECONDS);
$this->cache->connect('post')->connect('taxonomy');
- add_filter('render_block', [$this, 'render'], 990, 3);
+ add_filter('render_block', [$this, 'render'], 900, 3);
add_action('init', [$this, 'registerBlockStyles']);
}
@@ -613,10 +613,14 @@
'main';
if ($content == '') {
- global $post;
+ if(is_singular()) {
+ global $post;
- $block['innerBlocks'] = parse_blocks($post->post_content);
- $result = $this->innerBlocks($block);
+ $block['innerBlocks'] = parse_blocks($post->post_content);
+ $result = $this->innerBlocks($block);
+ }else {
+ $result = '';
+ }
} else {
$result = $this->inside($block, $tag, $content);
}
diff --git a/inc/blocks/SummaryBlock.php b/inc/blocks/SummaryBlock.php
index c236a7c..5538f8a 100644
--- a/inc/blocks/SummaryBlock.php
+++ b/inc/blocks/SummaryBlock.php
@@ -18,6 +18,8 @@
protected string $image;
protected string $header;
protected string $headerExtra;
+ protected bool $isOpen = false;
+ protected string $beforeSummary;
protected string $detailsTitle;
protected array $details;
@@ -25,6 +27,7 @@
{
$this->cache = Cache::for('summary_block', WEEK_IN_SECONDS);
add_action('init', [ $this, 'registerBlock' ]);
+ add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
if (JVB_TESTING) {
$this->cache->flush();
}
@@ -37,6 +40,11 @@
]);
}
+ public function enqueueScripts():void
+ {
+ wp_enqueue_script('jvb-page-nav');
+ }
+
protected function getConfig():string
{
return (is_tax()) ? 'tax' : 'content';
@@ -86,6 +94,11 @@
);
$this->headerExtra = ($headerExtra === '') ? '' : '<div>'.$headerExtra.'</div>';
+ $this->beforeSummary = apply_filters(
+ 'jvbBeforeSummary',
+ '',
+ $this->getType()
+ );
/**
* The HTML string that appears within the <summary> block
*/
@@ -95,6 +108,12 @@
$this->getType()
);
+ $this->isOpen = apply_filters(
+ 'jvbSummaryIsOpen',
+ false,
+ $this->getType()
+ );
+
/**
* The content of the <details> block.
* This should return either an empty array for no information (so no details block)
@@ -139,6 +158,9 @@
<?php endif; ?>
</header>
<?php
+ if (!empty($this->beforeSummary)) {
+ echo $this->beforeSummary;
+ }
}
protected function renderDetails():void
@@ -149,8 +171,9 @@
if (empty($details)) {
return;
}
+ $open = $this->isOpen ? ' open' : '';
?>
- <details class="info">
+ <details class="info"<?=$open?>>
<summary class="row btw"><?= $this->detailsTitle ?></summary>
<?php
foreach ($this->details as $key => $details) {
@@ -173,7 +196,8 @@
if (empty($this->details)) {
return;
}
- echo jvbOnThisPage(array_keys($this->details));
+ $IDs = apply_filters('jvbSummaryOnThisPage', array_keys($this->details), $this->getType());
+ echo jvbOnThisPage($IDs);
}
protected function getType():string
diff --git a/inc/helpers/renderFields.php b/inc/helpers/renderFields.php
index 87577a6..e422ab5 100644
--- a/inc/helpers/renderFields.php
+++ b/inc/helpers/renderFields.php
@@ -354,7 +354,6 @@
}
}
-
function jvbRenderTermList(array|bool|WP_Error $terms, string $label = ''):string {
if (!$terms || is_wp_error($terms) || empty($terms)) {
return '';
diff --git a/inc/helpers/terms.php b/inc/helpers/terms.php
index bf1c00c..e30cc9b 100644
--- a/inc/helpers/terms.php
+++ b/inc/helpers/terms.php
@@ -1,5 +1,7 @@
<?php
+use JVBase\registrar\Registrar;
+
if (!defined('ABSPATH')) {
exit;
}
@@ -41,6 +43,29 @@
return $out;
}
+function jvbMetaTermList(string $value, string $tax, bool $icon = true):string
+{
+ if ($value === '') {
+ return '';
+ }
+ $tax = jvbCheckBase($tax);
+ $terms = array_map('absint', explode(',', $value));
+ $out = [];
+ foreach ($terms as $t) {
+ $term = get_term($t, $tax);
+ if ($term && !is_wp_error($term)) {
+ $url = get_term_link($t, $tax);
+ $out[] = '<li><a href="'.$url.'" title="View more in '.$term->name.'" rel="tag">'.$term->name.'</a></li>';
+ }
+ }
+
+ $registrar = Registrar::getInstance($tax);
+ $icon = ($icon && $registrar) ? $registrar->getIcon() : '';
+ $icon = ($icon === '') ? '' : jvbIcon($icon);
+ $title = $registrar ? '<li class="title">'.$icon.$registrar->getSingular().'</li>' : '';
+ return (!empty($out)) ? '<ul class="term-list '.jvbNoBase($tax).'">'.$title.implode('',$out).'</ul>' : '';
+}
+
/**
* @param int $artistID
* @param string $taxonomy
diff --git a/inc/helpers/ui.php b/inc/helpers/ui.php
index a491e36..6928d43 100644
--- a/inc/helpers/ui.php
+++ b/inc/helpers/ui.php
@@ -297,15 +297,13 @@
?>
<nav id="<?=$id?>" class="on-this-page index">
- <label>Jump to:
- <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false">
- <?= jvbIcon('plus-square') ?>
- </button>
- </label>
+ <button type="button" aria-label="Show Index" title="Show Index" class="toggle main" aria-expanded="false">
+ <span>Jump To:</span><?= jvbIcon('plus-square') ?>
+ </button>
<ul>
- <li>
+ <li id="back-to-top">
<a href="#top" title="Back to Top">
- <?= jvbIcon('caret-circle-up') ?>
+ <?= jvbIcon('caret-circle-up') ?><span>Back to Top</span>
</a>
</li>
<?php
diff --git a/inc/integrations/GoogleMyBusiness.php b/inc/integrations/GoogleMyBusiness.php
index c86d3ab..9e86377 100644
--- a/inc/integrations/GoogleMyBusiness.php
+++ b/inc/integrations/GoogleMyBusiness.php
@@ -319,8 +319,6 @@
} else {
$result = $this->createPost($data);
$meta->set("_{$this->service_name}_item_id", $result['name']);
-
- $meta->save();
}
return [
@@ -374,7 +372,6 @@
} else {
$result = $this->createPost($data);
$meta->set("_{$this->service_name}_item_id", $result['name']);
- $meta->save();
}
return [
@@ -426,7 +423,6 @@
} else {
$result = $this->createPost($data);
$meta->set("_{$this->service_name}_item_id", $result['name']);
- $meta->save();
}
return [
diff --git a/inc/integrations/Square.php b/inc/integrations/Square.php
index 85032f3..cd200a6 100644
--- a/inc/integrations/Square.php
+++ b/inc/integrations/Square.php
@@ -1385,7 +1385,7 @@
*/
protected function getVariationMapping(string $post_type): array
{
- $registrar = Registrar::getInstance($post_type));
+ $registrar = Registrar::getInstance($post_type);
if (!$registrar) {
return [];
}
@@ -1459,7 +1459,7 @@
*/
protected function getFieldMapping(string $post_type): array
{
- $registrar = Registrar::getInstance($post_type));
+ $registrar = Registrar::getInstance($post_type);
if (!$registrar) {
return [];
}
@@ -1896,7 +1896,6 @@
}
$meta->setAll($updates);
- $meta->save();
// Trigger notification to customer if order is ready
if ($state === 'PREPARED') {
@@ -2353,7 +2352,6 @@
// Save all values at once
$meta->setAll($values_to_save);
- $meta->save();
}
/**
@@ -3495,7 +3493,6 @@
'created_at' => current_time('mysql'),
'updated_at' => current_time('mysql')
]);
- $meta->save();
// Index by Square order ID for quick webhook lookups
update_option(BASE . 'square_order_map_' . $order_data['square_order_id'], $order_post_id);
diff --git a/inc/integrations/Umami.php b/inc/integrations/Umami.php
index 3c89b76..048e784 100644
--- a/inc/integrations/Umami.php
+++ b/inc/integrations/Umami.php
@@ -138,14 +138,14 @@
*/
public function renderTrackingScript(): void
{
+ // Skip on local environments
+ if (JVB_TESTING) {
+ return;
+ }
if (!$this->isSetUp() || is_admin()) {
return;
}
- // Skip on local environments
- if (strpos(get_home_url(), JVB_LOCAL) !== false) {
- return;
- }
$script_url = $this->getTrackingScriptUrl();
$website_id = $this->getWebsiteId();
diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index 7ad84df..4051bc6 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -75,7 +75,12 @@
$this->skeleton->setCalendar();
}
- $this->skeleton->setDefaultStatus();
+ if ($this->registrar && $this->registrar->getType() === 'post') {
+ $this->skeleton->setDefaultStatus();
+ } else {
+ $this->skeleton->setStatuses([]);
+ }
+
// Views
$this->skeleton
@@ -154,7 +159,7 @@
* Initialize taxonomies from WordPress config
*/
protected function initTaxonomies(): void {
- $this->taxonomies = $this->registrar->registrar->taxonomies;
+ $this->taxonomies = ($this->registrar->getType() === 'post') ? $this->registrar->registrar->taxonomies : [];
}
/**
diff --git a/inc/managers/Cache.php b/inc/managers/Cache.php
index 6460971..7653b9a 100644
--- a/inc/managers/Cache.php
+++ b/inc/managers/Cache.php
@@ -491,7 +491,7 @@
****************************************************/
public static function onTermChange(int $termId, int $ttId, string $taxonomy): void
{
-// error_log('[Clearing cache for term change: '.$termId.']');
+ error_log('[Clearing cache for term change: '.$termId.']');
self::invalidateItem('taxonomy', $termId);
}
diff --git a/inc/managers/DashboardManager.php b/inc/managers/DashboardManager.php
index 1b5cb1c..a6fea2d 100644
--- a/inc/managers/DashboardManager.php
+++ b/inc/managers/DashboardManager.php
@@ -743,9 +743,12 @@
//Content
//content types
- //Taxonomies
- $availableContent = array_filter($pages, function($page, $key) {
- return !is_numeric($key) && array_key_exists($key, Registrar::getRegistered('post'));
+ $all = array_merge(
+ Registrar::getRegistered('post'),
+ Registrar::getFeatured('is_content', 'term')
+ );
+ $availableContent = array_filter($pages, function($page, $key) use($all) {
+ return !is_numeric($key) && in_array($key, $all) && JVB()->roles()->checkRole($this->user, $key);
}, ARRAY_FILTER_USE_BOTH);
if (!empty ($availableContent)){
$content = $menu->addItem('Your Content', jvbDashIcon('book-bookmark'))
@@ -758,20 +761,25 @@
$item = $content->addItem($page, $registrar->getIcon())
->url($this->baseURL.'/'.$slug);
- $taxonomies = $registrar->registrar->taxonomies;
- if (!empty ($taxonomies)) {
- //TODO: If we add a dedicated 'create item' page, remove this from the empty check
- $itemMenu = $item->submenu($slug);
- foreach ($taxonomies as $s) {
- $taxRegistrar = Registrar::getInstance($s);
- $itemMenu->addItem($taxRegistrar->getPlural(), $taxRegistrar->getIcon())
- ->url($this->baseURL.'/'.$s);
+ if ($registrar->getType() === 'post') {
+ $taxonomies = $registrar->registrar->taxonomies;
+ if (!empty ($taxonomies)) {
+ //TODO: If we add a dedicated 'create item' page, remove this from the empty check
+ $itemMenu = $item->submenu($slug);
+ foreach ($taxonomies as $s) {
+ $taxRegistrar = Registrar::getInstance($s);
+ $itemMenu->addItem($taxRegistrar->getPlural(), $taxRegistrar->getIcon())
+ ->url($this->baseURL.'/'.$s);
+ }
}
}
+
}
}
+ //Taxonomies
+
//Settings
$settings = $menu->addItem('Settings', jvbDashIcon('faders'))
->submenu('settings')
diff --git a/inc/managers/LoginManager.php b/inc/managers/LoginManager.php
index 5187724..88daabe 100644
--- a/inc/managers/LoginManager.php
+++ b/inc/managers/LoginManager.php
@@ -886,9 +886,8 @@
if (!form || !window.jvbForm) return;
window.jvbForm.registerForm(form, {
- autosave: false,
endpoint: '<?= $action ?>',
- formStatus: false,
+ showStatus: false,
cache: false,
});
diff --git a/inc/managers/SEO/render/SchemaOutput.php b/inc/managers/SEO/render/SchemaOutput.php
index 6109e90..909e6bb 100644
--- a/inc/managers/SEO/render/SchemaOutput.php
+++ b/inc/managers/SEO/render/SchemaOutput.php
@@ -7,6 +7,7 @@
use JVBase\managers\SEO\render\Thing\Intangible\OfferCatalog;
use JVBase\managers\SEO\render\Thing\Intangible\Service;
use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness;
+use JVBase\meta\Meta;
use JVBase\registrar\config\seo\Resolver;
use JVBase\registrar\Registrar;
@@ -23,8 +24,13 @@
public function buildSchema():array
{
$schema = [];
- if (!is_front_page()) {
- $schema[] = $this->buildBasicWebsiteSchema();
+
+ $schema[] = $this->buildWebsiteSchema(false);
+ if (is_front_page()) {
+ $test = $this->buildOrganizationSchema();
+ if (!empty($test)) {
+ $schema[] = $test;
+ }
}
if (is_singular($this->types)) {
$type = get_post_type();
@@ -74,7 +80,7 @@
}
if (!empty($schema)) {
- $website = get_option(BASE.'WebsiteSchema');
+ $website = JVB()->schemaHelper()::schema('website');
if (!empty($website)) {
if (JVB_TESTING) {
Cache::for('websiteSchema')->flush();
@@ -97,18 +103,20 @@
protected function websiteSchema():array
{
- $stored = get_option(BASE.'WebsiteSchema', apply_filters(BASE.'WebsiteSchema', []));
+ $stored = JVB()->schemaHelper()::schema('website');
+ $website = JVB()->schemaHelper()::classFromConfig($stored);
- $seo = new WebSite();
- foreach ($stored as $property => $value) {
- $method = 'set'.ucfirst($property);
- $seo->$method($value);
+ if (!$website->getUrl() || empty($website->getUrl())){
+ $website->setUrl(get_home_url());
}
- $seo->setUrl(get_home_url());
- $seo->setName(get_bloginfo('name'));
- $seo->setCreator($this->getCreator());
- return $seo->outputSchema();
+ if (!$website->getName() || empty($website->getName())){
+ $website->setName(get_bloginfo('name'));
+ }
+ if(!$website->getCreator() || empty($website->getCreator())) {
+ $website->setCreator($this->getCreator());
+ }
+ return $website->outputSchema();
}
public function outputSchema():void
@@ -129,24 +137,76 @@
return BreadcrumbManager::getInstance()->toSchema();
}
- public function buildBasicWebsiteSchema():array
+ public function buildWebsiteSchema(bool $full = true):array
{
if (JVB_TESTING){
Cache::for('websiteSchema')->flush();
}
+ $storedWebsite = JVB()->schemaHelper()::schema('website');
+ if (!$full) {
+ return Cache::for('websiteSchema')->remember(
+ 'reference',
+ function () use ($storedWebsite) {
+ $allowed = ['type','name', 'url', 'description', 'inLanguage'];
+ $storedWebsite = array_filter($storedWebsite, function ($key) use ($allowed) {
+ return in_array($key, $allowed);
+ }, ARRAY_FILTER_USE_KEY);
+
+ $website = JVB()->schemaHelper()::classFromConfig($storedWebsite);
+ if (!$website->getName() || empty($website->getName())) {
+ $website->setName(get_bloginfo('name'));
+ }
+ if (!$website->getUrl() || empty($website->getUrl())) {
+ $website->setUrl(get_home_url());
+ }
+ if (!$website->getId() || empty($website->getId())) {
+ $website->setId(get_home_url().'/#website');
+ }
+ if (!$website->getDescription() || empty($website->getDescription())) {
+ $website->setDescription(get_bloginfo('description'));
+ }
+ if (!$website->getInLanguage() || empty($website->getInLanguage())) {
+ $website->setInLanguage('en-CA');
+ }
+ if (!$website->getPublisher() || empty($website->getPublisher())) {
+ $publisher = $this->getOptionSchemaReference('organization');
+ if ($publisher){
+ $website->setPublisher($publisher);
+ }
+ }
+
+ $website->setCreator($this->getCreator(true));
+
+ return $website->outputSchema();
+ }
+ );
+ }
return Cache::for('websiteSchema')->remember(
- 'reference',
- function () {
- $website = new WebSite();
- $website->setName(get_bloginfo('name'));
- $website->setUrl(get_home_url());
- $website->setId(get_home_url().'/#website');
- $website->setDescription(get_bloginfo('description'));
- $website->setInLanguage('en-CA');
- $publisher = $this->getOptionSchemaReference('organization');
- if ($publisher){
- $website->setPublisher($publisher);
+ 'full',
+ function () use ($storedWebsite) {
+ error_log('StoredWebsite: '.print_r($storedWebsite, true));
+ $website = JVB()->schemaHelper()::classFromConfig($storedWebsite);
+ if (!$website->getName() || empty($website->getName())) {
+ $website->setName(get_bloginfo('name'));
+ }
+ if (!$website->getUrl() || empty($website->getUrl())) {
+ $website->setUrl(get_home_url());
+ }
+ if (!$website->getId() || empty($website->getId())) {
+ $website->setId(get_home_url().'/#website');
+ }
+ if (!$website->getDescription() || empty($website->getDescription())) {
+ $website->setDescription(get_bloginfo('description'));
+ }
+ if (!$website->getInLanguage() || empty($website->getInLanguage())) {
+ $website->setInLanguage('en-CA');
+ }
+ if (!$website->getPublisher() || empty($website->getPublisher())) {
+ $publisher = $this->getOptionSchemaReference('organization');
+ if ($publisher){
+ $website->setPublisher($publisher);
+ }
}
$website->setCreator($this->getCreator(true));
@@ -235,8 +295,8 @@
return null;
}
$action = BASE.ucfirst($option).'Schema';
- $stored = get_option($action, apply_filters($action, []));
-// $stored = get_option($action, apply_filters($action, jvbDefaultSchema($option)));
+ $stored = JVB()->schemaHelper()::reference($action);
+
if (empty($stored)){
error_log('Attempted to get schema reference for: '.$option.', but defaults not set.');
return null;
@@ -255,9 +315,19 @@
$value = array_key_exists($property, $stored) ? $stored[$property] : null;
if (!$value) {continue;}
- $class->$method(Resolver::resolve($property, $value));
+ if (str_contains($value, '{{')) {
+ $value = Resolver::resolve($property, $value);
+ }
+ $class->$method($value);
}
return $class;
}
+
+ public function buildOrganizationSchema():array
+ {
+ $config = JVB()->schemaHelper()::schema('organization');
+ $class = JVB()->schemaHelper()::classFromConfig($config);
+ return ($class)? $class->outputSchema() : [];
+ }
}
diff --git a/inc/managers/SEO/render/Traits/ThingSchema.php b/inc/managers/SEO/render/Traits/ThingSchema.php
index cfebbf6..c58861f 100644
--- a/inc/managers/SEO/render/Traits/ThingSchema.php
+++ b/inc/managers/SEO/render/Traits/ThingSchema.php
@@ -109,7 +109,7 @@
}
public function getId():string {
- return $this->id;
+ return $this->id??false;
}
public function setId(string $id):void
{
diff --git a/inc/managers/queue/executors/ContentExecutor.php b/inc/managers/queue/executors/ContentExecutor.php
index e8b6e72..4fbdba2 100644
--- a/inc/managers/queue/executors/ContentExecutor.php
+++ b/inc/managers/queue/executors/ContentExecutor.php
@@ -32,6 +32,7 @@
if (!in_array($operation->type, self::HANDLED_TYPES)) {
throw new Exception("ContentExecutor cannot handle type: {$operation->type}");
}
+ error_log('Executing ContentExecutor.php');
try {
$data = $operation->requestData;
@@ -74,95 +75,51 @@
);
}
- $results = [];
+ $results = [
+ 'errors' => [],
+ 'success' => [],
+ 'newPosts' => [],
+ 'timelineParents' => [],
+ 'timelineStatus' => [],
+ 'timelineSharedFields' => [],
+ ];
$errors = [];
- $success = [];
- $timelineParents = [];
- $timelineStatus = [];
- $timelineSharedFields = [];
- $newPostsMap = [];
foreach ($posts as $id => $postData) {
try {
- $content = $postData['content'] ?? '';
-
- // New post creation
- if (str_starts_with((string)$id, 'new')) {
- $newId = wp_insert_post([
- 'post_author' => $this->userId,
- 'post_type' => jvbCheckBase($content),
- 'post_title' => $postData['post_title'] ?? '',
- 'post_status' => $postData['status'] ?? 'draft',
- ]);
-
- if (!$newId || is_wp_error($newId)) {
- $errors[$id] = 'Could not create post';
- $progress->failItem($id, 'Could not create post');
- continue;
- }
-
- $newPostsMap[$id] = $newId;
- $this->savePostFields($newId, $postData);
- $success[$newId] = array_keys($postData);
- $progress->advance();
- continue;
- }
-
- // Existing post update
- if (!$this->verifyOwnership((int)$id)) {
- $progress->failItem($id, 'No permission to modify this post');
- $errors[$id] = 'No permission';
- continue;
- }
-
- $this->savePostFields((int)$id, $postData);
+ $content = $postData['content'] ?? false;
+ if (!$content) continue;
$registrar = Registrar::getInstance($content);
- if ($registrar && $registrar->hasFeature('is_timeline')) {
- $post = get_post((int)$id);
- $parentId = $post->post_parent > 0 ? $post->post_parent : $post->ID;
- $fields = $registrar->getFields();
- $sharedFields = array_keys(array_filter($fields, function ($field) {
- return !array_key_exists('for_all', $field) || !$field['for_all'];
- }));
-
- if (array_key_exists('post_date', $postData) && !in_array($parentId, $timelineParents)) {
- $timelineParents[] = $parentId;
- }
- if ($parentId === $id) {
- if (array_key_exists('post_status', $postData) && !array_key_exists($parentId, $timelineStatus)) {
- $timelineStatus[$parentId] = $postData['post_status'];
- }
-
- if (count(array_intersect($sharedFields, array_keys($postData))) > 0) {
- if (!array_key_exists($parentId, $timelineSharedFields)) {
- $timelineSharedFields[$parentId] = [];
- }
- $temp = array_intersect($sharedFields, array_keys($postData));
- $timelineSharedFields[$parentId] = array_unique(array_merge($timelineSharedFields[$parentId], $temp));
- }
- }
+ switch ($registrar->getType()) {
+ case 'post':
+ $results = $this->handlePost($id, $postData, $registrar, $results, $progress);
+ break;
+ case 'term':
+ $results = $this->handleTerm($id, $postData, $registrar, $results, $progress);
+ break;
+ case 'user':
+ $results = $this->handleUser($id, $postData, $registrar, $results, $progress);
+ break;
}
-
- $success[$id] = array_keys($postData);
- $progress->advance();
} catch (Exception $e) {
$progress->failItem($id, $e->getMessage());
- $errors[$id] = $e->getMessage();
+ $results['errors'][$id] = $e->getMessage();
}
}
+ error_log('Final Results: '.print_r($results, true));
try {
- if (!empty($timelineSharedFields)) {
- $this->checkSharedFields($timelineSharedFields);
+ if (!empty($results['timelineSharedFields'])) {
+ $this->checkSharedFields($results['timelineSharedFields']);
}
- if (!empty($timelineStatus)) {
- $this->handleTimelineStatusChange($timelineStatus);
+ if (!empty($results['timelineStatus'])) {
+ $this->handleTimelineStatusChange($results['timelineStatus']);
}
- if (!empty($timelineParents)) {
- $this->maybeReorderTimelines($timelineParents);
+ if (!empty($results['timelineParents'])) {
+ $this->maybeReorderTimelines($results['timelineParents']);
}
} catch (Exception $e) {
- $errors[] = $e->getMessage();
+ $results['errors'][] = $e->getMessage();
}
@@ -183,13 +140,7 @@
return new Result(
outcome: $outcome,
- result: [
- 'posts' => $success,
- 'errors' => $errors,
- 'new_posts' => $newPostsMap,
- 'updated_count' => count($success),
- 'failed_count' => count($errors)
- ]
+ result: $results,
);
}
@@ -202,14 +153,72 @@
return array_key_exists($key, $fields);
}, ARRAY_FILTER_USE_KEY);
+ //Remove values that are already saved
+ $check = Meta::forPost($postId)->getAll(array_keys($allowedFields));
+ error_log('Stored values: '.print_r($check, true));
+ $allowedFields = array_filter($allowedFields, function ($key) use ($allowedFields, $check) {
+ return $allowedFields[$key] !== $check[$key];
+ }, ARRAY_FILTER_USE_KEY);
+
if (empty($allowedFields)) {
return true;
}
return Meta::forPost($postId)
- ->setAll($allowedFields)
- ->save();
+ ->setAll($allowedFields);
}
+ private function saveTermFields(int $termId, array $data): bool
+ {
+ $content = $data['content'] ?? '';
+ error_log('Saving term fields: '.print_r($data, true));
+ $fields = Registrar::getFieldsFor($content);
+
+ $allowedFields = array_filter($data, function ($key) use ($fields) {
+ return array_key_exists($key, $fields);
+ }, ARRAY_FILTER_USE_KEY);
+
+
+ //Remove values that are already saved
+ $check = Meta::forTerm($termId)->getAll(array_keys($allowedFields));
+ error_log('Stored values: '.print_r($check, true));
+ $allowedFields = array_filter($allowedFields, function ($value, $key) use ($check) {
+ error_log('Sent value: '.print_r($value, true));
+ error_log('Stored Value: '.print_r($check[$key], true));
+ return $value !== $check[$key];
+ }, ARRAY_FILTER_USE_BOTH);
+
+ if (empty($allowedFields)) {
+ return true;
+ }
+
+ error_log('Allowed fields: '.print_r($allowedFields, true));
+
+ return Meta::forTerm($termId)
+ ->setAll($allowedFields);
+ }
+ private function saveUserFields(int $userId, array $data): bool
+ {
+ $content = $data['content'] ?? '';
+ $fields = Registrar::getFieldsFor($content);
+
+ $allowedFields = array_filter($data, function ($key) use ($fields) {
+ return array_key_exists($key, $fields);
+ }, ARRAY_FILTER_USE_KEY);
+
+ //Remove values that are already saved
+ $check = Meta::forUser($userId)->getAll(array_keys($allowedFields));
+ $allowedFields = array_filter($allowedFields, function ($key) use ($allowedFields, $check) {
+ return $allowedFields[$key] !== $check[$key];
+ }, ARRAY_FILTER_USE_KEY);
+
+ if (empty($allowedFields)) {
+ return true;
+ }
+
+ return Meta::forUser($userId)
+ ->setAll($allowedFields);
+ }
+
// ─────────────────────────────────────────────────────────────
// Helpers
@@ -328,7 +337,6 @@
if ($lastKey === $index) {
$latestTimestamp = strtotime($post->post_date);
}
- $meta->save();
$previousPost = $post;
}
@@ -449,7 +457,7 @@
}
foreach ($children as $child) {
- Meta::forPost($child)->setAll($values)->save(false);
+ Meta::forPost($child)->setAll($values);
}
}
}
@@ -483,4 +491,115 @@
}
}
}
+
+ protected function handlePost(string|int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array
+ {
+ // New post creation
+ if (str_starts_with((string)$ID, 'new')) {
+
+ $newId = wp_insert_post([
+ 'post_author' => $this->userId,
+ 'post_type' => $registrar->getBased(),
+ 'post_title' => $data['post_title'] ?? apply_filters('jvbDefaultTitle', '', $registrar->getSlug()),
+ 'post_status' => $data['status'] ?? 'draft',
+ ]);
+
+ if (!$newId || is_wp_error($newId)) {
+ $results['errors'][$ID] = 'Could not create post';
+ $progress->failItem($ID, 'Could not create post');
+ return $results;
+ }
+
+ $results['newPosts'][$ID] = $newId;
+ $this->savePostFields($newId, $data);
+ unset($data['content']);
+ $results['success'][$newId] = $data;
+ $progress->advance();
+ return $results;
+ }
+
+ //Existing post update
+ if (!$this->verifyOwnership((int)$ID)) {
+ $progress->failItem($ID, 'No permission to modify this post');
+ $results['errors'][$ID] = 'No permission';
+ return $results;
+ }
+
+ $result = $this->savePostFields((int)$ID, $data);
+ unset($data['content']);
+ if ($result) {
+ $results['success'][$ID] = $data;
+ } else {
+ $results['errors'][$ID] = 'Could not update post data';
+ }
+ if ($registrar && $registrar->hasFeature('is_timeline')) {
+ $post = get_post((int)$ID);
+ $parentId = $post->post_parent > 0 ? $post->post_parent : $post->ID;
+ $fields = $registrar->getFields();
+ $sharedFields = array_keys(array_filter($fields, function ($field) {
+ return !array_key_exists('for_all', $field) || !$field['for_all'];
+ }));
+
+ if (array_key_exists('post_date', $data) && !in_array($parentId, $results['timelineParents'])) {
+ $results['timelineParents'][] = $parentId;
+ }
+ if ($parentId === $ID) {
+ if (array_key_exists('post_status', $data) && !array_key_exists($parentId, $results['timelineStatus'])) {
+ $results['timelineStatus'][$parentId] = $data['post_status'];
+ }
+
+ if (count(array_intersect($sharedFields, array_keys($data))) > 0) {
+ if (!array_key_exists($parentId, $results['timelineSharedFields'])) {
+ $results['timelineSharedFields'][$parentId] = [];
+ }
+ $temp = array_intersect($sharedFields, array_keys($data));
+ $results['timelineSharedFields'][$parentId] = array_unique(array_merge($results['timelineSharedFields'][$parentId], $temp));
+ }
+ }
+ }
+ $progress->advance();
+ return $results;
+ }
+
+
+ protected function handleTerm(int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array
+ {
+ error_log('Handling term '.$ID.' with data: '.print_r($data, true));
+ //Existing term update
+ if ($registrar->hasFeature('is_ownable') && (!JVB()->roles()->isOwner($this->userId, $ID) && !JVB()->roles()->isManager($this->userId, $ID))) {
+ error_log('Term is ownable. User does not own this term.');
+ $progress->failItem($ID, 'No permission to modify this term');
+ $results['errors'][$ID] = 'No permission';
+ return $results;
+ }
+
+ $result = $this->saveTermFields($ID, $data);
+ unset($data['content']);
+ if ($result) {
+ $results['success'][$ID] = $data;
+ } else {
+ $results['errors'][$ID] = 'Could not update term data';
+ }
+ $progress->advance();
+ return $results;
+ }
+ protected function handleUser(int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array
+ {
+ //Existing term update
+ if ($ID !== $this->userId || !user_can($this->userId, 'manage_options')) {
+ $progress->failItem($ID, 'No permission to modify this term');
+ $results['errors'][$ID] = 'No permission';
+ return $results;
+ }
+
+ $result = $this->saveUserFields($ID, $data);
+ unset($data['content']);
+ if ($result) {
+ $results['success'][$ID] = $data;
+ } else {
+ $results['errors'][$ID] = 'Could not update post data';
+ }
+ $progress->advance();
+ return $results;
+ }
}
diff --git a/inc/managers/queue/executors/ContentTermExecutor.php b/inc/managers/queue/executors/ContentTermExecutor.php
index e8f4494..67d1cfe 100644
--- a/inc/managers/queue/executors/ContentTermExecutor.php
+++ b/inc/managers/queue/executors/ContentTermExecutor.php
@@ -34,6 +34,7 @@
public function execute(Operation $operation, Progress $progress): Result
{
+ error_log('Executing Content Term.... ');
// Extract taxonomy from operation type (e.g., "shop_update" -> "shop")
$data= $operation->requestData;
$taxonomy = $data['taxonomy']??false;
@@ -85,13 +86,9 @@
}
// Update metadata
- $meta->setAll($setData);
- $results = $meta->save();
+ $results = $meta->setAll($setData);
if ($results) {
- // Trigger any post-update actions (e.g., thumbnail generation)
- do_action(BASE . "{$taxonomy}_updated", $termID, $userID, $setData);
-
return Result::success([
'updated_fields' => array_keys($setData),
'term_id' => $termID
diff --git a/inc/managers/queue/executors/UploadExecutor.php b/inc/managers/queue/executors/UploadExecutor.php
index 91b4741..8e199d1 100644
--- a/inc/managers/queue/executors/UploadExecutor.php
+++ b/inc/managers/queue/executors/UploadExecutor.php
@@ -502,7 +502,6 @@
error_log('Could not find a gallery upload field for post '.$ID);
}
- $meta->save();
}
@@ -715,7 +714,6 @@
$meta->set($data['field_name'], implode(',', $allIds));
}
- $meta->save();
}
private function updateFieldValue(array $data, array $results): void
@@ -735,7 +733,6 @@
$allIds = array_unique(array_merge($existingIds, $attachmentIds));
$meta->set($data['field_name'], implode(',', $allIds));
- $meta->save();
}
private function getMetaManager(array $data): ?Meta
@@ -798,14 +795,12 @@
} elseif (str_starts_with($mimeType, 'video/')) {
$meta = Meta::forPost($postId);
$meta->set('video', $attachmentId);
- $meta->save();
} else {
$meta = Meta::forPost($postId);
$existing = $meta->get('documents');
$existingIds = !empty($existing) ? explode(',', $existing) : [];
$existingIds[] = $attachmentId;
$meta->set('documents', implode(',', $existingIds));
- $meta->save();
}
}
diff --git a/inc/meta/Field.php b/inc/meta/Field.php
index c6cf372..3c81135 100644
--- a/inc/meta/Field.php
+++ b/inc/meta/Field.php
@@ -17,6 +17,7 @@
public array $config;
public bool $isDirty = false;
public bool $isValid = true;
+ public bool $isDefault = false;
public array $errors = [];
public function __construct(string $name, mixed $value, array $config = [])
@@ -25,6 +26,9 @@
$this->value = $value;
$this->originalValue = $value;
$this->config = $config;
+ if (array_key_exists('wp', $config) && $config['wp'] === true) {
+ $this->isDefault = true;
+ }
}
/**
@@ -32,8 +36,12 @@
*/
public function set(mixed $value): self
{
- $this->value = $value;
- $this->isDirty = ($value !== $this->originalValue);
+ error_log('Checking if value is the same as old value: '.print_r($value, true));
+ if ($value !== $this->value) {
+ error_log('Saving new value: '.print_r($value, true));
+ $this->value = $value;
+ $this->isDirty = true;
+ }
return $this;
}
@@ -98,7 +106,7 @@
*/
public function isWpDefault(): bool
{
- return $this->config['_wp_default'] ?? false;
+ return $this->isDefault ?? false;
}
/**
@@ -106,7 +114,7 @@
*/
public function isTaxonomy(): bool
{
- return $this->type() === 'taxonomy' && !isset($this->config['taxonomy_type']);
+ return ($this->type() === 'taxonomy' || ($this->type() === 'selector' && isset($this->config['subtype']) && $this->config['subtype'] === 'taxonomy')) && !isset($this->config['isReference']);
}
/**
diff --git a/inc/meta/Meta.php b/inc/meta/Meta.php
index 8984345..371a059 100644
--- a/inc/meta/Meta.php
+++ b/inc/meta/Meta.php
@@ -2,49 +2,48 @@
namespace JVBase\meta;
use JVBase\registrar\Registrar;
+use WP_Post;
+use WP_Term;
+use WP_User;
if (!defined('ABSPATH')) {
exit;
}
-/**
- * Main facade for meta operations
- * Fluent API for getting/setting meta values with validation & sanitization
- *
- * Usage:
- * $meta = Meta::forPost($id);
- * $meta->price = 150;
- * $meta->save();
- *
- * Meta::forPost($id)->set('price', 150)->set('style', 'traditional')->save();
- */
class Meta
{
+ /**
+ * @var string post, term, user, or options
+ */
+ protected string $type;
+ /**
+ * @var string the full slug, with BASE
+ */
+ protected string $slug;
+
+ protected string $contentType;
protected Item $item;
protected Storage $storage;
protected Validator $validator;
protected Sanitizer $sanitizer;
+ protected array $fields;
+ protected WP_Post|WP_Term|WP_User|null $wpObject;
+ protected int|string $ID;
protected MetaTypeManager $typeManager;
+ protected static array $instances = ['post' => [],'term' => [], 'user'=>[],'options'=>[]];
- protected bool $autoValidate = true;
- protected bool $autoSanitize = true;
-
- /** @var array<string, callable[]> */
- protected array $onChangeCallbacks = [];
-
- /** @var array<string, callable> */
- protected array $computed = [];
-
- // ─────────────────────────────────────────────────────────────
- // Factory Methods
- // ─────────────────────────────────────────────────────────────
-
+ protected array $defaults = ['post_thumbnail'];
/**
* Create Meta instance for a post
*/
public static function forPost(int $id): self
{
- return new self($id, 'post');
+ if (array_key_exists($id, self::$instances['post'])) {
+ return self::$instances['post'][$id];
+ }
+ $new = new self($id, 'post');
+ self::$instances['post'][$id] = $new;
+ return $new;
}
/**
@@ -52,7 +51,12 @@
*/
public static function forTerm(int $id): self
{
- return new self($id, 'term');
+ if (array_key_exists($id, self::$instances['term'])) {
+ return self::$instances['term'][$id];
+ }
+ $new = new self($id, 'term');
+ self::$instances['term'][$id] = $new;
+ return $new;
}
/**
@@ -60,194 +64,99 @@
*/
public static function forUser(int $id): self
{
- return new self($id, 'user');
+ if (array_key_exists($id, self::$instances['user'])) {
+ return self::$instances['user'][$id];
+ }
+ $new = new self($id, 'user');
+ self::$instances['user'][$id] = $new;
+ return $new;
}
/**
* Create Meta instance for options
*/
- public static function forOptions(?string $baseKey = null): self
+ public static function forOptions(?string $baseKey = 'ajv'): self
{
- $instance = new self($baseKey, 'options');
- $instance->item->baseKey = $baseKey;
- return $instance;
+ if (array_key_exists($baseKey, self::$instances['options'])) {
+ return self::$instances['options'][$baseKey];
+ }
+ $new = new self($baseKey, 'options');
+ self::$instances['options'][$baseKey] = $new;
+ return $new;
}
-
- /**
- * Bulk load multiple posts with optional field preloading
- * @return array<int, Meta>
- */
- public static function bulkForPosts(array $ids, array $preloadFields = []): array
- {
- return self::bulkFor($ids, 'post', $preloadFields);
- }
-
- /**
- * Bulk load multiple terms with optional field preloading
- * @return array<int, Meta>
- */
- public static function bulkForTerms(array $ids, array $preloadFields = []): array
- {
- return self::bulkFor($ids, 'term', $preloadFields);
- }
-
- /**
- * Bulk load multiple users with optional field preloading
- * @return array<int, Meta>
- */
- public static function bulkForUsers(array $ids, array $preloadFields = []): array
- {
- return self::bulkFor($ids, 'user', $preloadFields);
- }
-
- /**
- * Generic bulk loader
- * @return array<int, Meta>
- */
- protected static function bulkFor(array $ids, string $type, array $preloadFields = []): array
- {
- if (empty($ids)) {
- return [];
- }
-
- $metas = [];
-
- // Create instances
- foreach ($ids as $id) {
- $metas[$id] = new self($id, $type);
- }
-
- // Preload fields if specified
- if (!empty($preloadFields)) {
- self::bulkPreload($metas, $type, $preloadFields);
- }
-
- return $metas;
- }
-
- /**
- * Bulk preload fields for multiple Meta instances
- * @param Meta[] $metas
- */
- protected static function bulkPreload(array $metas, string $objectType, array $fields): void
- {
- if (empty($metas) || empty($fields)) {
- return;
- }
-
- $ids = array_keys($metas);
- $values = Storage::getBulkValues($ids, $objectType, $fields);
-
- // Distribute results to Meta instances
- foreach ($values as $id => $fieldValues) {
- if (!isset($metas[$id])) {
- continue;
- }
-
- $meta = $metas[$id];
- foreach ($fieldValues as $name => $value) {
- $config = $meta->config($name) ?? ['type' => 'text'];
- $field = new Field($name, $value, $config);
- $meta->item()->setField($field);
- }
- }
- }
-
- /**
- * Save multiple Meta instances efficiently
- * @param Meta[] $metas
- * @return array<int, bool>
- */
- public static function saveBulk(array $metas, bool $updateTimestamp = true): array
- {
- // Validate all first
- $invalid = [];
- foreach ($metas as $id => $meta) {
- if (!$meta->isValid()) {
- $invalid[$id] = $meta->getErrors();
- }
- }
-
- if (!empty($invalid)) {
- JVB()->error()->log('meta', 'Bulk save has validation errors', [
- 'invalid_items' => $invalid
- ], 'warning');
- }
-
- // Filter to only valid metas
- $validMetas = array_filter($metas, fn($m) => $m->isValid());
-
- // Check overrides before bulk save
- foreach ($validMetas as $meta) {
- foreach ($meta->item()->getDirtyFields() as $field) {
- if ($meta->checkOverrides($field)) {
- $field->markClean();
- }
- }
- }
-
- $results = Storage::saveBulk($validMetas, $updateTimestamp);
-
- // Mark invalid ones as failed
- foreach ($invalid as $id => $errors) {
- $results[$id] = false;
- }
-
- return $results;
- }
-
- // ─────────────────────────────────────────────────────────────
- // Constructor
- // ─────────────────────────────────────────────────────────────
+ /***************************************************************
+ * Constructor
+ ***************************************************************/
public function __construct(int|string|null $id, string $type)
{
- $this->storage = new Storage();
$this->validator = new Validator();
$this->sanitizer = new Sanitizer();
$this->typeManager = new MetaTypeManager();
-
- $this->item = $this->buildItem($id, $type);
+ $this->type = $type;
+ $this->ID = $id;
+ $this->buildData($id, $type);
}
- protected function buildItem(int|string|null $id, string $type): Item
+ protected function buildData(int|string|null $id, string $type):void
{
- $contentType = null;
- $wpObject = null;
+ $this->wpObject = match($type) {
+ 'post' => get_post($id),
+ 'term' => get_term($id),
+ 'user', 'integrations' => get_userdata($id),
+ default => null
+ };
+ $this->slug = match($type) {
+ 'post' => $this->wpObject->post_type,
+ 'term' => $this->wpObject->taxonomy,
+ 'user' => jvbUserRole($id),
+ default => null
+ };
- if ($id && $type !== 'options') {
- [$wpObject, $contentType] = match ($type) {
- 'post' => [get_post($id), jvbNoBase(get_post_type($id))],
- 'term' => [get_term($id), jvbNoBase(get_term($id)->taxonomy)],
- 'user', 'integrations' => [get_user_by('id', $id), jvbUserRole($id)],
- default => [null, null]
- };
- }
- $item = new Item($id, $type, $contentType);
- $item->wpObject = $wpObject;
- $item->fieldConfigs = $this->loadFieldConfigs($contentType, $type);
- // Mark WP defaults in configs
- $defaults = Item::WP_DEFAULTS[$type] ?? [];
- foreach ($defaults as $name) {
- if (!isset($item->fieldConfigs[$name])) {
- $item->fieldConfigs[$name] = ['type' => 'text', '_wp_default' => true];
+ $registrar = Registrar::getInstance($this->slug);
+ $fields = $registrar ? $registrar->getFields() : [];
+ $meta = match($type) {
+ 'post' => get_post_meta($id),
+ 'term' => get_term_meta($id),
+ 'user' => get_user_meta($id),
+ default => []
+ };
+ $meta = array_map(fn($value) => maybe_unserialize($value[0]), $meta);
+
+ foreach ($fields as $fieldName => $config) {
+ $fieldName = jvbNoBase($fieldName);
+ if ($this->wpObject && property_exists($this->wpObject, $fieldName)) {
+ $config['wp'] = true;
+ $value = $this->wpObject->$fieldName;
+ } else if (in_array($fieldName, $this->defaults)) {
+ $config['wp'] = true;
+ switch ($fieldName) {
+ case 'post_thumbnail':
+ $value = get_post_thumbnail_id($this->ID);
+ break;
+ }
} else {
- $item->fieldConfigs[$name]['_wp_default'] = true;
+ $value = array_key_exists(BASE.$fieldName, $meta) ? $meta[BASE.$fieldName] : $config['default']??'';
+ switch ($config['type']) {
+ case 'taxonomy':
+ if (!$config['isReference']??true) {
+ $config['wp'] = true;
+ $value = implode(',', wp_get_post_terms($this->ID, jvbCheckBase($config['taxonomy']), ['fields' => 'ids']));
+ }
+ break;
+ case 'selector':
+ if (array_key_exists('subtype', $config) && $config['subtype'] === 'taxonomy' && !$config['isReference']??true) {
+ $config['wp'] = true;
+ $value = implode(',',wp_get_post_terms($this->ID, jvbCheckBase($config['taxonomy']), ['fields' => 'ids']));
+ }
+ break;
+ }
+
}
+ $this->fields[$fieldName] = new Field($fieldName, $value, $config);
}
-
- return $item;
- }
-
- protected function loadFieldConfigs(?string $contentType, string $objectType): array
- {
- if (!$contentType && $objectType !== 'options') {
- return [];
- }
-
- return Registrar::getFieldsFor($contentType??'options');
}
// ─────────────────────────────────────────────────────────────
@@ -266,7 +175,7 @@
public function __isset(string $name): bool
{
- return $this->item->hasField($name) || isset($this->computed[$name]);
+ return $this->item->hasField($name);
}
// ─────────────────────────────────────────────────────────────
@@ -283,77 +192,36 @@
return $this->getByPath($name);
}
- // Check computed fields first
- if (isset($this->computed[$name])) {
- return ($this->computed[$name])($this);
- }
-
- // Return from loaded field if exists
- if ($field = $this->item->getField($name)) {
- return $field->get();
- }
-
- // Load from storage
- $value = $this->storage->get($this->item, $name);
- $config = $this->item->getFieldConfig($name) ?? ['type' => 'text'];
-
- $field = new Field($name, $value, $config);
- $this->item->setField($field);
-
- return $value;
+ return $this->fields[$name]->get();
}
/**
* Set a field value (validates & sanitizes by default)
*/
- public function set(string $name, mixed $value): self
+ public function set(string $name, mixed $value, $autosave = true): self
{
// Handle repeater subfield path (e.g., "services:2:image")
if (str_contains($name, ':')) {
return $this->setByPath($name, $value);
}
- $config = $this->item->getFieldConfig($name);
-
- if (!$config) {
- // Allow setting unknown fields with minimal config
- $config = ['type' => 'text', 'name' => $name];
+ $field = $this->fields[$name]??false;
+ if (!$field) {
+ error_log('No config found for field: '.$name);
+ return $this;
}
// Validate
- if ($this->autoValidate && !$this->validator->validate($value, $config)) {
- $field = $this->item->getField($name) ?? new Field($name, $value, $config);
+ if (!$this->validator->validate($value, $field->config)) {
$field->addError("Validation failed for {$name}");
- $this->item->setField($field);
return $this;
}
// Sanitize
- if ($this->autoSanitize) {
- $value = $this->sanitizer->sanitize($value, $config);
- }
-
- // Get or create field
- $field = $this->item->getField($name);
- $oldValue = null;
-
- if ($field) {
- $oldValue = $field->value;
- $field->set($value);
- } else {
- // Load original to track dirty state
- $original = $this->storage->get($this->item, $name);
- $oldValue = $original;
- $field = new Field($name, $original, $config);
- $field->set($value);
- $this->item->setField($field);
- }
-
- // Fire change callbacks
- if (isset($this->onChangeCallbacks[$name]) && $oldValue !== $value) {
- foreach ($this->onChangeCallbacks[$name] as $callback) {
- $callback($value, $oldValue, $this);
- }
+ $value = $this->sanitizer->sanitize($value, $field->config);
+ $field->set($value);
+ if ($autosave && $field->isDirty) {
+ $this->save();
}
return $this;
@@ -365,54 +233,123 @@
public function getAll(array $fields = []): array
{
if (empty($fields) || $fields === ['all']) {
- $fields = array_keys($this->item->fieldConfigs);
+ $fields = array_keys($this->fields);
}
+ $fields = array_filter($this->fields, function ($field) use ($fields) {
+ return in_array($field, $fields);
+ }, ARRAY_FILTER_USE_KEY);
- // Load all from storage
- $values = $this->storage->getAll($this->item, $fields);
-
- // Create Field instances
- foreach ($values as $name => $value) {
- if (!$this->item->getField($name)) {
- $config = $this->item->getFieldConfig($name) ?? ['type' => 'text'];
- $this->item->setField(new Field($name, $value, $config));
- }
- }
-
- return $values;
+ return array_map(function ($field) {
+ return $field->value;
+ }, $fields);
}
/**
* Set multiple fields
*/
- public function setAll(array $data): self
+ public function setAll(array $data):bool
{
foreach ($data as $name => $value) {
- $this->set($name, $value);
+ error_log('Setting '.$name.' with value: '.print_r($value, true));
+ $this->set($name, $value, false);
}
- return $this;
+ return $this->save();
}
/**
* Save all dirty fields to database
*/
- public function save(bool $updateTimestamp = true): bool
+ public function save(): bool
{
- if (!$this->item->isValid()) {
- JVB()->error()->log('meta', 'Cannot save: validation errors exist', [
- 'fields' => array_keys($this->item->getInvalidFields())
- ], 'warning');
- return false;
- }
+ $dirtyFields = array_filter($this->fields, function ($field) {
+ return $field->isDirty;
+ });
+ $defaults = array_filter($dirtyFields, function ($field) {
+ return $field->isDefault;
+ });
+ $custom = array_filter($dirtyFields, function($field) {
+ return !$field->isDefault;
+ });
- // Check for field overrides before saving
- foreach ($this->item->getDirtyFields() as $field) {
- if ($this->checkOverrides($field)) {
- $field->markClean();
+ $success = true;
+
+ if (!empty($defaults)) {
+ $result = false;
+ switch ($this->type) {
+ case 'post':
+ //Deal with field exceptions, first, that cannot be set via wp_update_post
+ foreach ($defaults as $fieldName => $field) {
+ switch ($fieldName) {
+ case 'post_thumbnail':
+ $result = set_post_thumbnail($this->ID, $field->value);
+ unset($defaults[$fieldName]);
+ break;
+ }
+ //If it's a taxonomy, we use wp_set_object_terms
+ if (
+ ($field->config['type'] === 'selector' && $field->config['subtype'] === 'taxonomy')
+ || $field->config['type'] === 'taxonomy') {
+
+ $result = wp_set_object_terms($this->ID, array_map('absint', explode(',', $field->value)), jvbCheckBase($field->config['taxonomy']));
+ unset($defaults[$fieldName]);
+ }
+ }
+
+
+
+ if (!empty($defaults)) {
+ error_log('Remaining fields: '.print_r($defaults, true));
+ $defaults = array_map(function ($field) {
+ return $field->value;
+ }, $defaults);
+ error_log('Remaining values to save: '.print_r($defaults, true));
+ $data = array_merge([
+ 'post_type' => $this->slug,
+ 'ID' => $this->ID
+ ], $defaults);
+ error_log('Updating post: '.print_r($data, true));
+ $result = wp_update_post($data);
+ }
+ break;
+ case 'term':
+ $result = wp_update_term($this->ID, $this->slug, $defaults);
+ break;
+ case 'user':
+ $data = array_merge([
+ 'ID' => $this->ID
+ ], $defaults);
+ $result = wp_update_user($data);
+ break;
+ }
+ if (!$result || is_wp_error($result)) {
+ $success = false;
+ }
+ }
+ if (!empty($custom)) {
+ $function = match ($this->type) {
+ 'post' => 'update_post_meta',
+ 'term' => 'update_term_meta',
+ 'user' => 'update_user_meta',
+ 'options', 'option' => 'update_option',
+ default => false,
+ };
+ if (!$function) {
+ error_log('[Meta]::save() Invalid type, cannot save: '.$this->type);
+ return false;
+ }
+ foreach ($custom as $field) {
+ $result = $function($this->ID, BASE.$field->name, $field->value);
+ if (!$result) {
+ error_log('Problem saving field: '.$field->name.' with value: '.print_r($field->value, true));
+ }
+ }
+ if ($this->type === 'term' && Registrar::getInstance($this->slug)->hasFeature('is_content')) {
+ update_term_meta($this->ID, BASE.'date_modified', date('Y-m-d H:i:s'));
}
}
- return $this->storage->save($this->item, $updateTimestamp);
+
+ return $success;
}
/**
@@ -420,16 +357,28 @@
*/
public function delete(string $name): bool
{
- $result = $this->storage->delete($this->item, $name);
-
- if ($result && $field = $this->item->getField($name)) {
- $field->set($this->getDefaultValue($name));
- $field->markClean();
+ $field = $this->fields[$name]??false;
+ if (!$field) {
+ error_log('[Meta]::delete Could not delete field '.$name.': not registered');
+ return true;
}
- return $result;
+ if ($field->isTaxonomy()) {
+ wp_set_object_terms($this->ID, [], $this->slug);
+ return true;
+ }
+
+ return match ($this->type) {
+ 'post' => delete_post_meta($this->ID, BASE.$name),
+ 'term' => delete_term_meta($this->ID, BASE.$name),
+ 'user', 'integrations' => delete_user_meta($this->ID, BASE.$name),
+ 'options' => delete_option(BASE.$name),
+ default => false
+ };
}
+
+
/**
* Delete multiple field values
*/
@@ -442,9 +391,9 @@
return $results;
}
- // ─────────────────────────────────────────────────────────────
- // Repeater Access
- // ─────────────────────────────────────────────────────────────
+ /*****************************************************************
+ * Repeater Access
+ *****************************************************************/
/**
* Get repeater accessor for fluent repeater operations
@@ -456,6 +405,7 @@
protected function setByPath(string $path, mixed $value): self
{
+ error_log('Setting by path: '.$path.', with value: '.print_r($value, true));
$parts = explode(':', $path, 3);
if (count($parts) !== 3) {
return $this;
@@ -482,24 +432,6 @@
// Utility Methods
// ─────────────────────────────────────────────────────────────
- /**
- * Get all dirty (changed) field values
- */
- public function getDirty(): array
- {
- return array_map(
- fn(Field $f) => $f->value,
- $this->item->getDirtyFields()
- );
- }
-
- /**
- * Check if any fields have changed
- */
- public function isDirty(): bool
- {
- return $this->item->hasDirtyFields();
- }
/**
* Discard all unsaved changes
@@ -510,84 +442,22 @@
return $this;
}
- /**
- * Get validation errors
- */
- public function getErrors(): array
- {
- $errors = [];
- foreach ($this->item->getInvalidFields() as $name => $field) {
- $errors[$name] = $field->errors;
- }
- return $errors;
- }
-
- /**
- * Check if valid (no validation errors)
- */
- public function isValid(): bool
- {
- return $this->item->isValid();
- }
-
- /**
- * Disable auto-validation for bulk operations
- */
- public function withoutValidation(): self
- {
- $this->autoValidate = false;
- return $this;
- }
-
- /**
- * Disable auto-sanitization
- */
- public function withoutSanitization(): self
- {
- $this->autoSanitize = false;
- return $this;
- }
-
- /**
- * Re-enable validation and sanitization
- */
- public function withDefaults(): self
- {
- $this->autoValidate = true;
- $this->autoSanitize = true;
- return $this;
- }
-
- /**
- * Get the underlying Item
- */
- public function item(): Item
- {
- return $this->item;
- }
/**
* Get field configuration
*/
public function config(string $name): ?array
{
- return $this->item->getFieldConfig($name);
+ return $this->fields[$name]->config??null;
}
- /**
- * Get all field configurations
- */
- public function configs(): array
- {
- return $this->item->fieldConfigs;
- }
/**
* Get item ID
*/
public function id(): int|string|null
{
- return $this->item->id;
+ return $this->ID;
}
/**
@@ -595,7 +465,7 @@
*/
public function objectType(): string
{
- return $this->item->objectType;
+ return $this->type;
}
/**
@@ -603,47 +473,9 @@
*/
public function contentType(): ?string
{
- return $this->item->contentType;
+ return $this->contentType;
}
- /**
- * Eager load all fields
- */
- public function eager(): self
- {
- $this->getAll();
- return $this;
- }
-
- /**
- * Convert loaded fields to array
- */
- public function toArray(): array
- {
- return $this->item->toArray();
- }
-
- // ─────────────────────────────────────────────────────────────
- // Event Callbacks
- // ─────────────────────────────────────────────────────────────
-
- /**
- * Register callback for field changes
- */
- public function onChange(string $field, callable $callback): self
- {
- $this->onChangeCallbacks[$field][] = $callback;
- return $this;
- }
-
- /**
- * Register computed/virtual field
- */
- public function computed(string $name, callable $getter): self
- {
- $this->computed[$name] = $getter;
- return $this;
- }
// ─────────────────────────────────────────────────────────────
// Protected Helpers
diff --git a/inc/meta/MetaOld.php b/inc/meta/MetaOld.php
new file mode 100644
index 0000000..876af90
--- /dev/null
+++ b/inc/meta/MetaOld.php
@@ -0,0 +1,701 @@
+<?php
+namespace JVBase\meta;
+
+use JVBase\registrar\Registrar;
+
+if (!defined('ABSPATH')) {
+ exit;
+}
+
+/**
+ * Main facade for meta operations
+ * Fluent API for getting/setting meta values with validation & sanitization
+ *
+ * Usage:
+ * $meta = Meta::forPost($id);
+ * $meta->price = 150;
+ * $meta->save();
+ *
+ * Meta::forPost($id)->set('price', 150)->set('style', 'traditional')->save();
+ */
+class MetaOld
+{
+ protected Item $item;
+ protected Storage $storage;
+ protected Validator $validator;
+ protected Sanitizer $sanitizer;
+ protected MetaTypeManager $typeManager;
+
+ protected bool $autoValidate = true;
+ protected bool $autoSanitize = true;
+
+ /** @var array<string, callable[]> */
+ protected array $onChangeCallbacks = [];
+
+ /** @var array<string, callable> */
+ protected array $computed = [];
+
+ // ─────────────────────────────────────────────────────────────
+ // Factory Methods
+ // ─────────────────────────────────────────────────────────────
+
+ /**
+ * Create Meta instance for a post
+ */
+ public static function forPost(int $id): self
+ {
+ return new self($id, 'post');
+ }
+
+ /**
+ * Create Meta instance for a term
+ */
+ public static function forTerm(int $id): self
+ {
+ return new self($id, 'term');
+ }
+
+ /**
+ * Create Meta instance for a user
+ */
+ public static function forUser(int $id): self
+ {
+ return new self($id, 'user');
+ }
+
+ /**
+ * Create Meta instance for options
+ */
+ public static function forOptions(?string $baseKey = null): self
+ {
+ $instance = new self($baseKey, 'options');
+ $instance->item->baseKey = $baseKey;
+ return $instance;
+ }
+
+ /**
+ * Bulk load multiple posts with optional field preloading
+ * @return array<int, Meta>
+ */
+ public static function bulkForPosts(array $ids, array $preloadFields = []): array
+ {
+ return self::bulkFor($ids, 'post', $preloadFields);
+ }
+
+ /**
+ * Bulk load multiple terms with optional field preloading
+ * @return array<int, Meta>
+ */
+ public static function bulkForTerms(array $ids, array $preloadFields = []): array
+ {
+ return self::bulkFor($ids, 'term', $preloadFields);
+ }
+
+ /**
+ * Bulk load multiple users with optional field preloading
+ * @return array<int, Meta>
+ */
+ public static function bulkForUsers(array $ids, array $preloadFields = []): array
+ {
+ return self::bulkFor($ids, 'user', $preloadFields);
+ }
+
+ /**
+ * Generic bulk loader
+ * @return array<int, Meta>
+ */
+ protected static function bulkFor(array $ids, string $type, array $preloadFields = []): array
+ {
+ if (empty($ids)) {
+ return [];
+ }
+
+ $metas = [];
+
+ // Create instances
+ foreach ($ids as $id) {
+ $metas[$id] = new self($id, $type);
+ }
+
+ // Preload fields if specified
+ if (!empty($preloadFields)) {
+ self::bulkPreload($metas, $type, $preloadFields);
+ }
+
+ return $metas;
+ }
+
+ /**
+ * Bulk preload fields for multiple Meta instances
+ * @param Meta[] $metas
+ */
+ protected static function bulkPreload(array $metas, string $objectType, array $fields): void
+ {
+ if (empty($metas) || empty($fields)) {
+ return;
+ }
+
+ $ids = array_keys($metas);
+ $values = Storage::getBulkValues($ids, $objectType, $fields);
+
+ // Distribute results to Meta instances
+ foreach ($values as $id => $fieldValues) {
+ if (!isset($metas[$id])) {
+ continue;
+ }
+
+ $meta = $metas[$id];
+ foreach ($fieldValues as $name => $value) {
+ $config = $meta->config($name) ?? ['type' => 'text'];
+ $field = new Field($name, $value, $config);
+ $meta->item()->setField($field);
+ }
+ }
+ }
+
+ /**
+ * Save multiple Meta instances efficiently
+ * @param Meta[] $metas
+ * @return array<int, bool>
+ */
+ public static function saveBulk(array $metas, bool $updateTimestamp = true): array
+ {
+ // Validate all first
+ $invalid = [];
+ foreach ($metas as $id => $meta) {
+ if (!$meta->isValid()) {
+ $invalid[$id] = $meta->getErrors();
+ }
+ }
+
+ if (!empty($invalid)) {
+ JVB()->error()->log('meta', 'Bulk save has validation errors', [
+ 'invalid_items' => $invalid
+ ], 'warning');
+ }
+
+ // Filter to only valid metas
+ $validMetas = array_filter($metas, fn($m) => $m->isValid());
+
+ // Check overrides before bulk save
+ foreach ($validMetas as $meta) {
+ foreach ($meta->item()->getDirtyFields() as $field) {
+ if ($meta->checkOverrides($field)) {
+ $field->markClean();
+ }
+ }
+ }
+
+ $results = Storage::saveBulk($validMetas, $updateTimestamp);
+
+ // Mark invalid ones as failed
+ foreach ($invalid as $id => $errors) {
+ $results[$id] = false;
+ }
+
+ return $results;
+ }
+
+ // ─────────────────────────────────────────────────────────────
+ // Constructor
+ // ─────────────────────────────────────────────────────────────
+
+ public function __construct(int|string|null $id, string $type)
+ {
+ $this->storage = new Storage();
+ $this->validator = new Validator();
+ $this->sanitizer = new Sanitizer();
+ $this->typeManager = new MetaTypeManager();
+
+ $this->item = $this->buildItem($id, $type);
+ }
+
+ protected function buildItem(int|string|null $id, string $type): Item
+ {
+ $contentType = null;
+ $wpObject = null;
+
+ if ($id && $type !== 'options') {
+ [$wpObject, $contentType] = match ($type) {
+ 'post' => [get_post($id), jvbNoBase(get_post_type($id))],
+ 'term' => [get_term($id), jvbNoBase(get_term($id)->taxonomy)],
+ 'user', 'integrations' => [get_user_by('id', $id), jvbUserRole($id)],
+ default => [null, null]
+ };
+ }
+
+ $item = new Item($id, $type, $contentType);
+ $item->wpObject = $wpObject;
+ $item->fieldConfigs = $this->loadFieldConfigs($contentType, $type);
+
+ // Mark WP defaults in configs
+ $defaults = Item::WP_DEFAULTS[$type] ?? [];
+ foreach ($defaults as $name) {
+ if (!isset($item->fieldConfigs[$name])) {
+ $item->fieldConfigs[$name] = ['type' => 'text', '_wp_default' => true];
+ } else {
+ $item->fieldConfigs[$name]['_wp_default'] = true;
+ }
+ }
+
+ return $item;
+ }
+
+ protected function loadFieldConfigs(?string $contentType, string $objectType): array
+ {
+ if (!$contentType && $objectType !== 'options') {
+ return [];
+ }
+
+ return Registrar::getFieldsFor($contentType??'options');
+ }
+
+ // ─────────────────────────────────────────────────────────────
+ // Magic Methods for Fluent Access
+ // ─────────────────────────────────────────────────────────────
+
+ public function __get(string $name): mixed
+ {
+ return $this->get($name);
+ }
+
+ public function __set(string $name, mixed $value): void
+ {
+ $this->set($name, $value);
+ }
+
+ public function __isset(string $name): bool
+ {
+ return $this->item->hasField($name) || isset($this->computed[$name]);
+ }
+
+ // ─────────────────────────────────────────────────────────────
+ // Core API
+ // ─────────────────────────────────────────────────────────────
+
+ /**
+ * Get a field value
+ */
+ public function get(string $name): mixed
+ {
+ // Handle repeater subfield path
+ if (str_contains($name, ':')) {
+ return $this->getByPath($name);
+ }
+
+ // Check computed fields first
+ if (isset($this->computed[$name])) {
+ return ($this->computed[$name])($this);
+ }
+
+ // Return from loaded field if exists
+ if ($field = $this->item->getField($name)) {
+ return $field->get();
+ }
+
+ // Load from storage
+ $value = $this->storage->get($this->item, $name);
+ $config = $this->item->getFieldConfig($name) ?? ['type' => 'text'];
+
+ $field = new Field($name, $value, $config);
+ $this->item->setField($field);
+
+ return $value;
+ }
+
+ /**
+ * Set a field value (validates & sanitizes by default)
+ */
+ public function set(string $name, mixed $value): self
+ {
+ // Handle repeater subfield path (e.g., "services:2:image")
+ if (str_contains($name, ':')) {
+ return $this->setByPath($name, $value);
+ }
+
+ $config = $this->item->getFieldConfig($name);
+
+ if (!$config) {
+ // Allow setting unknown fields with minimal config
+ $config = ['type' => 'text', 'name' => $name];
+ }
+
+ // Validate
+ if ($this->autoValidate && !$this->validator->validate($value, $config)) {
+ $field = $this->item->getField($name) ?? new Field($name, $value, $config);
+ $field->addError("Validation failed for {$name}");
+ $this->item->setField($field);
+ return $this;
+ }
+
+ // Sanitize
+ if ($this->autoSanitize) {
+ $value = $this->sanitizer->sanitize($value, $config);
+ }
+
+ // Get or create field
+ $field = $this->item->getField($name);
+ $oldValue = null;
+
+ if ($field) {
+ error_log('Stored field found');
+ $oldValue = $field->value;
+ $field->set($value);
+ } else {
+ error_log('No stored field found. Creating a new one');
+ // Load original to track dirty state
+ $original = $this->storage->get($this->item, $name);
+ $oldValue = $original;
+ $field = new Field($name, $original, $config);
+ $field->set($value);
+ $this->item->setField($field);
+ }
+
+ // Fire change callbacks
+ if (isset($this->onChangeCallbacks[$name]) && $oldValue !== $value) {
+ foreach ($this->onChangeCallbacks[$name] as $callback) {
+ $callback($value, $oldValue, $this);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get multiple fields
+ */
+ public function getAll(array $fields = []): array
+ {
+ if (empty($fields) || $fields === ['all']) {
+ $fields = array_keys($this->item->fieldConfigs);
+ }
+
+ // Load all from storage
+ $values = $this->storage->getAll($this->item, $fields);
+
+ // Create Field instances
+ foreach ($values as $name => $value) {
+ if (!$this->item->getField($name)) {
+ $config = $this->item->getFieldConfig($name) ?? ['type' => 'text'];
+ $this->item->setField(new Field($name, $value, $config));
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Set multiple fields
+ */
+ public function setAll(array $data): self
+ {
+ error_log('Setting all Meta');
+ foreach ($data as $name => $value) {
+ error_log('Setting '.$name.' with value: '.print_r($value, true));
+ $this->set($name, $value);
+ }
+ return $this;
+ }
+
+ /**
+ * Save all dirty fields to database
+ */
+ public function save(bool $updateTimestamp = true): bool
+ {
+ if (!$this->item->isValid()) {
+ JVB()->error()->log('meta', 'Cannot save: validation errors exist', [
+ 'fields' => array_keys($this->item->getInvalidFields())
+ ], 'warning');
+ return false;
+ }
+
+ // Check for field overrides before saving
+ foreach ($this->item->getDirtyFields() as $field) {
+ if ($this->checkOverrides($field)) {
+ $field->markClean();
+ }
+ }
+
+ return $this->storage->save($this->item, $updateTimestamp);
+ }
+
+ /**
+ * Delete a field value
+ */
+ public function delete(string $name): bool
+ {
+ $result = $this->storage->delete($this->item, $name);
+
+ if ($result && $field = $this->item->getField($name)) {
+ $field->set($this->getDefaultValue($name));
+ $field->markClean();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Delete multiple field values
+ */
+ public function deleteAll(array $names): array
+ {
+ $results = [];
+ foreach ($names as $name) {
+ $results[$name] = $this->delete($name);
+ }
+ return $results;
+ }
+
+ // ─────────────────────────────────────────────────────────────
+ // Repeater Access
+ // ─────────────────────────────────────────────────────────────
+
+ /**
+ * Get repeater accessor for fluent repeater operations
+ */
+ public function repeater(string $name): Repeater
+ {
+ return new Repeater($this, $name);
+ }
+
+ protected function setByPath(string $path, mixed $value): self
+ {
+ error_log('Setting by path: '.$path.', with value: '.print_r($value, true));
+ $parts = explode(':', $path, 3);
+ if (count($parts) !== 3) {
+ return $this;
+ }
+
+ [$repeaterName, $rowIndex, $subField] = $parts;
+ $this->repeater($repeaterName)->setField((int) $rowIndex, $subField, $value);
+
+ return $this;
+ }
+
+ protected function getByPath(string $path): mixed
+ {
+ $parts = explode(':', $path, 3);
+ if (count($parts) !== 3) {
+ return null;
+ }
+
+ [$repeaterName, $rowIndex, $subField] = $parts;
+ return $this->repeater($repeaterName)->field((int) $rowIndex, $subField);
+ }
+
+ // ─────────────────────────────────────────────────────────────
+ // Utility Methods
+ // ─────────────────────────────────────────────────────────────
+
+ /**
+ * Get all dirty (changed) field values
+ */
+ public function getDirty(): array
+ {
+ return array_map(
+ fn(Field $f) => $f->value,
+ $this->item->getDirtyFields()
+ );
+ }
+
+ /**
+ * Check if any fields have changed
+ */
+ public function isDirty(): bool
+ {
+ return $this->item->hasDirtyFields();
+ }
+
+ /**
+ * Discard all unsaved changes
+ */
+ public function reset(): self
+ {
+ $this->item->resetAll();
+ return $this;
+ }
+
+ /**
+ * Get validation errors
+ */
+ public function getErrors(): array
+ {
+ $errors = [];
+ foreach ($this->item->getInvalidFields() as $name => $field) {
+ $errors[$name] = $field->errors;
+ }
+ return $errors;
+ }
+
+ /**
+ * Check if valid (no validation errors)
+ */
+ public function isValid(): bool
+ {
+ return $this->item->isValid();
+ }
+
+ /**
+ * Disable auto-validation for bulk operations
+ */
+ public function withoutValidation(): self
+ {
+ $this->autoValidate = false;
+ return $this;
+ }
+
+ /**
+ * Disable auto-sanitization
+ */
+ public function withoutSanitization(): self
+ {
+ $this->autoSanitize = false;
+ return $this;
+ }
+
+ /**
+ * Re-enable validation and sanitization
+ */
+ public function withDefaults(): self
+ {
+ $this->autoValidate = true;
+ $this->autoSanitize = true;
+ return $this;
+ }
+
+ /**
+ * Get the underlying Item
+ */
+ public function item(): Item
+ {
+ return $this->item;
+ }
+
+ /**
+ * Get field configuration
+ */
+ public function config(string $name): ?array
+ {
+ return $this->item->getFieldConfig($name);
+ }
+
+ /**
+ * Get all field configurations
+ */
+ public function configs(): array
+ {
+ return $this->item->fieldConfigs;
+ }
+
+ /**
+ * Get item ID
+ */
+ public function id(): int|string|null
+ {
+ return $this->item->id;
+ }
+
+ /**
+ * Get object type (post, term, user, options)
+ */
+ public function objectType(): string
+ {
+ return $this->item->objectType;
+ }
+
+ /**
+ * Get content type (tattoo, artist, etc)
+ */
+ public function contentType(): ?string
+ {
+ return $this->item->contentType;
+ }
+
+ /**
+ * Eager load all fields
+ */
+ public function eager(): self
+ {
+ $this->getAll();
+ return $this;
+ }
+
+ /**
+ * Convert loaded fields to array
+ */
+ public function toArray(): array
+ {
+ return $this->item->toArray();
+ }
+
+ // ─────────────────────────────────────────────────────────────
+ // Event Callbacks
+ // ─────────────────────────────────────────────────────────────
+
+ /**
+ * Register callback for field changes
+ */
+ public function onChange(string $field, callable $callback): self
+ {
+ $this->onChangeCallbacks[$field][] = $callback;
+ return $this;
+ }
+
+ /**
+ * Register computed/virtual field
+ */
+ public function computed(string $name, callable $getter): self
+ {
+ $this->computed[$name] = $getter;
+ return $this;
+ }
+
+ // ─────────────────────────────────────────────────────────────
+ // Protected Helpers
+ // ─────────────────────────────────────────────────────────────
+
+ /**
+ * Check for field update overrides
+ */
+ public function checkOverrides(Field $field): bool
+ {
+ $name = $field->name;
+ $type = $field->type();
+ $value = $field->value;
+
+ do_action('jvb_meta_update', $name, $value, $this->item->objectType);
+
+ $overrides = [
+ BASE . 'update_' . $name,
+ BASE . 'update_' . $type,
+ 'jvb_update_' . $name,
+ 'jvb_update_' . $type,
+ ];
+
+ foreach ($overrides as $override) {
+ if (function_exists($override)) {
+ $override($this->item->id, $value);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get default value for a field type
+ */
+ protected function getDefaultValue(string $name): mixed
+ {
+ $config = $this->item->getFieldConfig($name);
+ $type = $config['type'] ?? 'text';
+
+ return match ($this->typeManager->getMetaType($type)) {
+ 'object', 'array' => [],
+ 'boolean' => false,
+ 'integer' => 0,
+ default => '',
+ };
+ }
+
+}
diff --git a/inc/meta/Sanitizer.php b/inc/meta/Sanitizer.php
index 869065d..218a1ac 100644
--- a/inc/meta/Sanitizer.php
+++ b/inc/meta/Sanitizer.php
@@ -14,7 +14,6 @@
public static function sanitize(mixed $value, array $field_config): mixed
{
$callback = static::getCallback($field_config);
-
if (is_array($callback)) {
return call_user_func([static::class, $callback[1]], $value, $field_config);
}
@@ -173,10 +172,13 @@
return $sanitized;
}
- protected static function sanitizeSelector(string $value, array $config):string
+ protected static function sanitizeSelector(string|array $value, array $config):string
{
- if (array_key_exists('type', $config)) {
- return match ($config['type']) {
+ if (is_array($value)) {
+ $value = implode(',', $value);
+ }
+ if (array_key_exists('subtype', $config)) {
+ return match ($config['subtype']) {
'user' => self::sanitizeUser($value, $config),
'taxonomy'=> self::sanitizeTaxonomy($value, $config),
'post' => self::sanitizePost($value, $config),
diff --git a/inc/meta/Storage.php b/inc/meta/Storage.php
index fd0f927..386b7b4 100644
--- a/inc/meta/Storage.php
+++ b/inc/meta/Storage.php
@@ -77,7 +77,7 @@
&& (
($config['type'] ?? '') === 'taxonomy'
|| (($config['type']??'') === 'selector' && ($config['subtype']??'') === 'taxonomy')
- ) && !isset($config['taxonomy_type'])) {
+ ) && (!isset($config['taxonomy_type']) || !isset($config['isReference']))) {
$taxonomyFields[$name] = $config;
} else {
$metaFields[] = $name;
@@ -123,18 +123,21 @@
}
if ($field->isTaxonomy()) {
+ error_log('Saving Taxonomy field with set_object_terms');
return $this->saveTaxonomyField($item, $field);
}
$metaKey = BASE . $field->name;
-
- return match ($item->objectType) {
- 'post' => update_post_meta($item->id, $metaKey, $field->value) !== false,
- 'term' => update_term_meta($item->id, $metaKey, $field->value) !== false,
- 'user', 'integrations' => update_user_meta($item->id, $metaKey, $field->value) !== false,
+ $result = match ($item->objectType) {
+ 'post' => (bool)update_post_meta($item->id, $metaKey, $field->value),
+ 'term' => (bool)update_term_meta($item->id, $metaKey, $field->value),
+ 'user', 'integrations' => (bool)update_user_meta($item->id, $metaKey, $field->value),
'options' => $this->saveOption($item, $field),
default => false
};
+
+ error_log('Result: '.print_r($result, true));
+ return $result;
}
/**
@@ -149,10 +152,10 @@
}
$this->wpdb->query('START TRANSACTION');
-
try {
foreach ($dirty as $field) {
if (!$this->saveField($item, $field)) {
+ error_log("Could not save field: {$field->name}");
throw new Exception("Failed to save field: {$field->name}");
}
$field->markClean();
@@ -538,7 +541,6 @@
{
$taxonomy = jvbCheckBase($field->config['taxonomy']);
$value = $field->value;
-
if (empty(trim((string)$value))) {
wp_set_object_terms($item->id, [], $taxonomy, false);
return true;
diff --git a/inc/registrar/Fields.php b/inc/registrar/Fields.php
index 7386cd7..6887534 100644
--- a/inc/registrar/Fields.php
+++ b/inc/registrar/Fields.php
@@ -121,7 +121,7 @@
'label' => 'Description',
]
];
- if ($this->registrar->args()['hierarchical']??false && $this->registrar->args()['hierarchical'] === true){
+ if ($this->registrar->args()['hierarchical']??false){
$fields['parent'] = [
'type' => 'taxonomy',
'isReference' => true,
diff --git a/inc/registrar/Posts.php b/inc/registrar/Posts.php
index bb63d90..416f24b 100644
--- a/inc/registrar/Posts.php
+++ b/inc/registrar/Posts.php
@@ -73,7 +73,7 @@
* Set this to true for the post type to be available in the block editor.
* @var bool
*/
- public bool $show_in_rest;
+ public bool $show_in_rest = true;
/**
* To change the base URL of REST API route. Default is $post_type.
* @var string
diff --git a/inc/registrar/Registrar.php b/inc/registrar/Registrar.php
index 33a8687..a69a13f 100644
--- a/inc/registrar/Registrar.php
+++ b/inc/registrar/Registrar.php
@@ -186,8 +186,10 @@
private static array $instances = [];
+ protected string $based;
private function __construct(string $slug, string $singular, string $plural, string $type) {
$this->slug = $slug;
+ $this->based = jvbCheckBase($slug);
$this->type = $type;
$this->singular = $singular;
$this->plural = $plural;
@@ -383,6 +385,11 @@
return $this->slug;
}
+ public function getBased():string
+ {
+ return $this->based;
+ }
+
public function setTimeline(bool $set):self
{
$this->is_timeline = $set;
@@ -686,9 +693,43 @@
}
wp_reset_postdata();
}
-
add_filter('jvb_post_content_output', [$this, 'renderContent'], 20, 2);
+
+
+ //Add a date published and date modified fields, and auto-update them on term creation/modification
+ $this->fields()->addField('date_published', [
+ 'type' => 'datetime',
+ 'label' => 'Published',
+ 'hidden' => true,
+ ]);
+ $this->fields()->addField('date_modified', [
+ 'type' => 'datetime',
+ 'label' => 'Modified',
+ 'hidden' => true,
+ ]);
+ add_action('created_'.$this->based, [$this, 'addTermCreatedMeta']);
+ add_action('edited_'.$this->based, [$this, 'addTermUpdatedMeta']);
}
+ public function addTermCreatedMeta(int $termId):void
+ {
+ $meta = Meta::forTerm($termId);
+ $meta->set('date_published', date('Y-m-d H:i:s'));
+ }
+ public function handleContentTermMetaChange(int $meta_id, int $term_id, string $meta_key, $meta_value):void
+ {
+ $term = get_term($term_id);
+ $taxonomy = $term->taxonomy;
+ if ($taxonomy === $this->based && $meta_key !== BASE . 'date_modified') {
+ $meta = Meta::forTerm($term_id);
+ $meta->set('date_modified', date('Y-m-d H:i:s'));
+ }
+
+ }
+ public function addTermUpdatedMeta(int $termId):void
+ {
+ $meta = Meta::forTerm($termId);
+ $meta->set('date_modified', date('Y-m-d H:i:s'));
+ }
public function renderContent(string $content, array $block):string
{
if (!is_page($this->page)) {
@@ -716,7 +757,7 @@
$meta = Meta::forTerm($item);
$slug = sanitize_title($meta->get('name'));
$item = sprintf(
- '<li id="%s"><h3><a href="%s">%s</a></h3><p>%s</p><ul>',
+ '<li id="%s"><h2><a href="%s">%s</a></h2><p>%s</p><ul class="item-grid">',
$slug,
get_term_link($item, jvbCheckBase($this->slug))??'',
$meta->get('name'),
@@ -737,7 +778,7 @@
$img = $postMeta->get('post_thumbnail');
$img = !empty($img) ? jvbFormatImage((int)$img, 'tiny', 'medium') : '';
$item .= sprintf(
- '<li id="%s"><h4><a href="%s">%s</a></h4>%s</li>',
+ '<li id="%s" class="item"><h3><a href="%s">%s</a></h3>%s</li>',
$slug.'-'.sanitize_title(get_the_title($ID)),
get_the_permalink($ID),
$postMeta->get('post_title'),
diff --git a/inc/registrar/Terms.php b/inc/registrar/Terms.php
index e1b875e..eb15874 100644
--- a/inc/registrar/Terms.php
+++ b/inc/registrar/Terms.php
@@ -59,7 +59,7 @@
* Whether to include the taxonomy in the REST API. Set this to true for the taxonomy to be available in the block editor.
* @var bool
*/
- public bool $show_in_rest;
+ public bool $show_in_rest = true;
/**
* To change the base url of REST API route. Default is $taxonomy.
* @var string
diff --git a/inc/registrar/config/seo/Schema.php b/inc/registrar/config/seo/Schema.php
index b48b8de..5144fd2 100644
--- a/inc/registrar/config/seo/Schema.php
+++ b/inc/registrar/config/seo/Schema.php
@@ -169,7 +169,7 @@
$meta = Meta::forPost($ID);
$config = $this->getConfig();
- $class = $this->classFromConfig($config, $meta);
+ $class = JVB()->schemaHelper()::classFromConfig($config, $meta);
$class->setAuthor(JVB()->seo()->getCreator(true));
return $class->outputSchema();
@@ -188,7 +188,8 @@
$ID,
function() use ($ID) {
$action = BASE.ucfirst($this->slug).'Schema';
- $config = get_option($action, apply_filters($action, $this->defaultSchema));
+ $config = JVB()->schemaHelper()::schema($action);
+
if (!array_key_exists('type', $config)) {
$config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage';
}
@@ -196,7 +197,7 @@
error_log('No class found for archive schema output: '.$config['type']);
return [];
}
- $class = $this->classFromConfig($config);
+ $class = JVB()->schemaHelper()::classFromConfig($config);
$class->setIsPartOf(get_home_url().'/#website');
$itemList = new render\Thing\Intangible\ItemList\ItemList();
@@ -235,7 +236,7 @@
$this->slug,
function() {
$action = BASE.ucfirst($this->slug).'Archive';
- $config = get_option($action, apply_filters($action, $this->defaultArchive));
+ $config = JVB()->schemaHelper()->archive($this->slug);
if (!array_key_exists('type', $config)) {
$config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage';
}
@@ -246,7 +247,7 @@
$obj = get_queried_object();
$meta = (property_exists($obj, 'taxonomy')) ? Meta::forTerm($obj->term_id) : null;
- $class = $this->classFromConfig($config, $meta);
+ $class = JVB()->schemaHelper()::classFromConfig($config, $meta);
$class->setIsPartOf(get_home_url().'/#website');
$itemList = new render\Thing\Intangible\ItemList\ItemList();
@@ -301,7 +302,7 @@
$meta = null;
}
$config = $this->getConfig('archive');
- $class = $this->classFromConfig($config, $meta);
+ $class = JVB()->schemaHelper()::classFromConfig($config, $meta);
$class->delete('about');
switch ($type) {
@@ -327,9 +328,7 @@
error_log('[SEO]Schema::getConfig Invalid type: '.$type);
return [];
}
- $action = BASE.ucfirst($this->slug).ucfirst($type);
- $default = 'default'.ucfirst($type);
- return get_option($action, apply_filters($action, $this->$default));
+ return JVB()->schemaHelper()::getConfig($this->slug, $type);
}
public function define(string $property, string $value):void
@@ -394,39 +393,14 @@
$config = $this->getConfig('meta');
$meta = Meta::forPost(get_the_ID());
$title = Resolver::resolve($config['name'], $meta);
- } elseif (is_post_type_archive($based) || is_tax($based)) {
+ } elseif (is_post_type_archive($based) ) {
$config = $this->getConfig('archive');
$title = $config['name'];
+ } elseif (is_tax($based)) {
+ $config = $this->getConfig('archive');
+ $meta = Meta::forTerm(get_queried_object_id());
+ $title = Resolver::resolve($config['name'], $meta);
}
return $title;
}
-
- protected function classFromConfig(array $config, ?Meta $meta = null):mixed
- {
- if (!array_key_exists('type', $config)) {
- error_log('[Schema]::classFromConfig No class defined in config: '.print_r($config, true));
- return false;
- }
- $className = $config['type'];
- unset($config['type']);
- $class = new $className();
-
- foreach ($config as $property=>$value) {
- if (is_array ($value)) {
- $value = $this->classFromConfig($value, $meta);
- }
- $method = 'set'.ucfirst($property);
- if (!method_exists($class, $method)) {
- error_log('[Schema]::classFromConfig - method: '.$method.' does not exist in class: '.$className);
- continue;
- }
- if (is_string($value) && str_contains($value, '{{')) {
- $value = Resolver::resolveForSchema($property, $value, $config, $meta);
- }
- if (!empty($value)) {
- $class->$method($value);
- }
- }
- return $class;
- }
}
diff --git a/inc/registrar/fields/Field.php b/inc/registrar/fields/Field.php
index e34c80e..add97be 100644
--- a/inc/registrar/fields/Field.php
+++ b/inc/registrar/fields/Field.php
@@ -131,16 +131,16 @@
public function getConfig():array{
$config = get_object_vars($this);
- $config = array_map(function ($item) {
+ return array_map(function ($item) {
if (is_a($item, Field::class)) {
return $item->getConfig();
} else if (is_array($item)) {
$temp = [];
- foreach ($item as $v) {
+ foreach ($item as $k => $v) {
if (is_a($v, Field::class)) {
- $temp[] = $v->getConfig();
+ $temp[$k] = $v->getConfig();
} else {
- $temp[] = $v;
+ $temp[$k] = $v;
}
}
return $temp;
@@ -148,7 +148,5 @@
return $item;
}
}, $config);
-
- return $config;
}
}
diff --git a/inc/rest/Rest.php b/inc/rest/Rest.php
index 406a3ab..3357ca4 100644
--- a/inc/rest/Rest.php
+++ b/inc/rest/Rest.php
@@ -2,6 +2,7 @@
namespace JVBase\rest;
use JVBase\managers\Cache;
+use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
use JVBase\utility\Features;
use WP_REST_Request;
@@ -89,6 +90,21 @@
// CACHE MANAGEMENT
// =========================================================================
+ protected function checkCache(string $key, $request):WP_REST_Response|false
+ {
+ // Check HTTP cache headers with the specific content type
+ $cache_check = $this->checkHeaders($request, $key);
+ if ($cache_check) {
+ return $cache_check;
+ }
+
+ $cache = $this->cache->get($key);
+ if ($cache) {
+ $response = Response::success($cache);
+ return $this->addCacheHeaders($response);
+ }
+ return false;
+ }
/**
* Check request headers for conditional caching (ETag, If-Modified-Since)
*/
@@ -517,6 +533,9 @@
***************************************************************************/
protected function isTimeline($args, $data):bool
{
+ if (!array_key_exists('post_type', $args)) {
+ return false;
+ }
$post_types = is_array($args['post_type']) ? $args['post_type'] : [$args['post_type']];
$hasTimeline = array_map(function($item) { return jvbCheckBase($item); },Registrar::getFeatured('is_timeline', 'post'));
return !empty(array_intersect($post_types, $hasTimeline));
@@ -712,4 +731,177 @@
delete_user_meta($user_id, BASE . 'session_fingerprint');
delete_user_meta($user_id, BASE . 'session_timestamp');
}
+
+ /*******************************
+ * META HELPERS
+ *******************************/
+ public function getFieldsOfType(array $fields, string|array $type, Meta $meta, array $subType = []):array
+ {
+ $gotFields = [];
+ if (is_string($type)) {
+ $type = [$type];
+ }
+ foreach ($fields as $field => $value) {
+ //Skip empty values
+ if (empty($value)) {
+ continue;
+ }
+ $config = $meta->config($field);
+ if (in_array($config['type'], ['group', 'repeater', 'tagList'])) {
+ foreach ($config['fields'] as $subfield => $subConfig) {
+ if (is_numeric($subfield) && array_key_exists('name', $subConfig)) {
+ $subfield = $subConfig['name'];
+ }
+ if (is_numeric($subfield)) continue;
+ if (array_key_exists('type', $subConfig) && in_array($subConfig['type'], $type)) {
+ $gotFields[] = $field.':'.$subfield;
+ }
+ }
+ } elseif (in_array($config['type'], $type)) {
+ $gotFields[] = $field;
+ } else if ((!empty($subType) && in_array($config['type'], array_keys($subType)) && in_array($config['subtype'], array_values($subType)))) {
+ $gotFields[] = $field;
+ }
+ }
+ return $gotFields;
+ }
+
+
+ protected function extractImages(array $fields, Meta $meta):array
+ {
+ $images = [];
+ $get = $this->getFieldsOfType($fields, ['upload', 'gallery','image'], $meta);
+ if (!empty($get)) {
+ $baseFields = array_map(function($fieldName) {
+ return (str_contains($fieldName, ':')) ? strtok($fieldName, ':') : $fieldName;
+ }, $get);
+
+ $temp = array_map(
+ function($item) {
+ return explode(':', $item);
+ },
+ array_filter($get, function($fieldName) {
+ return str_contains($fieldName, ':');
+ })
+ );
+ $complex = [];
+ foreach ($temp as $tmp) {
+ $complex[$tmp[0]] = $tmp[1];
+ }
+
+ $fields = array_filter($fields, function ($field) use ($baseFields) {
+ return in_array($field, $baseFields);
+ }, ARRAY_FILTER_USE_KEY);
+
+ foreach ($fields as $fieldName => $value) {
+ //Check if it's a complex field
+ if (array_key_exists($fieldName, $complex)) {
+ $check = $complex[$fieldName];
+ foreach ($value as $row) {
+ foreach ($row as $fName => $fValue) {
+ if ($fName === $check && !empty($fValue)) {
+ $images = $this->addImages($fValue, $images);
+ }
+ }
+ }
+ } else {
+ $images = $this->addImages($value, $images);
+ }
+ }
+ }
+ return $images;
+ }
+ public function addImages(string $imgs, array $images):array
+ {
+ $temp = explode(',', $imgs);
+ foreach ($temp as $img) {
+ if (is_numeric($img) && !array_key_exists($img, $images) && $img > 0) {
+ $images[$img] = jvbImageData((int)$img);
+ }
+ }
+ return $images;
+ }
+
+ protected function extractTerms(array $fields, Meta $meta):array
+ {
+ $terms = [];
+ $get = $this->getFieldsOfType($fields, ['taxonomy'], $meta, ['selector' => 'taxonomy']);
+ if (!empty($get)) {
+ $baseFields = array_map(function($fieldName) {
+ return (str_contains($fieldName, ':')) ? strtok($fieldName, ':') : $fieldName;
+ }, $get);
+
+ $complex = array_map(
+ function($item) {
+ return explode(':', $item);
+ },
+ array_filter($get, function($fieldName) {
+ return str_contains($fieldName, ':');
+ })
+ );
+
+ $fields = array_filter($fields, function ($field) use ($baseFields) {
+ return in_array($field, $baseFields);
+ }, ARRAY_FILTER_USE_KEY);
+
+ foreach ($fields as $fieldName => $value) {
+ $config = $meta->config($fieldName);
+ //Check if it's a complex field
+ if (array_key_exists($fieldName, $complex)) {
+ foreach ($value as $row) {
+ foreach ($row as $fName => $fValue) {
+ if (in_array($fName, $complex[$fieldName])) {
+ $terms = $this->addTerms($fValue, $terms, $config);
+ }
+ }
+ }
+ } else {
+
+ $terms = $this->addTerms($value, $terms, $config);
+ }
+ }
+ }
+ return $terms;
+
+ }
+
+ protected function addTerms(string $value, array $terms, array $config):array
+ {
+ $taxonomy = jvbNoBase($config['taxonomy']);
+ if (empty($value)) {
+ return $terms;
+ }
+ $ids = array_map('absint', explode(',',$value));
+ $cache = Cache::for('term_data')->connect('taxonomy');
+ $cache->flush();
+ if (!array_key_exists($taxonomy, $terms)) {
+ $terms[$taxonomy] = [];
+ $registrar = Registrar::getInstance($taxonomy);
+ $terms[$taxonomy]['icon'] = $registrar ? $registrar->getIcon() : jvbDefaultIcon();;
+ }
+ foreach ($ids as $id) {
+ $data = $cache->remember(
+ $id,
+ function () use ($id, $taxonomy) {
+ $term = get_term($id, $taxonomy);
+ if ($term && !is_wp_error($term)) {
+ return [
+ 'id' => $term->term_id,
+ 'name' => $term->name,
+ 'slug' => $term->slug,
+ 'parent' => $term->parent,
+ 'path' => JVB()->routes('term')->getTermPath($term->term_id, $term->name, $taxonomy),
+ 'taxonomy' => jvbNoBase($term->taxonomy),
+ 'count' => $term->count,
+ ];
+ }
+ return [];
+ }
+ );
+ if (!empty($data)) {
+ $terms[$taxonomy][$id] = $data;
+ }
+ }
+ return $terms;
+ }
}
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index ea70a8b..d3f3068 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -14,6 +14,8 @@
use WP_Query;
use WP_REST_Request;
use WP_REST_Response;
+use WP_Term;
+use WP_Term_Query;
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
@@ -40,6 +42,7 @@
$this->cache->flush();
}
$this->cache->connect('post', true);
+ $this->cache->connect('term', true);
add_action('init', [$this, 'registerContentExecutors'], 5);
}
@@ -173,16 +176,12 @@
unset($data['id']);
error_log('[CONTENT]:'.print_r($data, true));
-
$queue = JVB()->queue();
$queue->queueOperation(
'content_update',
$user_id,
$data,
[
- 'count' => $count,
- 'chunk_key' => 'posts',
- 'chunk_size' => 10,
'operation_id' => $operationId
]
);
@@ -200,7 +199,24 @@
public function getContent(WP_REST_Request $request): WP_REST_Response
{
$params = $request->get_params();
- error_log('getContent params: '.print_r($params, true));
+ error_log('getContent::params '.print_r($params, true));
+
+ $registrar = Registrar::getInstance($params['content']);
+ switch ($registrar->getType()) {
+ case 'term':
+ return $this->getTerms($request, $params, $registrar);
+ case 'user':
+ //TODO maybe do something?
+ break;
+ case 'post':
+ return $this->getPosts($request, $params, $registrar);
+ }
+
+ return $this->error('Something went wrong, this does not appear to have a proper content type');
+ }
+
+ public function getPosts(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response
+ {
$user_id = $params['user'];
$post_status = $params['status'];
@@ -211,7 +227,6 @@
}
$post_type = str_replace('-', '_', jvbCheckBase($params['content']));
-
// Build query args
$args = [
'post_type' => $post_type,
@@ -222,15 +237,15 @@
'author' => $user_id,
'post_status' => $post_status
];
- //Only top level posts for timeline types
- $registrar = Registrar::getInstance($post_type);
- if ($registrar && $registrar->hasFeature('is_timeline')) {
+
+ //Only top level posts for timeline types
+ if ($registrar?->hasFeature('is_timeline')) {
$args['post_parent'] = 0;
}
//Calendar filters
- if ($registrar && $registrar->hasFeature('is_calendar')) {
+ if ($registrar?->hasFeature('is_calendar')) {
$args = $this->applyCalendarFilters($args, $params);
}
$taxonomies = array_filter($params, function ($param) {
@@ -257,6 +272,70 @@
$args['s'] = sanitize_text_field($params['search']);
}
+ $key = $this->cache->generateKey($args);
+ $cached = $this->checkCache($key, $request);
+ if ($cached) {
+ return $cached;
+ }
+
+ $this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
+
+ if (array_key_exists('s', $args)) {
+ $args = $this->applySearchFilters($args, $params);
+ }
+
+ // Run query
+ $query = new WP_Query($args);
+
+ $registrar = Registrar::getInstance($this->post_type);
+ $this->fields = $registrar->getFields()??[];
+ $this->taxonomies = $this->getTaxonomies($this->post_type);
+
+ $posts = array_map([$this, 'preparePost'], $query->posts);
+
+ $data = [
+ 'items' => $posts,
+ 'total' => $query->found_posts,
+ 'total_pages' => $query->max_num_pages,
+ 'has_more' => $args['paged']??1 < $query->max_num_pages,
+ ];
+
+
+ $this->cache->set($key, $data);
+
+ $response = Response::success($data);
+ return $this->addCacheHeaders($response);
+ }
+ public function getTerms(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response
+ {
+ // Build query args
+ $args = [
+ 'taxonomy' => jvbCheckBase($params['content']),
+ 'number' => $params['per_page'] ?? 30,
+ 'orderby' => 'name',
+ 'order' => 'DESC',
+ 'hide_empty' => false,
+ ];
+ $paged = $params['page']??1;
+ $args['page'] = $paged;
+ if ($paged > 1) {
+ $args['offset'] = ($paged-1) * $args['number'];
+ }
+
+ //TODO
+// if (array_key_exists('taxonomies', $params)) {
+// $args = $this->applyTaxonomyFilters($args, $params);
+// }
+// if (array_key_exists('date-filter', $params) || array_key_exists('dateFrom', $params)) {
+// $args = $this->applyDateFilters($args, $params);
+// }
+ if (array_key_exists('orderby', $params) || array_key_exists('order', $params)) {
+ $args = $this->applyOrderFilters($args, $params);
+ }
+
+ if (array_key_exists('search', $params)) {
+ $args['s'] = sanitize_text_field($params['search']);
+ }
$key = $this->cache->generateKey($args);
// Check HTTP cache headers with the specific content type
@@ -270,7 +349,6 @@
$response = Response::success($cache);
return $this->addCacheHeaders($response);
}
-
$this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
// Only expand search to taxonomies if we're actually going to query
if (array_key_exists('s', $args)) {
@@ -278,21 +356,33 @@
}
// Run query
- $query = new WP_Query($args);
+ $query = new WP_Term_Query($args);
- $registrar = Registrar::getInstance(str_replace('-', '_', $this->post_type));
- $this->fields = $registrar->getFields()??[];
-
- $this->taxonomies = $this->getTaxonomies($this->post_type);
- $posts = array_map([$this, 'prepareItem'], $query->posts);
-
+ $terms = $query->get_terms();
$data = [
- 'items' => $posts,
- 'total' => $query->found_posts,
- 'total_pages' => $query->max_num_pages,
- 'has_more' => $args['paged']??1 < $query->max_num_pages,
+ 'total' => 0,
+ 'total_pages' => 0,
+ 'has_more' => false
];
+ if (!is_wp_error($terms) && !empty($terms))
+ {
+ $total = get_terms([
+ 'taxonomy' => $args['taxonomy'],
+ 'hide_empty' => false,
+ 'fields' => 'count'
+ ]);
+ $data['total'] = $total;
+ $data['total_pages'] = max($total/$args['number'], 1);
+ $data['has_more'] = ($args['page'] * $args['number']) < $total;
+ } else {
+ $terms = [];
+ }
+
+ $this->fields = $registrar->getFields()??[];
+
+ $this->taxonomies = [];
+ $data['items'] =array_map([$this, 'prepareTerm'], $terms);
$this->cache->set($key, $data);
@@ -390,7 +480,7 @@
protected function getTaxonomies(string $content): array
{
$registrar = Registrar::getInstance($content);
- if (!$registrar) {
+ if (!$registrar || $registrar->getType()!== 'post') {
return [];
}
$out = [];
@@ -426,7 +516,7 @@
*
* @return array
*/
- protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true): array
+ protected function preparePost(WP_Post $post, bool $skip = false, bool $fields = true): array
{
$registrar = Registrar::getInstance($post->post_type);
if (!$skip && $registrar && $registrar->hasFeature('is_timeline')) {
@@ -449,104 +539,60 @@
'images' => [],
];
- // Add taxonomy terms
- foreach ($this->taxonomies as $taxonomy => $options) {
- $tax = str_replace(BASE, '', $taxonomy);
- $terms = wp_get_object_terms(
- $post->ID,
- $taxonomy,
- ['fields' => 'id=>name']
- );
- $data['taxonomies'][$tax] = [
- 'terms' => (is_wp_error($terms)) ? [] : $terms,
- 'name' => $options['label'],
- 'icon' => $tax
- ];
- }
-
- $images = $this->extractImages();
-
-
+ $images = $this->extractImages($fields, $this->meta);
if (!empty($images)) {
$data['images'] = $images;
}
+
+ $taxonomies = $this->extractTerms($fields, $this->meta);
+ if (!empty($taxonomies)) {
+ $data['taxonomies'] = $taxonomies;
+ }
return $data;
}
- protected function extractImages(array $fields = []): array
+ /**
+ * @param WP_Term $post the post object
+ *
+ * @return array
+ */
+ protected function prepareTerm(WP_Term $post, bool $fields = true): array
{
- //Extract images
- $images = [];
- $get = [];
- $fields = (empty($fields)) ? $this->fields : $fields;
- $get = $this->getUploadFields($fields);
- error_log('Upload fields: '.print_r($get, true));
+ $registrar = Registrar::getInstance($post->taxonomy);
- if (!empty($get)) {
- $actualGet = array_map(function($fieldName) {
- return (str_contains($fieldName, ':')) ? strtok($fieldName, ':') : $fieldName;
- }, $get);
- $complex = array_map(function($item) {
- return explode(':', $item);
- },array_filter($get, function($fieldName) {
- return str_contains($fieldName, ':');
- }));
+ $this->meta = Meta::forTerm($post->term_id);
+ $fields = ($fields) ? $this->meta->getAll() : [];
+ $data = [
+ 'id' => $post->term_id,
+ 'title' => $post->name,
+ 'date' => $fields['created_date']??'',
+ 'modified' => $fields['modified_date']??'',
+ 'thumbnail' => '',
+ 'icon' => $registrar->getIcon(),
+ 'taxonomies' => [],
+ 'fields' => $fields,
+ 'images' => [],
+ ];
- $allImages = $this->meta->getAll($actualGet);
- foreach ($allImages as $k => $imgs) {
- //It's a complex field
- if (is_array($imgs)) {
- foreach ($imgs as $row) {
- foreach ($row as $fName => $fValue) {
- foreach ($complex as $c) {
- if (in_array($k, $c)) {
- foreach ($c as $complexField) {
- if ($complexField === $fName) {
- $images = $this->addImages($fValue, $images);
- }
- }
- }
- }
- }
- }
- } else {
- $images = $this->addImages($imgs, $images);
- }
- }
- }
- return $images;
- }
- public function addImages(string $imgs, array $images):array
- {
- $temp = explode(',', $imgs);
- foreach ($temp as $img) {
- if (is_numeric($img) && !array_key_exists($img, $images)) {
- $images[$img] = jvbImageData((int)$img);
- }
- }
- return $images;
- }
- public function getUploadFields($fields):array
- {
- $get = [];
- foreach ($fields as $field => $config) {
- if (in_array($config['type'], ['group', 'repeater'])) {
- $nestedUploads = $this->getUploadFields($config['fields']);
- foreach ($nestedUploads as $nested) {
- $get[] = $field.':'.$nested;
- }
- } elseif ($config['type'] === 'upload' || $config['type'] === 'gallery' || $config['type'] === 'image' ) {
- $get[] = $field;
- }
+ $images = $this->extractImages($fields, $this->meta);
+ if (!empty($images)) {
+ $data['images'] = $images;
}
- return $get;
+ $taxonomies = $this->extractTerms($fields, $this->meta);
+ if (!empty($taxonomies)) {
+ $data['taxonomies'] = $taxonomies;
+ }
+
+ error_log('Term data: '.print_r($data, true));
+ return $data;
}
+
public function formatTimeline(WP_Post $post): array
{
- $item = $this->prepareItem($post, true, false);
+ $item = $this->preparePost($post, true, false);
//Step 1: Get the fields that apply to all posts
$mainMeta = Meta::forPost($post->ID);
$item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
diff --git a/inc/rest/routes/NewsRoutes.php b/inc/rest/routes/NewsRoutes.php
index 6a1fee5..6c25297 100644
--- a/inc/rest/routes/NewsRoutes.php
+++ b/inc/rest/routes/NewsRoutes.php
@@ -393,7 +393,6 @@
$m = $meta->set($key, $value);
$result[$key] = $m;
}
- $meta->save();
}
$this->cache->flush();
diff --git a/inc/rest/routes/SettingsRoutes.php b/inc/rest/routes/SettingsRoutes.php
index 227def2..b5d4926 100644
--- a/inc/rest/routes/SettingsRoutes.php
+++ b/inc/rest/routes/SettingsRoutes.php
@@ -145,8 +145,7 @@
}
$this->cache->flush();
}
- $tempMeta->save();
- $meta->save();
+
return [
'success' => true,
'result' => $results,
diff --git a/inc/rest/routes/ShopRoutes.php b/inc/rest/routes/ShopRoutes.php
index 923a74a..780b253 100644
--- a/inc/rest/routes/ShopRoutes.php
+++ b/inc/rest/routes/ShopRoutes.php
@@ -175,7 +175,6 @@
}
$meta->setAll($setData);
- $results = $meta->save();
// $results = [];
//
@@ -1385,10 +1384,8 @@
$userMeta->set($uMeta, $shops);
- $userMeta->save();
$artistMeta->set($aMeta, $shops);
- $artistMeta->save();
$owners = $shopMeta->get($sMeta);
$owners = ($owners === '') ? [] : explode(',', $shops);
@@ -1397,7 +1394,6 @@
}
$owners = implode(',', $owners);
$shopMeta->set($sMeta, $owners);
- $shopMeta->save();
return true;
}
}
diff --git a/inc/rest/routes/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index e46aa84..a31212e 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/inc/rest/routes/UploadRoutes.php
@@ -130,6 +130,8 @@
{
$data = $request->get_params();
$args = [];
+ $registrar = Registrar::getInstance($data['content']??'');
+
foreach ($data as $key => $value) {
switch ($key) {
case 'depends_on':
@@ -140,15 +142,25 @@
case 'item_id':
if (is_numeric($value)) {
$args['item_id'] = absint($value);
- if (!array_key_exists('post_id', $args)) {
- $args['post_id'] = absint($value);
+ if ($registrar) {
+ switch ($registrar->getType()) {
+ case 'post':
+ $args['post_id'] = absint($value);
+ break;
+ case 'term':
+ $args['term_id'] = absint($value);
+ break;
+ case 'user':
+ $args['user_id'] = absint($value);
+ break;
+ }
}
}
break;
// Post Type/Taxonomy
case 'content':
- $key = str_replace('-', '_', $key);
- if ($value === 'options' || array_key_exists($value, Registrar::getRegistered('post')) || Registrar::getInstance($key)->hasFeature('is_content')??false) {
+ $value = str_replace('-', '_', $value);
+ if ($value === 'options' || $registrar) {
$args['content'] = $value;
}
break;
@@ -640,7 +652,6 @@
// Update with comma-separated string
$meta->set($data['field_name'], implode(',', $all_ids));
- $meta->save();
}
/**
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index 5bb76e4..a5d0dc8 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -216,7 +216,6 @@
*/
public function addTaxonomyFilter(array $taxonomies, ?string $limit = null): self {
foreach($taxonomies as $taxonomy) {
- error_log('Fetchinig taxonomy: '.print_r($taxonomy, true));
$registrar = Registrar::getInstance($taxonomy);
if ($registrar) {
@@ -1555,7 +1554,10 @@
<input type="hidden" name="content" value="<?=$this->dataType?>" />
<div class="fields">
<?php
- echo Form::render('post_status', '', $this->getStatusFieldConfig('edit-'));
+ if (!empty($this->statuses)) {
+ echo Form::render('post_status', '', $this->getStatusFieldConfig('edit-'));
+ }
+
if (!empty($this->sections)) {
diff --git a/inc/utility/Image.php b/inc/utility/Image.php
index df5c2f3..83b95e7 100644
--- a/inc/utility/Image.php
+++ b/inc/utility/Image.php
@@ -72,8 +72,17 @@
if ($addLink) {
if (!$postSlug) {
- global $post;
- $postSlug = $post->post_name;
+ if (is_singular()) {
+ global $post;
+ $postSlug = $post->post_name;
+ }else if (is_tax()) {
+ $tax = get_queried_object();
+ $postSlug = jvbNoBase($tax->taxonomy);
+ }elseif (is_post_type_archive()) {
+ $obj = get_queried_object();
+ $postSlug = jvbNoBase($obj->post_type);
+ }
+
}
$full = wp_get_attachment_image_src($ID, 'full');
diff --git a/jvb.php b/jvb.php
index 6bbe5ef..b326bb1 100644
--- a/jvb.php
+++ b/jvb.php
@@ -11,6 +11,7 @@
use JVBase\JVB;
use JVBase\managers\IconsManager;
+use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
use JVBase\utility\Features;
@@ -76,8 +77,6 @@
}, 10, 3);
-const JVB_LOCAL = 'northeh.test';
-
function jvbIgnoredPostTypes():array
{
return [BASE.'directory', BASE.'dash', 'attachment', 'revision', 'nav_menu_item'];
@@ -397,6 +396,10 @@
wp_enqueue_script('jvb-page-nav');
}
+ if (has_block('jvb/summaryBlock')) {
+ wp_enqueue_script('jvb-page-nav');
+ }
+
// Only load on single shop pages or other relevant pages
if (is_tax(BASE.'shop') ||
is_singular(BASE.'partner')) {
@@ -507,6 +510,7 @@
//add_action('wp_head', 'jvbDumpIt');
function jvbDumpIt()
{
+
}
add_action('after_setup_theme', 'jvbImageSize');
diff --git a/src/fields/render.php b/src/fields/render.php
index 05636a1..1c2b31a 100644
--- a/src/fields/render.php
+++ b/src/fields/render.php
@@ -45,7 +45,7 @@
$meta = Meta::forPost($current->ID);
$artist = jvbContentFromUser((int)$current->post_author);
- $registrar = Registrar::getInstance($current->post_type));
+ $registrar = Registrar::getInstance($current->post_type);
$sections = [];
if ($registrar) {
$sections = $registrar->getSections();
--
Gitblit v1.10.0