From 181938ea32cf2c11d10d56018df22586adff7860 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 11 Jan 2026 17:07:14 +0000
Subject: [PATCH] =CRUD.js cleanup, DataStore.js filtering optimization
---
assets/js/min/crud.min.js | 2
assets/js/concise/PopulateForm.js | 2
assets/js/concise/TaxonomySelector.js | 67
assets/js/min/uploader.min.js | 2
inc/helpers/renderFields.php | 39
assets/js/concise/Queue.js | 155 +
inc/ui/CRUDSkeleton.php | 38
assets/css/dash.min.css | 2
assets/js/concise/DataStore.js | 154 +
assets/js/min/dataStore.min.js | 2
inc/rest/routes/ContentRoutes.php | 92 +
assets/js/min/selector.min.js | 2
inc/managers/CRUDManager.php | 2
assets/js/concise/HandleSelection.js | 332 ++-
inc/meta/MetaForm.php | 121
inc/rest/routes/UploadRoutes.php | 170 +-
assets/js/min/populate.min.js | 2
assets/js/min/queue.min.js | 2
assets/js/min/handleSelection.min.js | 2
assets/js/concise/CRUDOld.js | 778 ++++++++++
inc/registry/CheckCustomTables.php | 19
assets/js/concise/CRUD.js | 1896 ++++++++++++++++++-------
assets/js/concise/UploadManager.js | 333 ++--
23 files changed, 3,059 insertions(+), 1,155 deletions(-)
diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index 7435ee0..7bd590d 100644
--- a/assets/css/dash.min.css
+++ b/assets/css/dash.min.css
@@ -1 +1 @@
-:target{outline:0!important;padding:0!important}.dashboard .qtoggle{left:var(--btn_)}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace.replace{grid-column:full;padding:0 var(--btn_);max-width:none!important;margin:0!important}.replace .dashboard-page{max-width:var(--wide)}.group-display .item-grid{grid-template-columns:repeat(2,1fr)}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chipchip);padding:.5rem!important;min-width:0;min-height:var(--chipchip);width:var(--chipchip)}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--txt-small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--radius-outer);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}
\ No newline at end of file
+:target{outline:0!important;padding:0!important}.dashboard .qtoggle{left:var(--btn_)}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace.replace{grid-column:full;padding:0 var(--btn_);max-width:none!important;margin:0!important}.replace .dashboard-page{max-width:var(--wide)}.group-display .item-grid{grid-template-columns:repeat(2,1fr)}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}.list-view h3{font-size:var(--txt-medium)}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%;display:flex;justify-content:space-between}.all-filters summary [data-action=clear-filters]{--w:1em!important;width:max-content;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]{margin-left:auto;--w:1em!important;flex-wrap:nowrap;justify-content:flex-start;transition:var(--trans-size);display:flex;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]:focus,.all-filters [data-action=refresh]:hover{width:max-content}.all-filters [data-action=refresh] span{display:none;white-space:nowrap}.all-filters [data-action=refresh]:focus span,.all-filters [data-action=refresh]:hover span{display:block}.all-filters .btn+label{box-shadow:var(--shdw-none);color:var(--base-200)}.all-filters .radio-options input:not(.ch):checked+label{box-shadow:rgba(var(--base-rgb),var(--op-6)) var(--shdw-inset);color:var(--contrast-200);border-color:var(--contrast-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chip_);padding:.125rem!important;min-width:0;min-height:var(--chip_);width:var(--chip_)}.all-filters>.row{padding:.25rem 0}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--txt-small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--radius-outer);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}
\ No newline at end of file
diff --git a/assets/js/concise/CRUD.js b/assets/js/concise/CRUD.js
index 641a928..9e2e9bd 100644
--- a/assets/js/concise/CRUD.js
+++ b/assets/js/concise/CRUD.js
@@ -1,29 +1,182 @@
-/**
- * Main CRUD Manager - Coordinates everything
- */
class CRUDManager {
- constructor(config) {
+ constructor(){
+ this.container = document.querySelector('.crud[data-content]:not([data-ignore])');
+ if (!this.container) return;
+ 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.config = config;
- this.content = config.content || false;
- this.settings = window.jvbUserSettings;
this.a11y = window.jvbA11y;
- if (!this.content) {
- return;
- }
+ this.error = window.jvbError;
+ this.cache = new window.jvbCache(this.content);
+
+ this.activeItem = null;
this.isTimeline = false;
- this.currentItemID = null;
+ this.items = {
+ list: new Map(),
+ grid: new Map(),
+ table: new Map()
+ }; //DOM references
+ this.init();
+ }
+
+ init() {
this.initElements();
- this.updateBulkOptions();
+ this.initListeners();
+ let cached = this.initSettings();
+ this.initStore(cached);
+ this.checkHideFilters();
+ this.initIntegrations();
+ this.initUploader();
+ this.initModals();
+ }
- // Initialize components
+ 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 taxFilters = document.querySelectorAll('[data-filter="taxonomies"]');
+ if (taxFilters.length > 0) {
+ this.ui.filters.taxonomies = {};
+ taxFilters.forEach(tax => {
+ const taxonomy = tax.dataset.taxonomy;
+ this.ui.filters.taxonomies[taxonomy] = tax;
+ this.allowedFilters.push(`tax_${taxonomy}`);
+ });
+ }
+ this.isTimeline = !!document.querySelector('[data-timeline]');
+ }
+ initUploader() {
+ if (!this.ui.uploader) return;
+
+ window.jvbUploads.scanFields(this.ui.uploader);
+ window.jvbUploads.subscribe((event, data) => {
+ if (event === 'sent-to-queue') {
+ if (data === this.ui.uploader.dataset.uploader) {
+ window.debouncer.schedule('crud-complete', ()=> {
+ this.store.clearCache();
+ });
+ }
+ }
+ });
+ }
+ initModals() {
+ this.modals = {};
+ for (let [name, modal] of Object.entries(this.ui.modals)) {
+ if (!modal.modal) continue;
+ this.modals[name] = new window.jvbModal(modal.modal);
+
+ this.modals[name].subscribe((event, data) => {
+ switch (event) {
+ case 'modal-close':
+ this.activeItem = null;
+ const formId = this.ui.modals[name].form.dataset.formId;
+ if (formId) {
+ this.formController.cleanupForm(formId);
+ }
+
+ this.ui.modals[name].form.reset();
+
+ if (name === 'date') {
+ this.handleCustomDateSelection()
+ }
+ break;
+ case 'modal-open':
+
+ break;
+ }
+ })
+ }
+
+ }
+
+ initStore(cached) {
+ let filters = {
+ ... this.defaults,
+ ...cached
+ };
const store = window.jvbStore.register(
this.content,
{
storeName: this.content,
keyPath: 'id',
- endpoint: 'content',
+ endpoint: this.endpoint??'content', //for taxonomy stores
headers: {
'action_nonce': window.auth.getNonce('dash'),
},
@@ -34,52 +187,43 @@
{ name: 'modified', keyPath: 'modified'},
{ name: 'title', keyPath: 'title'}
],
- filters: {
- content: this.content,
- user: window.auth.getUser(),
- page: 1,
- status: 'all',
- orderby: 'modified', //or title
- order: 'desc'
- },
- TTL: 30 * 60 * 1000, //30 minutes cache
+ filters: filters,
+ ignore: ['content', 'user'],
+ TTL: 60 * 60 * 1000, //1 hour cache
showLoading: true,
- });
+ }
+ );
this.store = store[this.content];
- this.status = 'all';
- this.filterTimeout = null;
-
- this.viewController = new window.jvbViews(this.ui.container, this.store);
- this.tableForm = null;
- this.tableChanges = new Map();
-
-
- this.formController = (this.isTimeline) ? new window.jvbForm({collectFormData: () => this.collectTimelineData.bind(this)}) : new window.jvbForm();
- this.viewController.subscribe((event, form) => {
- if (event === 'table-view' && !this.tableForm) {
- if (!this.tableForm) {
- this.tableForm = this.formController.registerForm(form, {
- autosave: false,
- formStatus: false,
- isTable: true,
- });
- }
-
- } else if (event === 'not-table-view') {
- if (this.tableForm) {
-
- }
- } else if (event === 'order-changed') {
- let data = this.store.get(form);
- if (!data) {
- return;
- }
- let changes = {};
- changes[form] = data;
- this.savePosts(changes, `Updating progression order`);
+ this.store.subscribe((event, data) => {
+ switch (event) {
+ case 'data-loaded':
+ this.render();
+ this.selectionHandler.collectItems();
+ break;
+ }
+ })
+ }
+ initIntegrations() {
+ this.selected = new Set();
+ this.selectionHandler = new window.jvbHandleSelection(this.container, {
+ selectAll: {
+ checkbox: '.crud #select-all',
+ label: '.bulk-select label',
+ span: '.bulk-select label span'
+ },
+ wrapper: {
+ wrapper: '.wrap'
}
});
+ this.selectionHandler.subscribe((event, data) => {
+ this.selected = new Set([...data.selectedItems].map(id => parseInt(id)));
+ this.ui.bulk.control.hidden = this.selected.size === 0;
+ this.ui.bulk.count.hidden = this.selected.size === 0;
+ this.ui.bulk.count.textContent = `${this.selected.size} ${this.plural} selected`;
+ });
+
+ this.formController = new window.jvbForm();
this.formController.subscribe((event, data) => {
switch(event) {
@@ -91,406 +235,742 @@
});
this.queue.subscribe((event, data) => {
- if (!Object.hasOwn(data, 'endpoint') || !['content', 'uploads/groups'].includes(data.endpoint)) return;
- if (event === 'operation-completed') {
- this.handleQueueSuccess(event, data);
- } else if (event === 'operation-failed-permanent') {
- this.handleQueueFailure(event, data);
+ if (['image_upload', 'video_upload', 'document_upload'].includes(data.type)
+ && event === 'operation-status'
+ && data.status === 'completed') {
+ this.store.clearCache();
+ }
+ });
+ }
+
+ initSettings() {
+ this.defaults = {
+ content: this.content,
+ user: window.auth.getUser(),
+ page: 1,
+ status: 'all',
+ orderby: 'date',
+ order: 'desc',
+ search: '',
+ }
+
+ let updateFilters = {};
+ //current view (defaults to grid)
+ let defaultView = this.container.dataset.view??'grid'
+ this.view = this.cache.get('view')??defaultView;
+ if (this.view !== defaultView) {
+ this.ui.views[this.view].checked = true;
+ }
+ //current status (defaults to all)
+ this.status = this.cache.get('status')??this.defaults.status;
+ if (this.status !== this.defaults.status) {
+ this.ui.filters.status[this.status].checked = true;
+ updateFilters.status = this.status;
+ }
+ //orderby & order
+ this.orderby = this.cache.get('orderby')??this.defaults.orderby;
+ if (this.orderby !== this.defaults.orderby) {
+ this.ui.filters.orderby[this.orderby].checked = true;
+ updateFilters.orderBy = this.orderby;
+ }
+ this.order = this.cache.get('order')??this.defaults.order;
+ if (this.order !== this.defaults.order) {
+ this.ui.filters.order[this.order].checked = true;
+ updateFilters.order = this.order;
+ }
+
+ if (this.ui.filters.taxonomies) {
+ Object.entries(this.ui.filters.taxonomies).forEach(([taxonomy, element]) => {
+ const filterKey = `tax_${taxonomy}`;
+ const cached = this.cache.get(filterKey);
+ if (cached) {
+ element.value = cached;
+ updateFilters[filterKey] = cached;
+ }
+ });
+ }
+
+ let tabDirection = this.cache.get('tabNav')??'horizontal';
+ if (this.ui.table.nav && tabDirection === 'vertical') {
+ this.ui.table.nav.checked = true;
+ }
+
+
+
+ //Setup details open functionality
+ let details = {
+ showFilters: {
+ element: this.ui.filters.container,
+ default: 'closed',
+ },
+ showUploader: {
+ element: this.ui.uploader,
+ default: 'open'
+ }
+ };
+ for (let [name, conf] of Object.entries(details)) {
+ if (conf.element) {
+ let cached = this.cache.get(name)??conf.default;
+ conf.element.open = cached === 'open';
+ conf.element.addEventListener('toggle', ()=> {
+ this.cache.set(name, conf.element.open ? 'open' : 'closed');
+ });
+ }
+ }
+
+ return updateFilters;
+ }
+ /****************************************************************
+ EVENT LISTENERS
+ ****************************************************************/
+ initListeners() {
+ this.changeHandler = this.handleChange.bind(this);
+ this.clickHandler = this.handleClick.bind(this);
+ this.inputHandler = this.handleInput.bind(this);
+
+ document.addEventListener('change', this.changeHandler);
+ document.addEventListener('click', this.clickHandler);
+ if (this.ui.filters.search) {
+ this.ui.filters.search.addEventListener('input', this.inputHandler);
+ }
+ }
+ handleChange(e) {
+ const isSearch = window.targetCheck(e, this.selectors.filters.search);
+ if (isSearch) {
+ return;
+ }
+ const bulkAction = window.targetCheck(e, this.selectors.bulk.action);
+ if (bulkAction) {
+ this.handleBulkAction(bulkAction);
+ return;
+ }
+
+ let filter = window.targetCheck(e, '[data-filter]');
+ if (filter) {
+ this.handleFilterChange(filter);
+ return;
+ }
+
+ let view = window.targetCheck(e, 'input[data-view]');
+ if (view) {
+ this.handleViewChange(view);
+ return;
+ }
+
+ if (this.view === 'table') {
+ let target = window.targetCheck(e, '[data-id]');
+ if (target) {
+ this.handleTableChange(e);
+ return;
+ }
+
+ let multiSelect = window.targetCheck(e, 'details.multi-select');
+ if (multiSelect) {
+ this.toggleColumn(e.target.id, e.target.checked);
+ return;
+ }
+
+ let tabNav = window.targetCheck(e, this.selectors.table.nav);
+ if (tabNav) {
+ this.tabNav = tabNav.checked;
+ this.cache.set('tabNav', tabNav.checked ? 'vertical' : 'horizontal');
+ }
+ }
+ }
+ handleBulkAction(bulkAction) {
+ if (bulkAction.value.startsWith('tax-')) {
+ const selectedOption = bulkAction.options[bulkAction.selectedIndex];
+ const taxonomy = selectedOption.dataset.taxonomy;
+ const single = selectedOption.dataset.single;
+ const plural = selectedOption.dataset.plural;
+
+ window.jvbSelector.openEmpty(
+ taxonomy,
+ single,
+ plural,
+ (result) => this.handleBulkTaxonomy(result)
+ );
+ bulkAction.value = '';
+
+ return;
+ }
+ switch(bulkAction.value) {
+ case 'edit':
+ this.openBulkEditModal();
+ break;
+ case 'publish':
+ case 'trash':
+ case 'delete':
+ this.setBulkStatus(bulkAction.value);
+ break;
+ case 'draft':
+ case 'restore':
+ this.setBulkStatus('draft');
+ break;
+ }
+ }
+ handleBulkTaxonomy(result) {
+ if (!result.termIds.length || !this.selected.size) return;
+
+ const changes = {};
+ const taxonomyField = `tax_${result.taxonomy}`;
+
+ this.selected.forEach(itemID => {
+ const item = this.store.get(parseInt(itemID));
+ if (!item) return;
+
+ // Merge existing terms with new ones
+ const existingTerms = item.taxonomies?.[result.taxonomy] || [];
+ const existingIds = existingTerms.map(t => t.id);
+ const newIds = [...new Set([...existingIds, ...result.termIds])];
+
+ changes[itemID] = {
+ [taxonomyField]: newIds.join(','),
+ content: this.content
+ };
+ });
+
+ if (Object.keys(changes).length > 0) {
+ this.savePosts(
+ changes,
+ `Adding ${result.terms.length} ${result.taxonomy} to ${this.selected.size} ${this.plural}...`
+ );
+ }
+
+ this.selectionHandler.clearSelection();
+ }
+ handleFilterChange(target) {
+ let filter = target.dataset.filter;
+
+ if (filter === 'date' && target.value === 'custom') {
+ target.value = '';
+ this.modals.date.handleOpen();
+ return;
+ }
+
+ if (filter === 'date' && target.value !== '') {
+ this.setFilter('date-filter', target.value);
+ // Clear custom range
+ this.deleteFilter('dateFrom');
+ this.deleteFilter('dateTo');
+ this.checkHideFilters();
+ return;
+ }
+
+ if (filter === 'taxonomies') {
+ filter = `tax_${target.dataset.taxonomy}`;
+ }
+
+ this.setFilter(filter, target.value);
+ }
+ checkHideFilters() {
+ const filters = this.store.filters;
+ const hasActiveFilter = Object.entries(filters).some(([key, value]) => {
+ // Skip internal props
+ if (['content', 'user', 'page'].includes(key)) return false;
+ // Check if differs from default
+ return this.defaults[key] !== value && value !== '' && value !== null;
+ });
+
+ this.ui.buttons.clearFilters.hidden = !hasActiveFilter;
+ }
+ clearAllFilters() {
+ let currentFilters = this.store.filters;
+ this.store.clearFilters();
+ for (let [filter, value] of Object.entries(currentFilters)) {
+ this.cache.remove(filter);
+ this.deleteFilter(filter, value);
+ }
+ this.a11y.announce('All filters cleared');
+ }
+
+ handleCustomDateSelection() {
+
+ // Check if month select was used
+ if (this.ui.modals.date.month && this.ui.modals.date.month.value) {
+ const [year, month] = this.ui.modals.date.month.value.split('-');
+ const firstDay = `${year}-${month}-01`;
+ const lastDay = new Date(year, parseInt(month), 0).getDate();
+ const lastDayFormatted = `${year}-${month}-${String(lastDay).padStart(2, '0')}`;
+
+ this.setFilter('dateFrom', firstDay);
+ this.setFilter('dateTo', lastDayFormatted);
+
+ // Clear the regular date-filter
+ this.deleteFilter('date-filter');
+
+ // Reset month select for next time
+ this.ui.modals.date.month.value = '';
+ }
+ // Otherwise check custom range
+ else if (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);
+
+ // Clear the regular date-filter
+ this.deleteFilter('date-filter');
+
+ // Reset inputs for next time
+ this.ui.modals.date.start.value = '';
+ this.ui.modals.date.end.value = '';
+ }
+
+ this.checkHideFilters();
+ }
+ handleViewChange(view) {
+ this.view = view.dataset.view;
+ this.cache.set('view', this.view);
+ this.render();
+ }
+ handleClick(e) {
+ let clearSearch = window.targetCheck(e, '.clear-search');
+ if (clearSearch) {
+ this.deleteFilter('search', '');
+ }
+ let actionButton = window.targetCheck(e, '[data-action]');
+ if (actionButton) {
+ e.preventDefault();
+ let itemID = actionButton.dataset.id;
+
+ switch (actionButton.dataset.action) {
+ case 'edit':
+ this.openEditModal(itemID);
+ break;
+ case 'delete':
+ if (confirm('Delete this item? This cannot be undone')) {
+ let changes = {};
+ changes[itemID] = {
+ 'post_status': 'delete',
+ 'content': this.content
+ };
+ window.fade(actionButton.closest('.item'), false);
+ this.savePosts(changes, `Sending ${this.singular} to trash...`);
+ this.store.delete(itemID);
+ }
+ break;
+ case 'trash':
+ let changes = {};
+ changes[itemID] = {
+ 'post_status': 'trash',
+ 'content': this.content
+ };
+ window.fade(actionButton.closest('.item'), false);
+ this.savePosts(changes, `Sending ${this.singular} to trash...`);
+ break;
+ case 'bulk-edit':
+ if (this.selected.size > 0) {
+ this.openBulkEditModal();
+ }
+ break;
+ case 'bulk-delete':
+ if (this.selected.size > 0 && confirm(`Delete ${this.selected.size} items?`)) {
+ this.selected.forEach(id => this.store.delete(id));
+ this.selectionHandler.clearSelection();
+ }
+ break;
+ case 'refresh':
+ this.store.clearCache();
+ this.store.fetch();
+ break;
+ case 'clear-filters':
+ this.clearAllFilters();
+ break;
+ }
+ }
+
+ const applyDate = window.targetCheck(e, '.apply-date-filter');
+ if (applyDate) {
+ this.handleCustomDateSelection();
+ this.modals.date.handleClose();
+ return;
+ }
+
+ const createButton = window.targetCheck(e, this.selectors.buttons.create);
+ if (createButton) {
+ this.ui.modals.create.form.reset();
+ this.formController.registerForm(this.ui.modals.create.form);
+ this.modals.create.handleOpen();
+ }
+ }
+
+ handleInput(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ let query = e.target.value.trim();
+ let key = `${this.content}-search`;
+
+ console.log('Maybe search', query);
+ if (query.length === 0) {
+ this.deleteFilter('search', '');
+ return;
+ }
+
+ // Require minimum 2 characters
+ // if (query.length < 2) {
+ // return;
+ // }
+
+ window.debouncer.schedule(
+ key,
+ () => {
+ console.log('Searching for', query);
+ this.a11y.announce(`Searching for "${query}"...`);
+ this.store.setFilters({ search: query, page: 1 });
+ },
+ 300
+ );
+ }
+
+ handleKeys(e) {
+ if (!this.tabNav) return;
+
+ if (e.key === 'Tab') {
+ e.preventDefault();
+
+ const currentCell = e.target.closest('[data-field]');
+ const currentRow = e.target.closest('tr');
+
+ if (!currentCell || !currentRow) return;
+
+ const fieldName = currentCell.dataset.field;
+ const isShift = e.shiftKey;
+
+ // Find next editable row
+ let targetRow = this.findNextEditableRow(currentRow, isShift);
+
+ // If no target row found, wrap around
+ if (!targetRow) {
+ targetRow = this.wrapToRow(currentRow, isShift);
+ }
+
+ if (targetRow) {
+ this.focusFieldInRow(targetRow, fieldName, isShift);
+ }
+ }
+ }
+ findNextEditableRow(currentRow, goBackward = false) {
+ let row = goBackward ? currentRow.previousElementSibling : currentRow.nextElementSibling;
+
+ // For timeline tables, skip non-editable rows
+ while (row && !this.isEditableRow(row)) {
+ row = goBackward ? row.previousElementSibling : row.nextElementSibling;
+ }
+
+ return row;
+ }
+
+ wrapToRow(currentRow, goBackward = false) {
+ if (this.isTimeline) {
+ // For timeline, stay within the same tbody
+ const tbody = currentRow.closest('tbody');
+ if (!tbody) return null;
+
+ const rows = Array.from(tbody.querySelectorAll('tr'))
+ .filter(row => this.isEditableRow(row));
+
+ return goBackward ? rows[rows.length - 1] : rows[0];
+ } else {
+ // For regular tables, use all rows in tbody
+ if (!this.ui.table.body) return null;
+
+ const rows = Array.from(this.ui.table.body.querySelectorAll('tr'))
+ .filter(row => this.isEditableRow(row));
+
+ return goBackward ? rows[rows.length - 1] : rows[0];
+ }
+ }
+ isEditableRow(row) {
+ // Skip thead/tfoot
+ if (row.closest('thead') || row.closest('tfoot')) {
+ return false;
+ }
+
+ // For timeline, check for specific classes
+ if (this.isTimeline) {
+ return row.classList.contains('shared') || row.classList.contains('timeline-point');
+ }
+
+ // For regular tables, check for data-id
+ return !!row.dataset.id;
+ }
+
+ focusFieldInRow(row, fieldName, fromAbove = false) {
+ const targetCell = row.querySelector(`[data-field="${fieldName}"]`);
+ if (!targetCell) return;
+
+ const input = this.findFocusableInput(targetCell);
+ if (input) {
+ input.focus();
+
+ // Select text if it's a text input
+ if (input.select && input.type === 'text') {
+ input.select();
+ }
+
+ // Announce for accessibility
+ const direction = fromAbove ? 'next' : 'previous';
+ this.a11y?.announce(`Moved to ${fieldName} in ${direction} row`);
+ }
+ }
+
+ findFocusableInput(cell) {
+ const selectors = [
+ 'input:not([type="hidden"]):not([disabled])',
+ 'textarea:not([disabled])',
+ 'select:not([disabled])',
+ 'button:not([disabled])'
+ ];
+
+ for (const selector of selectors) {
+ const element = cell.querySelector(selector);
+ if (element) return element;
+ }
+
+ return null;
+ }
+
+ toggleTimelineListeners(on = true) {
+ if (!this.isTimeline || this.view !== 'table') return;
+
+ // Cleanup existing instances
+ if (this.timelineSortables) {
+ this.timelineSortables.forEach(sortable => sortable.destroy());
+ this.timelineSortables = [];
+ }
+
+ if (!on) return;
+
+ // Initialize sortable for each tbody
+ this.timelineSortables = [];
+ const tbodies = this.ui.table.form.querySelectorAll('tbody.item');
+
+ tbodies.forEach(tbody => {
+ const sortable = new Sortable(tbody, {
+ animation: 150,
+ handle: '.drag-handle',
+ draggable: 'tr.timeline-point',
+ ghostClass: 'sortable-ghost',
+ chosenClass: 'sortable-chosen',
+ dragClass: 'sortable-drag',
+
+ // Prevent dragging between different tbodies
+ group: {
+ name: `timeline-${tbody.dataset.id}`,
+ pull: false,
+ put: false
+ },
+
+ onEnd: (evt) => this.handleTimelineReorder(evt)
+ });
+
+ this.timelineSortables.push(sortable);
+ });
+ }
+
+ handleTimelineReorder(evt) {
+ const { item: draggedRow, oldIndex, newIndex, from: tbody } = evt;
+
+ // No change
+ if (oldIndex === newIndex) return;
+
+ const itemID = parseInt(tbody.dataset.id);
+ const item = this.store.get(itemID);
+ if (!item?.fields?.timeline) return;
+
+ // Get current order of image IDs from DOM
+ const newOrder = Array.from(tbody.querySelectorAll('tr.timeline-point'))
+ .map(row => row.dataset.imageId)
+ .filter(Boolean);
+
+ // Rebuild timeline object in new order
+ const reorderedTimeline = {};
+ newOrder.forEach((imgId, index) => {
+ if (item.fields.timeline[imgId]) {
+ reorderedTimeline[imgId] = {
+ ...item.fields.timeline[imgId],
+ order: index
+ };
}
});
+ item.fields.timeline = reorderedTimeline;
- // Track initialization
- this.initialized = false;
+ // Save to store and server
+ this.store.save(item);
+ this.savePosts(
+ { [itemID]: { timeline: reorderedTimeline, content: this.content } },
+ 'Reordering timeline...',
+ true
+ );
- this.init();
+ this.a11y?.announce(`Moved to position ${newIndex + 1}`);
}
+
+ /*******************************************************************
+ MODALS
+ *******************************************************************/
+ openEditModal(itemID) {
+ let item = this.store.get(parseInt(itemID));
+ if (!item) return;
+ this.activeItem = itemID;
+ 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}`;
+ this.ui.modals.edit.form.dataset.formId = `edit-${itemID}`;
+
+ this.ui.modals.edit.form.reset();
+ new window.jvbPopulate(this.ui.modals.edit.form, item);
+ this.formController.registerForm(this.ui.modals.edit.form);
+
+ this.modals.edit.handleOpen();
+ }
+ openBulkEditModal() {
+ window.removeChildren(this.ui.modals.bulkEdit.selected);
+ this.ui.modals.edit.form.reset();
+ this.modals.bulkEdit.handleOpen();
+ this.selected.forEach(itemId => {
+ let template = window.getTemplate('bulkItem');
+ if (!template) return;
+ let item = this.store.get(parseInt(itemId));
+ if (!item) return;
+ let [checkbox, img, label] = [template.querySelector('input'), template.querySelector('img'), template.querySelector('label')];
+ if (checkbox) {
+ checkbox.id = `bulk_${item.id}`;
+ checkbox.value = item.id;
+ checkbox.checked = true;
+ checkbox.name = 'selected[]';
+ }
+
+ let thumbnail = item.images[item.fields.post_thumbnail]??{};
+ if (img) {
+ img.src = thumbnail.medium??'';
+ img.alt = thumbnail.alt??''
+ }
+
+ label.title = item.fields['post_title']??'';
+ this.ui.modals.bulkEdit.selected.append(template);
+ });
+ if (this.ui.modals.bulkEdit.h2) {
+ this.ui.modals.bulkEdit.h2.textContent = this.selected.size;
+ }
+ this.formController.registerForm(this.ui.modals.bulkEdit.form);
+ }
+
+ /*****************************************************************
+ FIELD HANDLING
+ *****************************************************************/
handleFormChange(event, data) {
let title = data.fullData.post_title;
let changes = (Object.hasOwn(data, 'changes')) ? data.changes : data.fullData;
let theChanges = {};
if (this.isTimeline) {
- theChanges[this.currentItemID] = changes;
- this.savePosts(theChanges, title);
+ theChanges[this.activeItem] = changes;
+ if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
+ this.removeItems([this.activeItem]);
+ }
+ this.savePosts(theChanges, title, event === 'form-submit');
return;
}
- let itemsToRemove = [];
- console.log(data);
- switch (true) {
- case data.config.element === this.ui.forms.edit:
- theChanges[this.currentItemID] = changes;
- title = `Saving ${title} Changes`;
- // Check if status change requires removal
- if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
- itemsToRemove.push(this.currentItemID);
+ let remove = [];
+ let el = data.config.element;
+ if (el === this.ui.modals.edit.form) {
+ theChanges[this.activeItem] = changes;
+ title = `Saving ${title} Changes`;
+ if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
+ remove.push(this.activeItem);
+ }
+ } else if (el === this.ui.modals.bulkEdit.form) {
+ let num = 0;
+ el.querySelectorAll('.selected input:checked').forEach(selected => {
+ theChanges[selected.value] = changes;
+ if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
+ remove.push(selected.value);
}
- break;
- case data.config.element === this.ui.forms.bulkEdit:
- let selected = data.config.element.querySelectorAll('.selected input:checked');
- selected.forEach(sel => {
- theChanges[sel.value] = changes;
- // Check if status change requires removal
- if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
- itemsToRemove.push(sel.value);
- }
- });
-
- title = `Updating ${selected.length} ${this.config.plural??'posts'} Changes`;
- break;
- case data.config.element === this.ui.forms.create:
- if (event === 'form-submit') {
- theChanges[data.config.data['form-id']] = changes;
- title = `Saving ${title} Changes`;
- }
- break;
- }
-
- // Handle visual removal with stagger effect
- if (itemsToRemove.length > 0) {
- let delay = 0;
- itemsToRemove.forEach(itemId => {
- setTimeout(() => {
- const element = document.querySelector(`.item[data-id="${itemId}"]`);
- if (element) {
- window.fade(element, false);
- }
- }, delay);
- delay += 50; // Stagger by 50ms
+ num++;
});
-
- // Clear selection after bulk edit with staggered removal
- if (data.config.element === this.ui.forms.bulkEdit) {
- setTimeout(() => {
- this.viewController.clearSelection();
- }, delay + 100);
+ title = `Updating ${num} ${this.plural} Changes`;
+ } else if (el === this.ui.modals.create.form) {
+ if (event === 'form-submit') {
+ theChanges[el.dataset.formId] = changes;
+ title = `Saving ${title} Changes`;
}
}
-
+ if (remove.length > 0) {
+ this.removeItems(remove);
+ }
+ this.selectionHandler.clearSelection();
if (Object.keys(theChanges).length === 0) {
return;
}
-
- this.savePosts(theChanges, title);
+ this.savePosts(theChanges, title, event === 'form-submit');
}
+ savePosts(changes, title, delay = false) {
+ if (Object.keys(changes).length === 0) return;
- shouldRemoveItem(newStatus) {
- return (this.status === 'all' && !['publish', 'draft'].includes(newStatus)) ||
- (newStatus !== this.status);
- }
-
- savePosts(changes, title) {
- if (Object.keys(changes).length === 0) {
- return;
- }
-
- //ensure content is in each post
- for (let postId in changes) {
- if (!changes[postId]['content']) {
- changes[postId]['content'] = this.content;
+ let validChanges = {};
+ for (let itemId in changes) {
+ // Validate ID exists and is not null/undefined
+ if (!itemId || itemId === 'null' || itemId === 'undefined') {
+ console.warn('Skipping save for invalid ID:', itemId);
+ continue;
}
+ if (!changes[itemId]['content']) changes[itemId]['content'] = this.content;
+
+ validChanges[itemId] = changes[itemId];
}
+
let operation = {
- endpoint: 'content',
+ endpoint: this.endpoint,
headers: {
'action_nonce': window.auth.getNonce('dash'),
},
data: {
- posts: changes,
+ posts: validChanges,
},
- delay: true,
+ delay: delay,
popup: `Saving changes`,
title: title
};
-
this.queue.addToQueue(operation);
-
- }
- async handleQueueSuccess(event, data) {
- this.store.clearCache();
- this.store.fetch();
- }
- handleQueueFailure(event, data) {
- console.error('Operation failed permanently:', data);
- // Optionally show error notification to user
- this.a11y?.announce(`Operation failed: ${data.error_message || 'Unknown error'}`);
}
- initElements() {
- this.elements = {
- modals: {
- create: 'dialog.create',
- edit: 'dialog.edit',
- bulkEdit: 'dialog.bulkEdit'
- },
- container: '.crud[data-content]',
- grid: '.item-grid',
- bulkSelectActions: '.bulk-action-select',
- forms: {
- create: 'dialog.create form',
- edit: 'dialog.edit form',
- bulkEdit: 'dialog.bulkEdit form'
- },
- uploader: 'details.uploader'
- };
- this.ui = window.uiFromSelectors(this.elements);
- if (this.ui.uploader) {
- window.jvbUploads.scanFields(document.querySelector(this.elements.uploader));
-
- window.jvbUploads.subscribe((event, data) => {
- if (event === 'sent-to-queue') {
- console.log(data);
- if (data === this.ui.uploader.querySelector('[data-uploader]')?.dataset.uploader) {
- window.debouncer.schedule('crud-complete', ()=> {
- this.store.clearCache();
- });
- }
- }
- });
- }
- this.isTimeline = !!document.querySelector('[data-timeline]');
- }
- init() {
- if (this.ui.uploader){
- this.settings.addSetting(this.ui.uploader, 'open');
- this.ui.uploader.addEventListener('toggle', (e) =>{
- this.settings.saveSetting('open', this.ui.uploader.open ? 'on' : 'off');
- });
- }
-
- // Set up filter controls
- this.filterHandler = this.handleFilterChange.bind(this);
- this.changeHandler = this.handleChange.bind(this);
-
-
-
- this.modals = {};
- for (let [name, modal] of Object.entries(this.ui.modals)) {
- this.modals[name] = new window.jvbModal(modal);
-
- this.modals[name].subscribe((event, data) => {
- switch (event) {
- case 'modal-close':
- this.currentItemID = null;
- this.formController.cleanupForm(this.modals[name].modal.querySelector('form').dataset.formId);
- //double check we have finished saving
- break;
- case 'modal-open':
- //probably not needed in this class
- break;
- }
- });
- }
-
- // Set up global event delegation
- this.setupEventDelegation();
-
- this.setupFilters();
-
- this.initialized = true;
- }
-
- setupEventDelegation() {
- document.addEventListener('change', this.changeHandler);
- // Single event listener for all CRUD actions
- document.addEventListener('click', (e) => {
- // Check for action buttons
- const actionBtn = e.target.closest('[data-action]');
- if (actionBtn) {
- e.preventDefault();
- const action = actionBtn.dataset.action;
- const id = actionBtn.dataset.id;
-
- switch(action) {
- case 'edit':
- this.populateEditForm(id);
- this.modals.edit.handleOpen();
- break;
-
- case 'delete':
- if (confirm('Delete this item?')) {
- let changes = {};
- changes[actionBtn.dataset.id] = {
- 'post_status': 'delete',
- 'content': this.content
- };
- window.fade(actionBtn.closest('.item'), false);
- this.savePosts(changes, `Sending ${this.singular} to trash...`);
- this.store.delete(id);
- }
- break;
- case 'trash':
- let changes = {};
- changes[actionBtn.dataset.id] = {
- 'post_status': 'trash',
- 'content': this.content
- };
- window.fade(actionBtn.closest('.item'), false);
- this.savePosts(changes, `Sending ${this.singular} to trash...`);
- break;
-
- case 'create':
- this.modals.create.dataset.itemId = 'new';
- this.modals.create.dataset.content = this.content;
- this.modals.create.handleOpen();
- break;
-
- case 'bulk-edit':
- const selected = Array.from(this.viewController.selectedItems);
- if (selected.length > 0) {
-
- this.modals.bulkEdit.handleOpen();
- }
- break;
-
- case 'bulk-delete':
- const toDelete = Array.from(this.viewController.selectedItems);
- if (toDelete.length > 0 && confirm(`Delete ${toDelete.length} items?`)) {
- toDelete.forEach(id => this.store.delete(id));
- this.viewController.clearSelection();
- }
- break;
-
- case 'sync':
- // this.store.syncQueue();
- break;
-
- case 'refresh':
- this.store.fetch();
- break;
- }
- }
-
- let createButton = e.target.closest('.create-item');
- if (createButton) {
- this.formController.registerForm(this.ui.forms.create);
- this.modals.create.handleOpen();
- }
-
- let clearSelection = e.target.closest('.cancel-bulk');
- if (clearSelection) {
- this.viewController.selectAll(false);
- }
- });
-
- // Keyboard shortcuts
- document.addEventListener('keydown', (e) => {
- // Ctrl/Cmd + A to select all
- if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
- if (this.ui.container && this.ui.container.contains(document.activeElement)) {
- e.preventDefault();
- this.viewController.selectAll();
- }
- }
-
- // ESC to clear selection
- if (e.key === 'Escape' && this.viewController?.selectedItems.size > 0 && window.jvbModal.getAllModals().length === 0) {
- this.viewController.clearSelection();
- }
- });
- }
- handleChange(e) {
- if (e.target.closest('[data-id]')) {
- if (this.isTimeline) {
- this.handleTimelineTableChange(e);
- } else {
- this.handleTableChange(e);
- }
- return;
- }
- if (e.target.classList.contains('bulk-action-select')) {
- if (e.target.value.startsWith('tax-')) {
- const taxonomy = e.target.value.replace('tax-', '');
- this.openTaxonomyModal(taxonomy);
- e.target.value = '';
- return;
- }
-
- switch (e.target.value) {
- case 'edit':
- this.populateBulkEdit();
- this.modals.bulkEdit.handleOpen();
- break;
- case 'publish':
- this.setBulkStatus('publish');
- break;
- case 'draft':
- this.setBulkStatus('draft');
- break;
- case 'trash':
- this.setBulkStatus('trash');
- break;
- case 'restore':
- this.setBulkStatus('draft');
- break;
- case 'delete':
- this.setBulkStatus('delete');
- break;
- }
- }
- if (window.targetCheck(e, 'select[data-filter]')) {
- this.handleFilterChange(e);
- }
- }
handleTableChange(e) {
- const row = e.target.closest('tr[data-id]');
- if (!row) return;
+ const container = this.isTimeline
+ ? e.target.closest('tbody[data-id]')
+ : e.target.closest('tr[data-id]');
+ if (!container) return;
const input = e.target;
- const postID = parseInt(row.dataset.id);
- const fieldName = input.closest(['data-field'])?.dataset.field;
- if (!fieldName) return;
+ const field = input.closest('[data-field]')?.dataset.field;
+ if (!field) return;
- const item = this.store.get(postID);
- if (!item) return;
-
- item.fields[fieldName] = this.getInputValue(input);
-
- this.store.save(item);
-
- let post = {};
- post[postID] = item.fields;
- this.savePosts(post, `Saving changes to ${this.content}`);
- }
- handleTimelineTableChange(e) {
- const tbody = e.target.closest('tbody[data-id]');
- if (!tbody) return;
-
- const input = e.target;
- const fieldName = input.closest('[data-field]')?.dataset.field;
-
- if (!fieldName) return;
-
- const parentID = parseInt(tbody.dataset.id);
- const timelinePoint = input.closest('tr.timeline-point');
-
- const item = this.store.get(parentID);
+ const itemID = parseInt(container.dataset.id);
+ const item = this.store.get(itemID);
if (!item) return;
const value = this.getInputValue(input);
- // Check if this is a specific point, or a shared value
- if (timelinePoint) {
- const imgID = timelinePoint.dataset.imageId;
- if (!item.fields.timeline) {
- item.fields.timeline = {};
+ // Timeline-specific: check if it's a point or shared value
+ if (this.isTimeline) {
+ const timelinePoint = input.closest('tr.timeline-point');
+ if (timelinePoint) {
+ const imgID = timelinePoint.dataset.imageId;
+ item.fields.timeline ??= {};
+ item.fields.timeline[imgID] ??= {};
+ item.fields.timeline[imgID][field] = value;
+ } else {
+ item.fields[field] = value;
}
- if (!item.fields.timeline[imgID]) {
- item.fields.timeline[imgID] = {};
- }
- item.fields.timeline[imgID][fieldName] = value;
} else {
- item.fields[fieldName] = value;
+ item.fields[field] = value;
}
- //Update store directly
this.store.save(item);
-
- let changes = {};
- changes[parentID] = item.fields;
- this.savePosts(changes, 'Updating progress post');
+ this.savePosts({ [itemID]: item.fields }, `Saving changes...`, true);
}
+
getInputValue(input) {
if (input.type === 'checkbox') {
return input.checked ? (input.value || '1') : '';
@@ -500,54 +980,16 @@
}
return input.value;
}
-
- openTaxonomyModal(taxonomy) {
- // Check if jvbSelector exists
- if (!window.jvbSelector) {
- console.error('TaxonomySelector not initialized');
- return;
- }
-
- // Open the selector in filter mode
- window.jvbSelector.openForFilter(
- taxonomy,
- (selectedIds, taxonomy) => this.handleBulkTaxonomy(selectedIds, taxonomy)
- );
- }
- handleBulkTaxonomy(selectedIds, taxonomy) {
- // Callback when terms are selected
- if (selectedIds.length > 0) {
- selectedIds = selectedIds.join(',');
- let changes = {};
- let selected = Array.from(this.viewController.selectedItems);
-
-
- selected.forEach(sel => {
- changes[sel] = {
- content: this.content
- };
- changes[sel][taxonomy] = selectedIds;
- });
-
-
- let title = `Adding ${selected.length} ${this.config.plural??'posts'} to ${selectedIds.length} ${jvbSettings.labels[taxonomy].plural}`;
- this.viewController.clearSelection();
- this.savePosts(changes, title);
- }
- }
-
setBulkStatus(status) {
- if (!['publish', 'draft', 'trash', 'delete'].includes(status)){
- return;
- }
+ if (!['publish', 'draft', 'trash', 'delete'].includes(status)) return;
let changes = {};
- for (let selected of this.viewController.selectedItems) {
- changes[selected] = {
+ this.selected.forEach(itemID => {
+ changes[itemID] = {
post_status: status,
content: this.content
};
- }
+ });
let title;
switch (status) {
case 'delete':
@@ -556,150 +998,474 @@
default:
title = window.uppercaseFirst(status)+'ing';
}
-
- if ((this.status === 'all' && !['publish', 'draft'].includes(status)) || status !== this.status) {
- let delay = 0;
- for (let selected of this.viewController.selectedItems) {
- setTimeout(() => {
- const element = document.querySelector(`.item[data-id="${selected}"]`);
- if (element) {
- window.fade(element, false);
- }
- }, delay);
- delay += 50; // Increment delay for staggered effect
- }
+ if (this.shouldRemoveItemUI(status)) {
+ this.removeItems(Object.keys(changes));
}
- // Clear selection even if items aren't being removed
- this.viewController.clearSelection();
-
-
+ this.selectionHandler.clearSelection();
if (Object.keys(changes).length !== 0) {
- this.savePosts(changes, `${title} ${this.viewController.selectedItems.size} ${this.plural}...`);
+ this.savePosts(changes, `${title} ${Object.keys(changes).length} ${this.plural}...`);
+ }
+ }
+ /***************************************************************
+ VIEW
+ ***************************************************************/
+ render() {
+ const items = this.store.getFiltered();
+ if (items.length === 0) {
+ this.renderEmpty();
+ return;
+ }
+
+ switch (this.view) {
+ case 'grid':
+ this.renderGrid(items);
+ break;
+ case 'table':
+ this.renderTable(items);
+ break;
+ case 'list':
+ this.renderList(items);
+ break;
+ }
+ this.updateUI();
+ }
+ updateUI() {
+ if (this.ui.bulk.action) {
+ let options = false;
+ let hasEdit =this.ui.bulk.action.querySelector('[value="edit"]');
+ if (this.status === 'trash' && hasEdit) {
+ window.removeChildren(this.ui.bulk.action);
+ options = window.getTemplate('trashOptions');
+ } else if (this.status !== 'trash' && !hasEdit) {
+ window.removeChildren(this.ui.bulk.action);
+ options = window.getTemplate('notTrashOptions');
+ }
+ if (options) {
+ options.querySelectorAll('option').forEach((option, index)=> {
+ if (index === 0) option.checked = true;
+ this.ui.bulk.action.append(option);
+ });
+ }
+ this.ui.bulk.action.value = '';
+ }
+ if (this.selected.size > 0) {
+ this.selectionHandler.updateSelectionUI();
}
}
- handleFilterChange(e) {
- let target = e.target;
- let filter = target.dataset.filter;
- if (filter === 'taxonomies') {
- let taxonomy = target.dataset.taxonomy;
- this.store.setFilter(`tax_${taxonomy}`, target.value);
- } else {
- this[target.dataset.filter] = target.value;
- this.store.setFilter(target.dataset.filter, target.value);
- if (target.dataset.filter === 'status') {
- this.updateBulkOptions(target.value);
- }
+ renderEmpty() {
+ this.toggleTable(false);
+ window.removeChildren(this.ui.grid);
+ const empty = window.getTemplate('emptyState');
+ if (empty) {
+ this.ui.grid.append(empty);
+ this.a11y.announceItems(0,false,false);
}
}
- updateBulkOptions(status = 'all') {
- if (status === 'trash') {
- if (this.ui.bulkSelectActions?.querySelector('[value="edit"]')) {
- window.removeChildren(this.ui.bulkSelectActions);
- let options = window.getTemplate('trashOptions');
- options.querySelectorAll('option').forEach((option, index) => {
- if (index === 0) {
- option.checked = true;
+
+ toggleTable(on = true) {
+ if (this.ui.table.selectedColumns) this.ui.table.selectedColumns.hidden = !on;
+
+ if (on && !this.ui.table.form) {
+ let table = window.getTemplate('contentTable');
+ this.container.append(table);
+ this.ui.table = window.uiFromSelectors(this.selectors.table);
+ this.ui.table.columns = this.container.querySelectorAll(this.selectors.table.columns);
+ }
+
+ if (this.ui.table.form) {
+ this.ui.table.form.hidden = !on;
+ if (on) {
+ this.formController.registerForm(this.ui.table.form, {
+ autosave: false,
+ formStatus: false,
+ isTable: true
+ });
+ } else {
+ this.formController.cleanupForm(this.ui.table.form.dataset.formId)
+ }
+ if (this.ui.table.body) {
+ window.removeChildren(this.ui.table.body);
+ }
+ }
+ this.keyHandler = this.handleKeys.bind(this);
+ if (on) {
+ document.addEventListener('keydown', this.keyHandler);
+ } else {
+ document.removeEventListener('keydown', this.keyHandler);
+ }
+ if (this.isTimeline && !on) this.toggleTimelineListeners(on);
+ }
+
+ renderGrid(items) {
+ window.removeChildren(this.ui.grid);
+ this.toggleTable(false);
+
+ this.ui.grid.classList.remove('list-view');
+ this.ui.grid.classList.add('grid-view');
+
+ const fragment = document.createDocumentFragment();
+ items.forEach(item => {
+ let card = this.renderGridItem(item);
+ fragment.appendChild(card);
+ });
+ this.ui.grid.appendChild(fragment);
+ }
+
+ renderList(items) {
+ window.removeChildren(this.ui.grid);
+ this.toggleTable(false);
+
+ this.ui.grid.classList.remove('grid-view');
+ this.ui.grid.classList.add('list-view');
+ const fragment = document.createDocumentFragment();
+ items.forEach(item => {
+ let row = this.renderListItem(item);
+ fragment.append(row);
+ });
+ this.ui.grid.append(fragment);
+ }
+
+ renderTable(items) {
+ this.toggleTable();
+ window.removeChildren(this.ui.grid);
+
+ let fragment = document.createDocumentFragment();
+ items.forEach(item => {
+ let row = this.renderTableItem(item);
+ if (row) fragment.append(row);
+ });
+
+ if (this.ui.table.body) {
+ this.ui.table.body.append(fragment);
+ } else {
+ this.ui.table.table.insertBefore(fragment, this.ui.table.foot);
+ }
+
+ if (this.isTimeline) {
+ this.toggleTimelineListeners(true);
+ }
+ }
+
+ /***************************************************************
+ RENDER HELPERS
+ ***************************************************************/
+ setupItemElement(element, item, templateName) {
+ if (this.items[this.view].has(item.id)) {
+ return this.items[this.view].get(item.id);
+ }
+
+ element.dataset.id = item.id;
+ if (item._pending) element.classList.add('pending');
+
+ // Setup selection checkbox/label
+ this.setupItemSelection(element, item);
+
+ // Setup thumbnail
+ this.setupItemThumbnail(element, item);
+
+ // Setup action buttons
+ this.setupItemActions(element, item);
+
+ this.items[this.view].set(item.id, element);
+ return element;
+ }
+
+ setupItemSelection(element, item) {
+ const checkbox = element.querySelector('.select-item');
+ const label = element.querySelector('.select-item + label, .select-item-label');
+ if (!checkbox) return;
+
+ checkbox.id = `select-${item.id}`;
+ checkbox.value = item.id;
+ checkbox.checked = this.selected.has(parseInt(item.id));
+
+ if (label) {
+ label.htmlFor = `select-${item.id}`;
+ }
+ }
+
+ setupItemThumbnail(element, item) {
+ const img = element.querySelector('img');
+ if (!img) return;
+
+ const thumbnail = item.images?.[item.fields?.post_thumbnail] ?? {};
+ img.src = thumbnail.medium ?? '';
+ img.alt = thumbnail.alt ?? '';
+ }
+
+ setupItemActions(element, item) {
+ element.querySelectorAll('[data-action]').forEach(btn => {
+ btn.dataset.id = item.id;
+ });
+ }
+
+ renderGridItem(item) {
+ if (this.items.grid.has(item.id)) {
+ return this.items.grid.get(item.id);
+ }
+ const card = window.getTemplate('gridView');
+ if (!card) return null;
+ return this.setupItemElement(card, item, 'grid');
+ }
+
+ renderListItem(item) {
+ if (this.items.list.has(item.id)) {
+ return this.items.list.get(item.id);
+ }
+ let row = window.getTemplate('listView');
+ if (!row) return null;
+
+ row = this.setupItemElement(row, item, 'list');
+
+ // List-specific: populate data attributes
+ row.querySelectorAll('[data-attr]').forEach(el => {
+ const value = item[el.dataset.attr];
+ if (value && value !== '') {
+ el.textContent = value;
+ } else {
+ el.remove();
+ }
+ });
+
+ row.querySelectorAll('[data-field]').forEach(el => {
+ const value = item.fields?.[el.dataset.field];
+ if (value && value !== '') {
+ el.tagName === 'DIV' ? el.innerHTML = value : el.textContent = value;
+ } else {
+ el.remove();
+ }
+ });
+ this.items.list.set(item.id, row);
+
+ return row;
+ }
+
+ renderTableItem(item) {
+ if (this.items.table.has(item.id)) {
+ return this.items.table.get(item.id);
+ }
+ let row = window.getTemplate('tableView');
+ if (!row) return null;
+
+ row = this.setupItemElement(row, item, 'table');
+
+ const status = row.querySelector(`input[name="post_status"][value="${item.status}"]`);
+ if (status) status.checked = true;
+
+ if (this.isTimeline) {
+ // Timeline: populate shared row, clone points
+ const sharedRow = row.querySelector('tr.shared');
+ if (sharedRow) {
+ new window.jvbPopulate(sharedRow, item);
+ this.cleanupTableRow(sharedRow);
+ }
+
+ const pointTemplate = row.querySelector('tr.timeline-point');
+ if (pointTemplate && item.fields?.timeline) {
+ Object.entries(item.fields.timeline).forEach(([imgId, timeline], index) => {
+ const point = pointTemplate.cloneNode(true);
+ point.dataset.index = index;
+ point.dataset.imageId = imgId;
+
+ new window.jvbPopulate(point, {
+ fields: timeline,
+ images: item.images,
+ taxonomies: item.taxonomies
+ });
+ this.cleanupTableRow(point);
+
+ const imgData = item.images?.[timeline.post_thumbnail];
+ if (imgData) {
+ point.querySelector('.field.upload')?.setAttribute('title', imgData['image-title'] || '');
}
- this.ui.bulkSelectActions.append(option);
+
+ row.insertBefore(point, pointTemplate);
});
+ pointTemplate.remove();
}
} else {
- if (this.ui.bulkSelectActions && !this.ui.bulkSelectActions.querySelector('[value="edit"]')) {
- window.removeChildren(this.ui.bulkSelectActions);
-
- let options = window.getTemplate('notTrashOptions');
- options.querySelectorAll('option').forEach((option, index) => {
- this.ui.bulkSelectActions.append(option);
- });
+ // Standard table row
+ if (this.ui.table.form?.dataset.edit !== undefined) {
+ new window.jvbPopulate(row, item);
+ } else {
+ this.populateTableReadOnly(row, item);
}
+ this.cleanupTableRow(row);
}
- if (this.ui.bulkSelectActions) {
- this.ui.bulkSelectActions.value = '';
+
+ this.items.table.set(item.id, row);
+
+ return row;
+ }
+
+ populateTableReadOnly(row, item) {
+ for (const [key, value] of Object.entries(item)) {
+ const col = row.querySelector(`[data-field="${key}"]`);
+ if (!col) continue;
+
+ const p = col.querySelector('p');
+ if (p) {
+ p.textContent = col.dataset.fieldType === 'date'
+ ? window.formatTimeAgo(value)
+ : value;
+ }
}
}
- populateBulkEdit() {
- const container = this.modals.bulkEdit.modal.querySelector('form .selected');
- if (!container) return;
-
- window.removeChildren(container);
- for (let selected of this.viewController.selectedItems) {
- let item = this.store.get(selected);
-
- const img = window.getTemplate('bulkItem');
- if (!img) return;
-
- const checkbox = img.querySelector('input[type=checkbox]');
- const image = img.querySelector('img');
-
- if (checkbox) {
- checkbox.id = `bulk_${item.id}`;
- checkbox.value = item.id;
- checkbox.checked = true;
- }
-
- if (image && item.thumbnail) {
- image.src = item.thumbnail;
- image.alt = item.alt || '';
- }
-
- container.append(img);
- }
- let modal = this.modals.bulkEdit.modal;
- [
- modal.querySelector('h2 span').textContent
- ] = [
- this.viewController.selectedItems.size
- ];
-
- this.formController.registerForm(this.ui.forms.bulkEdit);
- }
-
- populateEditForm(itemID) {
- this.currentItemID = itemID;
-
- let item = this.store.get(parseInt(itemID));
- if (item) {
- this.ui.modals.edit.dataset.itemId = itemID;
- this.ui.modals.edit.dataset.content = this.content;
-
- let form = this.ui.modals.edit.querySelector('form');
- this.ui.modals.edit.querySelector('h2').textContent = `Editing ${item.fields.post_title}`;
- form.dataset.formId = `edit-${itemID}`;
-
- new window.jvbPopulate(form, item);
-
- this.formController.registerForm(this.ui.forms.edit);
- }
- }
-
- setupFilters() {
- // Search
- const searchInput = document.querySelector('.all-filters input[type="search"]');
- if (searchInput) {
- let searchTimeout;
- searchInput.addEventListener('input', () => {
- if (searchInput.value.length > 3) {
- clearTimeout(searchTimeout);
- searchTimeout = setTimeout(() => {
- this.store.setFilter('search', searchInput.value);
- }, 300);
- } else if (searchInput.value.length === 0) {
- this.store.removeFilter('search');
+ cleanupTableRow(row) {
+ row.querySelectorAll('td[data-field]').forEach(field => {
+ field.querySelectorAll('label:not(.select-item-label,.radio-option,[for*="select-item"])').forEach(label => {
+ if (!label.closest('.radio-options')) {
+ label.remove();
}
});
- }
+
+ //Remove toggle labels for true_false fields
+ if (field.dataset.fieldType === 'true_false') {
+ field.querySelector('.toggle-label')?.remove();
+ }
+
+ //Remove field label for checkbox/radio groups
+ if (['checkbox','radio','select'].includes(field.dataset.fieldType)) {
+ field.querySelector('.label')?.remove();
+ }
+
+ });
+ }
+ toggleColumn(column, show) {
+ this.ui.table.table.querySelectorAll(`.${column}`).forEach(el =>{
+ el.hidden = !show;
+ });
+ }
+ /***************************************************************
+ UTILITY
+ ***************************************************************/
+ shouldRemoveItemUI(newStatus) {
+ return (this.status === 'all' && !['publish', 'draft'].includes(newStatus))
+ || newStatus !== this.store.filters.status;
+ }
+ removeItems(items) {
+ let delay = 0;
+ items.forEach(itemId => {
+ setTimeout(() => {
+ let item = this.items[this.view].get(itemId);
+ if (item) {
+ window.fade(item, false);
+ }
+ }, delay);
+ delay += 50;
+ });
}
+ setFilters(filters) {
+ for (let [key, value] of Object.entries(filters)) {
+ if (!this.allowedFilters.includes(key)) {
+ delete filters[key];
+ continue;
+ }
+ this.cache.set(key, value);
+
+ let el = this.findFilterEl(key);
+ this.setElValue(el, value);
+ }
+ this.store.setFilters(filters);
+ }
+ setFilter(name, value) {
+ if (!this.allowedFilters.includes(name)) return;
+ this.cache.set(name, value);
+ let el = this.findFilterEl(name, value);
+ this.setElValue(el, value);
+ //TODO: If we set the element to checked, does that automatically call the change listener, which then also sets the store filter and cache?
+ this.store.setFilter(name, value);
+ }
+
+ deleteFilter(name, value) {
+ if (!this.allowedFilters.includes(name)) return;
+ if (Object.hasOwn(this.defaults, name)) {
+ this.setFilter(name, this.defaults[name]);
+ return;
+ }
+ let el = this.findFilterEl(name, value);
+ this.setElValue(el, false);
+ this.cache.remove(name);
+ this.setFilter(name, '');
+ }
+ setElValue(element, value) {
+ if (!element) return;
+ if (!value) {
+ if (['SELECT','TEXTAREA'].includes(element.tagName)) element.value = '';
+ if (['text', 'search'].includes(element.type)) element.value = '';
+ if (element.type === 'radio') element.checked = false;
+ return;
+ }
+
+ if (['SELECT','TEXTAREA'].includes(element.tagName)) element.value = value;
+ if (['text', 'search'].includes(element.type)) element.value = value;
+ if (element.type === 'radio') element.checked = true;
+ }
+ findFilterEl(name, value) {
+ //Handle exceptions first (custom date elements)
+ if (['date-filter', 'dateFrom', 'dateTo'].includes(name)) {
+ switch (name) {
+ case 'date-filter':
+ name = 'month';
+ break;
+ case 'dateFrom':
+ name = 'start';
+ break;
+ case 'dateTo':
+ name = 'end';
+ break;
+ }
+ return this.ui.modals.date[name];
+ }
+ // Handle taxonomy filters
+ if (name.includes('tax_')) {
+ const taxonomy = name.replace('tax_', '');
+ const element = this.ui.filters.taxonomies?.[taxonomy];
+ if (element) {
+ return element;
+ }
+ console.warn('Taxonomy filter element not found:', taxonomy);
+ return null;
+ }
+
+ if (!Object.hasOwn(this.ui.filters, name)) {
+ console.warn('Filter el not found: ', name);
+ return false;
+ }
+
+ let el = this.ui.filters[name];
+ if (typeof el === 'object') {
+ if (!Object.hasOwn(this.ui.filters[name], value)) {
+ console.log('Sub filter el not found: ', value);
+ console.log(name);
+ return false;
+ }
+ el = this.ui.filters[name][value];
+ }
+ return el;
+ }
+ /***************************************************************
+ CLEANUP
+ ***************************************************************/
destroy() {
- document.querySelectorAll('[data-filter]').forEach(filter => {
- filter.removeEventListener('change', this.filterHandler);
- });
+ if (this.timelineSortables) {
+ this.timelineSortables.forEach(sortable => sortable.destroy());
+ this.timelineSortables = [];
+ }
+ document.removeEventListener('click', this.clickHandler);
+ document.removeEventListener('change', this.changeHandler);
+ if (this.ui.filters.search) {
+ this.ui.filters.search.removeEventListener('input', this.handleInput);
+ }
+ for (let map of Object.values(this.items)) {
+ map.clear();
+ }
}
}
-// Initialize when ready
document.addEventListener('DOMContentLoaded', async function() {
window.auth.subscribe((event) => {
if (event === 'auth-loaded') {
diff --git a/assets/js/concise/CRUDOld.js b/assets/js/concise/CRUDOld.js
new file mode 100644
index 0000000..a9fc815
--- /dev/null
+++ b/assets/js/concise/CRUDOld.js
@@ -0,0 +1,778 @@
+/**
+ * Main CRUD Manager - Coordinates everything
+ */
+class CRUDManagerOld {
+ constructor(config) {
+ this.queue = window.jvbQueue;
+ this.config = config;
+ this.content = config.content || false;
+ this.settings = window.jvbUserSettings;
+ this.a11y = window.jvbA11y;
+ if (!this.content) {
+ return;
+ }
+ this.isTimeline = false;
+ this.currentItemID = null;
+
+ this.initElements();
+ this.updateBulkOptions();
+
+ // Initialize components
+ const store = window.jvbStore.register(
+ this.content,
+ {
+ storeName: this.content,
+ keyPath: 'id',
+ endpoint: 'content',
+ headers: {
+ '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: {
+ content: this.content,
+ user: window.auth.getUser(),
+ page: 1,
+ status: 'all',
+ orderby: 'modified', //or title
+ order: 'desc'
+ },
+ TTL: 30 * 60 * 1000, //30 minutes cache
+ showLoading: true,
+ });
+ this.store = store[this.content];
+
+ this.status = 'all';
+ this.filterTimeout = null;
+
+ this.viewController = new window.jvbViews(this.ui.container, this.store);
+ this.tableForm = null;
+ this.tableChanges = new Map();
+
+
+ this.formController = new window.jvbForm();
+ this.viewController.subscribe((event, form) => {
+ if (event === 'table-view' && !this.tableForm) {
+ if (!this.tableForm) {
+ this.tableForm = this.formController.registerForm(form, {
+ autosave: false,
+ formStatus: false,
+ isTable: true,
+ });
+ }
+
+ } else if (event === 'not-table-view') {
+ if (this.tableForm) {
+
+ }
+ } else if (event === 'order-changed') {
+ let data = this.store.get(form);
+ if (!data) {
+ return;
+ }
+ let changes = {};
+ changes[form] = data;
+ this.savePosts(changes, `Updating progression order`);
+ }
+ });
+
+ this.formController.subscribe((event, data) => {
+ switch(event) {
+ case 'form-submit':
+ case 'form-autosave':
+ this.handleFormChange(event,data);
+ break;
+ }
+ });
+
+ this.queue.subscribe((event, data) => {
+ if (!Object.hasOwn(data, 'endpoint') || !['content', 'uploads/groups'].includes(data.endpoint)) return;
+ if (event === 'operation-completed') {
+ this.handleQueueSuccess(event, data);
+ } else if (event === 'operation-failed-permanent') {
+ this.handleQueueFailure(event, data);
+ }
+ });
+
+
+ // Track initialization
+ this.initialized = false;
+
+ this.init();
+ }
+ handleFormChange(event, data) {
+ let title = data.fullData.post_title;
+ let changes = (Object.hasOwn(data, 'changes')) ? data.changes : data.fullData;
+
+ let theChanges = {};
+ if (this.isTimeline) {
+ theChanges[this.currentItemID] = changes;
+ this.savePosts(theChanges, title);
+ return;
+ }
+
+ let itemsToRemove = [];
+ console.log(data);
+ switch (true) {
+ case data.config.element === this.ui.forms.edit:
+ theChanges[this.currentItemID] = changes;
+ title = `Saving ${title} Changes`;
+ // Check if status change requires removal
+ if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
+ itemsToRemove.push(this.currentItemID);
+ }
+ break;
+ case data.config.element === this.ui.forms.bulkEdit:
+ let selected = data.config.element.querySelectorAll('.selected input:checked');
+ selected.forEach(sel => {
+ theChanges[sel.value] = changes;
+ // Check if status change requires removal
+ if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
+ itemsToRemove.push(sel.value);
+ }
+ });
+
+ title = `Updating ${selected.length} ${this.config.plural??'posts'} Changes`;
+ break;
+ case data.config.element === this.ui.forms.create:
+ if (event === 'form-submit') {
+ theChanges[data.config.data['form-id']] = changes;
+ title = `Saving ${title} Changes`;
+ }
+ break;
+ }
+
+ // Handle visual removal with stagger effect
+ if (itemsToRemove.length > 0) {
+ let delay = 0;
+ itemsToRemove.forEach(itemId => {
+ setTimeout(() => {
+ const element = document.querySelector(`.item[data-id="${itemId}"]`);
+ if (element) {
+ window.fade(element, false);
+ }
+ }, delay);
+ delay += 50; // Stagger by 50ms
+ });
+
+ // Clear selection after bulk edit with staggered removal
+ if (data.config.element === this.ui.forms.bulkEdit) {
+ setTimeout(() => {
+ this.viewController.clearSelection();
+ }, delay + 100);
+ }
+ }
+
+ if (Object.keys(theChanges).length === 0) {
+ return;
+ }
+
+ this.savePosts(theChanges, title);
+ }
+
+ shouldRemoveItem(newStatus) {
+ return (this.status === 'all' && !['publish', 'draft'].includes(newStatus)) ||
+ (newStatus !== this.status);
+ }
+
+ savePosts(changes, title) {
+ if (Object.keys(changes).length === 0) {
+ return;
+ }
+
+ //ensure content is in each post
+ for (let postId in changes) {
+ if (!changes[postId]['content']) {
+ changes[postId]['content'] = this.content;
+ }
+ }
+ let operation = {
+ endpoint: 'content',
+ headers: {
+ 'action_nonce': window.auth.getNonce('dash'),
+ },
+ data: {
+ posts: changes,
+ },
+ delay: true,
+ popup: `Saving changes`,
+ title: title
+ };
+
+ this.queue.addToQueue(operation);
+
+ }
+ async handleQueueSuccess(event, data) {
+ this.store.clearCache();
+ this.store.fetch();
+ }
+ handleQueueFailure(event, data) {
+ console.error('Operation failed permanently:', data);
+ // Optionally show error notification to user
+ this.a11y?.announce(`Operation failed: ${data.error_message || 'Unknown error'}`);
+ }
+
+ initElements() {
+ this.elements = {
+ modals: {
+ create: 'dialog.create',
+ edit: 'dialog.edit',
+ bulkEdit: 'dialog.bulkEdit'
+ },
+ container: '.crud[data-content]',
+ grid: '.item-grid',
+ bulkSelectActions: '.bulk-action-select',
+ forms: {
+ create: 'dialog.create form',
+ edit: 'dialog.edit form',
+ bulkEdit: 'dialog.bulkEdit form'
+ },
+ uploader: 'details.uploader'
+ };
+ this.ui = window.uiFromSelectors(this.elements);
+ if (this.ui.uploader) {
+ window.jvbUploads.scanFields(document.querySelector(this.elements.uploader));
+
+ window.jvbUploads.subscribe((event, data) => {
+ if (event === 'sent-to-queue') {
+ console.log(data);
+ if (data === this.ui.uploader.querySelector('[data-uploader]')?.dataset.uploader) {
+ window.debouncer.schedule('crud-complete', ()=> {
+ this.store.clearCache();
+ });
+ }
+ }
+ });
+ }
+ this.isTimeline = !!document.querySelector('[data-timeline]');
+ }
+ init() {
+ if (this.ui.uploader){
+ this.settings.addSetting(this.ui.uploader, 'open');
+ this.ui.uploader.addEventListener('toggle', (e) =>{
+ this.settings.saveSetting('open', this.ui.uploader.open ? 'on' : 'off');
+ });
+ }
+
+ // Set up filter controls
+ this.filterHandler = this.handleFilterChange.bind(this);
+ this.changeHandler = this.handleChange.bind(this);
+
+
+
+ this.modals = {};
+ for (let [name, modal] of Object.entries(this.ui.modals)) {
+ this.modals[name] = new window.jvbModal(modal);
+
+ this.modals[name].subscribe((event, data) => {
+ switch (event) {
+ case 'modal-close':
+ this.currentItemID = null;
+ this.formController.cleanupForm(this.modals[name].modal.querySelector('form').dataset.formId);
+ //double check we have finished saving
+ break;
+ case 'modal-open':
+ //probably not needed in this class
+ break;
+ }
+ });
+ }
+
+ // Set up global event delegation
+ this.setupEventDelegation();
+
+ this.setupFilters();
+
+ this.initialized = true;
+ }
+
+ setupEventDelegation() {
+ document.addEventListener('change', this.changeHandler);
+ // Single event listener for all CRUD actions
+ document.addEventListener('click', (e) => {
+ // Check for action buttons
+ const actionBtn = e.target.closest('[data-action]');
+ if (actionBtn) {
+ e.preventDefault();
+ const action = actionBtn.dataset.action;
+ const id = actionBtn.dataset.id;
+
+ switch(action) {
+ case 'edit':
+ this.populateEditForm(id);
+ this.modals.edit.handleOpen();
+ break;
+
+ case 'delete':
+ if (confirm('Delete this item?')) {
+ let changes = {};
+ changes[actionBtn.dataset.id] = {
+ 'post_status': 'delete',
+ 'content': this.content
+ };
+ window.fade(actionBtn.closest('.item'), false);
+ this.savePosts(changes, `Sending ${this.singular} to trash...`);
+ this.store.delete(id);
+ }
+ break;
+ case 'trash':
+ let changes = {};
+ changes[actionBtn.dataset.id] = {
+ 'post_status': 'trash',
+ 'content': this.content
+ };
+ window.fade(actionBtn.closest('.item'), false);
+ this.savePosts(changes, `Sending ${this.singular} to trash...`);
+ break;
+
+ case 'create':
+ this.modals.create.dataset.itemId = 'new';
+ this.modals.create.dataset.content = this.content;
+ this.modals.create.handleOpen();
+ break;
+
+ case 'bulk-edit':
+ const selected = Array.from(this.viewController.selectedItems);
+ if (selected.length > 0) {
+
+ this.modals.bulkEdit.handleOpen();
+ }
+ break;
+
+ case 'bulk-delete':
+ const toDelete = Array.from(this.viewController.selectedItems);
+ if (toDelete.length > 0 && confirm(`Delete ${toDelete.length} items?`)) {
+ toDelete.forEach(id => this.store.delete(id));
+ this.viewController.clearSelection();
+ }
+ break;
+
+ case 'sync':
+ // this.store.syncQueue();
+ break;
+
+ case 'refresh':
+ this.store.fetch();
+ break;
+ }
+ }
+
+ let createButton = e.target.closest('.create-item');
+ if (createButton) {
+ this.formController.registerForm(this.ui.forms.create);
+ this.modals.create.handleOpen();
+ }
+
+ let clearSelection = e.target.closest('.cancel-bulk');
+ if (clearSelection) {
+ this.viewController.selectAll(false);
+ }
+ });
+
+ // Keyboard shortcuts
+ document.addEventListener('keydown', (e) => {
+ // Ctrl/Cmd + A to select all
+ if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
+ if (this.ui.container && this.ui.container.contains(document.activeElement)) {
+ e.preventDefault();
+ this.viewController.selectAll();
+ }
+ }
+
+ // ESC to clear selection
+ if (e.key === 'Escape' && this.viewController?.selectedItems.size > 0 && window.jvbModal.getAllModals().length === 0) {
+ this.viewController.clearSelection();
+ }
+ });
+ }
+ handleChange(e) {
+ if (e.target.closest('[data-id]')) {
+ if (this.isTimeline) {
+ this.handleTimelineTableChange(e);
+ } else {
+ this.handleTableChange(e);
+ }
+ return;
+ }
+ if (e.target.classList.contains('bulk-action-select')) {
+ if (e.target.value.startsWith('tax-')) {
+ const taxonomy = e.target.value.replace('tax-', '');
+ this.openTaxonomyModal(taxonomy);
+ e.target.value = '';
+ return;
+ }
+
+ switch (e.target.value) {
+ case 'edit':
+ this.populateBulkEdit();
+ this.modals.bulkEdit.handleOpen();
+ break;
+ case 'publish':
+ this.setBulkStatus('publish');
+ break;
+ case 'draft':
+ this.setBulkStatus('draft');
+ break;
+ case 'trash':
+ this.setBulkStatus('trash');
+ break;
+ case 'restore':
+ this.setBulkStatus('draft');
+ break;
+ case 'delete':
+ this.setBulkStatus('delete');
+ break;
+ }
+ }
+ if (window.targetCheck(e, 'select[data-filter]')) {
+ this.handleFilterChange(e);
+ }
+ }
+ handleTableChange(e) {
+ const row = e.target.closest('tr[data-id]');
+ if (!row) return;
+
+ const input = e.target;
+ const postID = parseInt(row.dataset.id);
+ const fieldName = input.closest(['data-field'])?.dataset.field;
+ if (!fieldName) return;
+
+ const item = this.store.get(postID);
+ if (!item) return;
+
+ item.fields[fieldName] = this.getInputValue(input);
+
+ this.store.save(item);
+
+ let post = {};
+ post[postID] = item.fields;
+ this.savePosts(post, `Saving changes to ${this.content}`);
+ }
+ handleTimelineTableChange(e) {
+ const tbody = e.target.closest('tbody[data-id]');
+ if (!tbody) return;
+
+ const input = e.target;
+ const fieldName = input.closest('[data-field]')?.dataset.field;
+
+ if (!fieldName) return;
+
+ const parentID = parseInt(tbody.dataset.id);
+ const timelinePoint = input.closest('tr.timeline-point');
+
+ const item = this.store.get(parentID);
+ if (!item) return;
+
+ const value = this.getInputValue(input);
+
+ // Check if this is a specific point, or a shared value
+ if (timelinePoint) {
+ const imgID = timelinePoint.dataset.imageId;
+ if (!item.fields.timeline) {
+ item.fields.timeline = {};
+ }
+ if (!item.fields.timeline[imgID]) {
+ item.fields.timeline[imgID] = {};
+ }
+ item.fields.timeline[imgID][fieldName] = value;
+ } else {
+ item.fields[fieldName] = value;
+ }
+
+ //Update store directly
+ this.store.save(item);
+
+ let changes = {};
+ changes[parentID] = item.fields;
+ this.savePosts(changes, 'Updating progress post');
+ }
+ getInputValue(input) {
+ if (input.type === 'checkbox') {
+ return input.checked ? (input.value || '1') : '';
+ }
+ if (input.type === 'radio') {
+ return input.checked ? input.value : null;
+ }
+ return input.value;
+ }
+
+ collectTimelineData(form) {
+ const formData = new FormData(form);
+ const data = {};
+ const timeline = [];
+
+ // Get the base content type
+ data.content = form.closest('dialog')?.dataset.content || this.content;
+ data.user = window.auth.getUser();
+
+ // Collect timeline entries
+ const timelineGroups = form.querySelectorAll('.upload-group');
+
+ timelineGroups.forEach((group, index) => {
+ const groupId = group.dataset.groupId;
+ const entry = {
+ id: group.dataset.itemId || 'new',
+ order: index
+ };
+
+ // Get group-specific fields
+ const titleInput = group.querySelector(`[name="${groupId}[post_title]"], [name="${groupId}_post_title"]`);
+ const excerptInput = group.querySelector(`[name="${groupId}[post_excerpt]"], [name="${groupId}_post_excerpt"]`);
+
+ if (titleInput) entry.post_title = titleInput.value;
+ if (excerptInput) entry.post_excerpt = excerptInput.value;
+
+ // Get images in this group
+ const images = Array.from(group.querySelectorAll('.item-grid.group .item[data-upload-id]'));
+ entry.images = images.map(img => img.dataset.uploadId);
+
+ // Check for featured image
+ const featuredInput = group.querySelector('[name*="featured"]:checked');
+ if (featuredInput) {
+ entry.featured = featuredInput.value;
+ }
+
+ timeline.push(entry);
+ });
+
+ // Get ungrouped uploads (if any)
+ const mainGrid = form.querySelector('.item-grid.preview');
+ if (mainGrid) {
+ const ungroupedImages = Array.from(mainGrid.querySelectorAll('.item[data-upload-id]'));
+ ungroupedImages.forEach(img => {
+ timeline.push({
+ id: 'new',
+ images: [img.dataset.uploadId]
+ });
+ });
+ }
+
+ // Collect other form fields (shared fields)
+ for (let [key, value] of formData.entries()) {
+ // Skip group-specific fields
+ if (!key.includes('[') && !key.includes('_group_') && key !== 'timeline') {
+ data[key] = value;
+ }
+ }
+
+ data.timeline = timeline;
+
+ return data;
+ }
+
+ openTaxonomyModal(taxonomy) {
+ // Check if jvbSelector exists
+ if (!window.jvbSelector) {
+ console.error('TaxonomySelector not initialized');
+ return;
+ }
+
+ // Open the selector in filter mode
+ window.jvbSelector.openForFilter(
+ taxonomy,
+ (selectedIds, taxonomy) => this.handleBulkTaxonomy(selectedIds, taxonomy)
+ );
+ }
+ handleBulkTaxonomy(selectedIds, taxonomy) {
+ // Callback when terms are selected
+ if (selectedIds.length > 0) {
+ selectedIds = selectedIds.join(',');
+ let changes = {};
+ let selected = Array.from(this.viewController.selectedItems);
+
+
+ selected.forEach(sel => {
+ changes[sel] = {
+ content: this.content
+ };
+ changes[sel][taxonomy] = selectedIds;
+ });
+
+
+ let title = `Adding ${selected.length} ${this.config.plural??'posts'} to ${selectedIds.length} ${jvbSettings.labels[taxonomy].plural}`;
+ this.viewController.clearSelection();
+ this.savePosts(changes, title);
+ }
+ }
+
+ setBulkStatus(status) {
+ if (!['publish', 'draft', 'trash', 'delete'].includes(status)){
+ return;
+ }
+
+ let changes = {};
+ for (let selected of this.viewController.selectedItems) {
+ changes[selected] = {
+ post_status: status,
+ content: this.content
+ };
+ }
+ let title;
+ switch (status) {
+ case 'delete':
+ title = 'Deleting';
+ break;
+ default:
+ title = window.uppercaseFirst(status)+'ing';
+ }
+
+ if ((this.status === 'all' && !['publish', 'draft'].includes(status)) || status !== this.status) {
+ let delay = 0;
+ for (let selected of this.viewController.selectedItems) {
+ setTimeout(() => {
+ const element = document.querySelector(`.item[data-id="${selected}"]`);
+ if (element) {
+ window.fade(element, false);
+ }
+ }, delay);
+ delay += 50; // Increment delay for staggered effect
+ }
+ }
+ // Clear selection even if items aren't being removed
+ this.viewController.clearSelection();
+
+
+ if (Object.keys(changes).length !== 0) {
+ this.savePosts(changes, `${title} ${this.viewController.selectedItems.size} ${this.plural}...`);
+ }
+ }
+
+ handleFilterChange(e) {
+ let target = e.target;
+ let filter = target.dataset.filter;
+ if (filter === 'taxonomies') {
+ let taxonomy = target.dataset.taxonomy;
+ this.store.setFilter(`tax_${taxonomy}`, target.value);
+ } else {
+ this[target.dataset.filter] = target.value;
+ this.store.setFilter(target.dataset.filter, target.value);
+ if (target.dataset.filter === 'status') {
+ this.updateBulkOptions(target.value);
+ }
+ }
+ }
+ updateBulkOptions(status = 'all') {
+ if (status === 'trash') {
+ if (this.ui.bulkSelectActions?.querySelector('[value="edit"]')) {
+ window.removeChildren(this.ui.bulkSelectActions);
+ let options = window.getTemplate('trashOptions');
+ options.querySelectorAll('option').forEach((option, index) => {
+ if (index === 0) {
+ option.checked = true;
+ }
+ this.ui.bulkSelectActions.append(option);
+ });
+ }
+ } else {
+ if (this.ui.bulkSelectActions && !this.ui.bulkSelectActions.querySelector('[value="edit"]')) {
+ window.removeChildren(this.ui.bulkSelectActions);
+
+ let options = window.getTemplate('notTrashOptions');
+ options.querySelectorAll('option').forEach((option, index) => {
+ this.ui.bulkSelectActions.append(option);
+ });
+ }
+ }
+ if (this.ui.bulkSelectActions) {
+ this.ui.bulkSelectActions.value = '';
+ }
+ }
+
+ populateBulkEdit() {
+ const container = this.modals.bulkEdit.modal.querySelector('form .selected');
+ if (!container) return;
+
+ window.removeChildren(container);
+ for (let selected of this.viewController.selectedItems) {
+ let item = this.store.get(selected);
+
+ const img = window.getTemplate('bulkItem');
+ if (!img) return;
+
+ const checkbox = img.querySelector('input[type=checkbox]');
+ const image = img.querySelector('img');
+
+ if (checkbox) {
+ checkbox.id = `bulk_${item.id}`;
+ checkbox.value = item.id;
+ checkbox.checked = true;
+ }
+
+ if (image && item.thumbnail) {
+ image.src = item.thumbnail;
+ image.alt = item.alt || '';
+ }
+
+ container.append(img);
+ }
+ let modal = this.modals.bulkEdit.modal;
+ [
+ modal.querySelector('h2 span').textContent
+ ] = [
+ this.viewController.selectedItems.size
+ ];
+
+ this.formController.registerForm(this.ui.forms.bulkEdit);
+ }
+
+ populateEditForm(itemID) {
+ this.currentItemID = itemID;
+
+ let item = this.store.get(parseInt(itemID));
+ if (item) {
+ this.ui.modals.edit.dataset.itemId = itemID;
+ this.ui.modals.edit.dataset.content = this.content;
+
+ let form = this.ui.modals.edit.querySelector('form');
+ this.ui.modals.edit.querySelector('h2').textContent = `Editing ${item.fields.post_title}`;
+ form.dataset.formId = `edit-${itemID}`;
+
+ new window.jvbPopulate(form, item);
+
+ this.formController.registerForm(this.ui.forms.edit);
+ }
+ }
+
+ setupFilters() {
+ // Search
+ const searchInput = document.querySelector('.all-filters input[type="search"]');
+ if (searchInput) {
+ let searchTimeout;
+ searchInput.addEventListener('input', () => {
+ if (searchInput.value.length > 3) {
+ clearTimeout(searchTimeout);
+ searchTimeout = setTimeout(() => {
+ this.store.setFilter('search', searchInput.value);
+ }, 300);
+ } else if (searchInput.value.length === 0) {
+ this.store.removeFilter('search');
+ }
+ });
+ }
+ }
+
+ destroy() {
+ document.querySelectorAll('[data-filter]').forEach(filter => {
+ filter.removeEventListener('change', this.filterHandler);
+ });
+ }
+}
+
+// Initialize when ready
+document.addEventListener('DOMContentLoaded', async function() {
+ window.auth.subscribe((event) => {
+ if (event === 'auth-loaded') {
+ let container = document.querySelector('[data-content]');
+ if (container && !Object.hasOwn(container.dataset, 'ignore')) {
+ window.crudManager = new CRUDManager({
+ content: container.dataset.content,
+ });
+ }
+ }
+ });
+});
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 439225d..00211e2 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -82,6 +82,7 @@
endpoint: null,
apiBase: jvbSettings.api,
filters: {},
+ ignore: [], //any filters to ignore when filtering store locally
required: null,
// Cache
@@ -105,6 +106,11 @@
_initialized: false
};
+ store.ignoreFilters = new Set([
+ ... ['search', 'page', 'per_page', 'orderby', 'order'],
+ ... store.config.ignore
+ ]);
+
store.config.headers = {
'X-WP-Nonce': window.auth.getNonce(),
...store.config.headers
@@ -791,20 +797,20 @@
// Reject functions
if (type === 'function') {
if (validate) return { valid: false, error: `Function at ${path}` };
- console.debug(`[DataStore] Stripped function at ${path}`);
+
return { valid: true, data: undefined };
}
// DOM elements
if (obj instanceof HTMLElement || obj.nodeType !== undefined) {
if (validate) return { valid: false, error: `DOM element at ${path}` };
- console.debug(`[DataStore] Stripped DOM element at ${path}`);
+
return { valid: true, data: undefined };
}
// FormData - convert and continue
if (obj instanceof FormData) {
- console.debug(`[DataStore] Converting FormData at ${path}`);
+
return { valid: true, data: this.formDataToObject(obj) };
}
@@ -846,7 +852,7 @@
}
if (validate) return { valid: false, error: `Unknown type at ${path}` };
- console.debug(`[DataStore] Stripped unknown type at ${path}`);
+
return { valid: true, data: undefined };
}
@@ -939,51 +945,97 @@
const cacheEntry = store.cache.get(cacheKey);
// First check if we have cached results for exact filters
- if (cacheEntry && cacheEntry.items) {
- return cacheEntry.items.reduce((acc, id) => {
- const item = store.data.get(id);
- if (item) acc.push(item);
- return acc;
- }, []);
+ if (cacheEntry?.items) {
+ return this.applyOrdering(
+ cacheEntry.items.reduce((acc, id) => {
+ const item = store.data.get(id);
+ if (item) acc.push(item);
+ return acc;
+ }, []),
+ store
+ );
}
- // If we have a search filter and complete base data, filter locally
- if (store.filters.search && store.filters.search.trim()) {
- const searchQuery = store.filters.search.toLowerCase().trim();
+ const allItems = Array.from(store.data.values());
+ const searchQuery = store.filters.search?.toLowerCase().trim() || '';
- // Get all items and filter them locally
- const allItems = Array.from(store.data.values());
+ const filtered = allItems.filter(item => {
+ // Apply all non-ignored filters
+ for (const [key, value] of Object.entries(store.filters)) {
+ if (store.ignoreFilters.has(key)) continue;
+ if (value === null || value === undefined || value === '') continue;
+ if (value === 'all') continue;
- // Filter by current filters (excluding search and page)
- let filtered = allItems.filter(item => {
- // Apply all filters except search and page
- for (const [key, value] of Object.entries(store.filters)) {
- if (key === 'search' || key === 'page') continue;
-
- if (value !== null && value !== undefined && value !== '') {
- if (item[key] !== value) return false;
- }
+ // Comma-separated values
+ if (typeof value === 'string' && value.includes(',')) {
+ const accepted = value.split(',').map(v => v.trim());
+ if (!accepted.includes(String(item[key]))) return false;
+ continue;
}
- return true;
+
+ if (String(item[key]) !== String(value)) return false;
+ }
+
+ // Apply search if present
+ return !(searchQuery && !this.searchObject(item, searchQuery));
+
+
+ });
+
+ return this.applyOrdering(filtered, store);
+ }
+
+ applyOrdering(items, store) {
+ if (!Array.isArray(items)) items = Array.from(items);
+ if (items.length === 0) return items;
+
+ if (store.filters.orderby || store.filters.order) {
+ const orderby = store.filters.orderby || 'date';
+ const order = (store.filters.order || 'desc').toLowerCase();
+
+ items.sort((a, b) => {
+ let aVal, bVal;
+
+ switch (orderby) {
+ case 'alphabetical':
+ case 'title':
+ aVal = (a.fields?.post_title || a.title || a.name || '').toLowerCase();
+ bVal = (b.fields?.post_title || b.title || b.name || '').toLowerCase();
+ break;
+ case 'modified':
+ aVal = new Date(a.modified || 0);
+ bVal = new Date(b.modified || 0);
+ break;
+ case 'date':
+ default:
+ aVal = new Date(a.date || 0);
+ bVal = new Date(b.date || 0);
+ }
+
+ if (aVal < bVal) return order === 'asc' ? -1 : 1;
+ if (aVal > bVal) return order === 'asc' ? 1 : -1;
+ return 0;
});
-
- // Apply search filter to common searchable fields
- filtered = filtered.filter(item => {
- // Search in common fields: name, title, path, description
- const searchableFields = ['name', 'title', 'path', 'description', 'slug'];
-
- return searchableFields.some(field => {
- const value = item[field];
- if (!value) return false;
- return value.toLowerCase().includes(searchQuery);
- });
- });
-
- return filtered;
}
+ return items;
+ }
- // Fallback to all data
- return this.getAll(name);
+ searchObject(obj, search) {
+ if (!obj || typeof obj !== 'object') return false;
+
+ for (const value of Object.values(obj)) {
+ if (value === null || value === undefined) continue;
+
+ if (typeof value === 'object') {
+ if (this.searchObject(value, search)) return true;
+ continue;
+ }
+
+ if (typeof value === 'string' && value.toLowerCase().includes(search)) {
+ return true;
+ }
+ }
+ return false;
}
async clear(name) {
@@ -1017,14 +1069,18 @@
}
});
- const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters);
-
this.notify(name, 'filters-changed', {
oldFilters,
filters: store.filters,
updates
});
+ this.notify(name, 'data-loaded', {
+ cached: true,
+ items: this.getFiltered(name)
+ });
+
+ const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters);
if (store.config.endpoint && shouldFetch) {
await this.fetch(name);
} else if (store.config.endpoint) {
@@ -1046,8 +1102,18 @@
if (!store.config.endpoint || !store.lastResponse) {
return true;
}
+
+ if (store.lastResponse.has_more === false) {
+ // Check if new filters are a subset of what we have
+ const isSubsetFilter = Object.entries(updates).every(([key, value]) => {
+ if (store.ignoreFilters.has(key)) return true;
+ if (key === 'page') return true; // Handle pagination locally
+ return true; // We have all data, can filter locally
+ });
- // PAGE OPTIMIZATION: Don't fetch if trying to go beyond available pages
+ if (isSubsetFilter) return false;
+ }
+
if ('page' in updates) {
const newPage = updates.page;
const oldPage = oldFilters.page || 1;
diff --git a/assets/js/concise/HandleSelection.js b/assets/js/concise/HandleSelection.js
index 3034a1c..32c07c8 100644
--- a/assets/js/concise/HandleSelection.js
+++ b/assets/js/concise/HandleSelection.js
@@ -1,25 +1,77 @@
class HandleSelection {
- constructor(options) {
- this.container = options.container; // An actual element, not class name
- this.selectors = {
- item: options.item || '.item',
- count: options.count || '.selection-count',
- bulkControls: options.bulkControls || '.selection-actions',
- checkbox: options.checkbox || '[name*="select-item"]',
- selectAll: options.selectAll || '[data-select-all]',
- wrapper: options.wrapper || ':has(.item-grid)'
+ constructor(container, options = {}) {
+ this.container = container;
+ const defaults = {
+ selectAll: {
+ checkbox: '[data-select-all]',
+ label: '.selected label',
+ span: '.selected label span',
+ target: 'data-selects',
+ count: '.selected-count, .selected .info',
+ bulkControls: '.bulk-actions',
+ },
+ items: '.item-grid',
+ wrapper: {
+ wrapper: ':has(.item-grid, [data-select-all])',
+ id: 'selection',
+ },
+ item: {
+ item: '.item',
+ idAttribute: 'id',
+ checkbox: '[name="select-item"]',
+ },
+ wrappers: {},
};
+ this.selectors = window.deepMerge(defaults, options);
- this.ui = window.uiFromSelectors(this.selectors, this.container)
+ this.a11y = window.jvbA11y;
this.selectedItems = new Set();
this.lastSelected = null; // For shift+click range selection
this.lastSelectedWrapper = null; //Tracks which wrapper we're in
this.lastClicked = null;
this.subscribers = new Set();
+ this.items = new Map();
+ this.initElements();
this.initListeners();
+ //store items in memory if available
+ this.collectItems();
}
+ removeDataReferences() {
+ let selectors = JSON.parse(JSON.stringify(this.selectors));
+ delete selectors.item.idAttribute;
+ delete selectors.wrapper.id;
+ delete selectors.selectAll.target;
+ return selectors;
+ }
+ initElements() {
+ this.index = 0;
+ let selectors = this.removeDataReferences();
+ this.ui = window.uiFromSelectors(selectors, this.container);
+
+ this.container.querySelectorAll(this.selectors.wrapper.wrapper).forEach(wrapper => {
+ this.addWrapper(wrapper);
+ });
+ }
+ addWrapper(el) {
+ let id = this.selectors.wrapper.id;
+ if (!Object.hasOwn(el.dataset, id)) {
+ el.setAttribute(`data-${id}`, this.index);
+ this.index++;
+ }
+ let selectors = this.removeDataReferences().selectAll;
+
+ //store the DOM of the grid and selectAll
+ this.ui.wrappers[el.dataset[id]] = {
+ element: el,
+ items: el.querySelector(this.selectors.items),
+ selectAll: window.uiFromSelectors(selectors, el)
+ };
+ }
+ removeWrapper(el) {
+ delete this.ui.wrappers[el.dataset[this.selectors.wrapper.id]];
+ }
initListeners() {
this.clickHandler = this.handleClick.bind(this);
@@ -28,29 +80,29 @@
this.container.addEventListener('change', this.changeHandler);
this.container.addEventListener('click', this.clickHandler);
- this.container.addEventListener('keydown', this.keyHandler);
+ document.addEventListener('keydown', this.keyHandler);
}
handleChange(e) {
- // Select all
- if (e.target.matches(this.selectors.selectAll)) {
+ if (!this.container.contains(e.target)) return;
+
+ // Select all checkbox
+ if (e.target.matches(this.selectors.selectAll.checkbox)) {
this.handleSelectAll(e.target);
return;
}
- // Individual checkbox
- if (e.target.matches(this.selectors.checkbox)) {
- const item = e.target.closest(this.selectors.item);
+ // Individual checkbox - only process if not already handled by shift-click
+ if (e.target.matches(this.selectors.item.checkbox)) {
+ const item = e.target.closest(this.selectors.item.item);
if (!item) return;
- // Find the immediate wrapper - check group first, then preview
- const wrapper = this.getItemWrapper(item);
- const id = this.getItemId(item);
+ const wrapper = this.getItemWrapper(e.target);
+ if (!wrapper) return;
- // Clear selection if clicking in different wrapper without shift
- if (this.lastSelectedWrapper && wrapper && wrapper !== this.lastSelectedWrapper && !e.shiftKey) {
- this.clearSelection();
- }
+ this.lastClicked = wrapper.element;
+ const id = this.getItemId(item);
+ if (!id) return;
if (e.target.checked) {
this.select(id, false);
@@ -59,42 +111,38 @@
}
this.lastSelected = id;
- this.lastSelectedWrapper = wrapper;
+ this.lastSelectedWrapper = wrapper.element;
+ this.updateSelectionUI();
}
}
handleClick(e) {
- const item = e.target.closest(this.selectors.item);
- const wrapper = item ? this.getItemWrapper(item) : null;
-
- if (wrapper) {
- this.lastClicked = wrapper;
- }
-
- // Handle non-checkbox clicks on items
- if (item && !e.target.matches(this.selectors.checkbox)) {
- if (this.lastSelectedWrapper && wrapper && wrapper !== this.lastSelectedWrapper && !e.shiftKey) {
- this.clearSelection();
- this.lastSelectedWrapper = wrapper;
- }
- }
-
- // Shift+click for range selection
+ // Only care about shift-clicks on checkboxes/labels
if (!e.shiftKey) return;
- const checkbox = e.target.closest(this.selectors.checkbox);
- if (!checkbox || !this.lastSelected || !this.lastSelectedWrapper) return;
+ const item = e.target.closest(this.selectors.item.item);
+ if (!item) return;
- if (!item || !wrapper) return;
+ // Check if clicking checkbox or its label
+ const clickedCheckbox = e.target.matches(this.selectors.item.checkbox);
+ const clickedLabel = e.target.closest('label[for]');
- // Range selection only works within the same wrapper
- if (wrapper !== this.lastSelectedWrapper) return;
+ if (!clickedCheckbox && !clickedLabel) return;
- const items = Array.from(wrapper.querySelectorAll(this.selectors.item));
+ const wrapper = this.getItemWrapper(item);
+ if (!wrapper) return;
+ // Can't do range selection without a previous selection in same wrapper
+ if (!this.lastSelected || !this.lastSelectedWrapper || wrapper.element !== this.lastSelectedWrapper) {
+ return; // Let change handler deal with it normally
+ }
+
+ e.preventDefault(); // Stop the checkbox from toggling
+
+ // Do range selection
const currentId = this.getItemId(item);
-
- const lastIndex = items.findIndex(el => this.getItemId(el) === this.lastSelected);
- const currentIndex = items.findIndex(el => this.getItemId(el) === currentId);
+ const items = this.getWrapperChildren(wrapper);
+ const lastIndex = items.findIndex(itemId => itemId === this.lastSelected);
+ const currentIndex = items.findIndex(itemId => itemId === currentId);
if (lastIndex === -1 || currentIndex === -1) return;
@@ -102,106 +150,71 @@
const rangeItems = items.slice(start, end + 1);
rangeItems.forEach(rangeItem => {
- this.select(this.getItemId(rangeItem));
+ this.select(rangeItem,true,false);
});
+ this.lastSelected = currentId;
+ this.updateSelectionUI();
+
this.notify('range-selected', {
selectedItems: new Set(this.selectedItems),
wrapper: wrapper
});
}
+ getWrapperChildren(wrapper) {
+ return Array.from(wrapper.items.children)
+ .map(item => this.getItemId(item));
+ }
+
getItemWrapper(item) {
if (!item) return null;
+ let wrapper = item.closest(this.selectors.wrapper.wrapper);
+ if (!wrapper)return null;
+ return this.getWrapper(wrapper);
+ }
- // Split the compound selector and check each one
- const wrapperSelectors = this.selectors.wrapper.split(',').map(s => s.trim());
-
- for (const selector of wrapperSelectors) {
- const wrapper = item.closest(selector);
- if (wrapper) return wrapper;
- }
-
- return null;
+ getWrapper(wrapper) {
+ return this.ui.wrappers[wrapper.dataset[this.selectors.wrapper.id]]??null;
}
handleKeys(e) {
- // Ctrl/Cmd + A: Select all
- if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
- e.preventDefault();
-
- // If no lastClicked wrapper, clear everything
- if (!this.lastClicked) {
- this.clearSelection();
- if (window.jvbA11y) {
- window.jvbA11y.announce('Selection cleared');
- }
- return;
- }
-
- // First escape: clear items in the current wrapper
- const wrapperItems = this.lastClicked.querySelectorAll(this.selectors.item);
- const wrapperIds = Array.from(wrapperItems).map(item => this.getItemId(item));
- const hadWrapperSelection = wrapperIds.some(id => this.selectedItems.has(id));
-
- if (hadWrapperSelection) {
- // Clear just the wrapper's items
- wrapperIds.forEach(id => this.deselect(id));
-
- // If there are still items selected elsewhere, announce partial clear
- if (this.selectedItems.size > 0) {
- if (window.jvbA11y) {
- window.jvbA11y.announce('Selection cleared in current group');
- }
- } else {
- if (window.jvbA11y) {
- window.jvbA11y.announce('Selection cleared');
- }
- }
- } else {
- // Second escape or no selection in wrapper: clear everything
- this.clearSelection();
- if (window.jvbA11y) {
- window.jvbA11y.announce('All selections cleared');
- }
- }
- }
-
// Escape: Deselect all
if (e.key === 'Escape' && this.selectedItems.size > 0) {
- this.clearSelection();
- if (window.jvbA11y) {
- window.jvbA11y.announce('Selection cleared');
+ e.preventDefault();
+ if (Object.keys(this.ui.wrappers).length > 1 && this.lastClicked) {
+ this.clearWrapperSelection(this.lastClicked);
+ } else {
+ this.clearSelection();
}
}
}
handleSelectAll(trigger) {
- const wrapper = this.getItemWrapper(trigger) || trigger.closest(this.selectors.wrapper);
+ const wrapper = this.getItemWrapper(trigger);
if (!wrapper) return;
// Clear any existing selection from other wrappers first
- if (this.lastSelectedWrapper && wrapper !== this.lastSelectedWrapper) {
- this.clearSelection();
- }
+ //Add this back if it makes more sense to clear between select-alls
+ // if (this.lastSelectedWrapper && wrapper.element !== this.lastSelectedWrapper) {
+ // this.clearSelection();
+ // }
- const items = wrapper.querySelectorAll(this.selectors.item);
- const ids = Array.from(items).map(item => this.getItemId(item));
+ const ids = this.getWrapperChildren(wrapper);
if (trigger.checked) {
ids.forEach(id => this.select(id, true, false));
- this.lastSelectedWrapper = wrapper;
+ this.lastSelectedWrapper = wrapper.element;
} else {
ids.forEach(id => this.deselect(id, true, false));
- if (this.selectedItems.size === 0) {
- this.lastSelectedWrapper = null;
- }
+ this.lastSelectedWrapper = null;
}
- let label = trigger.nextElementSibling || trigger.previousElementSibling;
- if (label && label.tagName === 'LABEL') {
- label.textContent = (trigger.checked && items.length > 0) ? 'Clear Selection' : 'Select All';
+ // Update label text
+ if (wrapper.selectAll.span) {
+ wrapper.selectAll.span.textContent = (trigger.checked && ids.length > 0) ? 'Clear Selection' : 'Select All';
}
+
this.updateSelectionUI();
this.notify('select-all', {
@@ -213,7 +226,11 @@
}
getItemId(item) {
- return item.dataset.uploadId;
+ if (!item instanceof Element) {
+ item = item.element??false;
+ if (!item) return;
+ }
+ return item.dataset[`${this.selectors.item.idAttribute}`];
}
/*******************************************************************
@@ -247,22 +264,34 @@
}
clearSelection() {
- this.selectedItems.forEach(id => this.setCheckboxState(id, false));
+ this.selectedItems.forEach(id => this.deselect(id,true,false));
this.selectedItems.clear();
this.lastSelected = null;
this.lastSelectedWrapper = null;
// Uncheck all select-all triggers
- this.container.querySelectorAll(this.selectors.selectAll).forEach(trigger => {
- trigger.checked = false;
- const label = trigger.nextElementSibling || trigger.previousElementSibling;
- if (label?.tagName === 'LABEL') {
- label.textContent = 'Select All';
+ for (let wrapper of Object.values(this.ui.wrappers)) {
+ if (wrapper.selectAll.checkbox) wrapper.selectAll.checkbox.checked = false;
+ if (wrapper.selectAll.span) {
+ wrapper.selectAll.span.textContent = 'Select All';
}
- });
+ }
+ this.a11y.announce('Selection cleared');
this.updateSelectionUI();
this.notify('selection-cleared', { selectedItems: new Set() });
}
+ clearWrapperSelection(wrapper) {
+ wrapper = this.getWrapper(wrapper);
+ if(!wrapper) return;
+ this.getWrapperChildren(wrapper).forEach(id => this.deselect(id, true, false));
+ if (wrapper.selectAll.checkbox) wrapper.selectAll.checkbox.checked = false;
+ if (wrapper.selectAll.span) {
+ wrapper.selectAll.span.textContent = 'Select All';
+ }
+ this.a11y.announce('Selection cleared in group');
+ this.updateSelectionUI();
+ this.notify('wrapper-selection-cleared', {selectedItems: this.selectedItems});
+ }
isSelected(id) {
return this.selectedItems.has(id);
@@ -276,32 +305,67 @@
* DOM HELPERS
*******************************************************************/
setCheckboxState(id, checked) {
- const item = this.container.querySelector(`[data-upload-id="${id}"]`);
- const checkbox = item?.querySelector(this.selectors.checkbox);
- if (checkbox && checkbox.checked !== checked) {
- checkbox.checked = checked;
+ const item = this.getItem(id);
+ if (!item || !item.checkbox) return;
+
+ if (item.checkbox.checked !== checked) {
+ item.checkbox.checked = checked;
}
}
updateSelectionUI() {
- if (!this.lastClicked || !this.ui.count) return;
+ if (!this.lastClicked) return;
+
+ const wrapper = this.getWrapper(this.lastClicked);
+ if (!wrapper || !wrapper.selectAll) return;
const count = this.selectedItems.size;
- // Update bulk controls visibility
- let controls = this.lastClicked.querySelector(this.selectors.bulkControls);
+ // Fix property paths:
+ let controls = wrapper.selectAll.bulkControls;
if (controls) {
controls.hidden = count === 0;
}
- // Update count display
- let countEl = this.lastClicked.querySelector(this.selectors.count);
+ let countEl = wrapper.selectAll.count;
if (countEl) {
const itemText = count === 1 ? 'item' : 'items';
- countEl.textContent = count === 0 ? '' : `{ ${count} ${itemText} selected }`;
+ countEl.textContent = count === 0 ? '' : `${count} ${itemText} selected`;
countEl.hidden = count === 0;
}
}
/*******************************************************************
+ ITEM DOM CACHING
+ *******************************************************************/
+ collectItems() {
+ this.container.querySelectorAll(this.selectors.item.item).forEach(item => {
+ this.setItem(item, true);
+ });
+ }
+
+ getItem(id) {
+ if (this.items.has(id)) return this.items.get(id);
+ return this.setItem(id);
+ }
+ setItem(id, isElement = false) {
+ let element = (!isElement)
+ ? this.container.querySelector(`[data-${this.camelToKebab(this.selectors.item.idAttribute)}="${id}"]`)
+ : id;
+ if (!element) return null;
+
+ id = this.getItemId(element);
+ if (this.items.has(id)) return this.items.get(id);
+
+ this.items.set(id, {
+ element: element,
+ checkbox: element.querySelector(this.selectors.item.checkbox)
+ });
+ return this.items.get(id);
+ }
+
+ camelToKebab(str) {
+ return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
+ }
+ /*******************************************************************
* EVENT SYSTEM
*******************************************************************/
subscribe(callback) {
diff --git a/assets/js/concise/PopulateForm.js b/assets/js/concise/PopulateForm.js
index 11ed8ae..cf48f19 100644
--- a/assets/js/concise/PopulateForm.js
+++ b/assets/js/concise/PopulateForm.js
@@ -285,8 +285,6 @@
* Populate timeline gallery - FIXED iteration
*/
populateTimelineGallery(fieldWrapper, fieldName, fieldValue) {
- console.log('Populating Timeline Gallery', fieldValue);
-
if (!fieldValue || !Array.isArray(fieldValue)) {
console.warn('Timeline field value must be an array');
return;
diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index 478be92..db25262 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -152,7 +152,7 @@
window.addEventListener('beforeunload', this.unloadHandler);
}
handleOnline() {
- this.updatePanel();
+ this.updatePanel('synced');
if (this.getQueueByStatus(this.pendingStatuses).length > 0) {
this.processQueue();
}
@@ -174,6 +174,7 @@
const refresh = window.targetCheck(e, this.selectors.refresh.button);
if (refresh) {
this.store.clearCache();
+ this.store.clearFilters();
this.store.fetch();
return;
}
@@ -304,6 +305,7 @@
title: 'Operation',
status: 'queued',
timestamp: Date.now(),
+ created_at: new Date().toISOString(),
retries: 0,
user: this.user,
... operation
@@ -441,8 +443,7 @@
this.setProcessing(false);
this.stopActivityTracking();
- // this.toggleQueue(this.maybeStartPolling());
-
+ this.toggleQueue(this.maybeStartPolling());
}
async processOperation(operation) {
@@ -545,13 +546,37 @@
});
}
+ sortOperations(ops) {
+ const statusPriority = {
+ 'processing': 0,
+ 'uploading': 1,
+ 'pending': 2,
+ 'queued': 3,
+ 'localProcessing': 4,
+ 'failed': 5,
+ 'completed': 6,
+ 'failed_permanent': 7
+ };
+
+ return ops.sort((a, b) => {
+ // First by status priority
+ const priorityDiff = (statusPriority[a.status] ?? 99) - (statusPriority[b.status] ?? 99);
+ if (priorityDiff !== 0) return priorityDiff;
+
+ // Then by updated_at (most recent first)
+ const aTime = a.updated_at ?? a.timestamp ?? 0;
+ const bTime = b.updated_at ?? b.timestamp ?? 0;
+ return new Date(bTime) - new Date(aTime);
+ });
+ }
+
getAllQueue() {
let ops = [... new Set([
...Array.from(this.store.data.values()),
... Array.from(this.queue.values())
])];
//Sort operations by operation updated_at
- return this.sortByDate(ops);
+ return this.sortOperations(ops);
}
getQueueByStatus(status) {
@@ -563,7 +588,7 @@
...Array.from(this.store.filterByIndex({status: status})),
...Array.from(this.queue.values()).filter(op => status.includes(op.status))
])];
- return this.sortByDate(ops);
+ return this.sortOperations(ops);
}
@@ -589,32 +614,53 @@
POLLING
****************************************************************************/
maybeStartPolling() {
- const incomplete = this.getQueueByStatus(this.pendingStatuses);
+ const incomplete = this.getQueueByStatus([...this.pendingStatuses, ...this.workingStatuses]);
if (incomplete.length > 0) {
this.startPolling();
return true;
}
+ this.updatePanel('synced');
return false;
}
startPolling() {
if (this.isPolling) return;
this.isPolling = true;
this.updatePanel('pending');
+ this.runPollCycle();
+ }
- this.pollTimer = setInterval(async () => {
- try {
- this.store.clearCache();
- await this.store.fetch();
- if (!this.maybeStartPolling()) {
- this.stopPolling();
- this.updatePanel('synced');
- }
- } catch (error) {
- console.error('Polling error:', error);
+ async runPollCycle() {
+ if (!this.isPolling) return;
+
+ try {
+ this.store.clearCache();
+ await this.store.fetch();
+ if (!this.maybeStartPolling()) {
+ this.stopPolling();
+ this.updatePanel('synced');
+ return;
}
- },
- 5000);
- this.startCountdown();
+ } catch (error) {
+ console.error('Polling error:', error);
+ }
+
+ // Schedule next poll with countdown
+ this.startCountdown(5, () => this.runPollCycle());
+ }
+
+ startCountdown(count, onComplete) {
+ this.ui.refresh.countdown.classList.add('counting');
+ this.ui.refresh.countdown.textContent = count;
+
+ this.countdownTimer = setInterval(() => {
+ count--;
+ if (count > 0) {
+ this.ui.refresh.countdown.textContent = count;
+ } else {
+ this.stopCountdown();
+ if (onComplete) onComplete();
+ }
+ }, 1000);
}
stopPolling() {
@@ -626,24 +672,14 @@
}
this.stopCountdown();
}
- startCountdown(count = 5) {
- if (!this.isPolling) return;
- this.ui.refresh.countdown.textContent = count;
- this.countdownTimer = setInterval(async() => {
- count--;
- if (count >= 0) {
- this.ui.refresh.countdown.textContent = count;
- }else {
- this.ui.refresh.countdown.textContent = '';
- this.stopCountdown();
- }
- },1000);
- }
+
stopCountdown() {
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
+ this.ui.refresh.countdown.classList.remove('counting');
+ this.ui.refresh.countdown.textContent = '';
}
/****************************************************************************
UI
@@ -661,6 +697,12 @@
this.ui.actions.retry.disabled = operations.filter(op => op.status === 'failed').length === 0;
this.ui.actions.clear.disabled = operations.filter(op => op.status === 'completed').length ===0;
+ const activeCount = operations.filter(op =>
+ [...this.pendingStatuses, ...this.workingStatuses].includes(op.status)
+ ).length;
+ this.ui.toggle.count.hidden = activeCount === 0;
+ this.ui.toggle.count.textContent = activeCount;
+
for (let status of this.statuses) {
if (status === 'failed_permanent') continue;
let total = operations.filter(op => op.status === status).length;
@@ -681,26 +723,38 @@
const status = this.store.filters?.status ?? 'all';
const operations = (status === 'all') ? this.getAllQueue() : this.getQueueByStatus(status);
+ const sortedOps = this.sortOperations(operations);
- window.removeChildren(this.ui.items.container);
-
- if (operations.length === 0) {
+ if (sortedOps.length === 0) {
+ window.removeChildren(this.ui.items.container);
const empty = window.getTemplate('emptyQueue');
this.ui.items.container.append(empty);
this.a11y.announce('No items in queue');
return;
+ } else {
+ this.ui.items.container.querySelector('.empty-group')?.remove();
}
- operations.forEach(op => {
- let item = this.items.get(op.id);
+ // Track which items should exist
+ const expectedIds = new Set(sortedOps.map(op => op.id));
+ // Remove items that shouldn't exist
+ this.items.forEach((item, id) => {
+ if (!expectedIds.has(id)) {
+ item.element?.remove();
+ this.items.delete(id);
+ }
+ });
+
+ // Update/add items in order
+ sortedOps.forEach((op, index) => {
+ let item = this.items.get(op.id);
if (!item) {
- // Create new element and reference
item = this.createOperationElement(op);
}
-
if (item?.element) {
this.updateOperationUI(op.id);
+ // Reorder by re-appending (moves to end in correct order)
this.ui.items.container.append(item.element);
}
});
@@ -742,20 +796,15 @@
item.ui.startedAt.textContent = window.formatTimeAgo(op.created_at);
}
let text = op.status === 'completed' ? 'Completed: ' : 'Last updated: ';
- let shouldShow =Object.hasOwn(op, 'updated_at') || Object.hasOwn(op, 'completed_at');
- item.ui.completed.wrap.hidden = !shouldShow;
- if (shouldShow && item.ui.completed.label && item.ui.completed.time) {
- let time;
- if (Object.hasOwn(op, 'completed_at')) {
- time = op.completed_at;
- } else {
- time = op.updated_at;
- }
-
- item.ui.completed.label.textContent = text;
- item.ui.completed.time.setAttribute('datetime', time);
- item.ui.completed.time.textContent = window.formatTimeAgo(time);
+ const shouldShowCompleted = op.status === 'completed' && (op.completed_at || op.updated_at);
+ item.ui.completed.wrap.hidden = !shouldShowCompleted;
+ if (shouldShowCompleted) {
+ const completedTime = op.completed_at ?? op.updated_at;
+ item.ui.completed.label.textContent = 'Completed: ';
+ item.ui.completed.time.setAttribute('datetime', completedTime);
+ item.ui.completed.time.textContent = window.formatTimeAgo(completedTime);
}
+
window.showProgress(item.ui.progress, progress, 100, this.statusLabel(op.status));
if (item.ui.actions.cancel) item.ui.actions.cancel.hidden = this.completedStatuses.includes(op.status);
if (item.ui.actions['retry']) {
@@ -786,7 +835,7 @@
updatePanel(status = 'syncing') {
if (!this.panelStatuses.includes(status)) return;
- this.ui.panel.classList.remove(this.panelStatuses);
+ this.ui.panel.classList.remove(...this.panelStatuses);
this.ui.panel.classList.add(status);
}
/****************************************************************************
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index 1d84d93..5244f02 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -411,16 +411,74 @@
this.a11y.announce(message);
}
+ openEmpty(taxonomy, singular, plural, onComplete) {
+ // Store the callback for when modal closes
+ this.emptyCallback = onComplete;
+
+ // Create a temporary "field" for bulk operations
+ const bulkFieldId = `empty-${taxonomy}-${Date.now()}`;
+
+ if (!this.fields.has(bulkFieldId)) {
+ this.fields.set(bulkFieldId, {
+ id: bulkFieldId,
+ taxonomy: taxonomy,
+ singular: singular,
+ plural: plural,
+ canSearch: true,
+ canCreate: false,
+ hasAutocomplete: false,
+ isFilter: false,
+ isEmpty: true,
+ limit: 0,
+ ui: {},
+ element: null,
+ value: null,
+ toggle: null,
+ checked: true
+ });
+ this.selectedTerms.set(bulkFieldId, new Set());
+ }
+
+ this.setField(bulkFieldId);
+ this.ui.modal.title.textContent = `Add to ${plural}`;
+
+ if (this.ui.search?.container) {
+ this.ui.search.container.hidden = false;
+ }
+
+ window.removeChildren(this.ui.selected);
+ window.removeChildren(this.ui.terms.list);
+
+ this.modal.handleOpen();
+ }
+
closeModal() {
const field = this.fields.get(this.activeField);
if (!field) return;
this.observer.unobserve(this.ui.terms.sentinel);
window.removeChildren(this.ui.terms.list);
- this.notify('selected-terms', {
- terms: this.selectedTerms.get(this.activeField),
- taxonomy: field.taxonomy
- });
+ if (field.isEmpty && this.emptyCallback) {
+ const selectedTermIds = Array.from(this.selectedTerms.get(this.activeField) || []);
+ const selectedTerms = selectedTermIds.map(id => this.store.get(id)).filter(Boolean);
+
+ this.emptyCallback({
+ taxonomy: field.taxonomy,
+ termIds: selectedTermIds,
+ terms: selectedTerms
+ });
+
+ // Cleanup temporary bulk field
+ this.fields.delete(this.activeField);
+ this.selectedTerms.delete(this.activeField);
+ this.emptyCallback = null;
+ this.bulkAssignmentTaxonomy = null;
+ } else {
+ this.notify('selected-terms', {
+ terms: this.selectedTerms.get(this.activeField),
+ taxonomy: field.taxonomy
+ });
+ }
this.activeField = null;
@@ -675,6 +733,7 @@
.some(term=>term.taxonomy === taxonomy);
fields.forEach(field => {
+ if (!field.toggle) return;
field.toggle.disabled = !hasItems && !field.canCreate;
field.toggle.title = !hasItems
? `No ${field.singular} available`
diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index e743fa8..ebc4209 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -34,7 +34,7 @@
{ name: 'field', keyPath: 'field' },
{ name: 'status', keyPath: 'status' },
{ name: 'group', keyPath: 'group' },
- { name: 'src', keyPath: 'src' }
+ { name: 'src', keyPath: 'src' },
],
},
{
@@ -57,33 +57,23 @@
this.stores.uploads.subscribe(this.handleStores.bind(this, 'uploads'));
this.stores.groups.subscribe(this.handleStores.bind(this, 'groups'));
this.queue.subscribe((event, operation) => {
- console.log(event, operation);
- if (!Object.hasOwn(operation, 'endpoint') || !['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
- return;
+ if ((event === 'operation-status' || event === 'cancel-operation')
+ && ['image_upload', 'video_upload', 'document_upload'].includes(operation.type)) {
+ const data = operation.data instanceof FormData
+ ? this.stores.uploads.formDataToObject(operation.data)
+ : operation.data;
+ console.log(data);
+ let uploads = data['upload_ids'];
+ if (!uploads || uploads.length === 0) return;
+ if (event === 'cancel-operation') return this.handleOperationCancelled(uploads);
+ this.setBulkUpload(uploads, 'status', operation.status).then(()=>{});
+ if (operation.status === 'completed') {
+ uploads.forEach(upload => {
+ this.removeUpload(upload).then(()=>{});
+ });
+ }
}
-
- const fieldId = operation.data instanceof FormData
- ? operation.data.get('fieldId')
- : operation.data?.fieldId;
- if (!fieldId) {
- return;
- }
- switch (event) {
- case 'cancel-operation':
- this.handleOperationCancelled(fieldId).then(()=>{});
- break;
- case 'operation-status':
- this.handleFieldStatus(fieldId, operation).then(()=>{});
- break;
- case 'operation-completed':
- this.handleOperationComplete(operation, fieldId).then(()=>{});
- break;
- case 'operation-failed':
- case 'operation-failed-permanent':
- this.handleOperationFailed(operation, fieldId).then(()=>{});
- break;
- }
});
}
@@ -95,7 +85,7 @@
if (event === 'data-ready') {
this.stores.ready.push(storeName);
if (this.storesReady()) {
- this.checkRecovery();
+ this.checkRecovery().then(()=>{});
}
}
}
@@ -128,9 +118,9 @@
details: '.file-upload-container .progress .details',
icon: '.file-upload-container .progress .icon'
},
- selectAll: '[name="select-all-uploads"]',
+ selectAll: '[data-select-all]',
actions: '.selection-actions',
- count: '.selection-count',
+ count: '.selected .info',
hidden: 'input[type="hidden"]'
},
// groups = selectors that affect groups as a whole
@@ -151,8 +141,8 @@
total: '.group-content .group-count'
},
items: {
- item: '[data-upload-id]',
- checkbox: '[name*="select-item"]',
+ item: '.item.upload',
+ checkbox: '[name="select-item"]',
featured: '[name="featured"]',
image: 'img',
details: 'details',
@@ -296,23 +286,36 @@
this.queueUploadMeta(e).then(()=>{});
}
}
- handleGroupMetaChange(input) {
- const element = input.closest(this.selectors.group.fields);
- if (!element) return;
+ handleGroupMetaChange(input) {
+ // Get the groupId directly from the input's data attribute
+ const groupId = input.dataset.groupId;
+ if (!groupId) return;
- const groupId = element.dataset.groupId;
- const group = this.stores.groups.get(groupId); // Changed from this.groups
+ // Capture values immediately (before debouncer)
+ const inputName = input.name;
+ const inputValue = input.value;
+
+ // Extract the field name from the input name
+ // Names are like "groupId[post_title]" or "groupId_post_title"
+ const name = inputName
+ .replace(`${groupId}[`, '')
+ .replace(`${groupId}_`, '')
+ .replace(']', '');
+
+ // Schedule the save with captured values
+ window.debouncer.schedule(`group-meta-${groupId}-${name}`, async () => {
+ const group = this.stores.groups.get(groupId);
if (!group) return;
- window.debouncer.schedule(`group-meta-${groupId}`, async (input, groupId) => {
- let name = input.name
- .replace(`${groupId}_`, '')
- .replace(`${groupId}[`, '')
- .replace(']', '');
- group.fields[name] = input.value;
- await this.setGroup(groupId, group);
- }, 300);
- }
+ // Initialize fields object if it doesn't exist
+ if (!group.fields) {
+ group.fields = {};
+ }
+
+ group.fields[name] = inputValue;
+ await this.setGroup(groupId, group);
+ }, 300);
+ }
handleDragEnter(e) {
if (!e.dataTransfer.types.includes('Files')) return;
const dropZone = e.target.closest(this.selectors.fields.dropZone);
@@ -349,7 +352,9 @@
const fieldId = this.getFieldIdFromElement(dropZone);
if (fieldId) {
- this.processFiles(fieldId, files).then(()=>{});
+ this.processFiles(fieldId, files).then(()=>{
+ this.updateHandlerItems(fieldId);
+ });
this.a11y.announce(`${files.length} file(s) dropped for upload`);
}
}
@@ -440,6 +445,7 @@
},
append: '_upload'
}
+
try {
return await this.queue.addToQueue(operation);
} catch (error) {
@@ -460,12 +466,17 @@
let files = [];
for (const group of groups) {
+ const groupElement = this.groups.get(group.id)?.element;
+ const fields = this.collectGroupFieldsFromDOM(groupElement, group.id);
+
const post = {
images: [],
- fields: group.fields??{}
+ fields: fields
};
- const groupUploads = uploads.filter(u => u.group === group.id);
+ // Use helper to get uploads in stored order
+ const groupUploads = this.getGroupUploadsInOrder(group);
+
for (const upload of groupUploads) {
const file = this.formatFile(upload);
if (file) {
@@ -474,10 +485,13 @@
upload_id: upload.id,
index: uploadMap.length
};
- let uploadEl = this.uploads.get(upload.id);
- if (uploadEl.ui?.featured?.checked) {
+
+ const uploadEl = this.uploads.get(upload.id);
+ const featuredInput = uploadEl?.element?.querySelector(`input[name="${group.id}_featured"]`);
+ if (featuredInput?.checked) {
post.fields.featured = upload.id;
}
+
post.images.push(imageData);
uploadMap.push(upload.id);
}
@@ -485,8 +499,8 @@
posts.push(post);
}
+ // Handle remaining uploads not in any group
const remaining = uploads.filter(u => !u.group);
-
for (const upload of remaining) {
const post = {
images: [],
@@ -496,7 +510,6 @@
const file = this.formatFile(upload);
if (file) {
files.push(file);
-
const imageData = {
upload_id: upload.id,
index: uploadMap.length
@@ -506,9 +519,42 @@
}
posts.push(post);
}
+
return {posts, uploadMap, files};
}
+ getGroupUploadsInOrder(group) {
+ if (!group.uploads || group.uploads.length === 0) return [];
+
+ return group.uploads
+ .map(uploadId => this.stores.uploads.get(uploadId))
+ .filter(Boolean); // Remove any that don't exist
+ }
+
+ collectGroupFieldsFromDOM(groupElement, groupId) {
+ if (!groupElement) return {};
+
+ const fields = {};
+ const inputs = groupElement.querySelectorAll('input, textarea, select');
+
+ inputs.forEach(input => {
+ // Extract field name from input name like "groupId[post_title]"
+ const name = input.name
+ .replace(`${groupId}[`, '')
+ .replace(`${groupId}_`, '')
+ .replace(']', '');
+
+ // Skip system fields like featured, select-all
+ if (['featured', 'select-all'].some(skip => name.includes(skip))) return;
+
+ if (input.value) {
+ fields[name] = input.value;
+ }
+ });
+
+ return fields;
+ }
+
collectUploads(fieldId) {
let uploads = this.stores.uploads.filterByIndex({field: fieldId});
if (uploads.length === 0) return;
@@ -545,37 +591,6 @@
return await this.sendToQueue('uploads/meta', queueData, 'Uploading Meta', '', true);
}
- async handleOperationComplete(operation, fieldId) {
- const response = operation.response;
-
- // Handle direct upload results (from uploads endpoint)
- if (response?.data) {
- const results = Array.isArray(response.data) ? response.data : Object.values(response.data);
- for (const result of results) {
- if (result.upload_id && result.attachment_id) {
- const upload = this.stores.uploads.get(result.upload_id);
- if (upload) {
- upload.attachmentId = result.attachment_id;
- upload.status = 'completed';
- await this.stores.uploads.save(upload);
- }
- }
- }
- }
-
- // Clear completed uploads and groups
- const uploads = this.stores.uploads.filterByIndex({field: fieldId});
- const groups = this.stores.groups.filterByIndex({field: fieldId});
-
- await Promise.all([
- ...uploads
- .filter(upload => upload.status === 'completed')
- .map(upload => this.clearUpload(upload.id)),
- ...groups.map(group => this.stores.groups.delete(group.id))
- ]);
-
- this.notify('uploads-complete', { fieldId, response });
- }
/*********************************************************************
FIELD LOGIC
*********************************************************************/
@@ -963,12 +978,16 @@
document.body.append(notification);
notification = document.querySelector('dialog.restore-uploads');
this.restoreModal = new window.jvbModal(notification);
- this.restoreSelection = new window.jvbHandleSelection({
- container: notification,
- wrapper: '.restore-uploads .wrap',
- bulkControls: '.selection-actions',
- selectAll: '#select-all-restore',
- count: '.selection-count'
+ this.restoreSelection = new window.jvbHandleSelection(notification,
+ {
+ wrapper: {
+ wrapper: '.wrap'
+ },
+ selectAll: {
+ bulkControls: '.selection-actions',
+ checkbox: '#select-all-restore',
+ count: '.selection-count'
+ }
});
this.restoreModal.handleOpen();
}
@@ -1012,10 +1031,9 @@
let usedIds = [];
for (let gr of groups) {
-
let group = this.stores.groups.get(gr);
- await this.createGroup(fieldId,gr);
- let element = this.groups.get(gr);
+ await this.createGroup(fieldId, gr);
+ let element = this.groups.get(gr);
let theseUploads = uploads.filter(upload => upload.group === gr);
if (group && this.groups.has(gr)) {
@@ -1195,6 +1213,9 @@
async setBulkUpload(uploads, key, value) {
const promises = Array.from(uploads).map(async (upload) => {
+ if (typeof upload === 'string') upload = await this.stores.uploads.get(upload);
+ if (!upload) return;
+
if (key === 'status') {
await this.setUploadStatus(upload, value);
}
@@ -1205,6 +1226,8 @@
}
async setUploadStatus(upload, status) {
+ if (typeof upload === 'string') upload = await this.stores.uploads.get(upload);
+ if (!upload) return;
if (upload.progress) {
window.showProgress(upload.progress, this.getStatusProgress(status), 100, this.getStatusText(status), this.queue.icons[status]??'');
}
@@ -1218,6 +1241,8 @@
group.uploads = group.uploads.filter(id => id !== uploadId);
if (group.uploads.length === 0) {
await this.removeGroup(group.id, false);
+ } else {
+ await this.stores.groups.save(group);
}
}
@@ -1319,10 +1344,12 @@
let excerpt = container.querySelector('[name="post_excerpt"]');
if (title) {
+ title.dataset.groupId = groupId;
title.id = `${groupId}_title`;
title.name = `${groupId}[post_title]`;
}
if (excerpt) {
+ title.dataset.groupId = groupId;
excerpt.id = `${groupId}_excerpt`;
excerpt.name = `${groupId}[post_excerpt]`;
}
@@ -1339,6 +1366,8 @@
element: element,
ui: window.uiFromSelectors(this.selectors.group, element)
});
+
+ this.getSelectionHandler(fieldId)?.addWrapper(element);
return element;
}
@@ -1390,12 +1419,19 @@
group.uploads = group.uploads.filter(id => id !== uploadId);
if (group.uploads.length === 0) {
await this.removeGroup(group.id, false);
+ } else {
+ await this.stores.groups.save(group);
}
}
}
//clear any selection
if (element.ui.checkbox) element.ui.checkbox.checked = false;
+ // Remove from field-level selection
+ const fieldHandler = this.selectionHandlers.get(upload.field);
+ if (fieldHandler && fieldHandler.isSelected(uploadId)) {
+ fieldHandler.deselect(uploadId);
+ }
if (this.selected.get(upload.field)?.has(uploadId)) {
this.selected.get(upload.field).delete(uploadId);
}
@@ -1409,15 +1445,18 @@
if (group) {
group.uploads.push(uploadId);
upload.group = groupId;
- this.stores.groups.save(group);
+ await this.stores.groups.save(group);
}
}
let target = (groupId) ? this.groups.get(groupId)?.ui.grid : field.ui.grid;
if (target) {
- target.append(element.element)
+ target.append(element.element);
+ if (groupId) {
+ await this.handleReorder(upload.field, groupId);
+ }
}
- this.stores.uploads.save(upload);
+ await this.stores.uploads.save(upload);
}
handleDeleteGroup(button) {
@@ -1455,14 +1494,22 @@
keepUploads ? this.addToGroup(uploadId, null) : this.removeUpload(uploadId)
)
);
+ const field = this.fields.get(group.field);
+ if (field) {
+ const sortableKey = this.getGroupKey(group.field, groupId);
+ const selectionHandler = this.selectionHandlers.get(sortableKey);
+ if (selectionHandler?.destroy) {
+ selectionHandler.destroy();
+ }
+ this.selectionHandlers.get(group.field)?.removeWrapper(element.element);
- // Destroy the Sortable for this group
- const sortableKey = this.getGroupKey(group.field, groupId);
- const sortable = this.sortables.get(sortableKey);
- if (sortable?.destroy) {
- sortable.destroy();
+ // Existing sortable cleanup
+ const sortable = this.sortables.get(sortableKey);
+ if (sortable?.destroy) {
+ sortable.destroy();
+ }
+ this.sortables.delete(sortableKey);
}
- this.sortables.delete(sortableKey);
if (element?.element) {
element.element.remove();
@@ -1487,30 +1534,11 @@
/*******************************************************************************
OPERATION METHODS
*******************************************************************************/
- async handleOperationCancelled(fieldId) {
- const uploads = this.stores.uploads.filterByIndex({field: fieldId});
- const groups = this.stores.groups.filterByIndex({field: fieldId});
-
- await Promise.all([
- ...uploads.map(upload => this.removeUpload(upload.id)),
- ...groups.map(group => this.removeGroup(group.id, false))
- ]);
- this.a11y.announce('Upload Cancelled');
- }
-
- async handleOperationFailed(operation, fieldId) {
- // Mark uploads as failed, maybe show retry UI
- await this.setBulkUpload(
- this.stores.uploads.filterByIndex({field: fieldId}),
- 'status',
- 'failed'
- );
- }
-
- async handleFieldStatus(fieldId, operation) {
- let status = operation.status;
- let uploads = this.stores.uploads.filterByIndex({field: fieldId});
- await this.setBulkUpload(uploads, 'status', status);
+ async handleOperationCancelled(uploads) {
+ if (uploads.length === 0) return;
+ uploads.forEach(upload => {
+ this.removeUpload(upload);
+ });
}
/*******************************************************************************
SELECTION HANDLERS
@@ -1525,20 +1553,27 @@
if (!this.selectionHandlers.has(key)) {
let field = this.fields.get(fieldId);
if (!field) return;
- let handler = new window.jvbHandleSelection({
- container: field.element,
- item: this.selectors.items.item,
- count: this.selectors.fields.count,
- bulkControls: this.selectors.fields.actions,
- checkbox: this.selectors.items.checkbox,
- selectAll: this.selectors.fields.selectAll,
- wrapper: `${this.selectors.fields.preview}, ${this.selectors.group.item}`,
+ if (field.config.destination !== 'post_group') return;
+ let handler = new window.jvbHandleSelection(field.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'
+ },
});
handler.subscribe((event, data) => {
this.selected.set(fieldId, data.selectedItems);
- console.log(Array.from(this.selected));
- this.syncSortableSelection(fieldId, data.selectedItems);
+ this.syncSortableSelection(fieldId);
});
this.selectionHandlers.set(key, handler);
@@ -1546,6 +1581,11 @@
return this.selectionHandlers.get(key);
}
+ updateHandlerItems(fieldId) {
+ let handler = this.getSelectionHandler(fieldId);
+ if (!handler) return;
+ handler.collectItems();
+ }
/*******************************************************************************
SORTABLE
*******************************************************************************/
@@ -1652,19 +1692,20 @@
async sortableDrop(evt, fieldId) {
const dropTarget = evt.to;
-
const items = evt.items?.length > 0 ? Array.from(evt.items) : [evt.item];
const uploadIds = items.map(item => item.dataset.uploadId).filter(Boolean);
if (uploadIds.length === 0) return;
- // Determine target group from the grid's data attribute
const targetGroupId = dropTarget.dataset.groupId || null;
await Promise.all(
uploadIds.map(uploadId => this.addToGroup(uploadId, targetGroupId))
);
+ // After all moves complete, sync order from DOM
+ await this.handleReorder(fieldId, targetGroupId);
+
this.selectionHandlers.get(fieldId)?.clearSelection();
}
@@ -1689,28 +1730,34 @@
}
handleReorder(fieldId, groupId = null) {
- let target = (groupId) ? this.groups.get(groupId)?.ui.grid : this.fields.get(fieldId)?.ui.grid;
+ let target = (groupId)
+ ? this.groups.get(groupId)?.ui.grid
+ : this.fields.get(fieldId)?.ui.grid;
+
if (!target) {
- console.log ('Couldn\'t Reorder items...');
+ console.log('Couldn\'t Reorder items...');
return;
}
- //Get current order from DOM
- let items = Array.from(target.querySelectorAll(this.selectors.items.item+':not(.ghost)'))
+
+ // Get current order from DOM
+ let items = Array.from(target.children)
+ .filter(el => el.matches(this.selectors.items.item) && !el.classList.contains('ghost'))
.map(upload => upload.dataset.uploadId)
.filter(id => id);
-
if (!groupId) {
let hiddenInput = this.fields.get(fieldId)?.ui.hidden;
if (hiddenInput) {
hiddenInput.value = items.join(',');
}
} else {
- let group = this.groups.get(groupId);
+ let group = this.stores.groups.get(groupId);
if (group) {
group.uploads = items;
+ this.stores.groups.save(group).then(()=>{});
}
}
+
this.a11y.announce('Items reordered');
}
/*******************************************************************************
diff --git a/assets/js/min/crud.min.js b/assets/js/min/crud.min.js
index 7f71379..dfe392e 100644
--- a/assets/js/min/crud.min.js
+++ b/assets/js/min/crud.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e){if(this.queue=window.jvbQueue,this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.a11y=window.jvbA11y,!this.content)return;this.isTimeline=!1,this.currentItemID=null,this.initElements(),this.updateBulkOptions();const t=window.jvbStore.register(this.content,{storeName:this.content,keyPath:"id",endpoint:"content",headers:{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:{content:this.content,user:window.auth.getUser(),page:1,status:"all",orderby:"modified",order:"desc"},TTL:18e5,showLoading:!0});this.store=t[this.content],this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.tableForm=null,this.tableChanges=new Map,this.formController=this.isTimeline?new window.jvbForm({collectFormData:()=>this.collectTimelineData.bind(this)}):new window.jvbForm,this.viewController.subscribe(((e,t)=>{if("table-view"!==e||this.tableForm){if("not-table-view"===e)this.tableForm;else if("order-changed"===e){let e=this.store.get(t);if(!e)return;let s={};s[t]=e,this.savePosts(s,"Updating progression order")}}else this.tableForm||(this.tableForm=this.formController.registerForm(t,{autosave:!1,formStatus:!1,isTable:!0}))})),this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&["content","uploads/groups"].includes(t.endpoint)&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initialized=!1,this.init()}handleFormChange(e,t){let s=t.fullData.post_title,i=Object.hasOwn(t,"changes")?t.changes:t.fullData,l={};if(this.isTimeline)return l[this.currentItemID]=i,void this.savePosts(l,s);let o=[];switch(console.log(t),!0){case t.config.element===this.ui.forms.edit:l[this.currentItemID]=i,s=`Saving ${s} Changes`,i.post_status&&this.shouldRemoveItem(i.post_status)&&o.push(this.currentItemID);break;case t.config.element===this.ui.forms.bulkEdit:let a=t.config.element.querySelectorAll(".selected input:checked");a.forEach((e=>{l[e.value]=i,i.post_status&&this.shouldRemoveItem(i.post_status)&&o.push(e.value)})),s=`Updating ${a.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(l[t.config.data["form-id"]]=i,s=`Saving ${s} Changes`)}if(o.length>0){let e=0;o.forEach((t=>{setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50})),t.config.element===this.ui.forms.bulkEdit&&setTimeout((()=>{this.viewController.clearSelection()}),e+100)}0!==Object.keys(l).length&&this.savePosts(l,s)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(0===Object.keys(e).length)return;for(let t in e)e[t].content||(e[t].content=this.content);let s={endpoint:"content",headers:{action_nonce:window.auth.getNonce("dash")},data:{posts:e},delay:!0,popup:"Saving changes",title:t};this.queue.addToQueue(s)}async handleQueueSuccess(e,t){this.store.clearCache(),this.store.fetch()}handleQueueFailure(e,t){console.error("Operation failed permanently:",t),this.a11y?.announce(`Operation failed: ${t.error_message||"Unknown error"}`)}initElements(){this.elements={modals:{create:"dialog.create",edit:"dialog.edit",bulkEdit:"dialog.bulkEdit"},container:".crud[data-content]",grid:".item-grid",bulkSelectActions:".bulk-action-select",forms:{create:"dialog.create form",edit:"dialog.edit form",bulkEdit:"dialog.bulkEdit form"},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.elements),this.ui.uploader&&(window.jvbUploads.scanFields(document.querySelector(this.elements.uploader)),window.jvbUploads.subscribe(((e,t)=>{"sent-to-queue"===e&&(console.log(t),t===this.ui.uploader.querySelector("[data-uploader]")?.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearCache()})))}))),this.isTimeline=!!document.querySelector("[data-timeline]")}init(){this.ui.uploader&&(this.settings.addSetting(this.ui.uploader,"open"),this.ui.uploader.addEventListener("toggle",(e=>{this.settings.saveSetting("open",this.ui.uploader.open?"on":"off")}))),this.filterHandler=this.handleFilterChange.bind(this),this.changeHandler=this.handleChange.bind(this),this.modals={};for(let[e,t]of Object.entries(this.ui.modals))this.modals[e]=new window.jvbModal(t),this.modals[e].subscribe(((t,s)=>{if("modal-close"===t)this.currentItemID=null,this.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId)}));this.setupEventDelegation(),this.setupFilters(),this.initialized=!0}setupEventDelegation(){document.addEventListener("change",this.changeHandler),document.addEventListener("click",(e=>{const t=e.target.closest("[data-action]");if(t){e.preventDefault();const s=t.dataset.action,i=t.dataset.id;switch(s){case"edit":this.populateEditForm(i),this.modals.edit.handleOpen();break;case"delete":if(confirm("Delete this item?")){let e={};e[t.dataset.id]={post_status:"delete",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`),this.store.delete(i)}break;case"trash":let e={};e[t.dataset.id]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"create":this.modals.create.dataset.itemId="new",this.modals.create.dataset.content=this.content,this.modals.create.handleOpen();break;case"bulk-edit":Array.from(this.viewController.selectedItems).length>0&&this.modals.bulkEdit.handleOpen();break;case"bulk-delete":const s=Array.from(this.viewController.selectedItems);s.length>0&&confirm(`Delete ${s.length} items?`)&&(s.forEach((e=>this.store.delete(e))),this.viewController.clearSelection());break;case"sync":break;case"refresh":this.store.fetch()}}e.target.closest(".create-item")&&(this.formController.registerForm(this.ui.forms.create),this.modals.create.handleOpen()),e.target.closest(".cancel-bulk")&&this.viewController.selectAll(!1)})),document.addEventListener("keydown",(e=>{(e.ctrlKey||e.metaKey)&&"a"===e.key&&this.ui.container&&this.ui.container.contains(document.activeElement)&&(e.preventDefault(),this.viewController.selectAll()),"Escape"===e.key&&this.viewController?.selectedItems.size>0&&0===window.jvbModal.getAllModals().length&&this.viewController.clearSelection()}))}handleChange(e){if(e.target.closest("[data-id]"))this.isTimeline?this.handleTimelineTableChange(e):this.handleTableChange(e);else{if(e.target.classList.contains("bulk-action-select")){if(e.target.value.startsWith("tax-")){const t=e.target.value.replace("tax-","");return this.openTaxonomyModal(t),void(e.target.value="")}switch(e.target.value){case"edit":this.populateBulkEdit(),this.modals.bulkEdit.handleOpen();break;case"publish":this.setBulkStatus("publish");break;case"draft":case"restore":this.setBulkStatus("draft");break;case"trash":this.setBulkStatus("trash");break;case"delete":this.setBulkStatus("delete")}}window.targetCheck(e,"select[data-filter]")&&this.handleFilterChange(e)}}handleTableChange(e){const t=e.target.closest("tr[data-id]");if(!t)return;const s=e.target,i=parseInt(t.dataset.id),l=s.closest(["data-field"])?.dataset.field;if(!l)return;const o=this.store.get(i);if(!o)return;o.fields[l]=this.getInputValue(s),this.store.save(o);let a={};a[i]=o.fields,this.savePosts(a,`Saving changes to ${this.content}`)}handleTimelineTableChange(e){const t=e.target.closest("tbody[data-id]");if(!t)return;const s=e.target,i=s.closest("[data-field]")?.dataset.field;if(!i)return;const l=parseInt(t.dataset.id),o=s.closest("tr.timeline-point"),a=this.store.get(l);if(!a)return;const n=this.getInputValue(s);if(o){const e=o.dataset.imageId;a.fields.timeline||(a.fields.timeline={}),a.fields.timeline[e]||(a.fields.timeline[e]={}),a.fields.timeline[e][i]=n}else a.fields[i]=n;this.store.save(a);let r={};r[l]=a.fields,this.savePosts(r,"Updating progress post")}getInputValue(e){return"checkbox"===e.type?e.checked?e.value||"1":"":"radio"===e.type?e.checked?e.value:null:e.value}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(e.length>0){e=e.join(",");let s={},i=Array.from(this.viewController.selectedItems);i.forEach((i=>{s[i]={content:this.content},s[i][t]=e}));let l=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,l)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,s={};for(let t of this.viewController.selectedItems)s[t]={post_status:e,content:this.content};if("delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";if("all"===this.status&&!["publish","draft"].includes(e)||e!==this.status){let e=0;for(let t of this.viewController.selectedItems)setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50}this.viewController.clearSelection(),0!==Object.keys(s).length&&this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target;if("taxonomies"===t.dataset.filter){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,t.value)}else this[t.dataset.filter]=t.value,this.store.setFilter(t.dataset.filter,t.value),"status"===t.dataset.filter&&this.updateBulkOptions(t.value)}updateBulkOptions(e="all"){if("trash"===e){if(this.ui.bulkSelectActions?.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("trashOptions").querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulkSelectActions.append(e)}))}}else if(this.ui.bulkSelectActions&&!this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}this.ui.bulkSelectActions&&(this.ui.bulkSelectActions.value="")}populateBulkEdit(){const e=this.modals.bulkEdit.modal.querySelector("form .selected");if(!e)return;window.removeChildren(e);for(let t of this.viewController.selectedItems){let s=this.store.get(t);const i=window.getTemplate("bulkItem");if(!i)return;const l=i.querySelector("input[type=checkbox]"),o=i.querySelector("img");l&&(l.id=`bulk_${s.id}`,l.value=s.id,l.checked=!0),o&&s.thumbnail&&(o.src=s.thumbnail,o.alt=s.alt||""),e.append(i)}let t=this.modals.bulkEdit.modal;[t.querySelector("h2 span").textContent]=[this.viewController.selectedItems.size],this.formController.registerForm(this.ui.forms.bulkEdit)}populateEditForm(e){this.currentItemID=e;let t=this.store.get(parseInt(e));if(t){this.ui.modals.edit.dataset.itemId=e,this.ui.modals.edit.dataset.content=this.content;let s=this.ui.modals.edit.querySelector("form");this.ui.modals.edit.querySelector("h2").textContent=`Editing ${t.fields.post_title}`,s.dataset.formId=`edit-${e}`,new window.jvbPopulate(s,t),this.formController.registerForm(this.ui.forms.edit)}}setupFilters(){const e=document.querySelector('.all-filters input[type="search"]');if(e){let t;e.addEventListener("input",(()=>{e.value.length>3?(clearTimeout(t),t=setTimeout((()=>{this.store.setFilter("search",e.value)}),300)):0===e.value.length&&this.store.removeFilter("search")}))}}destroy(){document.querySelectorAll("[data-filter]").forEach((e=>{e.removeEventListener("change",this.filterHandler)}))}}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.cache=new window.jvbCache(this.content),this.activeItem=null,this.isTimeline=!1,this.items={list:new Map,grid:new Map,table:new Map},this.init())}init(){this.initElements(),this.initListeners();let e=this.initSettings();this.initStore(e),this.checkHideFilters(),this.initIntegrations(),this.initUploader(),this.initModals()}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)=>{"sent-to-queue"===e&&t===this.ui.uploader.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearCache()}))})))}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){this.activeItem=null;const t=this.ui.modals[e].form.dataset.formId;t&&this.formController.cleanupForm(t),this.ui.modals[e].form.reset(),"date"===e&&this.handleCustomDateSelection()}})))}initStore(e){let t={...this.defaults,...e};const i=window.jvbStore.register(this.content,{storeName:this.content,keyPath:"id",endpoint:this.endpoint??"content",headers:{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});this.store=i[this.content],this.store.subscribe(((e,t)=>{if("data-loaded"===e)this.render(),this.selectionHandler.collectItems()}))}initIntegrations(){this.selected=new Set,this.selectionHandler=new window.jvbHandleSelection(this.container,{selectAll:{checkbox:".crud #select-all",label:".bulk-select label",span:".bulk-select label span"},wrapper:{wrapper:".wrap"}}),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.formController=new window.jvbForm,this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),this.queue.subscribe(((e,t)=>{["image_upload","video_upload","document_upload"].includes(t.type)&&"operation-status"===e&&"completed"===t.status&&this.store.clearCache()}))}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}`,l=this.cache.get(s);l&&(i.value=l,e[s]=l)}));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),document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler),this.ui.filters.search&&this.ui.filters.search.addEventListener("input",this.inputHandler)}handleChange(e){if(window.targetCheck(e,this.selectors.filters.search))return;const t=window.targetCheck(e,this.selectors.bulk.action);if(t)return void this.handleBulkAction(t);let i=window.targetCheck(e,"[data-filter]");if(i)return void this.handleFilterChange(i);let s=window.targetCheck(e,"input[data-view]");if(s)this.handleViewChange(s);else if("table"===this.view){if(window.targetCheck(e,"[data-id]"))return void this.handleTableChange(e);if(window.targetCheck(e,"details.multi-select"))return void this.toggleColumn(e.target.id,e.target.checked);let t=window.targetCheck(e,this.selectors.table.nav);t&&(this.tabNav=t.checked,this.cache.set("tabNav",t.checked?"vertical":"horizontal"))}}handleBulkAction(e){if(e.value.startsWith("tax-")){const t=e.options[e.selectedIndex],i=t.dataset.taxonomy,s=t.dataset.single,l=t.dataset.plural;return window.jvbSelector.openEmpty(i,s,l,(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){if(!e.termIds.length||!this.selected.size)return;const t={},i=`tax_${e.taxonomy}`;this.selected.forEach((s=>{const l=this.store.get(parseInt(s));if(!l)return;const a=(l.taxonomies?.[e.taxonomy]||[]).map((e=>e.id)),o=[...new Set([...a,...e.termIds])];t[s]={[i]:o.join(","),content:this.content}})),Object.keys(t).length>0&&this.savePosts(t,`Adding ${e.terms.length} ${e.taxonomy} to ${this.selected.size} ${this.plural}...`),this.selectionHandler.clearSelection()}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(),l=`${e}-${t}-${String(s).padStart(2,"0")}`;this.setFilter("dateFrom",i),this.setFilter("dateTo",l),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){window.targetCheck(e,".clear-search")&&this.deleteFilter("search","");let t=window.targetCheck(e,"[data-action]");if(t){e.preventDefault();let i=t.dataset.id;switch(t.dataset.action){case"edit":this.openEditModal(i);break;case"delete":if(confirm("Delete this item? This cannot be undone")){let e={};e[i]={post_status:"delete",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`),this.store.delete(i)}break;case"trash":let e={};e[i]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"bulk-edit":this.selected.size>0&&this.openBulkEditModal();break;case"bulk-delete":this.selected.size>0&&confirm(`Delete ${this.selected.size} items?`)&&(this.selected.forEach((e=>this.store.delete(e))),this.selectionHandler.clearSelection());break;case"refresh":this.store.clearCache(),this.store.fetch();break;case"clear-filters":this.clearAllFilters()}}if(window.targetCheck(e,".apply-date-filter"))return this.handleCustomDateSelection(),void this.modals.date.handleClose();window.targetCheck(e,this.selectors.buttons.create)&&(this.ui.modals.create.form.reset(),this.formController.registerForm(this.ui.modals.create.form),this.modals.create.handleOpen())}handleInput(e){e.preventDefault(),e.stopPropagation();let t=e.target.value.trim(),i=`${this.content}-search`;console.log("Maybe search",t),0!==t.length?window.debouncer.schedule(i,(()=>{console.log("Searching for",t),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,l=e.shiftKey;let a=this.findNextEditableRow(i,l);a||(a=this.wrapToRow(i,l)),a&&this.focusFieldInRow(a,s,l)}}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.id)}focusFieldInRow(e,t,i=!1){const s=e.querySelector(`[data-field="${t}"]`);if(!s)return;const l=this.findFocusableInput(s);if(l){l.focus(),l.select&&"text"===l.type&&l.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}toggleTimelineListeners(e=!0){if(!this.isTimeline||"table"!==this.view)return;if(this.timelineSortables&&(this.timelineSortables.forEach((e=>e.destroy())),this.timelineSortables=[]),!e)return;this.timelineSortables=[];this.ui.table.form.querySelectorAll("tbody.item").forEach((e=>{const t=new Sortable(e,{animation:150,handle:".drag-handle",draggable:"tr.timeline-point",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",group:{name:`timeline-${e.dataset.id}`,pull:!1,put:!1},onEnd:e=>this.handleTimelineReorder(e)});this.timelineSortables.push(t)}))}handleTimelineReorder(e){const{item:t,oldIndex:i,newIndex:s,from:l}=e;if(i===s)return;const a=parseInt(l.dataset.id),o=this.store.get(a);if(!o?.fields?.timeline)return;const n=Array.from(l.querySelectorAll("tr.timeline-point")).map((e=>e.dataset.imageId)).filter(Boolean),r={};n.forEach(((e,t)=>{o.fields.timeline[e]&&(r[e]={...o.fields.timeline[e],order:t})})),o.fields.timeline=r,this.store.save(o),this.savePosts({[a]:{timeline:r,content:this.content}},"Reordering timeline...",!0),this.a11y?.announce(`Moved to position ${s+1}`)}openEditModal(e){let t=this.store.get(parseInt(e));t&&(this.activeItem=e,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.ui.modals.edit.form.reset(),new window.jvbPopulate(this.ui.modals.edit.form,t),this.formController.registerForm(this.ui.modals.edit.form),this.modals.edit.handleOpen())}openBulkEditModal(){window.removeChildren(this.ui.modals.bulkEdit.selected),this.ui.modals.edit.form.reset(),this.modals.bulkEdit.handleOpen(),this.selected.forEach((e=>{let t=window.getTemplate("bulkItem");if(!t)return;let i=this.store.get(parseInt(e));if(!i)return;let[s,l,a]=[t.querySelector("input"),t.querySelector("img"),t.querySelector("label")];s&&(s.id=`bulk_${i.id}`,s.value=i.id,s.checked=!0,s.name="selected[]");let o=i.images[i.fields.post_thumbnail]??{};l&&(l.src=o.medium??"",l.alt=o.alt??""),a.title=i.fields.post_title??"",this.ui.modals.bulkEdit.selected.append(t)})),this.ui.modals.bulkEdit.h2&&(this.ui.modals.bulkEdit.h2.textContent=this.selected.size),this.formController.registerForm(this.ui.modals.bulkEdit.form)}handleFormChange(e,t){let i=t.fullData.post_title,s=Object.hasOwn(t,"changes")?t.changes:t.fullData,l={};if(this.isTimeline)return l[this.activeItem]=s,s.post_status&&this.shouldRemoveItemUI(s.post_status)&&this.removeItems([this.activeItem]),void this.savePosts(l,i,"form-submit"===e);let a=[],o=t.config.element;if(o===this.ui.modals.edit.form)l[this.activeItem]=s,i=`Saving ${i} Changes`,s.post_status&&this.shouldRemoveItemUI(s.post_status)&&a.push(this.activeItem);else if(o===this.ui.modals.bulkEdit.form){let e=0;o.querySelectorAll(".selected input:checked").forEach((t=>{l[t.value]=s,s.post_status&&this.shouldRemoveItemUI(s.post_status)&&a.push(t.value),e++})),i=`Updating ${e} ${this.plural} Changes`}else o===this.ui.modals.create.form&&"form-submit"===e&&(l[o.dataset.formId]=s,i=`Saving ${i} Changes`);a.length>0&&this.removeItems(a),this.selectionHandler.clearSelection(),0!==Object.keys(l).length&&this.savePosts(l,i,"form-submit"===e)}savePosts(e,t,i=!1){if(0===Object.keys(e).length)return;let s={};for(let t in e)t&&"null"!==t&&"undefined"!==t?(e[t].content||(e[t].content=this.content),s[t]=e[t]):console.warn("Skipping save for invalid ID:",t);let l={endpoint:this.endpoint,headers:{action_nonce:window.auth.getNonce("dash")},data:{posts:s},delay:i,popup:"Saving changes",title:t};this.queue.addToQueue(l)}handleTableChange(e){const t=this.isTimeline?e.target.closest("tbody[data-id]"):e.target.closest("tr[data-id]");if(!t)return;const i=e.target,s=i.closest("[data-field]")?.dataset.field;if(!s)return;const l=parseInt(t.dataset.id),a=this.store.get(l);if(!a)return;const o=this.getInputValue(i);if(this.isTimeline){const e=i.closest("tr.timeline-point");if(e){const t=e.dataset.imageId;a.fields.timeline??={},a.fields.timeline[t]??={},a.fields.timeline[t][s]=o}else a.fields[s]=o}else a.fields[s]=o;this.store.save(a),this.savePosts({[l]:a.fields},"Saving changes...",!0)}getInputValue(e){return"checkbox"===e.type?e.checked?e.value||"1":"":"radio"===e.type?e.checked?e.value:null:e.value}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,i={};if(this.selected.forEach((t=>{i[t]={post_status:e,content:this.content}})),"delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";this.shouldRemoveItemUI(e)&&this.removeItems(Object.keys(i)),this.selectionHandler.clearSelection(),0!==Object.keys(i).length&&this.savePosts(i,`${t} ${Object.keys(i).length} ${this.plural}...`)}render(){const e=this.store.getFiltered();if(0!==e.length){switch(this.view){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e);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"]');"trash"===this.status&&t?(window.removeChildren(this.ui.bulk.action),e=window.getTemplate("trashOptions")):"trash"===this.status||t||(window.removeChildren(this.ui.bulk.action),e=window.getTemplate("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.getTemplate("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.getTemplate("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.formController.registerForm(this.ui.table.form,{autosave:!1,formStatus:!1,isTable:!0}):this.formController.cleanupForm(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),this.isTimeline&&!e&&this.toggleTimelineListeners(e)}renderGrid(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view");const t=document.createDocumentFragment();e.forEach((e=>{let i=this.renderGridItem(e);t.appendChild(i)})),this.ui.grid.appendChild(t)}renderList(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view");const t=document.createDocumentFragment();e.forEach((e=>{let i=this.renderListItem(e);t.append(i)})),this.ui.grid.append(t)}renderTable(e){this.toggleTable(),window.removeChildren(this.ui.grid);let t=document.createDocumentFragment();e.forEach((e=>{let i=this.renderTableItem(e);i&&t.append(i)})),this.ui.table.body?this.ui.table.body.append(t):this.ui.table.table.insertBefore(t,this.ui.table.foot),this.isTimeline&&this.toggleTimelineListeners(!0)}setupItemElement(e,t,i){return this.items[this.view].has(t.id)?this.items[this.view].get(t.id):(e.dataset.id=t.id,t._pending&&e.classList.add("pending"),this.setupItemSelection(e,t),this.setupItemThumbnail(e,t),this.setupItemActions(e,t),this.items[this.view].set(t.id,e),e)}setupItemSelection(e,t){const i=e.querySelector(".select-item"),s=e.querySelector(".select-item + label, .select-item-label");i&&(i.id=`select-${t.id}`,i.value=t.id,i.checked=this.selected.has(parseInt(t.id)),s&&(s.htmlFor=`select-${t.id}`))}setupItemThumbnail(e,t){const i=e.querySelector("img");if(!i)return;const s=t.images?.[t.fields?.post_thumbnail]??{};i.src=s.medium??"",i.alt=s.alt??""}setupItemActions(e,t){e.querySelectorAll("[data-action]").forEach((e=>{e.dataset.id=t.id}))}renderGridItem(e){if(this.items.grid.has(e.id))return this.items.grid.get(e.id);const t=window.getTemplate("gridView");return t?this.setupItemElement(t,e,"grid"):null}renderListItem(e){if(this.items.list.has(e.id))return this.items.list.get(e.id);let t=window.getTemplate("listView");return t?(t=this.setupItemElement(t,e,"list"),t.querySelectorAll("[data-attr]").forEach((t=>{const i=e[t.dataset.attr];i&&""!==i?t.textContent=i:t.remove()})),t.querySelectorAll("[data-field]").forEach((t=>{const i=e.fields?.[t.dataset.field];i&&""!==i?"DIV"===t.tagName?t.innerHTML=i:t.textContent=i:t.remove()})),this.items.list.set(e.id,t),t):null}renderTableItem(e){if(this.items.table.has(e.id))return this.items.table.get(e.id);let t=window.getTemplate("tableView");if(!t)return null;t=this.setupItemElement(t,e,"table");const i=t.querySelector(`input[name="post_status"][value="${e.status}"]`);if(i&&(i.checked=!0),this.isTimeline){const i=t.querySelector("tr.shared");i&&(new window.jvbPopulate(i,e),this.cleanupTableRow(i));const s=t.querySelector("tr.timeline-point");s&&e.fields?.timeline&&(Object.entries(e.fields.timeline).forEach((([i,l],a)=>{const o=s.cloneNode(!0);o.dataset.index=a,o.dataset.imageId=i,new window.jvbPopulate(o,{fields:l,images:e.images,taxonomies:e.taxonomies}),this.cleanupTableRow(o);const n=e.images?.[l.post_thumbnail];n&&o.querySelector(".field.upload")?.setAttribute("title",n["image-title"]||""),t.insertBefore(o,s)})),s.remove())}else void 0!==this.ui.table.form?.dataset.edit?new window.jvbPopulate(t,e):this.populateTableReadOnly(t,e),this.cleanupTableRow(t);return this.items.table.set(e.id,t),t}populateTableReadOnly(e,t){for(const[i,s]of Object.entries(t)){const t=e.querySelector(`[data-field="${i}"]`);if(!t)continue;const l=t.querySelector("p");l&&(l.textContent="date"===t.dataset.fieldType?window.formatTimeAgo(s):s)}}cleanupTableRow(e){e.querySelectorAll("td[data-field]").forEach((e=>{e.querySelectorAll('label:not(.select-item-label,.radio-option,[for*="select-item"])').forEach((e=>{e.closest(".radio-options")||e.remove()})),"true_false"===e.dataset.fieldType&&e.querySelector(".toggle-label")?.remove(),["checkbox","radio","select"].includes(e.dataset.fieldType)&&e.querySelector(".label")?.remove()}))}toggleColumn(e,t){this.ui.table.table.querySelectorAll(`.${e}`).forEach((e=>{e.hidden=!t}))}shouldRemoveItemUI(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.store.filters.status}removeItems(e){let t=0;e.forEach((e=>{setTimeout((()=>{let t=this.items[this.view].get(e);t&&window.fade(t,!1)}),t),t+=50}))}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);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 console.log("Sub filter el not found: ",t),console.log(e),!1;i=this.ui.filters[e][t]}return i}destroy(){this.timelineSortables&&(this.timelineSortables.forEach((e=>e.destroy())),this.timelineSortables=[]),document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),this.ui.filters.search&&this.ui.filters.search.removeEventListener("input",this.handleInput);for(let e of Object.values(this.items))e.clear()}}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 441b6ec..73152e3 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.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${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:{},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.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),delete:t=>this.delete(e,t),get:t=>this.get(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=()=>a(o.error);try{h=r(c,o)}catch(e){a(e)}}))):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))return this.notify(e,"data-loaded",{cached:!0,items:r.items||[]}),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){throw"AbortError"!==t.name&&(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||[],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 d={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,d),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.put(d)}))}))}_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}processForStorage(e,t=!0,s="root"){if(null==e)return{valid:!0,data:e};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}`}:(console.debug(`[DataStore] Stripped 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}`}:(console.debug(`[DataStore] Stripped DOM element at ${s}`),{valid:!0,data:void 0});if(e instanceof FormData)return console.debug(`[DataStore] Converting FormData at ${s}`),{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&&(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:(console.debug(`[DataStore] Stripped 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})}get(e,t){return this.stores.get(e).data.get(t)}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=>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&&r.items)return r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]);if(t.filters.search&&t.filters.search.trim()){const e=t.filters.search.toLowerCase().trim();let s=Array.from(t.data.values()).filter((e=>{for(const[s,r]of Object.entries(t.filters))if("search"!==s&&"page"!==s&&null!=r&&""!==r&&e[s]!==r)return!1;return!0}));return s=s.filter((t=>["name","title","path","description","slug"].some((s=>{const r=t[s];return!!r&&r.toLowerCase().includes(e)})))),s}return this.getAll(e)}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}));const a=await this.shouldFetchWithFilters(e,t,i);this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t}),r.config.endpoint&&a?await this.fetch(e):r.config.endpoint&&this.notify(e,"data-loaded")}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;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.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${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",...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),delete:t=>this.delete(e,t),get:t=>this.get(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=()=>a(o.error);try{h=r(c,o)}catch(e){a(e)}}))):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))return this.notify(e,"data-loaded",{cached:!0,items:r.items||[]}),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){throw"AbortError"!==t.name&&(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||[],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 d={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,d),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.put(d)}))}))}_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}processForStorage(e,t=!0,s="root"){if(null==e)return{valid:!0,data:e};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&&(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})}get(e,t){return this.stores.get(e).data.get(t)}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=>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)return this.applyOrdering(r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]),t);const i=Array.from(t.data.values()),a=t.filters.search?.toLowerCase().trim()||"",o=i.filter((e=>{for(const[s,r]of Object.entries(t.filters))if(!t.ignoreFilters.has(s)&&null!=r&&""!==r&&"all"!==r)if("string"==typeof r&&r.includes(",")){if(!r.split(",").map((e=>e.trim())).includes(String(e[s])))return!1}else if(String(e[s])!==String(r))return!1;return!(a&&!this.searchObject(e,a))}));return this.applyOrdering(o,t)}applyOrdering(e,t){if(Array.isArray(e)||(e=Array.from(e)),0===e.length)return e;if(t.filters.orderby||t.filters.order){const s=t.filters.orderby||"date",r=(t.filters.order||"desc").toLowerCase();e.sort(((e,t)=>{let i,a;switch(s){case"alphabetical":case"title":i=(e.fields?.post_title||e.title||e.name||"").toLowerCase(),a=(t.fields?.post_title||t.title||t.name||"").toLowerCase();break;case"modified":i=new Date(e.modified||0),a=new Date(t.modified||0);break;default:i=new Date(e.date||0),a=new Date(t.date||0)}return i<a?"asc"===r?-1:1:i>a?"asc"===r?1:-1:0}))}return e}searchObject(e,t){if(!e||"object"!=typeof e)return!1;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}),this.notify(e,"data-loaded",{cached:!0,items:this.getFiltered(e)});const a=await this.shouldFetchWithFilters(e,t,i);r.config.endpoint&&a?await this.fetch(e):r.config.endpoint&&this.notify(e,"data-loaded")}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/handleSelection.min.js b/assets/js/min/handleSelection.min.js
index 26a0fa2..cee4f65 100644
--- a/assets/js/min/handleSelection.min.js
+++ b/assets/js/min/handleSelection.min.js
@@ -1 +1 @@
-window.jvbHandleSelection=class{constructor(e){this.container=e.container,this.selectors={item:e.item||".item",count:e.count||".selection-count",bulkControls:e.bulkControls||".selection-actions",checkbox:e.checkbox||'[name*="select-item"]',selectAll:e.selectAll||"[data-select-all]",wrapper:e.wrapper||":has(.item-grid)"},this.ui=window.uiFromSelectors(this.selectors,this.container),this.selectedItems=new Set,this.lastSelected=null,this.lastSelectedWrapper=null,this.lastClicked=null,this.subscribers=new Set,this.initListeners()}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.keyHandler=this.handleKeys.bind(this),this.container.addEventListener("change",this.changeHandler),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("keydown",this.keyHandler)}handleChange(e){if(e.target.matches(this.selectors.selectAll))this.handleSelectAll(e.target);else if(e.target.matches(this.selectors.checkbox)){const t=e.target.closest(this.selectors.item);if(!t)return;const s=this.getItemWrapper(t),i=this.getItemId(t);this.lastSelectedWrapper&&s&&s!==this.lastSelectedWrapper&&!e.shiftKey&&this.clearSelection(),e.target.checked?this.select(i,!1):this.deselect(i,!1),this.lastSelected=i,this.lastSelectedWrapper=s}}handleClick(e){const t=e.target.closest(this.selectors.item),s=t?this.getItemWrapper(t):null;if(s&&(this.lastClicked=s),t&&!e.target.matches(this.selectors.checkbox)&&this.lastSelectedWrapper&&s&&s!==this.lastSelectedWrapper&&!e.shiftKey&&(this.clearSelection(),this.lastSelectedWrapper=s),!e.shiftKey)return;if(!e.target.closest(this.selectors.checkbox)||!this.lastSelected||!this.lastSelectedWrapper)return;if(!t||!s)return;if(s!==this.lastSelectedWrapper)return;const i=Array.from(s.querySelectorAll(this.selectors.item)),l=this.getItemId(t),c=i.findIndex((e=>this.getItemId(e)===this.lastSelected)),r=i.findIndex((e=>this.getItemId(e)===l));if(-1===c||-1===r)return;const[n,h]=[Math.min(c,r),Math.max(c,r)];i.slice(n,h+1).forEach((e=>{this.select(this.getItemId(e))})),this.notify("range-selected",{selectedItems:new Set(this.selectedItems),wrapper:s})}getItemWrapper(e){if(!e)return null;const t=this.selectors.wrapper.split(",").map((e=>e.trim()));for(const s of t){const t=e.closest(s);if(t)return t}return null}handleKeys(e){if((e.ctrlKey||e.metaKey)&&"a"===e.key){if(e.preventDefault(),!this.lastClicked)return this.clearSelection(),void(window.jvbA11y&&window.jvbA11y.announce("Selection cleared"));const t=this.lastClicked.querySelectorAll(this.selectors.item),s=Array.from(t).map((e=>this.getItemId(e)));s.some((e=>this.selectedItems.has(e)))?(s.forEach((e=>this.deselect(e))),this.selectedItems.size>0?window.jvbA11y&&window.jvbA11y.announce("Selection cleared in current group"):window.jvbA11y&&window.jvbA11y.announce("Selection cleared")):(this.clearSelection(),window.jvbA11y&&window.jvbA11y.announce("All selections cleared"))}"Escape"===e.key&&this.selectedItems.size>0&&(this.clearSelection(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared"))}handleSelectAll(e){const t=this.getItemWrapper(e)||e.closest(this.selectors.wrapper);if(!t)return;this.lastSelectedWrapper&&t!==this.lastSelectedWrapper&&this.clearSelection();const s=t.querySelectorAll(this.selectors.item),i=Array.from(s).map((e=>this.getItemId(e)));e.checked?(i.forEach((e=>this.select(e,!0,!1))),this.lastSelectedWrapper=t):(i.forEach((e=>this.deselect(e,!0,!1))),0===this.selectedItems.size&&(this.lastSelectedWrapper=null));let l=e.nextElementSibling||e.previousElementSibling;l&&"LABEL"===l.tagName&&(l.textContent=e.checked&&s.length>0?"Clear Selection":"Select All"),this.updateSelectionUI(),this.notify("select-all",{wrapper:t,checked:e.checked,ids:i,selectedItems:new Set(this.selectedItems)})}getItemId(e){return e.dataset.uploadId}select(e,t=!0,s=!0){this.selectedItems.has(e)||(this.selectedItems.add(e),t&&this.setCheckboxState(e,!0),s&&this.updateSelectionUI(),this.notify("item-selected",{id:e,selectedItems:new Set(this.selectedItems)}))}deselect(e,t=!0,s=!0){this.selectedItems.has(e)&&(this.selectedItems.delete(e),t&&this.setCheckboxState(e,!1),s&&this.updateSelectionUI(),this.notify("item-deselected",{id:e,selectedItems:new Set(this.selectedItems)}))}toggle(e){this.selectedItems.has(e)?this.deselect(e):this.select(e),this.updateSelectionUI()}clearSelection(){this.selectedItems.forEach((e=>this.setCheckboxState(e,!1))),this.selectedItems.clear(),this.lastSelected=null,this.lastSelectedWrapper=null,this.container.querySelectorAll(this.selectors.selectAll).forEach((e=>{e.checked=!1;const t=e.nextElementSibling||e.previousElementSibling;"LABEL"===t?.tagName&&(t.textContent="Select All")})),this.updateSelectionUI(),this.notify("selection-cleared",{selectedItems:new Set})}isSelected(e){return this.selectedItems.has(e)}getSelection(){return new Set(this.selectedItems)}setCheckboxState(e,t){const s=this.container.querySelector(`[data-upload-id="${e}"]`),i=s?.querySelector(this.selectors.checkbox);i&&i.checked!==t&&(i.checked=t)}updateSelectionUI(){if(!this.lastClicked||!this.ui.count)return;const e=this.selectedItems.size;let t=this.lastClicked.querySelector(this.selectors.bulkControls);t&&(t.hidden=0===e);let s=this.lastClicked.querySelector(this.selectors.count);if(s){const t=1===e?"item":"items";s.textContent=0===e?"":`{ ${e} ${t} selected }`,s.hidden=0===e}}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("HandleSelection subscriber error:",e)}}))}destroy(){this.container.removeEventListener("change",this.changeHandler),this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("keydown",this.keyHandler),this.subscribers.clear(),this.selectedItems.clear()}};
\ No newline at end of file
+window.jvbHandleSelection=class{constructor(e,t={}){this.container=e,this.selectors=window.deepMerge({selectAll:{checkbox:"[data-select-all]",label:".selected label",span:".selected label span",target:"data-selects",count:".selected-count, .selected .info",bulkControls:".bulk-actions"},items:".item-grid",wrapper:{wrapper:":has(.item-grid, [data-select-all])",id:"selection"},item:{item:".item",idAttribute:"id",checkbox:'[name="select-item"]'},wrappers:{}},t),this.a11y=window.jvbA11y,this.selectedItems=new Set,this.lastSelected=null,this.lastSelectedWrapper=null,this.lastClicked=null,this.subscribers=new Set,this.items=new Map,this.initElements(),this.initListeners(),this.collectItems()}removeDataReferences(){let e=JSON.parse(JSON.stringify(this.selectors));return delete e.item.idAttribute,delete e.wrapper.id,delete e.selectAll.target,e}initElements(){this.index=0;let e=this.removeDataReferences();this.ui=window.uiFromSelectors(e,this.container),this.container.querySelectorAll(this.selectors.wrapper.wrapper).forEach((e=>{this.addWrapper(e)}))}addWrapper(e){let t=this.selectors.wrapper.id;Object.hasOwn(e.dataset,t)||(e.setAttribute(`data-${t}`,this.index),this.index++);let s=this.removeDataReferences().selectAll;this.ui.wrappers[e.dataset[t]]={element:e,items:e.querySelector(this.selectors.items),selectAll:window.uiFromSelectors(s,e)}}removeWrapper(e){delete this.ui.wrappers[e.dataset[this.selectors.wrapper.id]]}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.keyHandler=this.handleKeys.bind(this),this.container.addEventListener("change",this.changeHandler),this.container.addEventListener("click",this.clickHandler),document.addEventListener("keydown",this.keyHandler)}handleChange(e){if(this.container.contains(e.target))if(e.target.matches(this.selectors.selectAll.checkbox))this.handleSelectAll(e.target);else if(e.target.matches(this.selectors.item.checkbox)){const t=e.target.closest(this.selectors.item.item);if(!t)return;const s=this.getItemWrapper(e.target);if(!s)return;this.lastClicked=s.element;const i=this.getItemId(t);if(!i)return;e.target.checked?this.select(i,!1):this.deselect(i,!1),this.lastSelected=i,this.lastSelectedWrapper=s.element,this.updateSelectionUI()}}handleClick(e){if(!e.shiftKey)return;const t=e.target.closest(this.selectors.item.item);if(!t)return;const s=e.target.matches(this.selectors.item.checkbox),i=e.target.closest("label[for]");if(!s&&!i)return;const l=this.getItemWrapper(t);if(!l)return;if(!this.lastSelected||!this.lastSelectedWrapper||l.element!==this.lastSelectedWrapper)return;e.preventDefault();const c=this.getItemId(t),r=this.getWrapperChildren(l),n=r.findIndex((e=>e===this.lastSelected)),a=r.findIndex((e=>e===c));if(-1===n||-1===a)return;const[h,d]=[Math.min(n,a),Math.max(n,a)];r.slice(h,d+1).forEach((e=>{this.select(e,!0,!1)})),this.lastSelected=c,this.updateSelectionUI(),this.notify("range-selected",{selectedItems:new Set(this.selectedItems),wrapper:l})}getWrapperChildren(e){return Array.from(e.items.children).map((e=>this.getItemId(e)))}getItemWrapper(e){if(!e)return null;let t=e.closest(this.selectors.wrapper.wrapper);return t?this.getWrapper(t):null}getWrapper(e){return this.ui.wrappers[e.dataset[this.selectors.wrapper.id]]??null}handleKeys(e){"Escape"===e.key&&this.selectedItems.size>0&&(e.preventDefault(),Object.keys(this.ui.wrappers).length>1&&this.lastClicked?this.clearWrapperSelection(this.lastClicked):this.clearSelection())}handleSelectAll(e){const t=this.getItemWrapper(e);if(!t)return;const s=this.getWrapperChildren(t);e.checked?(s.forEach((e=>this.select(e,!0,!1))),this.lastSelectedWrapper=t.element):(s.forEach((e=>this.deselect(e,!0,!1))),this.lastSelectedWrapper=null),t.selectAll.span&&(t.selectAll.span.textContent=e.checked&&s.length>0?"Clear Selection":"Select All"),this.updateSelectionUI(),this.notify("select-all",{wrapper:t,checked:e.checked,ids:s,selectedItems:new Set(this.selectedItems)})}getItemId(e){if(!(!e instanceof Element)||(e=e.element??!1))return e.dataset[`${this.selectors.item.idAttribute}`]}select(e,t=!0,s=!0){this.selectedItems.has(e)||(this.selectedItems.add(e),t&&this.setCheckboxState(e,!0),s&&this.updateSelectionUI(),this.notify("item-selected",{id:e,selectedItems:new Set(this.selectedItems)}))}deselect(e,t=!0,s=!0){this.selectedItems.has(e)&&(this.selectedItems.delete(e),t&&this.setCheckboxState(e,!1),s&&this.updateSelectionUI(),this.notify("item-deselected",{id:e,selectedItems:new Set(this.selectedItems)}))}toggle(e){this.selectedItems.has(e)?this.deselect(e):this.select(e),this.updateSelectionUI()}clearSelection(){this.selectedItems.forEach((e=>this.deselect(e,!0,!1))),this.selectedItems.clear(),this.lastSelected=null,this.lastSelectedWrapper=null;for(let e of Object.values(this.ui.wrappers))e.selectAll.checkbox&&(e.selectAll.checkbox.checked=!1),e.selectAll.span&&(e.selectAll.span.textContent="Select All");this.a11y.announce("Selection cleared"),this.updateSelectionUI(),this.notify("selection-cleared",{selectedItems:new Set})}clearWrapperSelection(e){(e=this.getWrapper(e))&&(this.getWrapperChildren(e).forEach((e=>this.deselect(e,!0,!1))),e.selectAll.checkbox&&(e.selectAll.checkbox.checked=!1),e.selectAll.span&&(e.selectAll.span.textContent="Select All"),this.a11y.announce("Selection cleared in group"),this.updateSelectionUI(),this.notify("wrapper-selection-cleared",{selectedItems:this.selectedItems}))}isSelected(e){return this.selectedItems.has(e)}getSelection(){return new Set(this.selectedItems)}setCheckboxState(e,t){const s=this.getItem(e);s&&s.checkbox&&s.checkbox.checked!==t&&(s.checkbox.checked=t)}updateSelectionUI(){if(!this.lastClicked)return;const e=this.getWrapper(this.lastClicked);if(!e||!e.selectAll)return;const t=this.selectedItems.size;let s=e.selectAll.bulkControls;s&&(s.hidden=0===t);let i=e.selectAll.count;if(i){const e=1===t?"item":"items";i.textContent=0===t?"":`${t} ${e} selected`,i.hidden=0===t}}collectItems(){this.container.querySelectorAll(this.selectors.item.item).forEach((e=>{this.setItem(e,!0)}))}getItem(e){return this.items.has(e)?this.items.get(e):this.setItem(e)}setItem(e,t=!1){let s=t?e:this.container.querySelector(`[data-${this.camelToKebab(this.selectors.item.idAttribute)}="${e}"]`);return s?(e=this.getItemId(s),this.items.has(e)||this.items.set(e,{element:s,checkbox:s.querySelector(this.selectors.item.checkbox)}),this.items.get(e)):null}camelToKebab(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}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("HandleSelection subscriber error:",e)}}))}destroy(){this.container.removeEventListener("change",this.changeHandler),this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("keydown",this.keyHandler),this.subscribers.clear(),this.selectedItems.clear()}};
\ No newline at end of file
diff --git a/assets/js/min/populate.min.js b/assets/js/min/populate.min.js
index 6dd9498..f05859a 100644
--- a/assets/js/min/populate.min.js
+++ b/assets/js/min/populate.min.js
@@ -1 +1 @@
-window.jvbPopulate=class{constructor(e,t={},a={},i={}){this.item=this.normalizeItemData(t,a),this.form=e,this.options=i;for(let[t,a]of Object.entries(this.item.fields)){let i=e.querySelector(`[data-field="${t}"]`);i&&this.populateField(i,t,a)}}normalizeItemData(e,t){return e&&"object"==typeof e&&"fields"in e?{fields:e.fields||{},images:e.images||{},taxonomies:e.taxonomies||{}}:{fields:e||{},images:t||{},taxonomies:{}}}isTaxonomyField(e){return Object.hasOwn(this.item.taxonomies,e)&&Object.keys(this.item.taxonomies[e]).length>0}isImageField(e){return!(!this.item.images||0===Object.keys(this.item.images).length)&&this.splitIDs(e).some((e=>Object.keys(this.item.images).includes(String(e))))}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e)&&e>0))}populateField(e,t,a,i={}){if(e&&null!=a)switch(this.getFieldType(e)){case"upload":case"gallery":case"image":this.populateUploadField(e,t,a);break;case"repeater":this.populateRepeaterField(e,t,a);break;case"selector":this.populateTaxonomyField(e,t,a);break;case"user":this.populateUserField(e,t,a);break;case"location":this.populateLocationField(e,t,a);break;case"set":case"checkbox":this.populateSetField(e,t,a);break;case"select":case"radio":this.populateSelectField(e,t,a);break;case"true_false":this.populateBooleanField(e,t,a);break;case"date":case"time":case"datetime":this.populateDateField(e,t,a);break;case"number":this.populateNumberField(e,t,a);break;case"textarea":e.querySelector(".editor-container")?this.populateEditorField(e,t,a):this.populateTextareaField(e,t,a);break;default:this.populateTextField(e,t,a)}}populateTaxonomyField(e,t,a){let i=[];if(Array.isArray(a))i=a.map((e=>String(e)));else if("string"==typeof a)try{const e=JSON.parse(a);i=Array.isArray(e)?e.map((e=>String(e))):[String(e)]}catch(e){i=a.split(",").map((e=>e.trim())).filter((e=>e))}else a&&(i=[String(a)]);if(0===i.length)return;const l=e.querySelector(`input[type="hidden"][name="${t}"]`);l&&(l.value=i.join(","),window.jvbTaxonomy&&requestAnimationFrame((()=>{window.jvbTaxonomy.updateFieldFromInput(l)})))}populateUploadField(e,t,a){if("timeline"===e.dataset.subtype||"timeline"===t)return void this.populateTimelineGallery(e,t,a);if(!a)return;const i=this.splitIDs(a);if(0===i.length)return;const l=e.querySelector(`input[type="hidden"][name="${t}"]`);l&&(l.value=i.join(","));const r=e.querySelector(".item-grid"),o=e.querySelector(".file-upload-container");r&&window.removeChildren(r),e.querySelector(".progress")?.remove(),r&&(i.forEach((e=>{const t=window.getTemplate("uploadItem");t?(this.populateUploadItem(t,e),r.append(t)):console.warn("uploadItem template not found")})),i.length>0&&o&&(o.hidden=!0))}populateUploadItem(e,t){let a=e.querySelector('input[name="select-item"]'),i=e.querySelector('label[for="select-item"]');e.dataset.id=t,a.name=`select-item-${t}`,a.id=a.name,i.htmlFor=a.name;const l=e.querySelector("img");if(e.querySelector("video")?.remove(),this.item.images[t]){const a=this.item.images[t];l&&(l.src=a.medium||a.small||a.large||"",l.alt=a["image-alt-text"]||a.alt||"");const i=e.querySelector('[name="image-title"]'),r=e.querySelector('[name="image-alt-text"]'),o=e.querySelector('[name="image-caption"]');i&&(i.value=a["image-title"]||a.title||""),r&&(r.value=a["image-alt-text"]||a.alt||""),o&&(o.value=a["image-caption"]||a.caption||"")}else console.warn(`No image data found for ID: ${t}`);e.querySelector("details .upload-meta > .hint")?.remove()}populateTimelineGallery(e,t,a){if(console.log("Populating Timeline Gallery",a),!a||!Array.isArray(a))return void console.warn("Timeline field value must be an array");if(0===a.length)return;const i=e.querySelector(".item-grid"),l=e.querySelector(".file-upload-container");if(i&&window.removeChildren(i),e.querySelector(".progress")?.remove(),i){for(let e of a){const t=window.getTemplate("timelineItem");if(!t){console.warn("timelineItem template not found");continue}const a=e.post_thumbnail,l=e.id;t.dataset.id=a,t.dataset.postId=l;let r=t.querySelector('input[name="select-item"]'),o=t.querySelector('label[for="select-item"]');r&&o&&(r.name=`select-item-${a}`,r.id=r.name,o.htmlFor=r.name),t.querySelector("video")?.remove(),t.querySelector(".select-item span")?.remove();const n=t.querySelector("img"),s=this.item.images[a];n&&s&&(n.src=s.medium||s.small||s.large||"",n.title=s["image-title"]||"",n.alt=s["image-alt-text"]||""),t.querySelectorAll(".field").forEach((t=>{if(t.classList.contains("group"))return;const a=t.querySelector('input:not([type="file"]), textarea');if(!a)return;const i=t.querySelector("label"),r=a.name.replace("upload_data::","").replace(/^\[.*?\]/,"");let o=e[r];void 0===o&&s&&(o=s[r]),null!=o&&this.populateField(t,r,o);const n=`[${l}]${r}`,c=n;a.name=n,a.id=c,i&&(i.htmlFor=c)})),i.append(t)}a.length>0&&l&&(l.hidden=!0)}}populateTextField(e,t,a){const i=e.querySelector(`[name="${t}"], input, textarea`);if(i&&"file"!==i.type&&(i.value=String(a||""),i.dataset.limit)){const t=e.querySelector(".char-count .current");t&&(t.textContent=i.value.length)}}populateTextareaField(e,t,a){const i=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea:not([data-editor="true"])');if(i&&(i.value=String(a||""),i.dispatchEvent(new Event("change",{bubbles:!0})),i.dataset.limit)){const t=e.querySelector(".char-count .current");if(t){t.textContent=i.value.length;const a=parseInt(i.dataset.limit,10);e.classList.toggle("reached",i.value.length>=a)}}}populateEditorField(e,t,a){const i=e.querySelector(`textarea[name="${t}"][data-editor="true"]`);if(!i)return;i.value=String(a||"");const l=e.querySelector(".editor"),r=a||"<p><br></p>";if(l){let e=l.__quill;if(!e&&window.Quill)for(let t of window.Quill.instances||[])if(t.container===l){e=t;break}e?(e.root.innerHTML=r,l.__quill=e):l.innerHTML=r}i.dispatchEvent(new Event("change",{bubbles:!0}))}getFieldType(e){if(e.dataset.fieldType)return e.dataset.fieldType;if(e.dataset.type)return e.dataset.type;const t=["upload","repeater","taxonomy","user","location","set","checkbox","select","radio","true_false","date","time","datetime","editor","number","text","textarea","email","url","tel","phone"];for(const a of t)if(e.classList.contains(a))return a;const a=e.querySelector("input, select, textarea");if(a){if("TEXTAREA"===a.tagName)return"true"===a.dataset.editor?"editor":"textarea";if(a.type)return"checkbox"!==a.type||e.classList.contains("true_false")?a.type:"set"}return"text"}populateNumberField(e,t,a){const i=e.querySelector(`[name="${t}"], input[type="number"]`);i&&(i.value=Number(a)||0)}populateBooleanField(e,t,a){const i=e.querySelector(`[name="${t}"], input[type="checkbox"]`);i&&(i.checked=Boolean(a))}populateSelectField(e,t,a){const i=String(a||""),l=e.querySelector(`select[name="${t}"]`);if(l)return void(l.value=i);const r=e.querySelector(`input[type="radio"][name="${t}"][value="${i}"]`);r&&(r.checked=!0)}populateSetField(e,t,a){let i=a;if("string"==typeof a)try{i=JSON.parse(a)}catch(e){i=a.split(",").map((e=>e.trim()))}Array.isArray(i)||(i=[String(i)]),e.querySelectorAll(`input[type="checkbox"][name*="${t}"]`).forEach((e=>{e.checked=i.includes(e.value)}))}populateDateField(e,t,a){const i=e.querySelector(`[name="${t}"], input`);if(i&&a){let e=a;"object"==typeof a&&a.date&&(e=a.date);try{const t=new Date(e);if(!isNaN(t.getTime()))switch(i.type){case"date":i.value=t.toISOString().split("T")[0];break;case"time":i.value=t.toTimeString().slice(0,5);break;case"datetime-local":i.value=t.toISOString().slice(0,16);break;default:i.value=e}}catch(t){i.value=e}}}populateLocationField(e,t,a){a&&"object"==typeof a&&["address","lat","lng","street","city","province","postal_code","country"].forEach((i=>{if(void 0!==a[i]){const l=e.querySelector(`[name="${t}_${i}"], [name="${i}"]`);l&&(l.value=String(a[i]||""))}}))}populateUserField(e,t,a){this.populateTaxonomyField(e,t,a)}populateRepeaterField(e,t,a){if(!a||!Array.isArray(a))return;const i=e.querySelector(".repeater-items"),l=e.querySelector("template");i&&l?(window.removeChildren(i),a.forEach(((a,r)=>{if(!a||"object"!=typeof a)return;const o=window.getTemplate(l.className);if(!o)return void console.warn(`Repeater field ${t}: template not found`);o.id=`${e.closest("form").id}-${t}-row-${r}`,o.dataset.index=r;const n=o.querySelector(".row-number");n&&(n.textContent=`#${r+1}`),o.querySelectorAll("input, select, textarea").forEach((e=>{const i=e.name,l=`${t}:${r}:${i}`,o=`${t}-${r}-${i}-${e.value}`;e.name=l,e.id=o;const n=e.nextElementSibling;n&&"LABEL"===n.tagName&&(n.htmlFor=o),void 0!==a[i]&&this.populateRepeaterFieldValue(e,i,a[i])})),i.appendChild(o)}))):console.warn(`Repeater field ${t}: missing container or template`)}populateRepeaterFieldValue(e,t,a){switch(e.type){case"checkbox":e.checked=Boolean(a);break;case"radio":e.checked=e.value===String(a);break;default:e.value=String(a||"")}}};
\ No newline at end of file
+window.jvbPopulate=class{constructor(e,t={},a={},i={}){this.item=this.normalizeItemData(t,a),this.form=e,this.options=i;for(let[t,a]of Object.entries(this.item.fields)){let i=e.querySelector(`[data-field="${t}"]`);i&&this.populateField(i,t,a)}}normalizeItemData(e,t){return e&&"object"==typeof e&&"fields"in e?{fields:e.fields||{},images:e.images||{},taxonomies:e.taxonomies||{}}:{fields:e||{},images:t||{},taxonomies:{}}}isTaxonomyField(e){return Object.hasOwn(this.item.taxonomies,e)&&Object.keys(this.item.taxonomies[e]).length>0}isImageField(e){return!(!this.item.images||0===Object.keys(this.item.images).length)&&this.splitIDs(e).some((e=>Object.keys(this.item.images).includes(String(e))))}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e)&&e>0))}populateField(e,t,a,i={}){if(e&&null!=a)switch(this.getFieldType(e)){case"upload":case"gallery":case"image":this.populateUploadField(e,t,a);break;case"repeater":this.populateRepeaterField(e,t,a);break;case"selector":this.populateTaxonomyField(e,t,a);break;case"user":this.populateUserField(e,t,a);break;case"location":this.populateLocationField(e,t,a);break;case"set":case"checkbox":this.populateSetField(e,t,a);break;case"select":case"radio":this.populateSelectField(e,t,a);break;case"true_false":this.populateBooleanField(e,t,a);break;case"date":case"time":case"datetime":this.populateDateField(e,t,a);break;case"number":this.populateNumberField(e,t,a);break;case"textarea":e.querySelector(".editor-container")?this.populateEditorField(e,t,a):this.populateTextareaField(e,t,a);break;default:this.populateTextField(e,t,a)}}populateTaxonomyField(e,t,a){let i=[];if(Array.isArray(a))i=a.map((e=>String(e)));else if("string"==typeof a)try{const e=JSON.parse(a);i=Array.isArray(e)?e.map((e=>String(e))):[String(e)]}catch(e){i=a.split(",").map((e=>e.trim())).filter((e=>e))}else a&&(i=[String(a)]);if(0===i.length)return;const r=e.querySelector(`input[type="hidden"][name="${t}"]`);r&&(r.value=i.join(","),window.jvbTaxonomy&&requestAnimationFrame((()=>{window.jvbTaxonomy.updateFieldFromInput(r)})))}populateUploadField(e,t,a){if("timeline"===e.dataset.subtype||"timeline"===t)return void this.populateTimelineGallery(e,t,a);if(!a)return;const i=this.splitIDs(a);if(0===i.length)return;const r=e.querySelector(`input[type="hidden"][name="${t}"]`);r&&(r.value=i.join(","));const l=e.querySelector(".item-grid"),o=e.querySelector(".file-upload-container");l&&window.removeChildren(l),e.querySelector(".progress")?.remove(),l&&(i.forEach((e=>{const t=window.getTemplate("uploadItem");t?(this.populateUploadItem(t,e),l.append(t)):console.warn("uploadItem template not found")})),i.length>0&&o&&(o.hidden=!0))}populateUploadItem(e,t){let a=e.querySelector('input[name="select-item"]'),i=e.querySelector('label[for="select-item"]');e.dataset.id=t,a.name=`select-item-${t}`,a.id=a.name,i.htmlFor=a.name;const r=e.querySelector("img");if(e.querySelector("video")?.remove(),this.item.images[t]){const a=this.item.images[t];r&&(r.src=a.medium||a.small||a.large||"",r.alt=a["image-alt-text"]||a.alt||"");const i=e.querySelector('[name="image-title"]'),l=e.querySelector('[name="image-alt-text"]'),o=e.querySelector('[name="image-caption"]');i&&(i.value=a["image-title"]||a.title||""),l&&(l.value=a["image-alt-text"]||a.alt||""),o&&(o.value=a["image-caption"]||a.caption||"")}else console.warn(`No image data found for ID: ${t}`);e.querySelector("details .upload-meta > .hint")?.remove()}populateTimelineGallery(e,t,a){if(!a||!Array.isArray(a))return void console.warn("Timeline field value must be an array");if(0===a.length)return;const i=e.querySelector(".item-grid"),r=e.querySelector(".file-upload-container");if(i&&window.removeChildren(i),e.querySelector(".progress")?.remove(),i){for(let e of a){const t=window.getTemplate("timelineItem");if(!t){console.warn("timelineItem template not found");continue}const a=e.post_thumbnail,r=e.id;t.dataset.id=a,t.dataset.postId=r;let l=t.querySelector('input[name="select-item"]'),o=t.querySelector('label[for="select-item"]');l&&o&&(l.name=`select-item-${a}`,l.id=l.name,o.htmlFor=l.name),t.querySelector("video")?.remove(),t.querySelector(".select-item span")?.remove();const n=t.querySelector("img"),s=this.item.images[a];n&&s&&(n.src=s.medium||s.small||s.large||"",n.title=s["image-title"]||"",n.alt=s["image-alt-text"]||""),t.querySelectorAll(".field").forEach((t=>{if(t.classList.contains("group"))return;const a=t.querySelector('input:not([type="file"]), textarea');if(!a)return;const i=t.querySelector("label"),l=a.name.replace("upload_data::","").replace(/^\[.*?\]/,"");let o=e[l];void 0===o&&s&&(o=s[l]),null!=o&&this.populateField(t,l,o);const n=`[${r}]${l}`,c=n;a.name=n,a.id=c,i&&(i.htmlFor=c)})),i.append(t)}a.length>0&&r&&(r.hidden=!0)}}populateTextField(e,t,a){const i=e.querySelector(`[name="${t}"], input, textarea`);if(i&&"file"!==i.type&&(i.value=String(a||""),i.dataset.limit)){const t=e.querySelector(".char-count .current");t&&(t.textContent=i.value.length)}}populateTextareaField(e,t,a){const i=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea:not([data-editor="true"])');if(i&&(i.value=String(a||""),i.dispatchEvent(new Event("change",{bubbles:!0})),i.dataset.limit)){const t=e.querySelector(".char-count .current");if(t){t.textContent=i.value.length;const a=parseInt(i.dataset.limit,10);e.classList.toggle("reached",i.value.length>=a)}}}populateEditorField(e,t,a){const i=e.querySelector(`textarea[name="${t}"][data-editor="true"]`);if(!i)return;i.value=String(a||"");const r=e.querySelector(".editor"),l=a||"<p><br></p>";if(r){let e=r.__quill;if(!e&&window.Quill)for(let t of window.Quill.instances||[])if(t.container===r){e=t;break}e?(e.root.innerHTML=l,r.__quill=e):r.innerHTML=l}i.dispatchEvent(new Event("change",{bubbles:!0}))}getFieldType(e){if(e.dataset.fieldType)return e.dataset.fieldType;if(e.dataset.type)return e.dataset.type;const t=["upload","repeater","taxonomy","user","location","set","checkbox","select","radio","true_false","date","time","datetime","editor","number","text","textarea","email","url","tel","phone"];for(const a of t)if(e.classList.contains(a))return a;const a=e.querySelector("input, select, textarea");if(a){if("TEXTAREA"===a.tagName)return"true"===a.dataset.editor?"editor":"textarea";if(a.type)return"checkbox"!==a.type||e.classList.contains("true_false")?a.type:"set"}return"text"}populateNumberField(e,t,a){const i=e.querySelector(`[name="${t}"], input[type="number"]`);i&&(i.value=Number(a)||0)}populateBooleanField(e,t,a){const i=e.querySelector(`[name="${t}"], input[type="checkbox"]`);i&&(i.checked=Boolean(a))}populateSelectField(e,t,a){const i=String(a||""),r=e.querySelector(`select[name="${t}"]`);if(r)return void(r.value=i);const l=e.querySelector(`input[type="radio"][name="${t}"][value="${i}"]`);l&&(l.checked=!0)}populateSetField(e,t,a){let i=a;if("string"==typeof a)try{i=JSON.parse(a)}catch(e){i=a.split(",").map((e=>e.trim()))}Array.isArray(i)||(i=[String(i)]),e.querySelectorAll(`input[type="checkbox"][name*="${t}"]`).forEach((e=>{e.checked=i.includes(e.value)}))}populateDateField(e,t,a){const i=e.querySelector(`[name="${t}"], input`);if(i&&a){let e=a;"object"==typeof a&&a.date&&(e=a.date);try{const t=new Date(e);if(!isNaN(t.getTime()))switch(i.type){case"date":i.value=t.toISOString().split("T")[0];break;case"time":i.value=t.toTimeString().slice(0,5);break;case"datetime-local":i.value=t.toISOString().slice(0,16);break;default:i.value=e}}catch(t){i.value=e}}}populateLocationField(e,t,a){a&&"object"==typeof a&&["address","lat","lng","street","city","province","postal_code","country"].forEach((i=>{if(void 0!==a[i]){const r=e.querySelector(`[name="${t}_${i}"], [name="${i}"]`);r&&(r.value=String(a[i]||""))}}))}populateUserField(e,t,a){this.populateTaxonomyField(e,t,a)}populateRepeaterField(e,t,a){if(!a||!Array.isArray(a))return;const i=e.querySelector(".repeater-items"),r=e.querySelector("template");i&&r?(window.removeChildren(i),a.forEach(((a,l)=>{if(!a||"object"!=typeof a)return;const o=window.getTemplate(r.className);if(!o)return void console.warn(`Repeater field ${t}: template not found`);o.id=`${e.closest("form").id}-${t}-row-${l}`,o.dataset.index=l;const n=o.querySelector(".row-number");n&&(n.textContent=`#${l+1}`),o.querySelectorAll("input, select, textarea").forEach((e=>{const i=e.name,r=`${t}:${l}:${i}`,o=`${t}-${l}-${i}-${e.value}`;e.name=r,e.id=o;const n=e.nextElementSibling;n&&"LABEL"===n.tagName&&(n.htmlFor=o),void 0!==a[i]&&this.populateRepeaterFieldValue(e,i,a[i])})),i.appendChild(o)}))):console.warn(`Repeater field ${t}: missing container or template`)}populateRepeaterFieldValue(e,t,a){switch(e.type){case"checkbox":e.checked=Boolean(a);break;case"radio":e.checked=e.value===String(a);break;default:e.value=String(a||"")}}};
\ No newline at end of file
diff --git a/assets/js/min/queue.min.js b/assets/js/min/queue.min.js
index b7501ce..725c753 100644
--- a/assets/js/min/queue.min.js
+++ b/assets/js/min/queue.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.user=window.auth.getUser(),this.canUpdateUI=!0,this.isProcessing=!1,this.isPolling=!1,this.queue=new Map,this.items=new Map,this.subscribers=new Set,this.api=jvbSettings.api,this.endpoint="queue",this.init()}init(){this.headers={"X-WP-Nonce":window.auth.getNonce()},this.initElements(),this.initListeners(),this.initStore(),this.canUpdateUI&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle.button,name:"Queue Panel"}))}initElements(){this.panelStatuses=["syncing","synced","pending","offline"],this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.pendingStatuses=["queued","localProcessing","uploading"],this.workingStatuses=["pending","processing"],this.completedStatuses=["completed","failed","failed_permanent"],this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:{button:"button.qtoggle",indicator:".qtoggle .indicator",count:".qtoggle .count"},refresh:{button:"#queue .refresh .refreshNow",countdown:"#queue .refresh .countdown"},popup:{popup:"#queue .popup",message:"#queue .popup span"},items:{container:"#queue .qitems"},actions:{retry:"#queue .retry-all",clear:"#queue .dismiss-all"},filters:{filter:"#queue [data-filter]",all:{label:'#queue [for="qfilter-all"]',radio:'#queue [data-filter="all"]',count:'#queue [data-filter="all"] .count'},queued:{label:'#queue [for="qfilter-queued"]',input:'#queue [data-filter="queued"]',count:'#queue [for="qfilter-queued"] .count'},localProcessing:{label:'#queue [for="qfilter-localProcessing"]',input:'#queue [data-filter="localProcessing"]',count:'#queue [for="qfilter-localProcessing"] .count'},uploading:{label:'#queue [for="qfilter-uploading"]',input:'#queue [data-filter="uploading"]',count:'#queue [for="qfilter-uploading"] .count'},pending:{label:'#queue [for="qfilter-pending"]',input:'#queue [data-filter="pending"]',count:'#queue [for="qfilter-pending"] .count'},processing:{label:'#queue [for="qfilter-processing"]',input:'#queue [data-filter="processing"]',count:'#queue [for="qfilter-processing"] .count'},completed:{label:'#queue [for="qfilter-completed"]',input:'#queue [data-filter="completed"]',count:'#queue [for="qfilter-completed"] .count'},failed:{label:'#queue [for="qfilter-failed"]',input:'#queue [data-filter="failed"]',count:'#queue [for="qfilter-failed"] .count'}},item:{type:".type",status:".status",details:".info .details",icon:".status .icon",startedAt:".started time",completed:{wrap:".completed",label:".completed span",time:".completed time"},progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},actions:{cancel:"button.cancel",retry:"button.retry",dismiss:"button.dismiss"}}},this.ui=window.uiFromSelectors(this.selectors),this.ui.panel||(this.canUpdateUI=!1)}initListeners(){this.activityListeners=null,this.clickHandler=this.handleClick.bind(this),this.onlineHandler=this.handleOnline.bind(this),this.offlineHandler=this.handleOffline.bind(this),this.unloadHandler=this.handleBeforeUnload.bind(this),document.addEventListener("click",this.clickHandler),window.addEventListener("online",this.onlineHandler),window.addEventListener("offline",this.offlineHandler),window.addEventListener("beforeunload",this.unloadHandler)}handleOnline(){this.updatePanel(),this.getQueueByStatus(this.pendingStatuses).length>0&&this.processQueue()}handleOffline(){this.updatePanel("offline")}handleBeforeUnload(e){if(this.getQueueByStatus(this.pendingStatuses).length>0)return e.preventDefault(),e.returnValue="",""}handleClick(e){if(!window.targetCheck(e,this.selectors.panel+", "+this.selectors.toggle.button))return;if(window.targetCheck(e,this.selectors.refresh.button))return this.store.clearCache(),void this.store.fetch();if(window.targetCheck(e,this.selectors.actions.clear))return void this.opActions("completed","dismiss").then((()=>{}));if(window.targetCheck(e,this.selectors.actions.retry))return void this.opActions("failed","retry").then((()=>{}));const t=window.targetCheck(e,"[data-action]");if(t){const e=t.closest("[data-id]")?.dataset.id;return void(e&&this.opActions(e,t.dataset.action))}const s=window.targetCheck(e,this.selectors.filters.filter);s&&this.setFilter(s.dataset.filter)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t.input?.dataset.filter===e&&(t.input.checked=!0)})),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}trackActivity(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}this.resetActivityTimer()}resetActivityTimer(){this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),1750)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}initStore(){if(!this.user)return;const e=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1});this.store=e.queue,this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-save":this.maybeStartPolling(),this.updateUI();break;case"item-saved":t.previousItem&&t.previousItem.status!==t.item.status&&this.updateOperationStatus(t.item.id,t.item.status),this.maybeStartPolling()}}))}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},delay:!1,canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return null;if(t.popup&&this.ui.popup?.message&&(this.ui.popup.message.textContent=t.popup,this.ui.popup.popup.hidden=!1,setTimeout((()=>this.ui.popup.popup.hidden=!0),2e3)),!t.delay)return this.queue.set(t.id,t),this.processOperation(t).then((()=>{})),this.store.clearCache(),this.maybeStartPolling(),this.toggleQueue(),t.id;const s=Array.from(this.getAllQueue()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.trackActivity(),e.id}return this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.trackActivity(),t.id}async opActions(e,t){if(this.statuses.includes(e)?e=this.getQueueByStatus(e).map((e=>e.id)):"string"==typeof e&&(e=[e]),0===e.length)return;if(!["cancel","dismiss","retry"].includes(t))return;const s=["cancel","dismiss"].includes(t);s&&e.forEach((e=>this.removeOperationUI(e)));try{const i=await fetch(`${this.api}${this.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({action:t,ids:e,user:this.user})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const n=await i.json();if(!n.success)throw new Error(n.message||`${t} operation failed`);return e.forEach((e=>{let i=this.getQueue(e);if(this.notify(`${t}-operation`,i),s)this.clearQueue(e);else{let t=this.getQueue(e);t.status="queued",this.setQueue(t),this.updateOperationStatus(t.id,t.status)}})),"retry"===t&&this.trackActivity(),this.updateUI(),n}catch(s){return await window.jvbError.log(s,{component:"Queue",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.opActions(e,t))),{success:!1,error:s.message}}}async processQueue(){if(this.isProcessing)return;const e=this.getQueueByStatus("queued");if(0!==e.length){this.setProcessing();for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking()}else this.stopActivityTracking()}async processOperation(e){try{this.queue.has(e.id)||this.queue.set(e.id,e);let t,s=!1;if(e.data?._isFormData&&!e.data instanceof FormData&&(s=!0,e.data=await this.store.objectToFormData(e.data)),this.updateOperationStatus(e.id,"uploading"),e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),t=e.data):(t=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json"),null==t)return;const i=await fetch(`${this.api}${e.endpoint}`,{method:e.method,headers:e.headers,body:t}),n=await i.json();if(s&&(e.data={}),!i.ok||!n.success)throw new Error(n.message||`HTTP ${i.status}`);n.id&&e.id!==n.id?e=await this.handleServerMerge(e,n):(e.status=n.status??"pending",e.serverData=n,this.updateOperationStatus(e.id,e.status)),this.a11y.announce(`${e.title} sent to server for processing`),this.setQueue(e)}catch(t){console.error("Operation failed: ",t),e.retries++,e.lastError=t.message,e.retries>=3?e.status="failed_permanent":e.status="failed",this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(e.status=t.status||"pending",e.serverData=t,this.mergeOp(s,e)):(this.clearQueue(e.id),this.setQueue(t),t)}mergeOp(e,t){return e.data=window.deepMerge(e.data,t.data),e.status=t.status,Object.hasOwn(t,"serverData")&&(e.serverData=t.serverData),this.updateOperationStatus(e.id,e.status),this.removeOperationUI(t.id),this.clearQueue(t.id),e}sortByDate(e){return e.sort(((e,t)=>(e.updated_at??e.timestamp??0)-(t.updated_at??t.timestamp??0)))}getAllQueue(){let e=[...new Set([...Array.from(this.store.data.values()),...Array.from(this.queue.values())])];return this.sortByDate(e)}getQueueByStatus(e){"string"==typeof e&&(e=[e]);let t=[...new Set([...Array.from(this.store.filterByIndex({status:e})),...Array.from(this.queue.values()).filter((t=>e.includes(t.status)))])];return this.sortByDate(t)}updateOperationStatus(e,t){let s=this.getQueue(e);s&&this.statuses.includes(t)&&(s.status=t,this.notify("operation-status",s),this.setQueue(s))}setQueue(e){this.store.save(e),this.queue.set(e.id,e)}getQueue(e){return this.queue.has(e)?this.queue.get(e):this.store.get(e)}clearQueue(e){this.queue.delete(e),this.store.delete(e)}maybeStartPolling(){return this.getQueueByStatus(this.pendingStatuses).length>0&&(this.startPolling(),!0)}startPolling(){this.isPolling||(this.isPolling=!0,this.updatePanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch(),this.maybeStartPolling()||(this.stopPolling(),this.updatePanel("synced"))}catch(e){console.error("Polling error:",e)}}),5e3),this.startCountdown())}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.stopCountdown())}startCountdown(e=5){this.isPolling&&(this.ui.refresh.countdown.textContent=e,this.countdownTimer=setInterval((async()=>{--e>=0?this.ui.refresh.countdown.textContent=e:(this.ui.refresh.countdown.textContent="",this.stopCountdown())}),1e3))}stopCountdown(){this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null)}updateUI(){this.canUpdateUI&&window.debouncer.schedule("queue-ui",this.handleUpdateUI.bind(this))}handleUpdateUI(){const e=this.getAllQueue();this.ui.actions.retry.disabled=0===e.filter((e=>"failed"===e.status)).length,this.ui.actions.clear.disabled=0===e.filter((e=>"completed"===e.status)).length;for(let t of this.statuses){if("failed_permanent"===t)continue;let s=e.filter((e=>e.status===t)).length;this.ui.filters[t].label.hidden=0===s,this.ui.filters[t].input.dataset.count=`${s}`,this.ui.filters[t].count.textContent=s>0?s:""}this.renderOperations()}renderOperations(){if(!this.ui.items.container)return;const e=this.store.filters?.status??"all",t="all"===e?this.getAllQueue():this.getQueueByStatus(e);if(window.removeChildren(this.ui.items.container),0===t.length){const e=window.getTemplate("emptyQueue");return this.ui.items.container.append(e),void this.a11y.announce("No items in queue")}t.forEach((e=>{let t=this.items.get(e.id);t||(t=this.createOperationElement(e)),t?.element&&(this.updateOperationUI(e.id),this.ui.items.container.append(t.element))}))}createOperationElement(e){const t=window.getTemplate("queueItem");t.dataset.id=e.id;const s={element:t,ui:window.uiFromSelectors(this.selectors.item,t)};return this.items.set(e.id,s),s}updateOperationUI(e){let t=this.items.has(e)?this.items.get(e):this.createOperationElement(e);if(!t)return;let s=this.getQueue(e),i=t.element;i.classList.remove(this.statuses),i.classList.add(s.status);let n=this.getProgress(s);t.ui.type&&t.ui.type.textContent!==s.title&&(t.ui.type.textContent=s.title),t.ui.status&&(t.ui.status.title=this.statusLabel(s.status)),t.ui.icon&&(t.ui.icon.className=`icon icon-${this.icons[s.status]}`),t.ui.details&&(t.ui.details.textContent=this.itemMessage(s)),t.ui.startedAt&&(t.ui.startedAt.setAttribute("datetime",s.created_at),t.ui.startedAt.textContent=window.formatTimeAgo(s.created_at));let a="completed"===s.status?"Completed: ":"Last updated: ",r=Object.hasOwn(s,"updated_at")||Object.hasOwn(s,"completed_at");if(t.ui.completed.wrap.hidden=!r,r&&t.ui.completed.label&&t.ui.completed.time){let e;e=Object.hasOwn(s,"completed_at")?s.completed_at:s.updated_at,t.ui.completed.label.textContent=a,t.ui.completed.time.setAttribute("datetime",e),t.ui.completed.time.textContent=window.formatTimeAgo(e)}window.showProgress(t.ui.progress,n,100,this.statusLabel(s.status)),t.ui.actions.cancel&&(t.ui.actions.cancel.hidden=this.completedStatuses.includes(s.status)),t.ui.actions.retry&&(s.retries>=3&&(t.ui.actions.retry.disabled=!0),t.ui.actions.retry.hidden="failed"!==s.status),t.ui.actions.dismiss&&(t.ui.actions.dismiss.hidden=this.pendingStatuses.includes(s.status))}getProgress(e){if(e.progress)return e.progress;if(!this.statuses.includes(e.status))return 0;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]??0}removeOperationUI(e){let t=this.items.get(e);t&&window.fade(t.element,!1)}updatePanel(e="syncing"){this.panelStatuses.includes(e)&&(this.ui.panel.classList.remove(this.panelStatuses),this.ui.panel.classList.add(e))}statusLabel(e){if(!this.statuses.includes(e))return"";return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed",failed_permanent:"Failed permanently"}[e]}itemMessage(e){if(Object.hasOwn(e,"message")&&""!==e.message)return e.message;if(Object.hasOwn(e,"error_message")&&e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":return e.progress?`${e.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}toggleQueue(e=!0){this.ui.panel.hidden=!e,this.ui.toggle.button.hidden=!e}setProcessing(e=!0){this.isProcessing=e,this.ui.toggle.button.classList.toggle("saving",e)}subscribe(e){if(this.subscribers)return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.isPolling&&this.stopPolling(),this.stopActivityTracking(),document.removeEventListener("click",this.clickHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbQueue=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.user=window.auth.getUser(),this.canUpdateUI=!0,this.isProcessing=!1,this.isPolling=!1,this.queue=new Map,this.items=new Map,this.subscribers=new Set,this.api=jvbSettings.api,this.endpoint="queue",this.init()}init(){this.headers={"X-WP-Nonce":window.auth.getNonce()},this.initElements(),this.initListeners(),this.initStore(),this.canUpdateUI&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle.button,name:"Queue Panel"}))}initElements(){this.panelStatuses=["syncing","synced","pending","offline"],this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.pendingStatuses=["queued","localProcessing","uploading"],this.workingStatuses=["pending","processing"],this.completedStatuses=["completed","failed","failed_permanent"],this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:{button:"button.qtoggle",indicator:".qtoggle .indicator",count:".qtoggle .count"},refresh:{button:"#queue .refresh .refreshNow",countdown:"#queue .refresh .countdown"},popup:{popup:"#queue .popup",message:"#queue .popup span"},items:{container:"#queue .qitems"},actions:{retry:"#queue .retry-all",clear:"#queue .dismiss-all"},filters:{filter:"#queue [data-filter]",all:{label:'#queue [for="qfilter-all"]',radio:'#queue [data-filter="all"]',count:'#queue [data-filter="all"] .count'},queued:{label:'#queue [for="qfilter-queued"]',input:'#queue [data-filter="queued"]',count:'#queue [for="qfilter-queued"] .count'},localProcessing:{label:'#queue [for="qfilter-localProcessing"]',input:'#queue [data-filter="localProcessing"]',count:'#queue [for="qfilter-localProcessing"] .count'},uploading:{label:'#queue [for="qfilter-uploading"]',input:'#queue [data-filter="uploading"]',count:'#queue [for="qfilter-uploading"] .count'},pending:{label:'#queue [for="qfilter-pending"]',input:'#queue [data-filter="pending"]',count:'#queue [for="qfilter-pending"] .count'},processing:{label:'#queue [for="qfilter-processing"]',input:'#queue [data-filter="processing"]',count:'#queue [for="qfilter-processing"] .count'},completed:{label:'#queue [for="qfilter-completed"]',input:'#queue [data-filter="completed"]',count:'#queue [for="qfilter-completed"] .count'},failed:{label:'#queue [for="qfilter-failed"]',input:'#queue [data-filter="failed"]',count:'#queue [for="qfilter-failed"] .count'}},item:{type:".type",status:".status",details:".info .details",icon:".status .icon",startedAt:".started time",completed:{wrap:".completed",label:".completed span",time:".completed time"},progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},actions:{cancel:"button.cancel",retry:"button.retry",dismiss:"button.dismiss"}}},this.ui=window.uiFromSelectors(this.selectors),this.ui.panel||(this.canUpdateUI=!1)}initListeners(){this.activityListeners=null,this.clickHandler=this.handleClick.bind(this),this.onlineHandler=this.handleOnline.bind(this),this.offlineHandler=this.handleOffline.bind(this),this.unloadHandler=this.handleBeforeUnload.bind(this),document.addEventListener("click",this.clickHandler),window.addEventListener("online",this.onlineHandler),window.addEventListener("offline",this.offlineHandler),window.addEventListener("beforeunload",this.unloadHandler)}handleOnline(){this.updatePanel("synced"),this.getQueueByStatus(this.pendingStatuses).length>0&&this.processQueue()}handleOffline(){this.updatePanel("offline")}handleBeforeUnload(e){if(this.getQueueByStatus(this.pendingStatuses).length>0)return e.preventDefault(),e.returnValue="",""}handleClick(e){if(!window.targetCheck(e,this.selectors.panel+", "+this.selectors.toggle.button))return;if(window.targetCheck(e,this.selectors.refresh.button))return this.store.clearCache(),this.store.clearFilters(),void this.store.fetch();if(window.targetCheck(e,this.selectors.actions.clear))return void this.opActions("completed","dismiss").then((()=>{}));if(window.targetCheck(e,this.selectors.actions.retry))return void this.opActions("failed","retry").then((()=>{}));const t=window.targetCheck(e,"[data-action]");if(t){const e=t.closest("[data-id]")?.dataset.id;return void(e&&this.opActions(e,t.dataset.action))}const s=window.targetCheck(e,this.selectors.filters.filter);s&&this.setFilter(s.dataset.filter)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t.input?.dataset.filter===e&&(t.input.checked=!0)})),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}trackActivity(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}this.resetActivityTimer()}resetActivityTimer(){this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),1750)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}initStore(){if(!this.user)return;const e=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1});this.store=e.queue,this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-save":this.maybeStartPolling(),this.updateUI();break;case"item-saved":t.previousItem&&t.previousItem.status!==t.item.status&&this.updateOperationStatus(t.item.id,t.item.status),this.maybeStartPolling()}}))}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},delay:!1,canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),created_at:(new Date).toISOString(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return null;if(t.popup&&this.ui.popup?.message&&(this.ui.popup.message.textContent=t.popup,this.ui.popup.popup.hidden=!1,setTimeout((()=>this.ui.popup.popup.hidden=!0),2e3)),!t.delay)return this.queue.set(t.id,t),this.processOperation(t).then((()=>{})),this.store.clearCache(),this.maybeStartPolling(),this.toggleQueue(),t.id;const s=Array.from(this.getAllQueue()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.trackActivity(),e.id}return this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.trackActivity(),t.id}async opActions(e,t){if(this.statuses.includes(e)?e=this.getQueueByStatus(e).map((e=>e.id)):"string"==typeof e&&(e=[e]),0===e.length)return;if(!["cancel","dismiss","retry"].includes(t))return;const s=["cancel","dismiss"].includes(t);s&&e.forEach((e=>this.removeOperationUI(e)));try{const i=await fetch(`${this.api}${this.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({action:t,ids:e,user:this.user})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const n=await i.json();if(!n.success)throw new Error(n.message||`${t} operation failed`);return e.forEach((e=>{let i=this.getQueue(e);if(this.notify(`${t}-operation`,i),s)this.clearQueue(e);else{let t=this.getQueue(e);t.status="queued",this.setQueue(t),this.updateOperationStatus(t.id,t.status)}})),"retry"===t&&this.trackActivity(),this.updateUI(),n}catch(s){return await window.jvbError.log(s,{component:"Queue",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.opActions(e,t))),{success:!1,error:s.message}}}async processQueue(){if(this.isProcessing)return;const e=this.getQueueByStatus("queued");if(0!==e.length){this.setProcessing();for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking(),this.toggleQueue(this.maybeStartPolling())}else this.stopActivityTracking()}async processOperation(e){try{this.queue.has(e.id)||this.queue.set(e.id,e);let t,s=!1;if(e.data?._isFormData&&!e.data instanceof FormData&&(s=!0,e.data=await this.store.objectToFormData(e.data)),this.updateOperationStatus(e.id,"uploading"),e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),t=e.data):(t=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json"),null==t)return;const i=await fetch(`${this.api}${e.endpoint}`,{method:e.method,headers:e.headers,body:t}),n=await i.json();if(s&&(e.data={}),!i.ok||!n.success)throw new Error(n.message||`HTTP ${i.status}`);n.id&&e.id!==n.id?e=await this.handleServerMerge(e,n):(e.status=n.status??"pending",e.serverData=n,this.updateOperationStatus(e.id,e.status)),this.a11y.announce(`${e.title} sent to server for processing`),this.setQueue(e)}catch(t){console.error("Operation failed: ",t),e.retries++,e.lastError=t.message,e.retries>=3?e.status="failed_permanent":e.status="failed",this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(e.status=t.status||"pending",e.serverData=t,this.mergeOp(s,e)):(this.clearQueue(e.id),this.setQueue(t),t)}mergeOp(e,t){return e.data=window.deepMerge(e.data,t.data),e.status=t.status,Object.hasOwn(t,"serverData")&&(e.serverData=t.serverData),this.updateOperationStatus(e.id,e.status),this.removeOperationUI(t.id),this.clearQueue(t.id),e}sortByDate(e){return e.sort(((e,t)=>(e.updated_at??e.timestamp??0)-(t.updated_at??t.timestamp??0)))}sortOperations(e){const t={processing:0,uploading:1,pending:2,queued:3,localProcessing:4,failed:5,completed:6,failed_permanent:7};return e.sort(((e,s)=>{const i=(t[e.status]??99)-(t[s.status]??99);if(0!==i)return i;const n=e.updated_at??e.timestamp??0,a=s.updated_at??s.timestamp??0;return new Date(a)-new Date(n)}))}getAllQueue(){let e=[...new Set([...Array.from(this.store.data.values()),...Array.from(this.queue.values())])];return this.sortOperations(e)}getQueueByStatus(e){"string"==typeof e&&(e=[e]);let t=[...new Set([...Array.from(this.store.filterByIndex({status:e})),...Array.from(this.queue.values()).filter((t=>e.includes(t.status)))])];return this.sortOperations(t)}updateOperationStatus(e,t){let s=this.getQueue(e);s&&this.statuses.includes(t)&&(s.status=t,this.notify("operation-status",s),this.setQueue(s))}setQueue(e){this.store.save(e),this.queue.set(e.id,e)}getQueue(e){return this.queue.has(e)?this.queue.get(e):this.store.get(e)}clearQueue(e){this.queue.delete(e),this.store.delete(e)}maybeStartPolling(){return this.getQueueByStatus([...this.pendingStatuses,...this.workingStatuses]).length>0?(this.startPolling(),!0):(this.updatePanel("synced"),!1)}startPolling(){this.isPolling||(this.isPolling=!0,this.updatePanel("pending"),this.runPollCycle())}async runPollCycle(){if(this.isPolling){try{if(this.store.clearCache(),await this.store.fetch(),!this.maybeStartPolling())return this.stopPolling(),void this.updatePanel("synced")}catch(e){console.error("Polling error:",e)}this.startCountdown(5,(()=>this.runPollCycle()))}}startCountdown(e,t){this.ui.refresh.countdown.classList.add("counting"),this.ui.refresh.countdown.textContent=e,this.countdownTimer=setInterval((()=>{--e>0?this.ui.refresh.countdown.textContent=e:(this.stopCountdown(),t&&t())}),1e3)}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.stopCountdown())}stopCountdown(){this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null),this.ui.refresh.countdown.classList.remove("counting"),this.ui.refresh.countdown.textContent=""}updateUI(){this.canUpdateUI&&window.debouncer.schedule("queue-ui",this.handleUpdateUI.bind(this))}handleUpdateUI(){const e=this.getAllQueue();this.ui.actions.retry.disabled=0===e.filter((e=>"failed"===e.status)).length,this.ui.actions.clear.disabled=0===e.filter((e=>"completed"===e.status)).length;const t=e.filter((e=>[...this.pendingStatuses,...this.workingStatuses].includes(e.status))).length;this.ui.toggle.count.hidden=0===t,this.ui.toggle.count.textContent=t;for(let t of this.statuses){if("failed_permanent"===t)continue;let s=e.filter((e=>e.status===t)).length;this.ui.filters[t].label.hidden=0===s,this.ui.filters[t].input.dataset.count=`${s}`,this.ui.filters[t].count.textContent=s>0?s:""}this.renderOperations()}renderOperations(){if(!this.ui.items.container)return;const e=this.store.filters?.status??"all",t="all"===e?this.getAllQueue():this.getQueueByStatus(e),s=this.sortOperations(t);if(0===s.length){window.removeChildren(this.ui.items.container);const e=window.getTemplate("emptyQueue");return this.ui.items.container.append(e),void this.a11y.announce("No items in queue")}this.ui.items.container.querySelector(".empty-group")?.remove();const i=new Set(s.map((e=>e.id)));this.items.forEach(((e,t)=>{i.has(t)||(e.element?.remove(),this.items.delete(t))})),s.forEach(((e,t)=>{let s=this.items.get(e.id);s||(s=this.createOperationElement(e)),s?.element&&(this.updateOperationUI(e.id),this.ui.items.container.append(s.element))}))}createOperationElement(e){const t=window.getTemplate("queueItem");t.dataset.id=e.id;const s={element:t,ui:window.uiFromSelectors(this.selectors.item,t)};return this.items.set(e.id,s),s}updateOperationUI(e){let t=this.items.has(e)?this.items.get(e):this.createOperationElement(e);if(!t)return;let s=this.getQueue(e),i=t.element;i.classList.remove(this.statuses),i.classList.add(s.status);let n=this.getProgress(s);t.ui.type&&t.ui.type.textContent!==s.title&&(t.ui.type.textContent=s.title),t.ui.status&&(t.ui.status.title=this.statusLabel(s.status)),t.ui.icon&&(t.ui.icon.className=`icon icon-${this.icons[s.status]}`),t.ui.details&&(t.ui.details.textContent=this.itemMessage(s)),t.ui.startedAt&&(t.ui.startedAt.setAttribute("datetime",s.created_at),t.ui.startedAt.textContent=window.formatTimeAgo(s.created_at));s.status;const a="completed"===s.status&&(s.completed_at||s.updated_at);if(t.ui.completed.wrap.hidden=!a,a){const e=s.completed_at??s.updated_at;t.ui.completed.label.textContent="Completed: ",t.ui.completed.time.setAttribute("datetime",e),t.ui.completed.time.textContent=window.formatTimeAgo(e)}window.showProgress(t.ui.progress,n,100,this.statusLabel(s.status)),t.ui.actions.cancel&&(t.ui.actions.cancel.hidden=this.completedStatuses.includes(s.status)),t.ui.actions.retry&&(s.retries>=3&&(t.ui.actions.retry.disabled=!0),t.ui.actions.retry.hidden="failed"!==s.status),t.ui.actions.dismiss&&(t.ui.actions.dismiss.hidden=this.pendingStatuses.includes(s.status))}getProgress(e){if(e.progress)return e.progress;if(!this.statuses.includes(e.status))return 0;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]??0}removeOperationUI(e){let t=this.items.get(e);t&&window.fade(t.element,!1)}updatePanel(e="syncing"){this.panelStatuses.includes(e)&&(this.ui.panel.classList.remove(...this.panelStatuses),this.ui.panel.classList.add(e))}statusLabel(e){if(!this.statuses.includes(e))return"";return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed",failed_permanent:"Failed permanently"}[e]}itemMessage(e){if(Object.hasOwn(e,"message")&&""!==e.message)return e.message;if(Object.hasOwn(e,"error_message")&&e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":return e.progress?`${e.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}toggleQueue(e=!0){this.ui.panel.hidden=!e,this.ui.toggle.button.hidden=!e}setProcessing(e=!0){this.isProcessing=e,this.ui.toggle.button.classList.toggle("saving",e)}subscribe(e){if(this.subscribers)return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.isPolling&&this.stopPolling(),this.stopActivityTracking(),document.removeEventListener("click",this.clickHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbQueue=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/selector.min.js b/assets/js/min/selector.min.js
index 1714dda..a36e129 100644
--- a/assets/js/min/selector.min.js
+++ b/assets/js/min/selector.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector("dialog#jvb-selector"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.subscribers=new Set,this.fields=new Map,this.selectedTerms=new Map,this.batchFetch=new Set,this.activeField=null,this.isInitializing=!0,this.messageText={},this.init())}init(){this.initStore(),this.initElements(),this.initModal(),this.scanExistingFields(),this.initListeners(),this.needsCreator()&&window.jvbTaxCreator&&(this.creator=new window.jvbTaxCreator(this)),this.isInitializing=!1,this.batchFetchTaxonomies().then((()=>{}))}initStore(){const e=window.jvbStore.register("taxonomies",{storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:12e4,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy",delayFetch:!0});this.store=e.terms,this.store.subscribe(this.handleStoreEvent.bind(this))}initElements(){this.selectors={search:{input:'[type="search"]',clear:".clear-search",container:".search-wrapper",results:".search-results"},create:{button:"button.submit-term",span:".submit-term span"},terms:{list:".items-container",wrap:".items-wrap",sentinel:".scroll-sentinel"},nav:{nav:"nav.term-navigation",back:".back-to-parent",child:".toggle-children",pathLevel:".path-level"},message:{message:"p.message",text:"p.message span"},selected:".selected-items",modal:{title:"#modal-title",content:".modal-content",count:".selection-count"},favourites:".favourite-terms",field:{toggle:'button.taxonomy-toggle, [data-filter="taxonomy"]',value:'input[type="hidden"]',selected:".selected-items",dropdown:{list:".search-results",wrapper:".auto-wrapper"},create:{button:".auto-wrapper .submit-term",span:".auto-wrapper button span"},search:"input[data-autocomplete]",message:{message:"p.message",text:"p.message span"}}},this.ui=window.uiFromSelectors(this.selectors,this.container)}initListeners(){this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.nextPage()}))}),{root:this.ui.terms.sentinel,threshold:.5}),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("input",this.inputHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0)}handleClick(e){const t=this.getFieldId(e.target)||this.activeField,s=this.fields.get(t);if(!t||!s)return;const i=window.targetCheck(e,".item.autocomplete");if(i){let e=parseInt(i.dataset.id);this.addSelected(e,t),this.scheduleHideDropdown(t),s.ui.search&&(s.ui.search.value="")}if(window.targetCheck(e,this.selectors.field.toggle))return e.preventDefault(),void this.openModal(t);const r=window.targetCheck(e,".remove-term");if(r){const e=r.closest("[data-id]").dataset.id??!1;return void(t&&e&&this.removeSelected(parseInt(e),t))}if(e.target.matches(".modal-close"))return this.updateFieldValue(t),void this.modal?.handleClose();if(window.targetCheck(e,this.selectors.nav.back))return void this.navigateToParent();if(window.targetCheck(e,this.selectors.nav.child)){const t=e.target.closest("li"),s=parseInt(t.dataset.id);return void(s&&this.navigateTo(s))}const a=window.targetCheck(e,this.selectors.nav.pathLevel);if(a){const e=parseInt(a.dataset.id)??0;this.navigateTo(e)}window.targetCheck(e,this.selectors.field.dropdown)&&this.scheduleHideDropdown(t);if(window.targetCheck(e,this.selectors.search.clear)){const e=this.currentField();e&&e.ui.search&&(e.ui.search.value="",this.store.setFilters({search:"",page:1,parent:this.store.filters.parent||0})),this.ui.search.input&&(this.ui.search.input.value="")}if(this.creator){window.targetCheck(e,this.selectors.create.button)&&this.maybeCreateTerm(e).then((()=>{}))}}handleChange(e){if(!this.container.contains(e.target))return;if(!["checkbox","button"].includes(e.target.type))return;e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.dataset.id);let s=this.getFieldId(e.target);e.target.checked?this.addSelected(t,s):this.removeSelected(t,s)}handleInput(e){let t=this.getFieldId(e.target)??this.activeField;if(!t)return;const s=this.fields.get(t);if(!s)return;if(["checkbox","button"].includes(e.target.type))return;e.preventDefault(),e.stopPropagation(),this.container.open||this.setField(t);let i=e.target.value.trim();this.setMessage(!0,`Searching for "${i}" in ${s.plural??"items"}`),window.debouncer.schedule(`${t}-search`,(async()=>{this.container.open&&window.removeChildren(this.ui.terms.list),await this.store.setFilters({taxonomy:s.taxonomy,search:i,page:1,parent:i?0:this.store.filters.parent||0})}),100)}setField(e){const t=this.fields.get(e);t?(this.activeField=e,this.setMessage(!0,`Loading ${t.plural}...`),this.resetFilters({taxonomy:t.taxonomy})):console.error("No field found...")}resetFilters(e){Object.hasOwn(e,"taxonomy")&&(e={page:1,search:"",parent:0,...e},this.store.setFilters(e))}handleFocus(e){const t=this.getFieldId(e.target),s=this.fields.get(t);t&&s&&(s.hasAutocomplete||s.hasSearch)&&(window.debouncer.cancel(`${t}-search-results`),this.container.open||this.setField(t))}handleBlur(e){const t=this.getFieldId(e.target),s=this.fields.get(t);t&&s&&s.hasAutocomplete&&!this.container.open&&(e.relatedTarget&&s.ui.dropdown.wrapper?.contains(e.relatedTarget)||this.scheduleHideDropdown(t))}scheduleHideDropdown(e){const t=this.fields.get(e);t&&window.debouncer.schedule(`${e}-search-results`,(()=>{this.container.open||(this.activeField=null),t.ui.dropdown.wrapper&&(t.ui.dropdown.wrapper.hidden=!0)}),1500)}initModal(){this.modalID="dialog#jvb-selector",this.container=document.querySelector(this.modalID),this.modal=new window.jvbModal(this.container,{handleForm:!1,open:null}),this.modal.subscribe(((e,t)=>{if("modal-close"===e)this.closeModal()}))}toggleModal(e,t=!0){this.fields.get(e)&&(t?this.openModal(e):this.closeModal())}openModal(e){const t=this.fields.get(e);if(!t)return;this.setField(e),this.ui.modal.title.textContent=t.isFilter?`Filter by ${t.singular}`:`Select ${t.plural}`,this.ui.search.container&&(this.ui.search.container.hidden=!t.canSearch),this.creator&&this.creator.handleOpen(t);let s=`Opened ${t.singular} selection. Choose from checkboxes, or search to filter results.`;window.removeChildren(this.ui.selected),window.removeChildren(this.ui.terms.list),this.modal.handleOpen(),this.a11y.announce(s)}closeModal(){const e=this.fields.get(this.activeField);if(!e)return;this.observer.unobserve(this.ui.terms.sentinel),window.removeChildren(this.ui.terms.list),this.notify("selected-terms",{terms:this.selectedTerms.get(this.activeField),taxonomy:e.taxonomy}),this.activeField=null;let t=`Closed ${e.singular} selector.`;this.a11y.announce(t)}navigateToParent(){const e=this.store.filters.parent;if(0===e)return;let t=this.store.get(parseInt(e));if(!t)return void this.navigateTo(0);let s=t.parent;this.navigateTo(parseInt(s))}navigateTo(e=0){e=parseInt(e)??0,this.store.setFilters({parent:e,page:1}),window.removeChildren(this.ui.terms.list),this.updateBreadcrumbs(e)}nextPage(){let e=this.store.filters.page,t=Math.min(e++,this.store.lastResponse.total);this.store.setFilters({page:t})}prevPage(){let e=this.store.filters.page,t=Math.max(e-1,1);this.store.setFilters({page:t})}addTermToModal(e){const t=this.store.get(e);if(!t)return;const s=this.currentField();if(!s)return;if(this.ui.selected.querySelector(`[data-id="${e}"]`))return;const i=window.getTemplate("selectedTerm");i&&(i.dataset.id=e,i.dataset.taxonomy=s.taxonomy,i.querySelector(".item-name").textContent=t.path,i.querySelector("button").title=`Remove ${t.name}`,this.ui.selected.append(i))}scanExistingFields(e=document.body){e.querySelectorAll('[data-type="selector"]').forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let s=e.querySelector('input[type="hidden"]');if(!s&&!Object.hasOwn(e.dataset,"filter"))return void console.warn("TaxonomySelector: No hidden input found for field",e);"fieldId"in e.dataset||(e.dataset.fieldId=window.generateID("selector"));const i=e.dataset.fieldId;let r=this.selectors.field;const a=Object.hasOwn(e.dataset,"filter")&&"taxonomy"===e.dataset.filter;let n=a?e:e.querySelector("button.taxonomy-toggle");if(0===Object.keys(t).length){if(!n)return;if(t={taxonomy:n.dataset.taxonomy,single:n.dataset.single,plural:n.dataset.plural,search:Object.hasOwn(n.dataset,"search"),autocomplete:Object.hasOwn(n.dataset,"autocomplete"),creatable:Object.hasOwn(n.dataset,"creatable")},0===Object.keys(t).length)return}else Object.hasOwn(t,"toggle")&&(n=document.querySelector(t.toggle),r.toggle=t.toggle);const o={id:i,value:s,element:e,taxonomy:t.taxonomy??!1,singular:t.single??"",plural:t.plural??"",name:e.dataset.field,canSearch:t.search??!1,limit:t.limit??0,hasAutocomplete:t.autocomplete??!1,canCreate:t.creatable??!1,isRequired:t.required??!1,isFilter:a,toggle:n,create:{button:null,span:null},selectors:r,ui:window.uiFromSelectors(r,e),checked:!1};if(a&&!o.ui.toggle&&(o.ui.toggle=e),o.taxonomy)return o.singular&&o.plural||(console.warn("TaxonomySelector: Field missing singular/plural labels",e),o.singular=o.taxonomy.replace("jvb_",""),o.plural=o.singular+"s"),this.fields.set(i,o),this.setSelectedFromValue(i,s),this.isInitializing&&this.batchFetch.add(o.taxonomy),this.updateFieldUI(i),i;console.error("TaxonomySelector: Field missing taxonomy",e)}setSelectedFromValue(e,t){if(!e)return;let s=this.fields.get(e);if(!s)return;if(!t&&!s.isFilter)return;let i=new Set;t&&t.value.trim().split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>i.add(e))),this.selectedTerms.set(e,i)}addSelected(e,t=null){t||(t=this.activeField);const s=this.fields.get(t),i=this.store.get(e);if(!s||!i)return;const r=this.selectedTerms.get(t);0!==s.limit&&r.size>=s.limit||(r.add(parseInt(e)),this.container.open||s.isFilter||this.updateFieldValue(t),this.addTermToDisplay(e,t),this.checkLimits(t))}removeSelected(e,t=null){t||(t=this.activeField);const s=this.fields.get(t),i=this.store.get(e);if(!s||!i)return;this.selectedTerms.get(t).delete(parseInt(e));const r=!!s.ui.selected&&s.ui.selected.querySelector(`[data-id="${e}"]`);if(r&&r.remove(),this.container.open){let t=!!this.ui.selected&&this.ui.selected.querySelector(`[data-id="${e}"]`);t&&t.remove();let s=this.ui.terms.list.querySelector(`[type=checkbox][data-id="${e}"]`);s&&(s.checked=!1)}this.container.open||s.isFilter||this.updateFieldValue(t),this.checkLimits(t)}updateFieldValue(e){const t=this.fields.get(e);if(!t)return;let s=Array.from(this.selectedTerms.get(e));t.ui.value.value=s.join(",")}checkLimits(e){if(!this.container.open)return;const t=this.fields.get(e);if(!t||!t.isFilter||0===t.limit)return;const s=this.selectedTerms.get(e).size>=t.limit;this.setCheckboxes(s)}updateFieldFromInput(e){const t=this.getFieldId(e);if(!t)return;this.fields.get(t)&&(this.setSelectedFromValue(t,e),this.updateFieldUI(t))}updateFieldUI(e){const t=this.fields.get(e);let s=this.selectedTerms.get(e)??new Set;t&&!t.isFilter&&0!==s.size&&Array.from(s).forEach((t=>{this.addTermToDisplay(t,e)}))}updateFieldsForTaxonomy(e){let t=Array.from(this.fields.values()).filter((t=>!t.checked&&t.taxonomy===e));const s=Array.from(this.store.data.values()).some((t=>t.taxonomy===e));t.forEach((e=>{e.toggle.disabled=!s&&!e.canCreate,e.toggle.title=s?`Select ${e.plural}`:`No ${e.singular} available`,e.checked=!0}))}showModalTerms(e=!1){const t=this.currentField(),s=this.store.getFiltered();0===s.length&&((this.store.filters.page??1)&&window.removeChildren(this.ui.terms.list),this.setMessage(!0,""===this.store.filters.search?`No matching ${t.plural}.`:`No ${t.plural} found.`,!1),this.ui.terms.sentinel&&this.observer.unobserve(this.ui.terms.sentinel)),this.setCreateButton(!0),this.ui.terms.sentinel&&(this.store.lastResponse?.has_more?this.observer.observe(this.ui.terms.sentinel):this.observer.unobserve(this.ui.terms.sentinel));const i=this.store.filters.parent??0;this.ui.nav.back.hidden=0===i;const r=document.createDocumentFragment();s.forEach((t=>{const s=this.createTermElement({show:e,...t});s&&r.append(s)})),s.length>0&&this.setMessage(!1),this.ui.terms.list.append(r)}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem");t.dataset.id=e.id;const s=this.selectedTerms.get(this.activeField).has(e.id);let[i,r,a]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("span, .term-name")],n=this.currentField(),o=n.limit>0&&this.selectedTerms.get(this.activeField).size>=n.limit;if(i&&r&&a&&([i.dataset.id,i.id,i.name,i.value,i.disabled,i.checked,r.htmlFor,r.title,r.dataset.path,a.textContent]=[e.id,`${n.element.id}-${e.id}`,`${n.element.id}-${n.taxonomy}-select`,e.id,!s&&o,s,`${n.element.id}-${e.id}`,e.path??e.name,e.path,e.show?e.path:e.name],e.hasChildren)){const s=window.getTemplate("termChildrenToggle");s&&(s.ariaLabel=`View ${n.plural} nested under ${e.name}`,t.append(s))}return t}showAutocompleteTerms(){const e=this.currentField(),t=this.currentTerms();if(!e)return;const s=e.ui.dropdown.list;s&&(window.removeChildren(s),0===t.length?this.setMessage(!0,`No ${e.plural} found.`,!1):(t.forEach((e=>{const t=this.createAutocompleteTerm(e);t&&s.append(t)})),this.setMessage(!1)),this.setCreateButton(!0),e.ui.dropdown.wrapper&&(e.ui.dropdown.wrapper.hidden=!1))}createAutocompleteTerm(e){const t=window.getTemplate("autocompleteItem");if(t)return t.dataset.id=e.id,t.textContent=e.path||e.name,t}addTermToDisplay(e,t){const s=this.store.get(e),i=this.fields.get(t);if(!s||!i)return;if(i.ui.selected&&i.ui.selected.querySelector(`[data-id="${e}"]`))return;const r=window.getTemplate("selectedTerm");if(r&&(r.dataset.id=e,r.dataset.taxonomy=i.taxonomy,r.querySelector(".item-name").textContent=s.path,r.querySelector("button").title=`Remove ${s.name}`,i.ui.selected&&i.ui.selected.append(r),this.container.open)){this.addTermToModal(e);const t=this.ui.terms.list.querySelector(`input[value="${e}"]`);t&&(t.checked=!0)}}updateBreadcrumbs(e){const t=this.ui.nav.nav;if(!t)return;const s=Array.from(t.children).find((t=>parseInt(t.dataset.id)===e));if(s){let e=s.nextElementSibling;for(;e;){const t=e;e=e.nextElementSibling,t.remove()}}else{const s=this.store.get(e);if(!s)return;const i=window.getTemplate("termBreadcrumb");if(!i)return;i.dataset.id=e,i.textContent=s.name,i.title=s.name,t.append(i)}}updateSelectionCount(){if(!this.container.open)return;const e=this.fields.get(this.activeField);if(e&&this.ui.modal.count){const t=this.selectedTerms.get(this.activeField).size;this.ui.modal.count.textContent=e.limit>0?`${t} of ${e.limit} ${e.plural} selected`:`${t} ${e.plural} selected`}}currentField(){return this.fields.get(this.activeField)??!1}currentTerms(){return this.store.getFiltered()}needsCreator(){return Array.from(this.fields.values()).some((e=>e.canCreate||e.hasAutocomplete))}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?.dataset.fieldId||null}setCheckboxes(e){this.ui.terms.list.querySelectorAll("input[type=checkbox]").forEach((t=>{t.checked||(t.disabled=e)}))}handleStoreEvent(e,t){const s={"data-loaded":()=>this.handleDataLoaded(),"filters-changed":()=>this.handleFiltersChanged(t),"fetch-error":()=>this.handleFetchError()};try{s[e]?.(t)}catch(t){console.error(`Error handling store event "${e}":`,t),this.setMessage(!0,"An error occurred loading data",!1)}}handleDataLoaded(){const e=this.store.filters.taxonomy;if(e){e.split(",").map((e=>e.trim())).forEach((e=>this.updateFieldsForTaxonomy(e)))}this.container.open?this.showResults():this.activeField?this.showResults(!0):this.setMessage(!1)}showResults(e=!1){this.setMessage(!1);const t=this.store.getFiltered(),s=this.store.filters,i=s.search&&s.search.length>0;this.notify("terms-loaded",{terms:t,filters:s}),e?this.showAutocompleteTerms():this.showModalTerms(i),this.a11y.announce(t.length)}handleFiltersChanged(e){}handleFetchError(e){const t=this.currentField(),s=t?`Failed to load ${t.plural}`:"Failed to load data";this.setMessage(!0,s,!1),console.error("Store fetch error:",e)}async batchFetchTaxonomies(){if(0===this.batchFetch.size)return;const e=Array.from(this.batchFetch);this.batchFetch.clear();try{await this.store.setFilters({taxonomy:e.join(","),page:1,search:"",parent:0})}catch(e){console.error("Failed to batch fetch taxonomies:",e)}}preloadTaxonomy(e){this.store.setFilters({taxonomy:e,page:1,search:"",parent:0})}setCreateButton(e=!0){const t=this.currentField();if(!t||!t.canCreate||!this.creator)return;const s=this.container.open?this.ui:t.ui;if(!s.create?.button||!s.create?.span)return;const i=s.create.button,r=s.create.span,a=this.container.open?s.search.input:s.search;if(!a)return;let n=(this.currentTerms()??[]).map((e=>e.name)),o=a.value;const l=e&&o.length>=2&&!n.includes(o);i.hidden=!l,l&&(r.textContent=a.value??"")}async maybeCreateTerm(e){const t=this.currentField();if(!t)return;window.debouncer.cancel(`${t.id}-search-results`);let s={taxonomy:t.taxonomy,parent:this.store.filters.parent??0};if(this.container.open&&""===this.ui.search.input.value?(s.parent=this.creator.ui.parent.value??s.parent,s.name=this.creator.ui.name.value??!1):s.name=this.container.open?this.ui.search.input.value:t.ui.search.value,void 0!==s.parent&&s.name){this.setMessage(!0,`Creating "${s.name}"...`),this.setCreateButton(!1),this.container.open?window.removeChildren(this.ui.terms.list):(t.ui.search.disabled=!0,window.removeChildren(t.ui.dropdown.list),t.ui.dropdown.wrapper&&(t.ui.dropdown.wrapper.hidden=!1));let e=await this.creator.handleTermCreation(s);e&&this.addSelected(e.id,t.id),this.container.open||(t.ui.search.disabled=!1,t.ui.search.value=""),this.scheduleHideDropdown(t.id),this.setMessage(!1)}}setMessage(e=!0,t="",s=!0){const i=this.currentField();if(!i)return;const r=this.container.open||i.isFilter?this.ui:i.isFilter?null:i.ui;if(!r?.message?.message)return;t=""===t?`No ${i.plural??"items"} found.`:t;const a=r.message.message,n=r.message.text;a.hidden=!e,e?t&&n&&(s&&window.typeLoop&&n?(this.messageText[i.id]&&(this.messageText[i.id](),delete this.messageText[i.id]),this.messageText[i.id]=window.typeLoop(n,t)):n.textContent=t):this.messageText[i.id]&&(this.messageText[i.id](),delete this.messageText[i.id])}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.fields.forEach(((e,t)=>{window.debouncer.cancel(`${t}-search`),window.debouncer.cancel(`${t}-search-results`)})),Object.keys(this.messageText).forEach((e=>{this.messageText[e]&&this.messageText[e]()})),this.messageText={},this.ui.terms?.sentinel&&this.observer?.unobserve(this.ui.terms.sentinel),this.observer?.disconnect(),document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("input",this.inputHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),this.subscribers.clear(),this.fields.clear(),this.selectedTerms.clear(),this.batchFetch.clear(),this.creator&&(this.creator.destroy(),this.creator=null),this.store&&(this.store=null)}}document.addEventListener("DOMContentLoaded",(function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbSelector=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector("dialog#jvb-selector"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.subscribers=new Set,this.fields=new Map,this.selectedTerms=new Map,this.batchFetch=new Set,this.activeField=null,this.isInitializing=!0,this.messageText={},this.init())}init(){this.initStore(),this.initElements(),this.initModal(),this.scanExistingFields(),this.initListeners(),this.needsCreator()&&window.jvbTaxCreator&&(this.creator=new window.jvbTaxCreator(this)),this.isInitializing=!1,this.batchFetchTaxonomies().then((()=>{}))}initStore(){const e=window.jvbStore.register("taxonomies",{storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:12e4,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy",delayFetch:!0});this.store=e.terms,this.store.subscribe(this.handleStoreEvent.bind(this))}initElements(){this.selectors={search:{input:'[type="search"]',clear:".clear-search",container:".search-wrapper",results:".search-results"},create:{button:"button.submit-term",span:".submit-term span"},terms:{list:".items-container",wrap:".items-wrap",sentinel:".scroll-sentinel"},nav:{nav:"nav.term-navigation",back:".back-to-parent",child:".toggle-children",pathLevel:".path-level"},message:{message:"p.message",text:"p.message span"},selected:".selected-items",modal:{title:"#modal-title",content:".modal-content",count:".selection-count"},favourites:".favourite-terms",field:{toggle:'button.taxonomy-toggle, [data-filter="taxonomy"]',value:'input[type="hidden"]',selected:".selected-items",dropdown:{list:".search-results",wrapper:".auto-wrapper"},create:{button:".auto-wrapper .submit-term",span:".auto-wrapper button span"},search:"input[data-autocomplete]",message:{message:"p.message",text:"p.message span"}}},this.ui=window.uiFromSelectors(this.selectors,this.container)}initListeners(){this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.nextPage()}))}),{root:this.ui.terms.sentinel,threshold:.5}),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("input",this.inputHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0)}handleClick(e){const t=this.getFieldId(e.target)||this.activeField,s=this.fields.get(t);if(!t||!s)return;const i=window.targetCheck(e,".item.autocomplete");if(i){let e=parseInt(i.dataset.id);this.addSelected(e,t),this.scheduleHideDropdown(t),s.ui.search&&(s.ui.search.value="")}if(window.targetCheck(e,this.selectors.field.toggle))return e.preventDefault(),void this.openModal(t);const r=window.targetCheck(e,".remove-term");if(r){const e=r.closest("[data-id]").dataset.id??!1;return void(t&&e&&this.removeSelected(parseInt(e),t))}if(e.target.matches(".modal-close"))return this.updateFieldValue(t),void this.modal?.handleClose();if(window.targetCheck(e,this.selectors.nav.back))return void this.navigateToParent();if(window.targetCheck(e,this.selectors.nav.child)){const t=e.target.closest("li"),s=parseInt(t.dataset.id);return void(s&&this.navigateTo(s))}const a=window.targetCheck(e,this.selectors.nav.pathLevel);if(a){const e=parseInt(a.dataset.id)??0;this.navigateTo(e)}window.targetCheck(e,this.selectors.field.dropdown)&&this.scheduleHideDropdown(t);if(window.targetCheck(e,this.selectors.search.clear)){const e=this.currentField();e&&e.ui.search&&(e.ui.search.value="",this.store.setFilters({search:"",page:1,parent:this.store.filters.parent||0})),this.ui.search.input&&(this.ui.search.input.value="")}if(this.creator){window.targetCheck(e,this.selectors.create.button)&&this.maybeCreateTerm(e).then((()=>{}))}}handleChange(e){if(!this.container.contains(e.target))return;if(!["checkbox","button"].includes(e.target.type))return;e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.dataset.id);let s=this.getFieldId(e.target);e.target.checked?this.addSelected(t,s):this.removeSelected(t,s)}handleInput(e){let t=this.getFieldId(e.target)??this.activeField;if(!t)return;const s=this.fields.get(t);if(!s)return;if(["checkbox","button"].includes(e.target.type))return;e.preventDefault(),e.stopPropagation(),this.container.open||this.setField(t);let i=e.target.value.trim();this.setMessage(!0,`Searching for "${i}" in ${s.plural??"items"}`),window.debouncer.schedule(`${t}-search`,(async()=>{this.container.open&&window.removeChildren(this.ui.terms.list),await this.store.setFilters({taxonomy:s.taxonomy,search:i,page:1,parent:i?0:this.store.filters.parent||0})}),100)}setField(e){const t=this.fields.get(e);t?(this.activeField=e,this.setMessage(!0,`Loading ${t.plural}...`),this.resetFilters({taxonomy:t.taxonomy})):console.error("No field found...")}resetFilters(e){Object.hasOwn(e,"taxonomy")&&(e={page:1,search:"",parent:0,...e},this.store.setFilters(e))}handleFocus(e){const t=this.getFieldId(e.target),s=this.fields.get(t);t&&s&&(s.hasAutocomplete||s.hasSearch)&&(window.debouncer.cancel(`${t}-search-results`),this.container.open||this.setField(t))}handleBlur(e){const t=this.getFieldId(e.target),s=this.fields.get(t);t&&s&&s.hasAutocomplete&&!this.container.open&&(e.relatedTarget&&s.ui.dropdown.wrapper?.contains(e.relatedTarget)||this.scheduleHideDropdown(t))}scheduleHideDropdown(e){const t=this.fields.get(e);t&&window.debouncer.schedule(`${e}-search-results`,(()=>{this.container.open||(this.activeField=null),t.ui.dropdown.wrapper&&(t.ui.dropdown.wrapper.hidden=!0)}),1500)}initModal(){this.modalID="dialog#jvb-selector",this.container=document.querySelector(this.modalID),this.modal=new window.jvbModal(this.container,{handleForm:!1,open:null}),this.modal.subscribe(((e,t)=>{if("modal-close"===e)this.closeModal()}))}toggleModal(e,t=!0){this.fields.get(e)&&(t?this.openModal(e):this.closeModal())}openModal(e){const t=this.fields.get(e);if(!t)return;this.setField(e),this.ui.modal.title.textContent=t.isFilter?`Filter by ${t.singular}`:`Select ${t.plural}`,this.ui.search.container&&(this.ui.search.container.hidden=!t.canSearch),this.creator&&this.creator.handleOpen(t);let s=`Opened ${t.singular} selection. Choose from checkboxes, or search to filter results.`;window.removeChildren(this.ui.selected),window.removeChildren(this.ui.terms.list),this.modal.handleOpen(),this.a11y.announce(s)}openEmpty(e,t,s,i){this.emptyCallback=i;const r=`empty-${e}-${Date.now()}`;this.fields.has(r)||(this.fields.set(r,{id:r,taxonomy:e,singular:t,plural:s,canSearch:!0,canCreate:!1,hasAutocomplete:!1,isFilter:!1,isEmpty:!0,limit:0,ui:{},element:null,value:null,toggle:null,checked:!0}),this.selectedTerms.set(r,new Set)),this.setField(r),this.ui.modal.title.textContent=`Add to ${s}`,this.ui.search?.container&&(this.ui.search.container.hidden=!1),window.removeChildren(this.ui.selected),window.removeChildren(this.ui.terms.list),this.modal.handleOpen()}closeModal(){const e=this.fields.get(this.activeField);if(!e)return;if(this.observer.unobserve(this.ui.terms.sentinel),window.removeChildren(this.ui.terms.list),e.isEmpty&&this.emptyCallback){const t=Array.from(this.selectedTerms.get(this.activeField)||[]),s=t.map((e=>this.store.get(e))).filter(Boolean);this.emptyCallback({taxonomy:e.taxonomy,termIds:t,terms:s}),this.fields.delete(this.activeField),this.selectedTerms.delete(this.activeField),this.emptyCallback=null,this.bulkAssignmentTaxonomy=null}else this.notify("selected-terms",{terms:this.selectedTerms.get(this.activeField),taxonomy:e.taxonomy});this.activeField=null;let t=`Closed ${e.singular} selector.`;this.a11y.announce(t)}navigateToParent(){const e=this.store.filters.parent;if(0===e)return;let t=this.store.get(parseInt(e));if(!t)return void this.navigateTo(0);let s=t.parent;this.navigateTo(parseInt(s))}navigateTo(e=0){e=parseInt(e)??0,this.store.setFilters({parent:e,page:1}),window.removeChildren(this.ui.terms.list),this.updateBreadcrumbs(e)}nextPage(){let e=this.store.filters.page,t=Math.min(e++,this.store.lastResponse.total);this.store.setFilters({page:t})}prevPage(){let e=this.store.filters.page,t=Math.max(e-1,1);this.store.setFilters({page:t})}addTermToModal(e){const t=this.store.get(e);if(!t)return;const s=this.currentField();if(!s)return;if(this.ui.selected.querySelector(`[data-id="${e}"]`))return;const i=window.getTemplate("selectedTerm");i&&(i.dataset.id=e,i.dataset.taxonomy=s.taxonomy,i.querySelector(".item-name").textContent=t.path,i.querySelector("button").title=`Remove ${t.name}`,this.ui.selected.append(i))}scanExistingFields(e=document.body){e.querySelectorAll('[data-type="selector"]').forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let s=e.querySelector('input[type="hidden"]');if(!s&&!Object.hasOwn(e.dataset,"filter"))return void console.warn("TaxonomySelector: No hidden input found for field",e);"fieldId"in e.dataset||(e.dataset.fieldId=window.generateID("selector"));const i=e.dataset.fieldId;let r=this.selectors.field;const a=Object.hasOwn(e.dataset,"filter")&&"taxonomy"===e.dataset.filter;let n=a?e:e.querySelector("button.taxonomy-toggle");if(0===Object.keys(t).length){if(!n)return;if(t={taxonomy:n.dataset.taxonomy,single:n.dataset.single,plural:n.dataset.plural,search:Object.hasOwn(n.dataset,"search"),autocomplete:Object.hasOwn(n.dataset,"autocomplete"),creatable:Object.hasOwn(n.dataset,"creatable")},0===Object.keys(t).length)return}else Object.hasOwn(t,"toggle")&&(n=document.querySelector(t.toggle),r.toggle=t.toggle);const o={id:i,value:s,element:e,taxonomy:t.taxonomy??!1,singular:t.single??"",plural:t.plural??"",name:e.dataset.field,canSearch:t.search??!1,limit:t.limit??0,hasAutocomplete:t.autocomplete??!1,canCreate:t.creatable??!1,isRequired:t.required??!1,isFilter:a,toggle:n,create:{button:null,span:null},selectors:r,ui:window.uiFromSelectors(r,e),checked:!1};if(a&&!o.ui.toggle&&(o.ui.toggle=e),o.taxonomy)return o.singular&&o.plural||(console.warn("TaxonomySelector: Field missing singular/plural labels",e),o.singular=o.taxonomy.replace("jvb_",""),o.plural=o.singular+"s"),this.fields.set(i,o),this.setSelectedFromValue(i,s),this.isInitializing&&this.batchFetch.add(o.taxonomy),this.updateFieldUI(i),i;console.error("TaxonomySelector: Field missing taxonomy",e)}setSelectedFromValue(e,t){if(!e)return;let s=this.fields.get(e);if(!s)return;if(!t&&!s.isFilter)return;let i=new Set;t&&t.value.trim().split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>i.add(e))),this.selectedTerms.set(e,i)}addSelected(e,t=null){t||(t=this.activeField);const s=this.fields.get(t),i=this.store.get(e);if(!s||!i)return;const r=this.selectedTerms.get(t);0!==s.limit&&r.size>=s.limit||(r.add(parseInt(e)),this.container.open||s.isFilter||this.updateFieldValue(t),this.addTermToDisplay(e,t),this.checkLimits(t))}removeSelected(e,t=null){t||(t=this.activeField);const s=this.fields.get(t),i=this.store.get(e);if(!s||!i)return;this.selectedTerms.get(t).delete(parseInt(e));const r=!!s.ui.selected&&s.ui.selected.querySelector(`[data-id="${e}"]`);if(r&&r.remove(),this.container.open){let t=!!this.ui.selected&&this.ui.selected.querySelector(`[data-id="${e}"]`);t&&t.remove();let s=this.ui.terms.list.querySelector(`[type=checkbox][data-id="${e}"]`);s&&(s.checked=!1)}this.container.open||s.isFilter||this.updateFieldValue(t),this.checkLimits(t)}updateFieldValue(e){const t=this.fields.get(e);if(!t)return;let s=Array.from(this.selectedTerms.get(e));t.ui.value.value=s.join(",")}checkLimits(e){if(!this.container.open)return;const t=this.fields.get(e);if(!t||!t.isFilter||0===t.limit)return;const s=this.selectedTerms.get(e).size>=t.limit;this.setCheckboxes(s)}updateFieldFromInput(e){const t=this.getFieldId(e);if(!t)return;this.fields.get(t)&&(this.setSelectedFromValue(t,e),this.updateFieldUI(t))}updateFieldUI(e){const t=this.fields.get(e);let s=this.selectedTerms.get(e)??new Set;t&&!t.isFilter&&0!==s.size&&Array.from(s).forEach((t=>{this.addTermToDisplay(t,e)}))}updateFieldsForTaxonomy(e){let t=Array.from(this.fields.values()).filter((t=>!t.checked&&t.taxonomy===e));const s=Array.from(this.store.data.values()).some((t=>t.taxonomy===e));t.forEach((e=>{e.toggle&&(e.toggle.disabled=!s&&!e.canCreate,e.toggle.title=s?`Select ${e.plural}`:`No ${e.singular} available`,e.checked=!0)}))}showModalTerms(e=!1){const t=this.currentField(),s=this.store.getFiltered();0===s.length&&((this.store.filters.page??1)&&window.removeChildren(this.ui.terms.list),this.setMessage(!0,""===this.store.filters.search?`No matching ${t.plural}.`:`No ${t.plural} found.`,!1),this.ui.terms.sentinel&&this.observer.unobserve(this.ui.terms.sentinel)),this.setCreateButton(!0),this.ui.terms.sentinel&&(this.store.lastResponse?.has_more?this.observer.observe(this.ui.terms.sentinel):this.observer.unobserve(this.ui.terms.sentinel));const i=this.store.filters.parent??0;this.ui.nav.back.hidden=0===i;const r=document.createDocumentFragment();s.forEach((t=>{const s=this.createTermElement({show:e,...t});s&&r.append(s)})),s.length>0&&this.setMessage(!1),this.ui.terms.list.append(r)}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem");t.dataset.id=e.id;const s=this.selectedTerms.get(this.activeField).has(e.id);let[i,r,a]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("span, .term-name")],n=this.currentField(),o=n.limit>0&&this.selectedTerms.get(this.activeField).size>=n.limit;if(i&&r&&a&&([i.dataset.id,i.id,i.name,i.value,i.disabled,i.checked,r.htmlFor,r.title,r.dataset.path,a.textContent]=[e.id,`${n.element.id}-${e.id}`,`${n.element.id}-${n.taxonomy}-select`,e.id,!s&&o,s,`${n.element.id}-${e.id}`,e.path??e.name,e.path,e.show?e.path:e.name],e.hasChildren)){const s=window.getTemplate("termChildrenToggle");s&&(s.ariaLabel=`View ${n.plural} nested under ${e.name}`,t.append(s))}return t}showAutocompleteTerms(){const e=this.currentField(),t=this.currentTerms();if(!e)return;const s=e.ui.dropdown.list;s&&(window.removeChildren(s),0===t.length?this.setMessage(!0,`No ${e.plural} found.`,!1):(t.forEach((e=>{const t=this.createAutocompleteTerm(e);t&&s.append(t)})),this.setMessage(!1)),this.setCreateButton(!0),e.ui.dropdown.wrapper&&(e.ui.dropdown.wrapper.hidden=!1))}createAutocompleteTerm(e){const t=window.getTemplate("autocompleteItem");if(t)return t.dataset.id=e.id,t.textContent=e.path||e.name,t}addTermToDisplay(e,t){const s=this.store.get(e),i=this.fields.get(t);if(!s||!i)return;if(i.ui.selected&&i.ui.selected.querySelector(`[data-id="${e}"]`))return;const r=window.getTemplate("selectedTerm");if(r&&(r.dataset.id=e,r.dataset.taxonomy=i.taxonomy,r.querySelector(".item-name").textContent=s.path,r.querySelector("button").title=`Remove ${s.name}`,i.ui.selected&&i.ui.selected.append(r),this.container.open)){this.addTermToModal(e);const t=this.ui.terms.list.querySelector(`input[value="${e}"]`);t&&(t.checked=!0)}}updateBreadcrumbs(e){const t=this.ui.nav.nav;if(!t)return;const s=Array.from(t.children).find((t=>parseInt(t.dataset.id)===e));if(s){let e=s.nextElementSibling;for(;e;){const t=e;e=e.nextElementSibling,t.remove()}}else{const s=this.store.get(e);if(!s)return;const i=window.getTemplate("termBreadcrumb");if(!i)return;i.dataset.id=e,i.textContent=s.name,i.title=s.name,t.append(i)}}updateSelectionCount(){if(!this.container.open)return;const e=this.fields.get(this.activeField);if(e&&this.ui.modal.count){const t=this.selectedTerms.get(this.activeField).size;this.ui.modal.count.textContent=e.limit>0?`${t} of ${e.limit} ${e.plural} selected`:`${t} ${e.plural} selected`}}currentField(){return this.fields.get(this.activeField)??!1}currentTerms(){return this.store.getFiltered()}needsCreator(){return Array.from(this.fields.values()).some((e=>e.canCreate||e.hasAutocomplete))}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?.dataset.fieldId||null}setCheckboxes(e){this.ui.terms.list.querySelectorAll("input[type=checkbox]").forEach((t=>{t.checked||(t.disabled=e)}))}handleStoreEvent(e,t){const s={"data-loaded":()=>this.handleDataLoaded(),"filters-changed":()=>this.handleFiltersChanged(t),"fetch-error":()=>this.handleFetchError()};try{s[e]?.(t)}catch(t){console.error(`Error handling store event "${e}":`,t),this.setMessage(!0,"An error occurred loading data",!1)}}handleDataLoaded(){const e=this.store.filters.taxonomy;if(e){e.split(",").map((e=>e.trim())).forEach((e=>this.updateFieldsForTaxonomy(e)))}this.container.open?this.showResults():this.activeField?this.showResults(!0):this.setMessage(!1)}showResults(e=!1){this.setMessage(!1);const t=this.store.getFiltered(),s=this.store.filters,i=s.search&&s.search.length>0;this.notify("terms-loaded",{terms:t,filters:s}),e?this.showAutocompleteTerms():this.showModalTerms(i),this.a11y.announce(t.length)}handleFiltersChanged(e){}handleFetchError(e){const t=this.currentField(),s=t?`Failed to load ${t.plural}`:"Failed to load data";this.setMessage(!0,s,!1),console.error("Store fetch error:",e)}async batchFetchTaxonomies(){if(0===this.batchFetch.size)return;const e=Array.from(this.batchFetch);this.batchFetch.clear();try{await this.store.setFilters({taxonomy:e.join(","),page:1,search:"",parent:0})}catch(e){console.error("Failed to batch fetch taxonomies:",e)}}preloadTaxonomy(e){this.store.setFilters({taxonomy:e,page:1,search:"",parent:0})}setCreateButton(e=!0){const t=this.currentField();if(!t||!t.canCreate||!this.creator)return;const s=this.container.open?this.ui:t.ui;if(!s.create?.button||!s.create?.span)return;const i=s.create.button,r=s.create.span,a=this.container.open?s.search.input:s.search;if(!a)return;let n=(this.currentTerms()??[]).map((e=>e.name)),o=a.value;const l=e&&o.length>=2&&!n.includes(o);i.hidden=!l,l&&(r.textContent=a.value??"")}async maybeCreateTerm(e){const t=this.currentField();if(!t)return;window.debouncer.cancel(`${t.id}-search-results`);let s={taxonomy:t.taxonomy,parent:this.store.filters.parent??0};if(this.container.open&&""===this.ui.search.input.value?(s.parent=this.creator.ui.parent.value??s.parent,s.name=this.creator.ui.name.value??!1):s.name=this.container.open?this.ui.search.input.value:t.ui.search.value,void 0!==s.parent&&s.name){this.setMessage(!0,`Creating "${s.name}"...`),this.setCreateButton(!1),this.container.open?window.removeChildren(this.ui.terms.list):(t.ui.search.disabled=!0,window.removeChildren(t.ui.dropdown.list),t.ui.dropdown.wrapper&&(t.ui.dropdown.wrapper.hidden=!1));let e=await this.creator.handleTermCreation(s);e&&this.addSelected(e.id,t.id),this.container.open||(t.ui.search.disabled=!1,t.ui.search.value=""),this.scheduleHideDropdown(t.id),this.setMessage(!1)}}setMessage(e=!0,t="",s=!0){const i=this.currentField();if(!i)return;const r=this.container.open||i.isFilter?this.ui:i.isFilter?null:i.ui;if(!r?.message?.message)return;t=""===t?`No ${i.plural??"items"} found.`:t;const a=r.message.message,n=r.message.text;a.hidden=!e,e?t&&n&&(s&&window.typeLoop&&n?(this.messageText[i.id]&&(this.messageText[i.id](),delete this.messageText[i.id]),this.messageText[i.id]=window.typeLoop(n,t)):n.textContent=t):this.messageText[i.id]&&(this.messageText[i.id](),delete this.messageText[i.id])}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.fields.forEach(((e,t)=>{window.debouncer.cancel(`${t}-search`),window.debouncer.cancel(`${t}-search-results`)})),Object.keys(this.messageText).forEach((e=>{this.messageText[e]&&this.messageText[e]()})),this.messageText={},this.ui.terms?.sentinel&&this.observer?.unobserve(this.ui.terms.sentinel),this.observer?.disconnect(),document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("input",this.inputHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),this.subscribers.clear(),this.fields.clear(),this.selectedTerms.clear(),this.batchFetch.clear(),this.creator&&(this.creator.destroy(),this.creator=null),this.store&&(this.store=null)}}document.addEventListener("DOMContentLoaded",(function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbSelector=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 7650155..565cbd3 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.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.previewUrls=new Set,this.initElements(),this.initListeners()}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(console.log(e,t),!Object.hasOwn(t,"endpoint")||!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;if(s)switch(e){case"cancel-operation":this.handleOperationCancelled(s).then((()=>{}));break;case"operation-status":this.handleFieldStatus(s,t).then((()=>{}));break;case"operation-completed":this.handleOperationComplete(t,s).then((()=>{}));break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,s).then((()=>{}))}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery())}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-container",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:'[name="select-all-uploads"]',actions:".selection-actions",count:".selection-count",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:"[data-upload-id]",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)return;if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);return void(s.length>0&&this.processFiles(t,s).then((()=>{})))}if(e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]'))return;let s=this.fields.get(t);s&&s.config.autoUpload&&("post_group"===s.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e).then((()=>{})))}handleGroupMetaChange(e){const t=e.closest(this.selectors.group.fields);if(!t)return;const s=t.dataset.groupId,r=this.stores.groups.get(s);r&&window.debouncer.schedule(`group-meta-${s}`,(async(e,t)=>{let s=e.name.replace(`${t}_`,"").replace(`${t}[`,"").replace("]","");r.fields[s]=e.value,await this.setGroup(t,r)}),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 r=this.getFieldIdFromElement(t);r&&(this.processFiles(r,s).then((()=>{})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t){let s=new FormData;const r=this.fields.get(t);if(!r)return;let i=this.stores.uploads.filterByIndex({field:t});if(0===i.length)return;const[o,a]=["uploads"===e,"uploads/groups"===e];let l,n,d,u,c;s.append("fieldId",r.id),s.append("content",r.config.content),o&&(s.append("mode",r.config.mode),s.append("field_name",r.config.name),s.append("fieldId",r.id),s.append("field_type",r.config.type),s.append("subtype",r.config.subtype),s.append("item_id",r.config.itemID),s.append("destination",r.config.destination)),a?({posts:l,uploadMap:n,files:d}=this.collectGroups(t)):o&&({uploadMap:n,files:d}=this.collectUploads(t)),a&&s.append("posts",JSON.stringify(l)),d.forEach((e=>{s.append("files[]",e)})),s.append("upload_ids",JSON.stringify(n)),o?(u=`Uploading ${i.length} file${i.length>1?"s":""} to server...`,c=`Uploading ${i.length} file${i.length>1?"s":""}...`):a&&(u=`Creating ${l.length} ${r.config.content}${l.length>1?"s":""} from uploads...`,c=`Creating ${l.length} post${l.length>1?"s":""}...`),await this.setBulkUpload(i,"status","queued");let p=this.sendToQueue(e,s,u,c);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1)}return p?(r.operationId=p,await this.setBulkUpload(i,"operationId",p),await this.setBulkUpload(i,"status","uploading"),await this.setBulkGroup(t,"operationId",p),this.fields.set(r.id,r)):await this.setBulkUpload(i,"status","failed"),this.notify("sent-to-queue",t),p}async sendToQueue(e,t,s="",r="",i=!1){""===r&&(r=s);const o={endpoint:e,method:"POST",data:t,title:s,popup:r,canMerge:i,sendNow:"uploads/groups"===e,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(o)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e}),r=[],i=[],o=[];for(const e of s){const s={images:[],fields:e.fields??{}},a=t.filter((t=>t.group===e.id));for(const e of a){const t=this.formatFile(e);if(t){o.push(t);const r={upload_id:e.id,index:i.length};let a=this.uploads.get(e.id);a.ui?.featured?.checked&&(s.fields.featured=e.id),s.images.push(r),i.push(e.id)}}r.push(s)}const a=t.filter((e=>!e.group));for(const e of a){const t={images:[],fields:{}},s=this.formatFile(e);if(s){o.push(s);const r={upload_id:e.id,index:i.length};t.images.push(r),i.push(e.id)}r.push(t)}return{posts:r,uploadMap:i,files:o}}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],r=[];for(const e of t){const t=this.formatFile(e);t&&(r.push(t),s.push(e.id))}return{uploadMap:s,files:r}}async queueUploadMeta(e){const t=e.target.closest(this.selectors.items.item)?.dataset.uploadId,s=this.stores.uploads.get(t);if(!t||!s)return;if(!this.fields.get(s.field))return;let r={};r[e.target.name]=e.target.value,s.fields={...s.fields,...r},await this.setUpload(s.id,s);let i={};return i[s.attachmentId??s.id]=s.fields,await this.sendToQueue("uploads/meta",i,"Uploading Meta","",!0)}async handleOperationComplete(e,t){const s=e.response;if(s?.data){const e=Array.isArray(s.data)?s.data:Object.values(s.data);for(const t of e)if(t.upload_id&&t.attachment_id){const e=this.stores.uploads.get(t.upload_id);e&&(e.attachmentId=t.attachment_id,e.status="completed",await this.stores.uploads.save(e))}}const r=this.stores.uploads.filterByIndex({field:t}),i=this.stores.groups.filterByIndex({field:t});await Promise.all([...r.filter((e=>"completed"===e.status)).map((e=>this.clearUpload(e.id))),...i.map((e=>this.stores.groups.delete(e.id)))]),this.notify("uploads-complete",{fieldId:t,response:s})}scanFields(e,t=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t)))}registerField(e,t=!0,s=null){const r={element:e,id:s||this.determineFieldId(e),config:this.extractFieldConfig(e,t),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){return{autoUpload:t,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"}}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+"_";return`${t}${s}${e.dataset.field||""}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,r){const i=this.fields.get(e);i&&window.showProgress(i.ui.progress,t,s,r)}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,r=this.workerState.tasks.get(t);r&&(clearTimeout(r.timeoutId),r.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 r=[],i=[...e],o=this.workerState.settings.maxConcurrent,a=async()=>{for(;i.length>0;){const e=i.shift();r.push(await this.processImage(e,t,s))}};return await Promise.all(Array.from({length:Math.min(o,e.length)},(()=>a()))),r}async processImage(e,t=2200,s=2200,r=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),r)}catch(r){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:r,restart:i}=this.workerState;if(i.count>=i.max)throw new Error("Worker max restarts exceeded");const o=await createImageBitmap(e);let{width:a,height:l}=o;if(a>t||l>s){const e=Math.min(t/a,s/l);a=Math.round(a*e),l=Math.round(l*e)}const n=this.getWorker(),d=crypto.randomUUID();return new Promise(((t,s)=>{const i=setTimeout((()=>{this.workerState.tasks.delete(d),r.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),r.timeout);this.workerState.tasks.set(d,{resolve:t,reject:s,timeoutId:i}),n.postMessage({id:d,imageBitmap:o,width:a,height:l,type:e.type,quality:.9},[o])}))}resizeImage(e,t,s){return new Promise((r=>{const i=new Image;i.onload=()=>{URL.revokeObjectURL(i.src);let{width:o,height:a}=i;if(o>t||a>s){const e=Math.min(t/o,s/a);o=Math.round(o*e),a=Math.round(a*e)}const l=document.createElement("canvas");l.width=o,l.height=a,l.getContext("2d").drawImage(i,0,0,o,a),l.toBlob(r,e.type,.9)},i.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 r=t.length;let i=0;this.updateFieldProgress(e,0,r,"Processing files...");const o=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),r=await this.setUpload(s,{id:s,field:e,status:"local_processing",blob:null,fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),i=await this.createUpload(s,t,e);return this.uploads.set(s,{element:i,ui:window.uiFromSelectors(this.selectors.items,i)}),await this.addToGroup(s,null),{uploadId:s,upload:r,file:t}}))),a=o.filter((e=>e.file.type.startsWith("image/"))),l=o.filter((e=>!e.file.type.startsWith("image/"))),n=await this.processImages(a.map((e=>e.file)));for(let t=0;t<a.length;t++){const{uploadId:s,upload:o}=a[t];o.blob=n[t],o.fields.size=n[t].size,o.status="queued",await this.setUpload(s,o),i++,this.updateFieldProgress(e,i,r,"Processing files...")}for(const{uploadId:t,upload:s,file:o}of l)s.blob=o,s.status="queued",await this.setUpload(t,s),i++,this.updateFieldProgress(e,i,r,"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"]});if(0===e.length)return;let t=window.getTemplate("restoreNotification");if(!t)return void this.error.log("No restore notification",{component:"UploadManager",src:window.location.href});const s=new Map;e.forEach((e=>{const t=e.src||"unknown";s.has(t)||s.set(t,[]),s.get(t).push(e)}));const r=window.location.href;let i=s.size>1?` across ${s.size} pages`:"",o=e.length>1?"uploads":"upload",a=`${e.length} ${o} can be recovered${i}`,l=t.querySelector(".details");l&&(l.textContent=a);let n=1;for(const[e,i]of s){let s=window.getTemplate("restoreField");if(!s)continue;let o=this.registerField(s,!1,"recovery_"+n),a=this.fields.get(o);n++;let l=e===r,[d,u,c]=[s.querySelector("h3"),s.querySelector("h3 a"),s.querySelector(".item-grid")];s.open=l,l?(u.remove(),d.textContent="From this page:"):[u.href,u.title,u.textContent]=[e,"Navigate to Page and Restore",e];let p=[...new Set(i.map((e=>e.group??"preview")))];for(let e of p){let t="preview"===e||this.stores.groups.get(e);if(!t)continue;let s=await this.createGroupElement(e,a.id),r=s.querySelector(".item-grid"),o=i.filter((t=>t.group===("preview"===e)?null:e));for(const[e,r]of Object.entries(t.fields??{})){let t=s.querySelector(`input[name*="${e}"]`);t&&(t.value=r)}for(let e of o){let t=await this.createUpload(e.id,this.formatFile(e),a.id);r.append(t)}c.append(s)}t.querySelector(".wrap").append(s)}document.body.append(t),t=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(t),this.restoreSelection=new window.jvbHandleSelection({container:t,wrapper:".restore-uploads .wrap",bulkControls:".selection-actions",selectAll:"#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)),r=[...new Set(s.map((e=>e.group)))].filter(Boolean),i=s[0].field;if(!document.querySelector(`[data-uploader="${i}"]`))return void console.log("No field found for "+i);let o=this.fields.get(i);o.groupUI.container&&(o.groupUI.container.hidden=!1);let a=[];for(let e of r){let t=this.stores.groups.get(e);await this.createGroup(i,e);let r=this.groups.get(e),o=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=r.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of o){let s=await this.createUpload(t.id,this.formatFile(t),i);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),a.push(t.id)}}let l=s.filter((e=>!a.includes(e.id)));for(let e of l){let t=await this.createUpload(e.id,this.formatFile(e),i);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 r=window.getTemplate("uploadItem");if(!r)return null;let i=this.fields.get(s);if(!i)return null;r.dataset.uploadId=e;let o=this.getSubtypeFromMime(t.type)||"image";r.dataset.subtype=o;let[a,l,n,d,u]=[r.querySelector('[name="featured"]'),r.querySelector("img"),r.querySelector("video"),r.querySelector("label > span"),r.querySelector("details")];switch(a&&(a.value=e),o){case"image":if(l){const e=this.createPreviewUrl(t);l.src=e,l.alt=t.name||"",l.dataset.previewUrl=e}n?.remove(),d?.remove();break;case"video":if(n){const e=this.createPreviewUrl(t);n.src=e,n.dataset.previewUrl=e}l?.remove(),d?.remove();break;case"document":let e=t.name.split(".").pop()?.toLowerCase()??"",s={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},r=window.getIcon(s[e]??"file");d&&(d.innerText=t.name,d.prepend(r)),l?.remove(),n?.remove()}if(u){let e=window.getTemplate("uploadMeta");e&&u.append(e)}return r.draggable="single"!==i.config.type??!1,r.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e,i=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,i&&(i.htmlFor=r)}})),r}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;confirm("Remove this item?")&&(await this.removeUpload(s),this.a11y.announce("Item removed"))}async setBulkUpload(e,t,s){const r=Array.from(e).map((async e=>("status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e))));await Promise.all(r)}async setUploadStatus(e,t){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;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.clearUpload(e),this.maybeLockUploads(t.field);let s=this.selectionHandlers.get(t.field);s&&s.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 r=this.createGroupElement(t,e);if(!r)return null;s.groupUI.grid.append(r);const i=r.querySelector(".item-grid");i&&(i.dataset.groupId=t,this.createSortable(e,i,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=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,t&&(s.dataset.fieldId=t);const r=s.querySelector("[data-select-all]");if(r){const t=`select-all-${e}`,i=s.querySelector(`label[for="${r.id}"]`);r.id=t,r.name=t,i&&(i.htmlFor=t)}let i=window.getTemplate("groupMetadata"),o=s.querySelector(".fields");if(i&&o){o.append(i);let t=o.querySelector('[name="post_title"]'),s=o.querySelector('[name="post_excerpt"]');t&&(t.id=`${e}_title`,t.name=`${e}[post_title]`),s&&(s.id=`${e}_excerpt`,s.name=`${e}[post_excerpt]`)}else s.querySelector("details")?.remove();const a=s.querySelector(".item-grid");return a&&(a.dataset.groupId=e),this.groups.set(e,{element:s,ui:window.uiFromSelectors(this.selectors.group,s)}),s}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 r=this.stores.groups.filterByIndex({field:e});if(0===r.length)return;let i=r.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(i)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),r=this.uploads.get(e);if(!s||!r)return;const i=this.fields.get(s.field);if(!i)return;if(null!==r.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))}if(r.ui.checkbox&&(r.ui.checkbox.checked=!1),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),r.ui.featured&&(r.ui.featured.hidden=!t),t){r.ui.featured&&(r.ui.featured.name=`${t}_featured`);let i=this.stores.groups.get(t);i&&(i.uploads.push(e),s.group=t,this.stores.groups.save(i))}else s.group=null;let o=t?this.groups.get(t)?.ui.grid:i.ui.grid;o&&o.append(r.element),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 r=this.stores.uploads.filterByIndex({group:s});Promise.all(r.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),r=this.stores.groups.get(e);if(!r)return;let i=!0;t&&r.uploads.length>0&&(i=window.confirm("Keep uploads in this group?")),await Promise.all(r.uploads.map((e=>i?this.addToGroup(e,null):this.removeUpload(e))));const o=this.getGroupKey(r.field,e),a=this.sortables.get(o);a?.destroy&&a.destroy(),this.sortables.delete(o),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,r=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=r}async handleOperationCancelled(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all([...t.map((e=>this.removeUpload(e.id))),...s.map((e=>this.removeGroup(e.id,!1)))]),this.a11y.announce("Upload Cancelled")}async handleOperationFailed(e,t){await this.setBulkUpload(this.stores.uploads.filterByIndex({field:t}),"status","failed")}async handleFieldStatus(e,t){let s=t.status,r=this.stores.uploads.filterByIndex({field:e});await this.setBulkUpload(r,"status",s)}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;let r=new window.jvbHandleSelection({container:s.element,item:this.selectors.items.item,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions,checkbox:this.selectors.items.checkbox,selectAll:this.selectors.fields.selectAll,wrapper:`${this.selectors.fields.preview}, ${this.selectors.group.item}`});r.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems),console.log(Array.from(this.selected)),this.syncSortableSelection(e,s.selectedItems)})),this.selectionHandlers.set(t,r)}return this.selectionHandlers.get(t)}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 r=this.getGroupKey(e,s);if(this.sortables.has(r))return this.sortables.get(r);const i=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},ghostClass:"ghost",chosenClass:"chosen",dragClass:"dragging",onStart:t=>{const s=t.item,r=s?.dataset.uploadId,i=this.selected.get(e);if(r&&(!i||!i.has(r))){const t=this.selectionHandlers.get(e);t&&t.select(r)}this.syncSortableSelection(e)},onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(r,i),i}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),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(),s.classList.remove("drag-over");const r=this.selected.get(e);if(!r||0===r.size)return;const i=await this.createGroup(e);i&&(await Promise.all(Array.from(r).map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,r=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===r.length)return;const i=s.dataset.groupId||null;await Promise.all(r.map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(t)?.clearSelection()}syncSortableSelection(e){const t=this.selected.get(e)||new Set;for(const[s,r]of this.uploads){const i=this.stores.uploads.get(s);if(!i||i.field!==e)continue;const o=r.element;if(!o)continue;const a=t.has(s);a&&!o.classList.contains("selected")?Sortable.utils.select(o):!a&&o.classList.contains("selected")&&Sortable.utils.deselect(o)}}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(!s)return void console.log("Couldn't Reorder items...");let r=Array.from(s.querySelectorAll(this.selectors.items.item+":not(.ghost)")).map((e=>e.dataset.uploadId)).filter((e=>e));if(t){let e=this.groups.get(t);e&&(e.uploads=r)}else{let t=this.fields.get(e)?.ui.hidden;t&&(t.value=r.join(","))}this.a11y.announce("Items reordered")}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),r=e.dataset.field,i=this.stores.uploads.filterByIndex({field:t});for(const e of i){const t=this.formatFile(e);t&&s.push({file:t,fieldName:r,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.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.previewUrls=new Set,this.initElements(),this.initListeners()}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)){const s=t.data instanceof FormData?this.stores.uploads.formDataToObject(t.data):t.data;console.log(s);let r=s.upload_ids;if(!r||0===r.length)return;if("cancel-operation"===e)return this.handleOperationCancelled(r);this.setBulkUpload(r,"status",t.status).then((()=>{})),"completed"===t.status&&r.forEach((e=>{this.removeUpload(e).then((()=>{}))}))}}))}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-container",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)return;if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);return void(s.length>0&&this.processFiles(t,s).then((()=>{})))}if(e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]'))return;let s=this.fields.get(t);s&&s.config.autoUpload&&("post_group"===s.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e).then((()=>{})))}handleGroupMetaChange(e){const t=e.dataset.groupId;if(!t)return;const s=e.name,r=e.value,i=s.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");window.debouncer.schedule(`group-meta-${t}-${i}`,(async()=>{const e=this.stores.groups.get(t);e&&(e.fields||(e.fields={}),e.fields[i]=r,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 r=this.getFieldIdFromElement(t);r&&(this.processFiles(r,s).then((()=>{this.updateHandlerItems(r)})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t){let s=new FormData;const r=this.fields.get(t);if(!r)return;let i=this.stores.uploads.filterByIndex({field:t});if(0===i.length)return;const[o,a]=["uploads"===e,"uploads/groups"===e];let l,n,d,u,c;s.append("fieldId",r.id),s.append("content",r.config.content),o&&(s.append("mode",r.config.mode),s.append("field_name",r.config.name),s.append("fieldId",r.id),s.append("field_type",r.config.type),s.append("subtype",r.config.subtype),s.append("item_id",r.config.itemID),s.append("destination",r.config.destination)),a?({posts:l,uploadMap:n,files:d}=this.collectGroups(t)):o&&({uploadMap:n,files:d}=this.collectUploads(t)),a&&s.append("posts",JSON.stringify(l)),d.forEach((e=>{s.append("files[]",e)})),s.append("upload_ids",JSON.stringify(n)),o?(u=`Uploading ${i.length} file${i.length>1?"s":""} to server...`,c=`Uploading ${i.length} file${i.length>1?"s":""}...`):a&&(u=`Creating ${l.length} ${r.config.content}${l.length>1?"s":""} from uploads...`,c=`Creating ${l.length} post${l.length>1?"s":""}...`),await this.setBulkUpload(i,"status","queued");let p=this.sendToQueue(e,s,u,c);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1)}return p?(r.operationId=p,await this.setBulkUpload(i,"operationId",p),await this.setBulkUpload(i,"status","uploading"),await this.setBulkGroup(t,"operationId",p),this.fields.set(r.id,r)):await this.setBulkUpload(i,"status","failed"),this.notify("sent-to-queue",t),p}async sendToQueue(e,t,s="",r="",i=!1){""===r&&(r=s);const o={endpoint:e,method:"POST",data:t,title:s,popup:r,canMerge:i,sendNow:"uploads/groups"===e,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(o)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e}),r=[],i=[],o=[];for(const e of s){const t=this.groups.get(e.id)?.element,s={images:[],fields:this.collectGroupFieldsFromDOM(t,e.id)},a=this.getGroupUploadsInOrder(e);for(const t of a){const r=this.formatFile(t);if(r){o.push(r);const a={upload_id:t.id,index:i.length},l=this.uploads.get(t.id),n=l?.element?.querySelector(`input[name="${e.id}_featured"]`);n?.checked&&(s.fields.featured=t.id),s.images.push(a),i.push(t.id)}}r.push(s)}const a=t.filter((e=>!e.group));for(const e of a){const t={images:[],fields:{}},s=this.formatFile(e);if(s){o.push(s);const r={upload_id:e.id,index:i.length};t.images.push(r),i.push(e.id)}r.push(t)}return{posts:r,uploadMap:i,files:o}}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 r=e.name.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");["featured","select-all"].some((e=>r.includes(e)))||e.value&&(s[r]=e.value)})),s}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],r=[];for(const e of t){const t=this.formatFile(e);t&&(r.push(t),s.push(e.id))}return{uploadMap:s,files:r}}async queueUploadMeta(e){const t=e.target.closest(this.selectors.items.item)?.dataset.uploadId,s=this.stores.uploads.get(t);if(!t||!s)return;if(!this.fields.get(s.field))return;let r={};r[e.target.name]=e.target.value,s.fields={...s.fields,...r},await this.setUpload(s.id,s);let i={};return i[s.attachmentId??s.id]=s.fields,await this.sendToQueue("uploads/meta",i,"Uploading Meta","",!0)}scanFields(e,t=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t)))}registerField(e,t=!0,s=null){const r={element:e,id:s||this.determineFieldId(e),config:this.extractFieldConfig(e,t),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){return{autoUpload:t,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"}}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+"_";return`${t}${s}${e.dataset.field||""}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,r){const i=this.fields.get(e);i&&window.showProgress(i.ui.progress,t,s,r)}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,r=this.workerState.tasks.get(t);r&&(clearTimeout(r.timeoutId),r.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 r=[],i=[...e],o=this.workerState.settings.maxConcurrent,a=async()=>{for(;i.length>0;){const e=i.shift();r.push(await this.processImage(e,t,s))}};return await Promise.all(Array.from({length:Math.min(o,e.length)},(()=>a()))),r}async processImage(e,t=2200,s=2200,r=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),r)}catch(r){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:r,restart:i}=this.workerState;if(i.count>=i.max)throw new Error("Worker max restarts exceeded");const o=await createImageBitmap(e);let{width:a,height:l}=o;if(a>t||l>s){const e=Math.min(t/a,s/l);a=Math.round(a*e),l=Math.round(l*e)}const n=this.getWorker(),d=crypto.randomUUID();return new Promise(((t,s)=>{const i=setTimeout((()=>{this.workerState.tasks.delete(d),r.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),r.timeout);this.workerState.tasks.set(d,{resolve:t,reject:s,timeoutId:i}),n.postMessage({id:d,imageBitmap:o,width:a,height:l,type:e.type,quality:.9},[o])}))}resizeImage(e,t,s){return new Promise((r=>{const i=new Image;i.onload=()=>{URL.revokeObjectURL(i.src);let{width:o,height:a}=i;if(o>t||a>s){const e=Math.min(t/o,s/a);o=Math.round(o*e),a=Math.round(a*e)}const l=document.createElement("canvas");l.width=o,l.height=a,l.getContext("2d").drawImage(i,0,0,o,a),l.toBlob(r,e.type,.9)},i.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 r=t.length;let i=0;this.updateFieldProgress(e,0,r,"Processing files...");const o=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),r=await this.setUpload(s,{id:s,field:e,status:"local_processing",blob:null,fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),i=await this.createUpload(s,t,e);return this.uploads.set(s,{element:i,ui:window.uiFromSelectors(this.selectors.items,i)}),await this.addToGroup(s,null),{uploadId:s,upload:r,file:t}}))),a=o.filter((e=>e.file.type.startsWith("image/"))),l=o.filter((e=>!e.file.type.startsWith("image/"))),n=await this.processImages(a.map((e=>e.file)));for(let t=0;t<a.length;t++){const{uploadId:s,upload:o}=a[t];o.blob=n[t],o.fields.size=n[t].size,o.status="queued",await this.setUpload(s,o),i++,this.updateFieldProgress(e,i,r,"Processing files...")}for(const{uploadId:t,upload:s,file:o}of l)s.blob=o,s.status="queued",await this.setUpload(t,s),i++,this.updateFieldProgress(e,i,r,"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"]});if(0===e.length)return;let t=window.getTemplate("restoreNotification");if(!t)return void this.error.log("No restore notification",{component:"UploadManager",src:window.location.href});const s=new Map;e.forEach((e=>{const t=e.src||"unknown";s.has(t)||s.set(t,[]),s.get(t).push(e)}));const r=window.location.href;let i=s.size>1?` across ${s.size} pages`:"",o=e.length>1?"uploads":"upload",a=`${e.length} ${o} can be recovered${i}`,l=t.querySelector(".details");l&&(l.textContent=a);let n=1;for(const[e,i]of s){let s=window.getTemplate("restoreField");if(!s)continue;let o=this.registerField(s,!1,"recovery_"+n),a=this.fields.get(o);n++;let l=e===r,[d,u,c]=[s.querySelector("h3"),s.querySelector("h3 a"),s.querySelector(".item-grid")];s.open=l,l?(u.remove(),d.textContent="From this page:"):[u.href,u.title,u.textContent]=[e,"Navigate to Page and Restore",e];let p=[...new Set(i.map((e=>e.group??"preview")))];for(let e of p){let t="preview"===e||this.stores.groups.get(e);if(!t)continue;let s=await this.createGroupElement(e,a.id),r=s.querySelector(".item-grid"),o=i.filter((t=>t.group===("preview"===e)?null:e));for(const[e,r]of Object.entries(t.fields??{})){let t=s.querySelector(`input[name*="${e}"]`);t&&(t.value=r)}for(let e of o){let t=await this.createUpload(e.id,this.formatFile(e),a.id);r.append(t)}c.append(s)}t.querySelector(".wrap").append(s)}document.body.append(t),t=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(t),this.restoreSelection=new window.jvbHandleSelection(t,{wrapper:{wrapper:".wrap"},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)),r=[...new Set(s.map((e=>e.group)))].filter(Boolean),i=s[0].field;if(!document.querySelector(`[data-uploader="${i}"]`))return void console.log("No field found for "+i);let o=this.fields.get(i);o.groupUI.container&&(o.groupUI.container.hidden=!1);let a=[];for(let e of r){let t=this.stores.groups.get(e);await this.createGroup(i,e);let r=this.groups.get(e),o=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=r.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of o){let s=await this.createUpload(t.id,this.formatFile(t),i);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),a.push(t.id)}}let l=s.filter((e=>!a.includes(e.id)));for(let e of l){let t=await this.createUpload(e.id,this.formatFile(e),i);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 r=window.getTemplate("uploadItem");if(!r)return null;let i=this.fields.get(s);if(!i)return null;r.dataset.uploadId=e;let o=this.getSubtypeFromMime(t.type)||"image";r.dataset.subtype=o;let[a,l,n,d,u]=[r.querySelector('[name="featured"]'),r.querySelector("img"),r.querySelector("video"),r.querySelector("label > span"),r.querySelector("details")];switch(a&&(a.value=e),o){case"image":if(l){const e=this.createPreviewUrl(t);l.src=e,l.alt=t.name||"",l.dataset.previewUrl=e}n?.remove(),d?.remove();break;case"video":if(n){const e=this.createPreviewUrl(t);n.src=e,n.dataset.previewUrl=e}l?.remove(),d?.remove();break;case"document":let e=t.name.split(".").pop()?.toLowerCase()??"",s={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},r=window.getIcon(s[e]??"file");d&&(d.innerText=t.name,d.prepend(r)),l?.remove(),n?.remove()}if(u){let e=window.getTemplate("uploadMeta");e&&u.append(e)}return r.draggable="single"!==i.config.type??!1,r.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e,i=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,i&&(i.htmlFor=r)}})),r}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;confirm("Remove this item?")&&(await this.removeUpload(s),this.a11y.announce("Item removed"))}async setBulkUpload(e,t,s){const r=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(r)}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;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.maybeLockUploads(t.field);let s=this.selectionHandlers.get(t.field);s&&s.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 r=this.createGroupElement(t,e);if(!r)return null;s.groupUI.grid.append(r);const i=r.querySelector(".item-grid");i&&(i.dataset.groupId=t,this.createSortable(e,i,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=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,t&&(s.dataset.fieldId=t);const r=s.querySelector("[data-select-all]");if(r){const t=`select-all-${e}`,i=s.querySelector(`label[for="${r.id}"]`);r.id=t,r.name=t,i&&(i.htmlFor=t)}let i=window.getTemplate("groupMetadata"),o=s.querySelector(".fields");if(i&&o){o.append(i);let t=o.querySelector('[name="post_title"]'),s=o.querySelector('[name="post_excerpt"]');t&&(t.dataset.groupId=e,t.id=`${e}_title`,t.name=`${e}[post_title]`),s&&(t.dataset.groupId=e,s.id=`${e}_excerpt`,s.name=`${e}[post_excerpt]`)}else s.querySelector("details")?.remove();const a=s.querySelector(".item-grid");return a&&(a.dataset.groupId=e),this.groups.set(e,{element:s,ui:window.uiFromSelectors(this.selectors.group,s)}),this.getSelectionHandler(t)?.addWrapper(s),s}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 r=this.stores.groups.filterByIndex({field:e});if(0===r.length)return;let i=r.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(i)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),r=this.uploads.get(e);if(!s||!r)return;const i=this.fields.get(s.field);if(!i)return;if(null!==r.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))}r.ui.checkbox&&(r.ui.checkbox.checked=!1);const o=this.selectionHandlers.get(s.field);if(o&&o.isSelected(e)&&o.deselect(e),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),r.ui.featured&&(r.ui.featured.hidden=!t),t){r.ui.featured&&(r.ui.featured.name=`${t}_featured`);let i=this.stores.groups.get(t);i&&(i.uploads.push(e),s.group=t,await this.stores.groups.save(i))}else s.group=null;let a=t?this.groups.get(t)?.ui.grid:i.ui.grid;a&&(a.append(r.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 r=this.stores.uploads.filterByIndex({group:s});Promise.all(r.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),r=this.stores.groups.get(e);if(!r)return;let i=!0;t&&r.uploads.length>0&&(i=window.confirm("Keep uploads in this group?")),await Promise.all(r.uploads.map((e=>i?this.addToGroup(e,null):this.removeUpload(e))));if(this.fields.get(r.field)){const t=this.getGroupKey(r.field,e),i=this.selectionHandlers.get(t);i?.destroy&&i.destroy(),this.selectionHandlers.get(r.field)?.removeWrapper(s.element);const o=this.sortables.get(t);o?.destroy&&o.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,r=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=r}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 r=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"}});r.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems),this.syncSortableSelection(e)})),this.selectionHandlers.set(t,r)}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 r=this.getGroupKey(e,s);if(this.sortables.has(r))return this.sortables.get(r);const i=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},ghostClass:"ghost",chosenClass:"chosen",dragClass:"dragging",onStart:t=>{const s=t.item,r=s?.dataset.uploadId,i=this.selected.get(e);if(r&&(!i||!i.has(r))){const t=this.selectionHandlers.get(e);t&&t.select(r)}this.syncSortableSelection(e)},onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(r,i),i}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),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(),s.classList.remove("drag-over");const r=this.selected.get(e);if(!r||0===r.size)return;const i=await this.createGroup(e);i&&(await Promise.all(Array.from(r).map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,r=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===r.length)return;const i=s.dataset.groupId||null;await Promise.all(r.map((e=>this.addToGroup(e,i)))),await this.handleReorder(t,i),this.selectionHandlers.get(t)?.clearSelection()}syncSortableSelection(e){const t=this.selected.get(e)||new Set;for(const[s,r]of this.uploads){const i=this.stores.uploads.get(s);if(!i||i.field!==e)continue;const o=r.element;if(!o)continue;const a=t.has(s);a&&!o.classList.contains("selected")?Sortable.utils.select(o):!a&&o.classList.contains("selected")&&Sortable.utils.deselect(o)}}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(!s)return void console.log("Couldn't Reorder items...");let r=Array.from(s.children).filter((e=>e.matches(this.selectors.items.item)&&!e.classList.contains("ghost"))).map((e=>e.dataset.uploadId)).filter((e=>e));if(t){let e=this.stores.groups.get(t);e&&(e.uploads=r,this.stores.groups.save(e).then((()=>{})))}else{let t=this.fields.get(e)?.ui.hidden;t&&(t.value=r.join(","))}this.a11y.announce("Items reordered")}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),r=e.dataset.field,i=this.stores.uploads.filterByIndex({field:t});for(const e of i){const t=this.formatFile(e);t&&s.push({file:t,fieldName:r,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/inc/helpers/renderFields.php b/inc/helpers/renderFields.php
index 7384fc7..3fa3207 100644
--- a/inc/helpers/renderFields.php
+++ b/inc/helpers/renderFields.php
@@ -558,39 +558,38 @@
$dataID = ($ID) ? ['image-id' => $ID] : false;
$addID = ($ID) ? '-'.$ID : '';
-
$defaultFields = [
'image-title'.$addID => [
- 'type' => 'text',
- 'label' => 'Image Title',
- 'value' => $title,
- 'data' => $dataID
+ 'type' => 'text',
+ 'label' => 'Image Title',
+ 'value' => $title,
+ 'data' => $dataID
],
'image-alt-text'.$addID => [
- 'type' => 'text',
- 'label' => 'Alt Text',
- 'value' => $alt,
- 'hint' => 'Alt text helps the visually impaired, as well as some benefits for SEO.',
- 'data' => $dataID
+ 'type' => 'text',
+ 'label' => 'Alt Text',
+ 'value' => $alt,
+ 'hint' => 'Alt text helps the visually impaired, as well as some benefits for SEO.',
+ 'data' => $dataID
],
'image-caption'.$addID => [
- 'type' => 'textarea',
- 'value' => $caption,
- 'label' => 'Image Caption',
- 'data' => $dataID
+ 'type' => 'textarea',
+ 'value' => $caption,
+ 'label' => 'Image Caption',
+ 'data' => $dataID
]
];
$fields = array_merge($defaultFields, $fields);
$config = [
- 'type' => 'group',
- 'wrap' => 'details',
- 'label' => 'Image Info',
- 'hint' => 'These will be automatically generated if left blank.',
- 'fields' => $fields
+ 'type' => 'group',
+ 'wrap' => 'details',
+ 'label' => 'Image Info',
+ 'hint' => 'These will be automatically generated if left blank.',
+ 'fields' => $fields
];
- return $form->render('image_data',null, $config,false, true);
+ return $form->render('image_data', null, $config, false, true);
}
diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index d425070..b868510 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -41,6 +41,8 @@
$this->content = $content;
$this->cache = CacheManager::for($content);
+ $this->cache->clear();
+
// Create and configure skeleton
$this->skeleton = new CRUDSkeleton();
$this->configure();
diff --git a/inc/meta/MetaForm.php b/inc/meta/MetaForm.php
index afaf791..2324496 100644
--- a/inc/meta/MetaForm.php
+++ b/inc/meta/MetaForm.php
@@ -1100,71 +1100,72 @@
$dataID = ($id) ? ['id' => $id] : '';
?>
<div class="item upload"<?= ($id) ? ' data-id="'.$id.'"' : '' ?>>
- <div class="preview">
- <?php jvbRenderProgressBar('',true) ?>
- <input type="checkbox" class="upload-select" name="select-item" id="select-item<?=$addID?>">
- <label for="select-item<?=$addID?>" aria-label="Select image">
- <?= ($attachment) ?: '<img>
- <video></video>
- <span></span>' ?>
- </label>
- <div class="item-actions row btw">
- <div class="radio-button">
- <input type="radio" class="featured btn" name="featured" id="featured" hidden>
- <label for="featured">
- <?=jvbIcon('star')?>
- <?=jvbIcon('star', ['style' => 'fill'])?>
- <span class="screen-reader-text">Set as featured image</span>
- </label>
- </div>
-
- <button type="button" data-action="delete-upload" title="Remove from Group">
- <?=jvbIcon('trash')?>
- </button>
+ <div class="preview">
+ <?php jvbRenderProgressBar('',true) ?>
+ <input type="checkbox" class="upload-select" name="select-item" id="select-item<?=$addID?>">
+ <label for="select-item<?=$addID?>" aria-label="Select image">
+ <?= ($attachment) ?: '<img>
+ <video></video>
+ <span></span>' ?>
+ </label>
+ <div class="item-actions row btw">
+ <div class="radio-button">
+ <input type="radio" class="featured btn" name="featured" id="featured" hidden>
+ <label for="featured">
+ <?=jvbIcon('star')?>
+ <?=jvbIcon('star', ['style' => 'fill'])?>
+ <span class="screen-reader-text">Set as featured image</span>
+ </label>
</div>
+
+ <button type="button" data-action="delete-upload" title="Remove from Group">
+ <?=jvbIcon('trash')?>
+ </button>
</div>
- <details>
- <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
+ </div>
+ <details>
+ <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
- <?php
+ <?php
+ $fields = array_key_exists('fields', $config) ? $config['fields'] : [];
- $fields = array_key_exists('fields', $config) ? $config['fields'] : [];
- $fields = array_merge([
- 'upload_data' => [
- 'type' => 'group',
- 'wrap' => 'details',
- 'label' => 'Image Info',
- 'hint' => 'These will be automatically generated if left blank.',
- 'fields' => [
- 'image-title'.$addID => [
- 'type' => 'text',
- 'label' => 'Image Title',
- 'value' => $title,
- 'data' => $dataID
- ],
- 'image-alt-text'.$addID => [
- 'type' => 'text',
- 'label' => 'Alt Text',
- 'value' => $alt,
- 'hint' => 'Alt text helps the visually impaired, as well as some benefits for SEO.',
- 'data' => $dataID
- ],
- 'image-caption'.$addID => [
- 'type' => 'textarea',
- 'value' => $caption,
- 'label' => 'Image Caption',
- 'data' => $dataID
- ]
- ]
- ]
- ], $fields);
+ // Only add image_data if not already provided
+ if (!array_key_exists('image_data', $fields)) {
+ $fields['image_data'] = [
+ 'type' => 'group',
+ 'wrap' => 'details',
+ 'label' => 'Image Info',
+ 'hint' => 'These will be automatically generated if left blank.',
+ 'fields' => [
+ 'image-title'.$addID => [
+ 'type' => 'text',
+ 'label' => 'Image Title',
+ 'value' => $title,
+ 'data' => $dataID
+ ],
+ 'image-alt-text'.$addID => [
+ 'type' => 'text',
+ 'label' => 'Alt Text',
+ 'value' => $alt,
+ 'hint' => 'Alt text helps the visually impaired, as well as some benefits for SEO.',
+ 'data' => $dataID
+ ],
+ 'image-caption'.$addID => [
+ 'type' => 'textarea',
+ 'value' => $caption,
+ 'label' => 'Image Caption',
+ 'data' => $dataID
+ ]
+ ]
+ ];
+ }
- $meta = new MetaManager($id);
- foreach ($fields as $field => $config) {
- $meta->render('form', $field, $config);
- }
- ?>
- </details>
+ $meta = new MetaManager($id);
+ foreach ($fields as $field => $config) {
+ $meta->render('form', $field, $config);
+ }
+ ?>
+ </details>
</div>
<?php
}
diff --git a/inc/registry/CheckCustomTables.php b/inc/registry/CheckCustomTables.php
index 118111c..e587f0d 100644
--- a/inc/registry/CheckCustomTables.php
+++ b/inc/registry/CheckCustomTables.php
@@ -44,25 +44,6 @@
$this->userIDType = $this->getColumnType($this->userTable, 'ID');
$this->termIDType = $this->getColumnType($this->wpdb->terms, 'term_id');
$this->postIDType = $this->getColumnType($this->wpdb->posts, 'ID');
-
- error_log("JVB DEBUG: userTable = " . $this->userTable);
- error_log("JVB DEBUG: Users table exists = " . ($this->wpdb->get_var("SHOW TABLES LIKE '{$this->userTable}'") ? 'yes' : 'no'));
- error_log("JVB DEBUG: Users engine = " . $this->wpdb->get_var("SELECT ENGINE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{$this->userTable}'"));
- error_log("JVB DEBUG: MySQL version = " . $this->wpdb->db_version());
- $usersStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->userTable}'");
- $termsStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->wpdb->terms}'");
- error_log("JVB DEBUG: wp_users collation = " . $usersStatus->Collation);
- error_log("JVB DEBUG: wp_terms collation = " . $termsStatus->Collation);
- error_log("JVB DEBUG: Our charset_collate = " . $this->wpdb->get_charset_collate());
-
- error_log("JVB FK Types: users.ID={$this->userIDType}, terms.term_id={$this->termIDType}, posts.ID={$this->postIDType}");
-
- $usersDb = $this->wpdb->get_var(
- "SELECT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{$this->userTable}' LIMIT 1"
- );
- error_log("JVB DEBUG: wp_users is in database = " . $usersDb);
- error_log("JVB DEBUG: Current DB_NAME = " . DB_NAME);
- error_log("JVB DEBUG: Tables being created in = " . $this->wpdb->dbname);
}
protected function getMultisiteUsersTable():string
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index 3d455d5..38f9ddc 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -274,14 +274,14 @@
if (array_key_exists('taxonomies', $params)) {
$args = $this->applyTaxonomyFilters($args, $params);
}
- if (array_key_exists('date', $params) && !empty($params['date'])) {
+ 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 (!empty($params['search'])) {
+ if (array_key_exists('search', $params)) {
$args['s'] = sanitize_text_field($params['search']);
}
@@ -302,10 +302,15 @@
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)) {
+ $args = $this->applySearchFilters($args, $params);
+ }
+
// Run query
$query = new WP_Query($args);
- $this->post_type = $params['content']??$params['type'];
$this->fields = jvbGetFields(str_replace('-','_',$this->post_type));
$this->taxonomies = $this->getTaxonomies($this->post_type);
@@ -325,6 +330,87 @@
return $this->addCacheHeaders($response);
}
+ protected function applySearchFilters(array $args, array $params): array
+ {
+ $search_term = sanitize_text_field($params['search']);
+
+ // Search term is already in $args['s'] from earlier
+
+ // Get all taxonomies registered to this post type
+ $taxonomies = get_object_taxonomies($this->post_type, 'names');
+
+ if (empty($taxonomies)) {
+ return $args;
+ }
+
+ // Cache the taxonomy term lookup per search term + post type
+ $term_cache_key = 'search_terms_' . md5($search_term . $this->post_type);
+ $matching_term_ids = $this->cache->get($term_cache_key);
+
+ if ($matching_term_ids === false) {
+ $matching_term_ids = [];
+
+ foreach ($taxonomies as $taxonomy) {
+ $terms = get_terms([
+ 'taxonomy' => $taxonomy,
+ 'search' => $search_term,
+ 'hide_empty' => false,
+ 'fields' => 'ids'
+ ]);
+
+ if (!is_wp_error($terms) && !empty($terms)) {
+ $matching_term_ids = array_merge($matching_term_ids, $terms);
+ }
+ }
+
+ // Cache term IDs for 1 hour
+ $this->cache->set($term_cache_key, $matching_term_ids, 3600);
+ }
+
+ if (empty($matching_term_ids)) {
+ return $args;
+ }
+
+ // Build tax_query for matching terms
+ $term_queries = [];
+
+ foreach ($taxonomies as $taxonomy) {
+ $taxonomy_term_ids = array_filter($matching_term_ids, function($term_id) use ($taxonomy) {
+ $term = get_term($term_id);
+ return !is_wp_error($term) && $term->taxonomy === $taxonomy;
+ });
+
+ if (!empty($taxonomy_term_ids)) {
+ $term_queries[] = [
+ 'taxonomy' => $taxonomy,
+ 'field' => 'term_id',
+ 'terms' => array_values($taxonomy_term_ids),
+ 'operator' => 'IN'
+ ];
+ }
+ }
+
+ if (!empty($term_queries)) {
+ if (isset($args['tax_query'])) {
+ $args['tax_query'] = [
+ 'relation' => 'OR',
+ $args['tax_query'],
+ [
+ 'relation' => 'OR',
+ ...$term_queries
+ ]
+ ];
+ } else {
+ $args['tax_query'] = [
+ 'relation' => 'OR',
+ ...$term_queries
+ ];
+ }
+ }
+
+ return $args;
+ }
+
/**
* Gets allowed taxonomies for a particular content
* @param string $content
diff --git a/inc/rest/routes/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index 614e74c..941207b 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/inc/rest/routes/UploadRoutes.php
@@ -270,26 +270,27 @@
{
$uploader = new UploadManager();
$secured_files = [];
- $upload_map = [];
$errors = [];
$context = $args;
unset($context['upload_ids']);
- $config = $this->getFieldConfig($args);
$file_array = $files['files'] ?? $files;
- $tmp_names = isset($file_array['tmp_name'][0]) && is_array($file_array['tmp_name'][0])
- ? $file_array['tmp_name'][0]
- : $file_array['tmp_name'];
+ if (!is_array($file_array['tmp_name'])) {
+ $file_array = [
+ 'name' => [$file_array['name']],
+ 'type' => [$file_array['type']],
+ 'tmp_name' => [$file_array['tmp_name']],
+ 'error' => [$file_array['error']],
+ 'size' => [$file_array['size']]
+ ];
+ }
+ $tmp_names = $file_array['tmp_name'] ?? [];
+
+
foreach ($tmp_names as $index => $tmp_name) {
- $file_data = isset($file_array['tmp_name'][0]) && is_array($file_array['tmp_name'][0]) ? [
- 'name' => $file_array['name'][0][$index],
- 'type' => $file_array['type'][0][$index],
- 'tmp_name' => $tmp_name,
- 'error' => $file_array['error'][0][$index],
- 'size' => $file_array['size'][0][$index]
- ] : [
+ $file_data = [
'name' => $file_array['name'][$index],
'type' => $file_array['type'][$index],
'tmp_name' => $tmp_name,
@@ -309,8 +310,8 @@
throw new Exception('Failed to secure file');
}
- $frontend_id = $args['upload_ids'][$index] ?? 'upload_' . $index;
- $upload_map[$index] = $frontend_id;
+ // Embed upload_id directly in the secured file data
+ $secured['upload_id'] = $args['upload_ids'][$index] ?? 'upload_' . $index;
$secured_files[] = $secured;
} catch (Exception $e) {
@@ -320,7 +321,6 @@
return [
'files' => $secured_files,
- 'upload_map' => $upload_map,
'errors' => $errors
];
}
@@ -343,7 +343,7 @@
/**
* Queue files for processing
*/
- protected function queueProcessing(array $secured_data, array $args):string
+ protected function queueProcessing(array $secured_data, array $args): string
{
$operation_type = $this->determineOperationType($secured_data['files'][0] ?? []);
@@ -353,20 +353,18 @@
} elseif ($operation_type === 'document') {
$chunkSize = 10;
}
+
JVB()->queue()->queueOperation(
$operation_type,
$args['user'],
array_merge(
- [
- 'secured_files' => $secured_data['files'],
- 'upload_map' => $secured_data['upload_map'],
- ],
+ ['secured_files' => $secured_data['files']],
$args
),
[
- 'operation_id' => $args['upload'],
- 'chunk_key' => 'secured_files',
- 'chunk_size' => $chunkSize
+ 'operation_id' => $args['upload'],
+ 'chunk_key' => 'secured_files',
+ 'chunk_size' => $chunkSize
]
);
@@ -376,9 +374,9 @@
$args['user'],
$args,
[
- 'priority' => 'high',
- 'operation_id' => $args['id'],
- 'depends_on' => $args['upload']
+ 'priority' => 'high',
+ 'operation_id' => $args['id'],
+ 'depends_on' => $args['upload']
]
);
}
@@ -387,16 +385,17 @@
'temporary_cleanup',
$args['user'],
[
- 'files' => $secured_data['files'],
- 'context' => $args,
+ 'files' => $secured_data['files'],
+ 'context' => $args,
],
[
- 'priority' => 'low',
- 'chunk_size' => 5,
- 'chunk_key' => 'files',
- 'depends_on' => $args['upload']
+ 'priority' => 'low',
+ 'chunk_size' => 5,
+ 'chunk_key' => 'files',
+ 'depends_on' => $args['upload']
]
);
+
return $args['id'];
}
@@ -579,9 +578,8 @@
$args = $data;
unset($args['secured_files']);
- unset($args['upload_map']);
- foreach ($data['secured_files'] as $index => $secured_file) {
+ foreach ($data['secured_files'] as $secured_file) {
$result = $uploader->processUpload(
$secured_file['temp_path'],
array_merge(
@@ -593,7 +591,7 @@
'term_id' => (int)($data['term_id'] ?? 0),
'original_name' => $secured_file['original_name'],
'mime_type' => $secured_file['mime_type'],
- 'content' => $data['content'] ?? '', // For directory pattern
+ 'content' => $data['content'] ?? '',
]
)
);
@@ -601,20 +599,19 @@
if (!is_wp_error($result)) {
$standardized = $this->standardizeResult($result);
- // Map to frontend upload ID
- if (isset($data['upload_map'][$index])) {
- $standardized['upload_id'] = $data['upload_map'][$index];
+ // Use embedded upload_id from the secured file itself
+ $standardized['upload_id'] = $secured_file['upload_id'] ?? null;
+
+ if ($standardized['upload_id']) {
+ $processed_results[$standardized['upload_id']] = $standardized;
+ } else {
+ $processed_results[] = $standardized;
}
// Apply frontend metadata if provided
- if (!empty($data['frontend_metadata'][$index])) {
- $this->applyMeta(
- $standardized['attachment_id'],
- $data['frontend_metadata'][$index]
- );
+ if (!empty($secured_file['metadata'])) {
+ $this->applyMeta($standardized['attachment_id'], $secured_file['metadata']);
}
-
- $processed_results[$standardized['upload_id']] = $standardized;
}
}
@@ -639,8 +636,8 @@
]
);
return [
- 'success' => false,
- 'result' => $e->getMessage()
+ 'success' => false,
+ 'result' => $e->getMessage()
];
}
}
@@ -1089,16 +1086,12 @@
try {
$files = $request->get_file_params();
$args = $this->buildUploadArgs($request);
- $data = $request->get_params();
-
- global $_FILES;
if (!$args['content'] || !$args['user'] || !$args['posts']) {
-
$this->logError('Missing required data');
return new WP_REST_Response([
- 'success' => false,
- 'message' => 'Missing required data'
+ 'success' => false,
+ 'message' => 'Missing required data'
]);
}
@@ -1126,10 +1119,7 @@
$operation_type,
$args['user'],
array_merge(
- [
- 'secured_files' => $secured_files['files'],
- 'upload_map' => $secured_files['upload_map'],
- ],
+ ['secured_files' => $secured_files['files']],
$args
),
[
@@ -1185,40 +1175,55 @@
$queue = JVB()->queue();
$all_uploaded_images = [];
- // Get the upload operation ID from data
+ // Get the upload operation ID from dependencies
$dependencies = json_decode($operation->dependencies, true);
- $uploads = [];
- foreach($dependencies as $dependency) {
- $op = $queue->getOperation($dependency);
- $res = json_decode($op->result, true);
- if ($op->status === 'completed') {
- $uploads = array_merge($uploads, $res);
+ if (empty($dependencies)) {
+ return [
+ 'success' => false,
+ 'result' => 'No dependencies found'
+ ];
+ }
+
+ // Collect uploads from all dependency operations
+ $uploads = [];
+ foreach ($dependencies as $dependency) {
+ // Use getOperationValue like ContentRoutes does
+ $res = $queue->getOperationValue($dependency, 'result');
+
+ if (empty($res)) {
+ continue;
+ }
+
+ // Results are stored at root level, keyed by upload_id
+ // Filter to only include actual upload results (arrays with attachment_id)
+ foreach ($res as $key => $value) {
+ if (is_array($value) && isset($value['attachment_id'])) {
+ $uploads[$key] = $value;
+ }
}
}
-
if (empty($uploads)) {
return [
- 'success' => false,
- 'result' => 'No uploads to process'
+ 'success' => false,
+ 'result' => 'No uploads to process'
];
}
// Build map of upload_id => attachment_id
- foreach ($uploads as $img) {
- if (isset($img['upload_id'], $img['attachment_id'])) {
- $all_uploaded_images[$img['upload_id']] = [
- 'upload_id' => $img['upload_id'],
- 'attachment_id' => (int)$img['attachment_id']
- ];
- }
+ foreach ($uploads as $upload_id => $img) {
+ $all_uploaded_images[$upload_id] = [
+ 'upload_id' => $upload_id,
+ 'attachment_id' => (int)$img['attachment_id']
+ ];
}
$content = jvbCheckBase($data['content']);
if (Features::forContent($data['content'])->has('is_timeline')) {
return $this->processTimelineUploads($data, $uploads, $all_uploaded_images, $operation);
}
+
$user = (int)$data['user'];
$created_posts = [];
$used_upload_ids = [];
@@ -1233,7 +1238,7 @@
? sanitize_textarea_field($post['fields']['post_excerpt'])
: '';
- $args =[
+ $args = [
'post_type' => $content,
'post_author' => $user,
'post_status' => 'draft',
@@ -1246,20 +1251,18 @@
if ($new_post_id && !is_wp_error($new_post_id)) {
$created_posts[] = $new_post_id;
- // Get featured image upload_id
-
-
- $featured_upload_id = array_key_exists('featured', $post['fields']) ? (int)$post['fields']['featured'] : null;
+ // Get featured image upload_id - string, not int!
+ $featured_upload_id = $post['fields']['featured'] ?? null;
$featured_attachment_id = null;
$gallery_attachment_ids = [];
// Process all images for this post
- foreach ($post['images'] as $key => $img) {
+ foreach ($post['images'] as $img) {
$upload_id = $img['upload_id'];
$used_upload_ids[] = $upload_id;
if (isset($all_uploaded_images[$upload_id])) {
- $attachment_id = (int)$all_uploaded_images[$upload_id]['attachment_id'];
+ $attachment_id = $all_uploaded_images[$upload_id]['attachment_id'];
if ($upload_id === $featured_upload_id) {
$featured_attachment_id = $attachment_id;
@@ -1271,10 +1274,9 @@
// Set featured image
if ($featured_attachment_id) {
- set_post_thumbnail($new_post_id, (int)$featured_attachment_id);
+ set_post_thumbnail($new_post_id, $featured_attachment_id);
} elseif (!empty($gallery_attachment_ids)) {
- // If no featured set, use first image
- set_post_thumbnail($new_post_id, (int)$gallery_attachment_ids[0]);
+ set_post_thumbnail($new_post_id, $gallery_attachment_ids[0]);
array_shift($gallery_attachment_ids);
}
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index 1268132..ecef889 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -604,17 +604,19 @@
protected function renderContent(): void {
$dataIgnore = $this->useCRUDjs ? '' : ' data-ignore';
?>
- <section class="items-list <?= esc_attr($this->dataType) ?> crud" data-content="<?= esc_attr($this->dataType) ?>" data-view="<?= $this->defaultView?>"<?=$dataIgnore?>>
- <?php
- $this->renderControlsAndFilters();
+ <section class="items-list <?= esc_attr($this->dataType) ?> crud" data-content="<?= esc_attr($this->dataType) ?>" data-singular="<?=$this->singular?>" data-plural="<?=$this->plural?>" data-view="<?= $this->defaultView?>"<?=$dataIgnore?>>
+ <div class="wrap">
+ <?php
+ $this->renderControlsAndFilters();
- if ($this->showBulkControls) {
- $this->renderBulkActions();
- }
- ?>
+ if ($this->showBulkControls) {
+ $this->renderBulkActions();
+ }
+ ?>
- <div class="<?= esc_attr($this->dataType) ?> item-grid" role="grid"></div>
- <div class="scroll-sentinel" aria-hidden="true"></div>
+ <div class="<?= esc_attr($this->dataType) ?> item-grid" role="grid"></div>
+ <div class="scroll-sentinel" aria-hidden="true"></div>
+ </div>
</section>
<?php
}
@@ -628,7 +630,7 @@
}
?>
<details class="all-filters col start" data-ignore>
- <summary>Filters</summary>
+ <summary>Filters <button hidden data-action="clear-filters" data-ignore><?=jvbIcon('x')?><span>Clear Filters</span></span></button></summary>
<?php
$this->renderSearch();
@@ -640,6 +642,7 @@
$this->renderColumnSelector();
}
?>
+ <button data-action="refresh" data-ignore><?=jvbIcon('arrows-clockwise')?><span>Hard Refresh</span></span></button>
</details>
<?php
}
@@ -747,8 +750,11 @@
$i = 0;
foreach ($option as $opt => $label) {
$icon = $opt === 'date' ? 'calendar' : $opt;
+ $value = $opt;
+ $value = ($value === 'sort-ascending') ? 'asc' : $value;
+ $value = ($value === 'sort-descending') ? 'desc' : $value;
?>
- <input id="<?=$opt?>" class="btn" type="radio" name="<?=$o?>" data-filter="<?=$o?>" value="<?=$opt?>"<?=$i===0 ? ' checked':''?>>
+ <input id="<?=$opt?>" class="btn" type="radio" name="<?=$o?>" data-filter="<?=$o?>" value="<?=$value?>"<?=$i===0 ? ' checked':''?>>
<label for="<?=$opt?>" title="<?=$label?>"><?=jvbDashIcon($icon)?></label>
<?php
@@ -822,11 +828,11 @@
<label for="date-start" class="col">
From
</label>
- <input type="date" id="date-start" class="date-start">
+ <input type="date" id="date-start" class="date-start" name="date-start">
<label for="date-end" class="col">
To
</label>
- <input type="date" id="date-end" class="date-end">
+ <input type="date" id="date-end" class="date-end" name="date-end">
</div>
<div class="month-picker">
<label>
@@ -870,7 +876,7 @@
$out = '';
if (!empty($terms)) {
$out .= sprintf(
- '<div class="row nowrap"><label for="filter-%s">%s<span class="screen-reader-text">Filter by %s</span></label>
+ '<div class="row nowrap"><label class="m-0" for="filter-%s">%s<span class="screen-reader-text">Filter by %s</span></label>
<select id="filter-%s" class="filter %s" name="%s" data-filter="taxonomies" data-taxonomy="%s">
<option value="">by %s</option>',
$taxonomy,
@@ -986,7 +992,7 @@
}
foreach ($this->taxonomies as $taxonomy => $config) {
?>
- <option value="tax-<?=$taxonomy?>">Add to <?= JVB_TAXONOMY[$taxonomy]['singular']??$config['label'] ?></option>
+ <option value="tax-<?=$taxonomy?>" data-type="selector" data-single="<?=JVB_TAXONOMY[$taxonomy]['singular']?>" data-plural="<?=JVB_TAXONOMY[$taxonomy]['plural']?>" data-taxonomy="<?=$taxonomy?>">Add to <?= JVB_TAXONOMY[$taxonomy]['singular']??$config['label'] ?></option>
<?php
}
?>
@@ -1169,11 +1175,11 @@
<?= $this->renderItemSelect()?>
<?=$this->renderImage() ?>
<div class="col start w-full">
- <?= $this->renderItemActions()?>
<h3 data-field="post_title"></h3>
<p data-attr="date"></p>
<p data-field="price"></p>
<div data-field="post_excerpt"></div>
+ <?= $this->renderItemActions()?>
</div>
</div>
</template>
--
Gitblit v1.10.0