Jake Vanderwerf
2026-01-11 181938ea32cf2c11d10d56018df22586adff7860
=CRUD.js cleanup, DataStore.js filtering optimization
22 files modified
1 files added
3882 ■■■■ changed files
assets/css/dash.min.css 2 ●●● patch | view | raw | blame | history
assets/js/concise/CRUD.js 1850 ●●●● patch | view | raw | blame | history
assets/js/concise/CRUDOld.js 778 ●●●●● patch | view | raw | blame | history
assets/js/concise/DataStore.js 144 ●●●● patch | view | raw | blame | history
assets/js/concise/HandleSelection.js 330 ●●●●● patch | view | raw | blame | history
assets/js/concise/PopulateForm.js 2 ●●●●● patch | view | raw | blame | history
assets/js/concise/Queue.js 133 ●●●●● patch | view | raw | blame | history
assets/js/concise/TaxonomySelector.js 59 ●●●●● patch | view | raw | blame | history
assets/js/concise/UploadManager.js 301 ●●●●● patch | view | raw | blame | history
assets/js/min/crud.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/dataStore.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/handleSelection.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/populate.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/queue.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/selector.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/uploader.min.js 2 ●●● patch | view | raw | blame | history
inc/helpers/renderFields.php 1 ●●●● patch | view | raw | blame | history
inc/managers/CRUDManager.php 2 ●●●●● patch | view | raw | blame | history
inc/meta/MetaForm.php 11 ●●●● patch | view | raw | blame | history
inc/registry/CheckCustomTables.php 19 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ContentRoutes.php 92 ●●●●● patch | view | raw | blame | history
inc/rest/routes/UploadRoutes.php 122 ●●●● patch | view | raw | blame | history
inc/ui/CRUDSkeleton.php 22 ●●●●● patch | view | raw | blame | history
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)}
: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)}
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,
                    });
        this.store.subscribe((event, data) => {
            switch (event) {
                case 'data-loaded':
                    this.render();
                    this.selectionHandler.collectItems();
                    break;
                }
            } 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`);
    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;
        let remove = [];
        let el = data.config.element;
        if (el === this.ui.modals.edit.form) {
            theChanges[this.activeItem] = changes;
                title = `Saving ${title} Changes`;
                // Check if status change requires removal
                if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
                    itemsToRemove.push(this.currentItemID);
            if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
                remove.push(this.activeItem);
                }
                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);
        } 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);
                    }
                num++;
                });
                title = `Updating ${selected.length} ${this.config.plural??'posts'} Changes`;
                break;
            case data.config.element === this.ui.forms.create:
            title = `Updating ${num} ${this.plural} Changes`;
        } else if (el === this.ui.modals.create.form) {
                if (event === 'form-submit') {
                    theChanges[data.config.data['form-id']] = changes;
                theChanges[el.dataset.formId] = 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);
        if (remove.length > 0) {
            this.removeItems(remove);
                    }
                }, 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);
            }
        }
        this.selectionHandler.clearSelection();
        if (Object.keys(theChanges).length === 0) {
            return;
        }
        this.savePosts(theChanges, title, event === 'form-submit');
    }
    savePosts(changes, title, delay = false) {
        if (Object.keys(changes).length === 0) return;
        this.savePosts(theChanges, title);
        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];
    }
    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',
            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
        // 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;
            if (!item.fields.timeline) {
                item.fields.timeline = {};
            }
            if (!item.fields.timeline[imgID]) {
                item.fields.timeline[imgID] = {};
            }
            item.fields.timeline[imgID][fieldName] = value;
                item.fields.timeline ??= {};
                item.fields.timeline[imgID] ??= {};
                item.fields.timeline[imgID][field] = value;
        } else {
            item.fields[fieldName] = value;
                item.fields[field] = value;
            }
        } else {
            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);
        if (this.shouldRemoveItemUI(status)) {
            this.removeItems(Object.keys(changes));
                    }
                }, delay);
                delay += 50; // Increment delay for staggered effect
            }
        }
        // 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;
        }
    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);
        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');
    }
    updateBulkOptions(status = 'all') {
        if (status === 'trash') {
            if (this.ui.bulkSelectActions?.querySelector('[value="edit"]')) {
                window.removeChildren(this.ui.bulkSelectActions);
                let options = window.getTemplate('trashOptions');
            if (options) {
                options.querySelectorAll('option').forEach((option, index) => {
                    if (index === 0) {
                        option.checked = true;
                    }
                    this.ui.bulkSelectActions.append(option);
                    if (index === 0) option.checked = true;
                    this.ui.bulk.action.append(option);
                });
            }
        } else {
            if (this.ui.bulkSelectActions && !this.ui.bulkSelectActions.querySelector('[value="edit"]')) {
                window.removeChildren(this.ui.bulkSelectActions);
            this.ui.bulk.action.value = '';
        }
        if (this.selected.size > 0) {
            this.selectionHandler.updateSelectionUI();
        }
    }
                let options = window.getTemplate('notTrashOptions');
                options.querySelectorAll('option').forEach((option, index) => {
                    this.ui.bulkSelectActions.append(option);
    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);
        }
    }
    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);
            }
        }
        if (this.ui.bulkSelectActions) {
            this.ui.bulkSelectActions.value = '';
        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);
        }
    }
    populateBulkEdit() {
        const container = this.modals.bulkEdit.modal.querySelector('form .selected');
        if (!container) return;
    /***************************************************************
    RENDER HELPERS
     ***************************************************************/
    setupItemElement(element, item, templateName) {
        if (this.items[this.view].has(item.id)) {
            return this.items[this.view].get(item.id);
        }
        window.removeChildren(container);
        for (let selected of this.viewController.selectedItems) {
            let item = this.store.get(selected);
        element.dataset.id = item.id;
        if (item._pending) element.classList.add('pending');
            const img = window.getTemplate('bulkItem');
        // 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 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;
        const thumbnail = item.images?.[item.fields?.post_thumbnail] ?? {};
        img.src = thumbnail.medium ?? '';
        img.alt = thumbnail.alt ?? '';
            }
            if (image && item.thumbnail) {
                image.src = item.thumbnail;
                image.alt = item.alt || '';
    setupItemActions(element, item) {
        element.querySelectorAll('[data-action]').forEach(btn => {
            btn.dataset.id = item.id;
        });
            }
            container.append(img);
    renderGridItem(item) {
        if (this.items.grid.has(item.id)) {
            return this.items.grid.get(item.id);
        }
        let modal = this.modals.bulkEdit.modal;
        [
            modal.querySelector('h2 span').textContent
        ] = [
            this.viewController.selectedItems.size
        ];
        this.formController.registerForm(this.ui.forms.bulkEdit);
        const card = window.getTemplate('gridView');
        if (!card) return null;
        return this.setupItemElement(card, item, 'grid');
    }
    populateEditForm(itemID) {
        this.currentItemID = itemID;
    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;
        let item = this.store.get(parseInt(itemID));
        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'] || '');
                    }
                    row.insertBefore(point, pointTemplate);
                });
                pointTemplate.remove();
            }
        } else {
            // Standard table row
            if (this.ui.table.form?.dataset.edit !== undefined) {
                new window.jvbPopulate(row, item);
            } else {
                this.populateTableReadOnly(row, item);
            }
            this.cleanupTableRow(row);
        }
        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;
            }
        }
    }
    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) {
            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);
                    window.fade(item, false);
        }
    }
    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');
                }
            }, 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') {
assets/js/concise/CRUDOld.js
New file
@@ -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,
                });
            }
        }
    });
});
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) => {
        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();
            // Get all items and filter them locally
            const allItems = Array.from(store.data.values());
        const searchQuery = store.filters.search?.toLowerCase().trim() || '';
            // Filter by current filters (excluding search and page)
            let filtered = allItems.filter(item => {
                // Apply all filters except search and page
        const filtered = allItems.filter(item => {
            // Apply all non-ignored filters
                for (const [key, value] of Object.entries(store.filters)) {
                    if (key === 'search' || key === 'page') continue;
                if (store.ignoreFilters.has(key)) continue;
                if (value === null || value === undefined || value === '') continue;
                if (value === 'all') 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;
                    }
                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;
            });
        }
        return items;
    }
    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;
            });
            // 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;
        }
        // Fallback to all data
        return this.getAll(name);
        }
        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) {
@@ -1047,7 +1103,17 @@
            return true;
        }
        // PAGE OPTIMIZATION: Don't fetch if trying to go beyond available pages
        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
            });
            if (isSubsetFilter) return false;
        }
        if ('page' in updates) {
            const newPage = updates.page;
            const oldPage = oldFilters.page || 1;
assets/js/concise/HandleSelection.js
@@ -1,24 +1,76 @@
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() {
@@ -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
        });
    }
    getItemWrapper(item) {
        if (!item) return null;
        // 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;
    getWrapperChildren(wrapper) {
        return Array.from(wrapper.items.children)
            .map(item => this.getItemId(item));
        }
        return null;
    getItemWrapper(item) {
        if (!item) return null;
        let wrapper = item.closest(this.selectors.wrapper.wrapper);
        if (!wrapper)return null;
        return this.getWrapper(wrapper);
    }
    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) {
            e.preventDefault();
            if (Object.keys(this.ui.wrappers).length > 1 && this.lastClicked) {
                this.clearWrapperSelection(this.lastClicked);
            } else {
            this.clearSelection();
            if (window.jvbA11y) {
                window.jvbA11y.announce('Selection cleared');
            }
        }
    }
    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;
            }
        // Update label text
        if (wrapper.selectAll.span) {
            wrapper.selectAll.span.textContent = (trigger.checked && ids.length > 0) ? 'Clear Selection' : 'Select All';
        }
        let label = trigger.nextElementSibling || trigger.previousElementSibling;
        if (label && label.tagName === 'LABEL') {
            label.textContent = (trigger.checked && items.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) {
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;
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 () => {
    async runPollCycle() {
        if (!this.isPolling) return;
            try {
                this.store.clearCache();
                await this.store.fetch();
                if (!this.maybeStartPolling()) {
                    this.stopPolling();
                    this.updatePanel('synced');
                return;
                }
            } catch (error) {
                console.error('Polling error:', error);
            }
        },
            5000);
        this.startCountdown();
        // 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);
        if (sortedOps.length === 0) {
        window.removeChildren(this.ui.items.container);
        if (operations.length === 0) {
            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;
            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);
                }
                item.ui.completed.label.textContent = text;
                item.ui.completed.time.setAttribute('datetime', time);
                item.ui.completed.time.textContent = window.formatTimeAgo(time);
            }
            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);
    }
    /****************************************************************************
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);
        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`
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',
@@ -297,19 +287,32 @@
        }
    }
        handleGroupMetaChange(input) {
            const element = input.closest(this.selectors.group.fields);
            if (!element) return;
        // 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;
            // Initialize fields object if it doesn't exist
            if (!group.fields) {
                group.fields = {};
            }
            group.fields[name] = inputValue;
                await this.setGroup(groupId, group);
            }, 300);
        }
@@ -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',
        this.restoreSelection = new window.jvbHandleSelection(notification,
            {
                wrapper: {
                    wrapper: '.wrap'
                },
                selectAll: {
            bulkControls: '.selection-actions',
            selectAll: '#select-all-restore',
                    checkbox: '#select-all-restore',
            count: '.selection-count'
                }
        });
        this.restoreModal.handleOpen();
    }
@@ -1012,7 +1031,6 @@
        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);
@@ -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)
            )
        );
        // Destroy the Sortable for this group
        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);
            // Existing sortable cleanup
        const sortable = this.sortables.get(sortableKey);
        if (sortable?.destroy) {
            sortable.destroy();
        }
        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,
            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,
                    bulkControls: this.selectors.fields.actions
                },
                item: {
                    item: this.selectors.items.item,
                checkbox: this.selectors.items.checkbox,
                selectAll: this.selectors.fields.selectAll,
                wrapper: `${this.selectors.fields.preview}, ${this.selectors.group.item}`,
                    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...');
            return;
        }
        //Get current order from DOM
        let items = Array.from(target.querySelectorAll(this.selectors.items.item+':not(.ghost)'))
        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');
    }
    /*******************************************************************************
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}))}}))}))})();
(()=>{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}))}}))}))})();
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)}))}))})();
(()=>{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)}))}))})();
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()}};
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()}};
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||"")}}};
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||"")}}};
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)}))}))})();
(()=>{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)}))}))})();
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)}))}))})();
(()=>{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)}))}))})();
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)}))}))})();
(()=>{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)}))}))})();
inc/helpers/renderFields.php
@@ -558,7 +558,6 @@
    $dataID = ($ID) ? ['image-id' => $ID] : false;
    $addID = ($ID) ? '-'.$ID : '';
    $defaultFields = [
        'image-title'.$addID => [
            'type'  => 'text',
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();
inc/meta/MetaForm.php
@@ -1127,10 +1127,11 @@
                    <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
        <?php
        $fields = array_key_exists('fields', $config) ? $config['fields'] : [];
        $fields = array_merge([
            'upload_data'   => [
                // 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',
@@ -1156,8 +1157,8 @@
                        'data'  => $dataID
                    ]
                ]
            ]
        ], $fields);
                    ];
                }
        $meta = new MetaManager($id);
        foreach ($fields as $field => $config) {
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
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
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
        ];
    }
@@ -353,14 +353,12 @@
        } 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
            ),
            [
@@ -397,6 +395,7 @@
                '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;
                }
            }
@@ -1089,12 +1086,8 @@
        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,
@@ -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,18 +1175,34 @@
            $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);
            if (empty($dependencies)) {
                return [
                    'success'   => false,
                    'result'    => 'No dependencies found'
                ];
            }
            // Collect uploads from all dependency operations
            $uploads = [];
            foreach($dependencies as $dependency) {
                $op = $queue->getOperation($dependency);
                // Use getOperationValue like ContentRoutes does
                $res = $queue->getOperationValue($dependency, 'result');
                $res = json_decode($op->result, true);
                if ($op->status === 'completed') {
                    $uploads = array_merge($uploads, $res);
                }
                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 [
@@ -1206,19 +1212,18 @@
            }
            // 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'],
            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 = [];
@@ -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);
                    }
inc/ui/CRUDSkeleton.php
@@ -604,7 +604,8 @@
    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?>>
        <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();
@@ -615,6 +616,7 @@
            <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>