From bad59c66549eb601fed963ed013f9b79305ca003 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Wed, 07 Jan 2026 20:09:14 +0000
Subject: [PATCH] =Feedblock integrated with refactored taxonomy selector
---
assets/js/concise/TaxonomySelector.js | 94 +
assets/css/forms.min.css | 2
assets/js/min/creator.min.js | 2
build/feed/view.asset.php | 2
assets/js/concise/DataStore.js | 59 +
src/feed/style.scss | 107 ++
inc/helpers/ui.php | 31
inc/forms/TaxonomySelector.php | 21
assets/js/concise/TaxonomyCreator.js | 3
build/feed/style-index-rtl.css | 2
inc/rest/routes/TermRoutes.php | 6
assets/js/min/dataStore.min.js | 2
build/feed/view.js | 2
assets/js/min/selector.min.js | 2
src/feed/viewOld.js | 1277 +++++++++++++++--------------
build/feed/style-index.css | 2
inc/blocks/FeedBlock.php | 6
src/feed/view.js | 949 ++++++++++------------
18 files changed, 1,345 insertions(+), 1,224 deletions(-)
diff --git a/assets/css/forms.min.css b/assets/css/forms.min.css
index 8861cf9..a0dac7b 100644
--- a/assets/css/forms.min.css
+++ b/assets/css/forms.min.css
@@ -1 +1 @@
-input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]),textarea{font-family:var(--body);font-size:var(--txt-medium);color:var(--contrast);padding:var(--p-y) var(--p-x);border-radius:var(--radius);background-color:var(--base);outline:0;border:1px solid var(--base-100);border-bottom:2px solid var(--contrast-200);width:100%;max-width:100%;margin:0 4px}input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]):focus,textarea:focus{outline:var(--action-50);background-color:var(--base-100);color:var(--contrast)}input::placeholder,textarea::placeholder{font-family:var(--body);color:var(--base-200)}@media (min-width:768px){:root{--p-y:1rem}}select{background:var(--base);border:2px solid var(--base-100);border-radius:var(--radius);color:var(--contrast);cursor:pointer;font-family:var(--body);font-size:var(--txt-small);padding:.5rem 1rem;width:100%}select:disabled{background-color:var(--base-50);border-color:var(--base-100);color:var(--base-200);cursor:not-allowed}select option{background:var(--base);color:var(--contrast);padding:.5rem}select option:active,select option:checked,select option:focus,select option:hover{background:var(--action-0);color:var(--base);box-shadow:0 0 0 100px var(--action-0) inset}select option:checked{background:var(--action-0) linear-gradient(0deg,var(--action-0) 0,var(--action-0) 100%);color:var(--base)}select:hover{border-color:var(--action-0)}select:focus{border-color:var(--action-0)}input[type=search]:focus+.clear-search{opacity:1;cursor:pointer}.search-container .clear-search{opacity:0;cursor:default}.search-container .icon.search{padding:4px 8px;color:var(--contrast-200);--w:3rem}input[type=search]::-moz-search-clear-button,input[type=search]::-ms-clear,input[type=search]::-ms-reveal,input[type=search]::search-cancel-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:none;visibility:hidden}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration,input[type=search]::-webkit-search-results-button,input[type=search]::-webkit-search-results-decoration{-webkit-appearance:none}input[type=url]{background:var(--linkIcon);background-position:.5em;background-size:1em;background-repeat:no-repeat;padding-left:2em}.integration .label,label{text-transform:uppercase;font-weight:700;margin-bottom:.5rem;display:block}.field{margin:2rem 0;position:relative}.field:has(.has-tooltip) label{margin-left:2rem}legend{padding:0 1rem}.date-wrapper{position:relative;display:inline-block}input[type=date]{padding:8px 36px 8px 8px;border-radius:4px}input[type=date]::-webkit-calendar-picker-indicator{opacity:0;width:100%;height:100%;position:absolute;top:0;left:0;cursor:pointer}input[type=date]+.icon{--w:20px;position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none}input:is([type=time],[type=datetime-local],[type=date]){padding:.5rem;border:1px solid var(--contrast-200);border-radius:4px;font-size:14px;min-width:180px;background:var(--base);color:var(--contrast);cursor:pointer}.date-wrapper input[type=date]:focus,.datetime-wrapper input[type=datetime-local]:focus,.field-input-wrapper input:is([type=time],[type=datetime-local],[type=date]):focus,.time-wrapper input[type=time]:focus{border-color:var(--action-0);box-shadow:0 0 0 2px rgba(var(--action-rgb),.1)}.date-wrapper .icon,.datetime-wrapper .icon,.field-input-wrapper .icon,.time-wrapper .icon{width:18px;height:18px;background-color:var(--contrast);opacity:.7}.selected-items{width:100%;padding:.5rem;border-radius:var(--radius);background-color:var(--base);--justify:flex-start;--gap:.5rem;margin-bottom:.5rem}.selected-items.selected-items:empty{padding:0;background-color:transparent;margin:0}.selected-item{padding:.25rem .5rem;margin:.125em;background:var(--base-100);border-radius:.25rem;font-size:var(--txt-medium);border:1px solid var(--base-200);position:relative}.remove-term.remove-term{min-height:0;height:var(--chip);width:var(--chip);padding:0;--w:.75em}.clear-filters{margin-left:auto;border:1px solid var(--base-200)}[type=checkbox],[type=radio],input.ch{position:absolute;opacity:0;left:-200vw}[type=checkbox]+label,[type=radio]+label,input.ch+label{position:relative;cursor:pointer}[type=checkbox]+label:hover,[type=radio]+label:hover{color:var(--action-0)}[type=checkbox]+label::after,[type=checkbox]+label::before,[type=radio]+label::after,[type=radio]+label::before,input.ch+label::after,input.ch+label::before{content:'';position:absolute;top:50%}[type=checkbox]+label::after,[type=radio]+label::after,input.ch+label::after{left:5px;transform:translateY(-70%) rotate(45deg);width:5px;height:10px;border:solid var(--light-0);border-width:0 2px 2px 0;display:none}[type=checkbox]+label::before,[type=radio]+label::before,input.ch+label::before{left:0;transform:translateY(-50%);width:1rem;height:1rem;border:2px solid var(--contrast-200);background-color:var(--base);border-radius:var(--radius)}[type=checkbox]:hover+label::before,[type=radio]:hover+label::before,input.ch:hover+label::before{border-color:var(--action-200)}[type=checkbox]:checked+label::before,[type=radio]:checked+label::before,input.ch:checked+label::before{background-color:var(--action-0);border-color:var(--action-100)}[type=radio]:checked+label::before{border-radius:50%}[type=checkbox]:checked+label::after,input.ch:checked+label::after{display:block;left:5px;top:50%;transform:translateY(-70%) rotate(45deg);width:.35rem;height:.66rem;border:solid var(--light-0);border-width:0 2px 2px 0}[type=checkbox]:disabled+label,[type=radio]:disabled+label,input.ch:disabled+label{cursor:not-allowed;background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label:hover,[type=radio]:disabled+label:hover,input.ch:disabled+label:hover{background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label::before,[type=radio]:disabled+label::before,input.ch:disabled+label::before{border-color:var(--base-200)}[type=checkbox]:not(.btn)+label,[type=radio]:not(.btn)+label,input.ch+label{flex:1;padding-left:2rem;transform-origin:top center;will-change:transform}.btn+label::after,.btn+label::before{display:none}.btn+label{--w:1.2em;border:1px solid var(--base-200);border-radius:var(--radius);min-width:2rem;min-height:2rem;margin:0;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;gap:.5rem;color:var(--contrast-200);opacity:.8}.radio-options.status label{padding:0 .5rem}.btn:checked+label{border-color:var(--contrast);color:var(--contrast);opacity:1}.btn+label:hover{color:var(--action-50);border-color:var(--action-50)}.btn[hidden]+label,input[hidden]+label{display:none!important}.checkbox-options{--gap:.5rem 2rem}.checkbox-options label{flex:unset!important}.radio-options{--gap:.125rem .5rem}.radio-options input:not(.ch)+label::before{display:none!important}.radio-options input:not(.ch)+label{flex:unset!important;padding:.25rem!important;border-radius:4px;border:1px solid var(--base-100);color:var(--contrast-200);font-weight:400;text-align:center}.radio-options input:not(.ch)+label:hover,.radio-options input:not(.ch):checked+label{border-color:var(--action-0);color:var(--action-0)}.quantity{margin:0;display:inline-flex;width:fit-content;align-items:center;justify-content:center;border:1px solid transparent;border-radius:4px;position:relative}.quantity:focus-within{border-color:var(--action-0)}.quantity label{margin:0;font-size:var(--txt-small)}.quantity button{background:var(--base);padding:0;width:var(--chip_);height:var(--chip_);min-height:0;z-index:0;position:relative;border:1px solid var(--base-200);color:var(--contrast-200)}.quantity button:hover:not(:disabled){color:var(--action-0);border-color:var(--action-0);background-color:var(--base)}.quantity button:active:not(:disabled){background-color:var(--action-0);color:var(--light-0);transform:scale(.95)}.quantity button:disabled{opacity:.5;cursor:not-allowed}.quantity input[type=number]{z-index:1;border:1px solid var(--base-200);background:var(--base);text-align:center;font-size:1.1rem;width:60px;height:48px;margin:0;padding:0!important;appearance:textfield}.quantity input[type=number]::-webkit-inner-spin-button,.quantity input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.quantity input[type=number]:focus{background-color:var(--base-50)}.quantity button.increase{left:-2px;border-radius:0 4px 4px 0}.quantity button.decrease{right:-2px;border-radius:4px 0 0 4px}.items-container{margin:0;padding:0;width:100%}details.create-term{margin-top:auto;width:100%}#jvb-selector .search-wrapper{margin-top:auto;width:100%}.search-wrapper:not(.open) .search{margin-left:auto}.search-wrapper+details.create-term{margin-top:0}.create-term[open] summary,details.create-term .field{margin-bottom:1rem}details.create-term .field{max-width:100%}#jvb-selector>.wrap{--wrap:nowrap;--justify:flex-start}#jvb-selector .items-wrap{width:100%;padding:1rem 0;border-top:1px solid var(--base-200)}#jvb-selector .items-container{display:grid;grid-template-columns:repeat(2,1fr)}#jvb-selector li{list-style:none}#jvb-selector li label{padding-left:0!important}.tab-content[hidden]{display:block!important;transform:scaleY(0);height:0;overflow:hidden}.tab-content[hidden]:focus-within{transform:scaleY(1);height:auto}nav.tabs h2{margin:0!important;line-height:1;font-size:var(--txt-medium);display:flex;color:var(--contrast);white-space:nowrap;gap:1rem}nav.tabs .active h2{color:var(--action-contrast)}nav.tabs button{padding:.75rem 1.5rem;border-radius:0;position:relative;border:2px solid var(--action-0)}nav.tabs>button:first-of-type{border-top-left-radius:var(--radius)}nav.tabs>button:last-of-type{border-top-right-radius:var(--radius)}.tabs>button:focus,.tabs>button:hover{background-color:var(--base-200)}.tabs>button::after{content:'';position:absolute;bottom:-2px;left:0;width:0;height:3px;background-color:var(--action-50);transition:width .3s}.tabs>button.active::after,.tabs>button:hover::after{width:100%}.tabs>button.active::after{background-color:var(--action-200)}.tabs>button.active{background-color:var(--action-0);color:var(--action-contrast)}.tabs>button.active:focus,.tabs>button.active:hover{background-color:var(--action-100)}.tab-content h2{display:none}details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--content)}}.empty-group,.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--op-1));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--txt-large)}.dragover,.empty-group,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--op-2));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.empty-group p,.file-upload-text{color:var(--contrast);margin:0}.empty-group p strong,.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.groups{grid-template-columns:repeat(1,1fr)}.item-grid.group{margin-bottom:0}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0;padding-left:var(--chipchip)}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--op-1))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--op-4));opacity:1}.item-grid.preview summary span{display:none}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .icon-star-fi,[type=radio].featured:checked+label .icon-star{display:none}[type=radio].featured+label .icon-star,[type=radio].featured:checked+label .icon-star-fi{display:inline-block}.restore.restore.item,.upload.upload.item{border-radius:var(--radius);aspect-ratio:unset;overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore-item [for=select-item],.upload.item [for=select-item]{aspect-ratio:1}.upload.item:has(details[open]){grid-column:1/-1}.restore.item img,.upload.item img{transition:transform var(--trans-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--trans-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--radius);background-color:rgba(var(--action-rgb),var(--op-1))}.upload-group .selected .field{margin:0}.upload-group .selection-actions button{aspect-ratio:unset}.submit-uploads{position:fixed;bottom:0;right:var(--btn_);z-index:var(--z-6);height:var(--btn);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);border-radius:var(--radius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{order:-1;grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--radius);margin:10px 0;cursor:pointer;transition:all var(--trans-base);text-align:center;background-color:rgba(var(--action-rgb),var(--op-1))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-7);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--op-3))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--z-9);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--radius-outer);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--op-3));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--op-4)));transform:scale(1.04)}}.selection-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--btn);bottom:var(--btn);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--op-6));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{--wrap:nowrap;height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--op-6));z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--op-6))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--btn);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;--wrap:nowrap;max-height:calc(100vh - var(--btnbtn));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.item-grid.restore{grid-template-columns:repeat(1,1fr)}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--radius);border-top-right-radius:var(--radius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--radius);border-bottom-right-radius:var(--radius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:100%;height:100%}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px rgba(var(--base-rgb),var(--op-6));color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/form{--step-size:2.5rem}.form-progress{padding:0 1rem}.form-progress .progress{background:var(--base-100);border-radius:var(--radius);padding:1rem}.form-progress .bar{height:6px;background:var(--base-200);border-radius:3px;overflow:hidden;margin-bottom:.5rem}.form-progress .fill{height:100%;background:linear-gradient(90deg,var(--action-0),var(--action-200));width:0%;transition:width .4s ease;border-radius:3px}.form-progress .step-text{font-size:var(--txt-small);font-weight:600;color:var(--contrast-200)}form nav.tabs{position:relative;top:0;left:0;right:0;padding:1rem 0;gap:0;z-index:0}form nav.tabs button{position:relative;background:0 0;border:none;padding:.5rem 1rem .5rem 3rem;z-index:1}form nav.tabs .step-number{width:2.5rem;height:100%;border-radius:50% 0 0 50%;position:absolute;left:0;top:0;background:var(--base-200);color:var(--contrast-50);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--txt-small);border:3px solid var(--base)}form nav.tabs button.pending .step-number{background:var(--base-100);color:var(--contrast-200)}form nav.tabs button.active .step-number,form nav.tabs button.current .step-number{background:var(--action-0);color:var(--action-contrast);border-color:var(--action-200)}form nav.tabs button.completed .step-number{background:var(--successBack);color:var(--successBack);border-color:var(--successText)}form nav.tabs button.completed .step-number::before{content:'✓';font-size:1.2rem;color:var(--successText);position:absolute}form nav.tabs button.completed h2{color:var(--contrast-200)}.step-navigation{margin-top:2rem;padding-top:2rem;border-top:1px solid var(--base-200);gap:1rem}.step-navigation .prev-step{background:var(--base-100)}.step-navigation .next-step,.step-navigation button[type=submit]{margin-left:auto}.field input.error,.field select.error,.field textarea.error{border-color:var(--errorBack)}.error-message{color:var(--errorText);font-size:var(--txt-small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--txt-small)}form nav.tabs button h2{font-size:var(--txt-small)}form{--step-size:2rem}}.field-input-wrapper{position:relative;display:flex;align-items:center;gap:.5rem}.field-input-wrapper input,.field-input-wrapper select,.field-input-wrapper textarea{flex:1}.validation-icon{display:flex;align-items:center;justify-content:center;font-size:1.25rem;animation:scaleIn .3s ease;--w:1.25rem}.validation-icon.error{color:var(--error)}.validation-icon.success{color:var(--success)}@keyframes scaleIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.validation-message{color:var(--error-0);font-size:var(--txt-small);margin-top:.25rem;display:block;animation:slideDown .2s ease}@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--error);background-color:var(--errorBack)}.field.has-error input:focus,.field.has-error select:focus,.field.has-error textarea:focus{outline-color:var(--error);box-shadow:0 0 0 3px rgba(var(--error-rgb),.2)}.field.has-success input,.field.has-success select,.field.has-success textarea{border-color:var(--success)}.field label .required{color:var(--error);margin-left:.25rem}.form-summary{padding:2rem;border-radius:8px;margin-top:2rem;border:2px dashed var(--contrast-200)}.form-summary .message{margin-bottom:2rem}.form-summary .result+.result{position:relative;margin-top:1.5rem;padding-top:1.5rem}.form-summary .result+.result::before{position:absolute;top:0;left:16.5%;content:'';width:67%;height:1px;border-bottom:1px solid var(--base-200)}.form-summary h2{margin:1rem 0}.form-summary h4{background-color:var(--base-100);padding:.5rem 2rem;position:relative;left:-2rem;color:var(--contrast-200);font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem}.form-summary p{color:var(--text);margin:0}.group-summary,.repeater-summary{background:var(--base-100);padding:1rem;border-radius:4px;margin-top:.5rem}.repeater-row{margin-bottom:1rem}.repeater-row:last-child{margin-bottom:0}.ql-toolbar button{min-height:0;padding:.5rem}.success-message{color:var(--success);background-color:var(--successBack);border:1px solid var(--success);padding:.75rem 1rem;border-radius:var(--radius);margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.success-message .success-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.success-box{background-color:var(--successBack);border:2px solid var(--success);padding:1.5rem;border-radius:var(--radius-outer);margin-bottom:1rem;text-align:center}.success-box h3{color:var(--success);margin-bottom:.5rem}.success-box p{margin:.5rem 0}.form-success{opacity:.9}.form-success .field:not(.form-success-message):not(.success-box){display:none}.form-success button[type=submit]{opacity:.6;pointer-events:none}.field-error input,.field-error select,.field-error textarea{border-color:var(--error)}.error-message{color:var(--error);font-size:var(--txt-small);margin-top:.25rem;display:block}.form-error{background-color:var(--errorBack);border:1px solid var(--error);padding:.75rem;border-radius:var(--radius);margin-bottom:1rem}.has-success input,.has-success select,.has-success textarea{border-color:var(--success)}.form-error{display:flex;align-items:center;gap:.5rem}.form-error .error-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.autocomplete-dropdown{width:100%;background-color:var(--base-100);padding:.5rem;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.invite details{margin-bottom:1.5rem}.field.tag-list .tag-input-row{display:flex;gap:.5rem;align-items:flex-start;margin-bottom:1rem;flex-wrap:wrap}.field.tag-list .tag-input-row .field{flex:1;min-width:150px;margin:0}.field.tag-list .tag-input-row .add-tag-item{flex-shrink:0;white-space:nowrap;margin-top:calc(var(--txt-medium) + 1rem)}.field.tag-list .tag-items{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem;min-height:2rem}.field.tag-list .tag-item{background:var(--base-200);padding:.4rem .75rem;border-radius:4px;display:inline-flex;align-items:center;gap:.5rem;font-size:.9rem;line-height:1.2}.field.tag-list .tag-item:hover{background:var(--base-100)}.field.tag-list .tag-label{max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field.tag-list .remove-tag{min-height:0;padding:.25rem;color:var(--contrast);transition:transform .2s;box-shadow:none}.field.tag-list .remove-tag:hover{transform:scale(1.2)}@media (max-width:768px){.field.tag-list .tag-input-row{flex-direction:column;align-items:stretch}.field.tag-list .tag-input-row .field{min-width:100%}}
\ No newline at end of file
+input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]),textarea{font-family:var(--body);font-size:var(--txt-medium);color:var(--contrast);padding:var(--p-y) var(--p-x);border-radius:var(--radius);background-color:var(--base);outline:0;border:1px solid var(--base-100);border-bottom:2px solid var(--contrast-200);width:100%;max-width:100%;margin:0 4px}input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]):focus,textarea:focus{outline:var(--action-50);background-color:var(--base-100);color:var(--contrast)}input::placeholder,textarea::placeholder{font-family:var(--body);color:var(--base-200)}@media (min-width:768px){:root{--p-y:1rem}}select{background:var(--base);border:2px solid var(--base-100);border-radius:var(--radius);color:var(--contrast);cursor:pointer;font-family:var(--body);font-size:var(--txt-small);padding:.5rem 1rem;width:100%}select:disabled{background-color:var(--base-50);border-color:var(--base-100);color:var(--base-200);cursor:not-allowed}select option{background:var(--base);color:var(--contrast);padding:.5rem}select option:active,select option:checked,select option:focus,select option:hover{background:var(--action-0);color:var(--base);box-shadow:0 0 0 100px var(--action-0) inset}select option:checked{background:var(--action-0) linear-gradient(0deg,var(--action-0) 0,var(--action-0) 100%);color:var(--base)}select:hover{border-color:var(--action-0)}select:focus{border-color:var(--action-0)}input[type=search]:focus+.clear-search{opacity:1;cursor:pointer}.search-container .clear-search{opacity:0;cursor:default}.search-container .icon.search{padding:4px 8px;color:var(--contrast-200);--w:3rem}input[type=search]::-moz-search-clear-button,input[type=search]::-ms-clear,input[type=search]::-ms-reveal,input[type=search]::search-cancel-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:none;visibility:hidden}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration,input[type=search]::-webkit-search-results-button,input[type=search]::-webkit-search-results-decoration{-webkit-appearance:none}input[type=url]{background:var(--linkIcon);background-position:.5em;background-size:1em;background-repeat:no-repeat;padding-left:2em}.integration .label,label{text-transform:uppercase;font-weight:700;margin-bottom:.5rem;display:block}.field{margin:2rem 0;position:relative}.field:has(.has-tooltip) label{margin-left:2rem}legend{padding:0 1rem}.date-wrapper{position:relative;display:inline-block}input[type=date]{padding:8px 36px 8px 8px;border-radius:4px}input[type=date]::-webkit-calendar-picker-indicator{opacity:0;width:100%;height:100%;position:absolute;top:0;left:0;cursor:pointer}input[type=date]+.icon{--w:20px;position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none}input:is([type=time],[type=datetime-local],[type=date]){padding:.5rem;border:1px solid var(--contrast-200);border-radius:4px;font-size:14px;min-width:180px;background:var(--base);color:var(--contrast);cursor:pointer}.date-wrapper input[type=date]:focus,.datetime-wrapper input[type=datetime-local]:focus,.field-input-wrapper input:is([type=time],[type=datetime-local],[type=date]):focus,.time-wrapper input[type=time]:focus{border-color:var(--action-0);box-shadow:0 0 0 2px rgba(var(--action-rgb),.1)}.date-wrapper .icon,.datetime-wrapper .icon,.field-input-wrapper .icon,.time-wrapper .icon{width:18px;height:18px;background-color:var(--contrast);opacity:.7}[type=checkbox],[type=radio],input.ch{position:absolute;opacity:0;left:-200vw}[type=checkbox]+label,[type=radio]+label,input.ch+label{position:relative;cursor:pointer}[type=checkbox]+label:hover,[type=radio]+label:hover{color:var(--action-0)}[type=checkbox]+label::after,[type=checkbox]+label::before,[type=radio]+label::after,[type=radio]+label::before,input.ch+label::after,input.ch+label::before{content:'';position:absolute;top:50%}[type=checkbox]+label::after,[type=radio]+label::after,input.ch+label::after{left:5px;transform:translateY(-70%) rotate(45deg);width:5px;height:10px;border:solid var(--light-0);border-width:0 2px 2px 0;display:none}[type=checkbox]+label::before,[type=radio]+label::before,input.ch+label::before{left:0;transform:translateY(-50%);width:1rem;height:1rem;border:2px solid var(--contrast-200);background-color:var(--base);border-radius:var(--radius)}[type=checkbox]:hover+label::before,[type=radio]:hover+label::before,input.ch:hover+label::before{border-color:var(--action-200)}[type=checkbox]:checked+label::before,[type=radio]:checked+label::before,input.ch:checked+label::before{background-color:var(--action-0);border-color:var(--action-100)}[type=radio]:checked+label::before{border-radius:50%}[type=checkbox]:checked+label::after,input.ch:checked+label::after{display:block;left:5px;top:50%;transform:translateY(-70%) rotate(45deg);width:.35rem;height:.66rem;border:solid var(--light-0);border-width:0 2px 2px 0}[type=checkbox]:disabled+label,[type=radio]:disabled+label,input.ch:disabled+label{cursor:not-allowed;background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label:hover,[type=radio]:disabled+label:hover,input.ch:disabled+label:hover{background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label::before,[type=radio]:disabled+label::before,input.ch:disabled+label::before{border-color:var(--base-200)}[type=checkbox]:not(.btn)+label,[type=radio]:not(.btn)+label,input.ch+label{flex:1;padding-left:2rem;transform-origin:top center;will-change:transform}.btn+label::after,.btn+label::before{display:none}.btn+label{--w:1.2em;border:1px solid var(--base-200);border-radius:var(--radius);min-width:2rem;min-height:2rem;margin:0;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;gap:.5rem;color:var(--contrast-200);opacity:.8}.radio-options.status label{padding:0 .5rem}.btn:checked+label{border-color:var(--contrast);color:var(--contrast);opacity:1}.btn+label:hover{color:var(--action-50);border-color:var(--action-50)}.btn[hidden]+label,input[hidden]+label{display:none!important}.checkbox-options{--gap:.5rem 2rem}.checkbox-options label{flex:unset!important}.radio-options{--gap:.125rem .5rem}.radio-options input:not(.ch)+label::before{display:none!important}.radio-options input:not(.ch)+label{flex:unset!important;padding:.25rem!important;border-radius:4px;border:1px solid var(--base-100);color:var(--contrast-200);font-weight:400;text-align:center}.radio-options input:not(.ch)+label:hover,.radio-options input:not(.ch):checked+label{border-color:var(--action-0);color:var(--action-0)}.quantity{margin:0;display:inline-flex;width:fit-content;align-items:center;justify-content:center;border:1px solid transparent;border-radius:4px;position:relative}.quantity:focus-within{border-color:var(--action-0)}.quantity label{margin:0;font-size:var(--txt-small)}.quantity button{background:var(--base);padding:0;width:var(--chip_);height:var(--chip_);min-height:0;z-index:0;position:relative;border:1px solid var(--base-200);color:var(--contrast-200)}.quantity button:hover:not(:disabled){color:var(--action-0);border-color:var(--action-0);background-color:var(--base)}.quantity button:active:not(:disabled){background-color:var(--action-0);color:var(--light-0);transform:scale(.95)}.quantity button:disabled{opacity:.5;cursor:not-allowed}.quantity input[type=number]{z-index:1;border:1px solid var(--base-200);background:var(--base);text-align:center;font-size:1.1rem;width:60px;height:48px;margin:0;padding:0!important;appearance:textfield}.quantity input[type=number]::-webkit-inner-spin-button,.quantity input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.quantity input[type=number]:focus{background-color:var(--base-50)}.quantity button.increase{left:-2px;border-radius:0 4px 4px 0}.quantity button.decrease{right:-2px;border-radius:4px 0 0 4px}.tab-content[hidden]{display:block!important;transform:scaleY(0);height:0;overflow:hidden}.tab-content[hidden]:focus-within{transform:scaleY(1);height:auto}nav.tabs h2{margin:0!important;line-height:1;font-size:var(--txt-medium);display:flex;color:var(--contrast);white-space:nowrap;gap:1rem}nav.tabs .active h2{color:var(--action-contrast)}nav.tabs button{padding:.75rem 1.5rem;border-radius:0;position:relative;border:2px solid var(--action-0)}nav.tabs>button:first-of-type{border-top-left-radius:var(--radius)}nav.tabs>button:last-of-type{border-top-right-radius:var(--radius)}.tabs>button:focus,.tabs>button:hover{background-color:var(--base-200)}.tabs>button::after{content:'';position:absolute;bottom:-2px;left:0;width:0;height:3px;background-color:var(--action-50);transition:width .3s}.tabs>button.active::after,.tabs>button:hover::after{width:100%}.tabs>button.active::after{background-color:var(--action-200)}.tabs>button.active{background-color:var(--action-0);color:var(--action-contrast)}.tabs>button.active:focus,.tabs>button.active:hover{background-color:var(--action-100)}.tab-content h2{display:none}details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--content)}}.empty-group,.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--op-1));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--txt-large)}.dragover,.empty-group,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--op-2));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.empty-group p,.file-upload-text{color:var(--contrast);margin:0}.empty-group p strong,.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.groups{grid-template-columns:repeat(1,1fr)}.item-grid.group{margin-bottom:0}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0;padding-left:var(--chipchip)}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--op-1))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--op-4));opacity:1}.item-grid.preview summary span{display:none}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .icon-star-fi,[type=radio].featured:checked+label .icon-star{display:none}[type=radio].featured+label .icon-star,[type=radio].featured:checked+label .icon-star-fi{display:inline-block}.restore.restore.item,.upload.upload.item{border-radius:var(--radius);aspect-ratio:unset;overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore-item [for=select-item],.upload.item [for=select-item]{aspect-ratio:1}.upload.item:has(details[open]){grid-column:1/-1}.restore.item img,.upload.item img{transition:transform var(--trans-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--trans-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--radius);background-color:rgba(var(--action-rgb),var(--op-1))}.upload-group .selected .field{margin:0}.upload-group .selection-actions button{aspect-ratio:unset}.submit-uploads{position:fixed;bottom:0;right:var(--btn_);z-index:var(--z-6);height:var(--btn);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);border-radius:var(--radius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{order:-1;grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--radius);margin:10px 0;cursor:pointer;transition:all var(--trans-base);text-align:center;background-color:rgba(var(--action-rgb),var(--op-1))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-7);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--op-3))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--z-9);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--radius-outer);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--op-3));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--op-4)));transform:scale(1.04)}}.selection-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--btn);bottom:var(--btn);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--op-6));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{--wrap:nowrap;height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--op-6));z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--op-6))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--btn);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;--wrap:nowrap;max-height:calc(100vh - var(--btnbtn));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.item-grid.restore{grid-template-columns:repeat(1,1fr)}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--radius);border-top-right-radius:var(--radius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--radius);border-bottom-right-radius:var(--radius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:100%;height:100%}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px rgba(var(--base-rgb),var(--op-6));color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/form{--step-size:2.5rem}.form-progress{padding:0 1rem}.form-progress .progress{background:var(--base-100);border-radius:var(--radius);padding:1rem}.form-progress .bar{height:6px;background:var(--base-200);border-radius:3px;overflow:hidden;margin-bottom:.5rem}.form-progress .fill{height:100%;background:linear-gradient(90deg,var(--action-0),var(--action-200));width:0%;transition:width .4s ease;border-radius:3px}.form-progress .step-text{font-size:var(--txt-small);font-weight:600;color:var(--contrast-200)}form nav.tabs{position:relative;top:0;left:0;right:0;padding:1rem 0;gap:0;z-index:0}form nav.tabs button{position:relative;background:0 0;border:none;padding:.5rem 1rem .5rem 3rem;z-index:1}form nav.tabs .step-number{width:2.5rem;height:100%;border-radius:50% 0 0 50%;position:absolute;left:0;top:0;background:var(--base-200);color:var(--contrast-50);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--txt-small);border:3px solid var(--base)}form nav.tabs button.pending .step-number{background:var(--base-100);color:var(--contrast-200)}form nav.tabs button.active .step-number,form nav.tabs button.current .step-number{background:var(--action-0);color:var(--action-contrast);border-color:var(--action-200)}form nav.tabs button.completed .step-number{background:var(--successBack);color:var(--successBack);border-color:var(--successText)}form nav.tabs button.completed .step-number::before{content:'✓';font-size:1.2rem;color:var(--successText);position:absolute}form nav.tabs button.completed h2{color:var(--contrast-200)}.step-navigation{margin-top:2rem;padding-top:2rem;border-top:1px solid var(--base-200);gap:1rem}.step-navigation .prev-step{background:var(--base-100)}.step-navigation .next-step,.step-navigation button[type=submit]{margin-left:auto}.field input.error,.field select.error,.field textarea.error{border-color:var(--errorBack)}.error-message{color:var(--errorText);font-size:var(--txt-small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--txt-small)}form nav.tabs button h2{font-size:var(--txt-small)}form{--step-size:2rem}}.field-input-wrapper{position:relative;display:flex;align-items:center;gap:.5rem}.field-input-wrapper input,.field-input-wrapper select,.field-input-wrapper textarea{flex:1}.validation-icon{display:flex;align-items:center;justify-content:center;font-size:1.25rem;animation:scaleIn .3s ease;--w:1.25rem}.validation-icon.error{color:var(--error)}.validation-icon.success{color:var(--success)}@keyframes scaleIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.validation-message{color:var(--error-0);font-size:var(--txt-small);margin-top:.25rem;display:block;animation:slideDown .2s ease}@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--error);background-color:var(--errorBack)}.field.has-error input:focus,.field.has-error select:focus,.field.has-error textarea:focus{outline-color:var(--error);box-shadow:0 0 0 3px rgba(var(--error-rgb),.2)}.field.has-success input,.field.has-success select,.field.has-success textarea{border-color:var(--success)}.field label .required{color:var(--error);margin-left:.25rem}.form-summary{padding:2rem;border-radius:8px;margin-top:2rem;border:2px dashed var(--contrast-200)}.form-summary .message{margin-bottom:2rem}.form-summary .result+.result{position:relative;margin-top:1.5rem;padding-top:1.5rem}.form-summary .result+.result::before{position:absolute;top:0;left:16.5%;content:'';width:67%;height:1px;border-bottom:1px solid var(--base-200)}.form-summary h2{margin:1rem 0}.form-summary h4{background-color:var(--base-100);padding:.5rem 2rem;position:relative;left:-2rem;color:var(--contrast-200);font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem}.form-summary p{color:var(--text);margin:0}.group-summary,.repeater-summary{background:var(--base-100);padding:1rem;border-radius:4px;margin-top:.5rem}.repeater-row{margin-bottom:1rem}.repeater-row:last-child{margin-bottom:0}.ql-toolbar button{min-height:0;padding:.5rem}.success-message{color:var(--success);background-color:var(--successBack);border:1px solid var(--success);padding:.75rem 1rem;border-radius:var(--radius);margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.success-message .success-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.success-box{background-color:var(--successBack);border:2px solid var(--success);padding:1.5rem;border-radius:var(--radius-outer);margin-bottom:1rem;text-align:center}.success-box h3{color:var(--success);margin-bottom:.5rem}.success-box p{margin:.5rem 0}.form-success{opacity:.9}.form-success .field:not(.form-success-message):not(.success-box){display:none}.form-success button[type=submit]{opacity:.6;pointer-events:none}.field-error input,.field-error select,.field-error textarea{border-color:var(--error)}.error-message{color:var(--error);font-size:var(--txt-small);margin-top:.25rem;display:block}.form-error{background-color:var(--errorBack);border:1px solid var(--error);padding:.75rem;border-radius:var(--radius);margin-bottom:1rem}.has-success input,.has-success select,.has-success textarea{border-color:var(--success)}.form-error{display:flex;align-items:center;gap:.5rem}.form-error .error-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.invite details{margin-bottom:1.5rem}.field.tag-list .tag-input-row{display:flex;gap:.5rem;align-items:flex-start;margin-bottom:1rem;flex-wrap:wrap}.field.tag-list .tag-input-row .field{flex:1;min-width:150px;margin:0}.field.tag-list .tag-input-row .add-tag-item{flex-shrink:0;white-space:nowrap;margin-top:calc(var(--txt-medium) + 1rem)}.field.tag-list .tag-items{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem;min-height:2rem}.field.tag-list .tag-item{background:var(--base-200);padding:.4rem .75rem;border-radius:4px;display:inline-flex;align-items:center;gap:.5rem;font-size:.9rem;line-height:1.2}.field.tag-list .tag-item:hover{background:var(--base-100)}.field.tag-list .tag-label{max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field.tag-list .remove-tag{min-height:0;padding:.25rem;color:var(--contrast);transition:transform .2s;box-shadow:none}.field.tag-list .remove-tag:hover{transform:scale(1.2)}@media (max-width:768px){.field.tag-list .tag-input-row{flex-direction:column;align-items:stretch}.field.tag-list .tag-input-row .field{min-width:100%}}
\ No newline at end of file
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index fd5d3cb..11f479f 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -670,6 +670,12 @@
queue_stats: data.queue_stats || {}
};
+ for (let [key, value] of Object.entries(store.filters)) {
+ if (typeof value === 'string' && value.includes(',')) {
+ this.createSplitCacheEntries(name, items, key, store.filters, response);
+ }
+ }
+
// Emit events for items with status changes
changes.forEach(changeInfo => {
if (changeInfo.statusChanged) {
@@ -682,6 +688,39 @@
});
}
+ createSplitCacheEntries(name, items, key, filters, response) {
+ const store = this.stores.get(name);
+ const keys = filters[key].split(',').map(v => v.trim());
+
+ keys.forEach(value => {
+ let temp = {};
+ temp[key] = value;
+ const newFilters = {
+ ... filters,
+ [key]: value
+ };
+ const cacheKey = this.generateCacheKey(newFilters);
+ if(store.cache.has(cacheKey)) return;
+ let filteredItems = this.filterByIndex(name,temp).map(item => this.getItemKey(item, store.config.keyPath));
+
+ const entry = {
+ key: cacheKey,
+ items: filteredItems,
+ timestamp: Date.now(),
+ endpoint: store.config.endpoint,
+ filters: newFilters,
+ etag: response.headers.get('Etag'),
+ lastModified: response.headers.get('Last-Modified'),
+ has_more: filteredItems.length === 20,
+ }
+ store.cache.set(cacheKey, entry);
+ if (store.db?.objectStoreNames.contains('cache')) {
+ this.withTransaction(name, 'cache', 'readwrite', (objectStore) =>{
+ objectStore.put(entry);
+ });
+ }
+ })
+ }
/***********************************************************************
* SAVE OPERATIONS
***********************************************************************/
@@ -969,16 +1008,16 @@
if (clearAll) {
store.filters = { ...store.config.filters };
- } else {
- // Apply updates (null/undefined/'' = delete)
- Object.entries(updates).forEach(([key, value]) => {
- if (value === null || value === undefined || value === '') {
- delete store.filters[key];
- } else {
- store.filters[key] = value;
- }
- });
}
+
+ Object.entries(updates).forEach(([key, value]) => {
+ if (value === null || value === undefined || value === '') {
+ delete store.filters[key];
+ } else {
+ store.filters[key] = value;
+ }
+ });
+
const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters);
this.notify(name, 'filters-changed', {
@@ -1083,6 +1122,8 @@
const hasChanges = Object.keys(filters).some(
key => store.filters[key] !== filters[key]
+ ) || Object.keys(store.filters).some(
+ key => !(key in filters) && filters !== store.config.filters
);
if (!hasChanges) return;
diff --git a/assets/js/concise/TaxonomyCreator.js b/assets/js/concise/TaxonomyCreator.js
index 68232ce..11281e3 100644
--- a/assets/js/concise/TaxonomyCreator.js
+++ b/assets/js/concise/TaxonomyCreator.js
@@ -43,9 +43,6 @@
if (this.ui.label.parent) {
this.ui.label.parent.textContent = `Nest it under`;
}
- if (this.ui.button) {
- this.ui.button.textContent = `Add ${field.singular}`;
- }
}
}
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index 0565dee..1d84d93 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -1,3 +1,4 @@
+
class TaxonomySelector {
constructor() {
this.container = document.querySelector('dialog#jvb-selector');
@@ -67,7 +68,7 @@
initElements() {
this.selectors = {
search: {
- input: '[type=search]',
+ input: '[type="search"]',
clear: '.clear-search',
container: '.search-wrapper',
results: '.search-results',
@@ -99,7 +100,7 @@
},
favourites: '.favourite-terms',
field: {
- toggle: 'button.taxonomy-toggle',
+ toggle: 'button.taxonomy-toggle, [data-filter="taxonomy"]',
value: 'input[type="hidden"]',
selected: '.selected-items',
dropdown: {
@@ -147,18 +148,16 @@
}
handleClick(e) {
- const fieldId = (this.container.open) ? this.activeField : this.getFieldId(e.target);
+ const fieldId = this.getFieldId(e.target) || this.activeField;
const field = this.fields.get(fieldId);
if (!fieldId || !field) return;
- const autoComplete = window.targetCheck(e, '[data-autocomplete-select]');
+ const autoComplete = window.targetCheck(e, '.item.autocomplete');
+
if (autoComplete) {
let termId = parseInt(autoComplete.dataset.id);
this.addSelected(termId, fieldId);
- if (field.ui.dropdown.wrapper) {
- field.ui.dropdown.wrapper.hidden = true;
- }
-
+ this.scheduleHideDropdown(fieldId);
if (field.ui.search) {
field.ui.search.value = '';
}
@@ -244,7 +243,7 @@
if (!this.container.contains(e.target)) {
return;
}
- if (e.target.type !== 'checkbox') return;
+ if (!['checkbox', 'button'].includes(e.target.type)) return;
e.preventDefault();
e.stopPropagation();
@@ -262,7 +261,7 @@
if (!fieldId) return;
const field = this.fields.get(fieldId);
if (!field) return;
- if (e.target.type === 'checkbox') return;
+ if (['checkbox', 'button'].includes(e.target.type)) return;
e.preventDefault();
e.stopPropagation();
@@ -334,6 +333,7 @@
const field = this.fields.get(fieldId);
if (!fieldId || ! field) return;
if (!field.hasAutocomplete || this.container.open) return;
+ if (e.relatedTarget && field.ui.dropdown.wrapper?.contains(e.relatedTarget)) return;
this.scheduleHideDropdown(fieldId);
}
@@ -372,7 +372,9 @@
);
this.modal.subscribe((event, data) => {
switch (event) {
-
+ case 'modal-close':
+ this.closeModal()
+ break;
}
});
}
@@ -393,7 +395,7 @@
if (!field) return;
this.setField(fieldId);
- this.ui.modal.title.textContent = `Select ${field.plural}`;
+ this.ui.modal.title.textContent = (field.isFilter) ?`Filter by ${field.singular}` : `Select ${field.plural}`;
if (this.ui.search.container) {
this.ui.search.container.hidden = !field.canSearch;
}
@@ -402,6 +404,7 @@
}
let message = `Opened ${field.singular} selection. Choose from checkboxes, or search to filter results.`;
+ window.removeChildren(this.ui.selected);
window.removeChildren(this.ui.terms.list);
this.modal.handleOpen();
@@ -409,7 +412,6 @@
}
closeModal() {
- this.modal.handleClose();
const field = this.fields.get(this.activeField);
if (!field) return;
this.observer.unobserve(this.ui.terms.sentinel);
@@ -493,7 +495,7 @@
registerField(element, options = {}) {
let input = element.querySelector('input[type="hidden"]');
- if (!input) {
+ if (!input && !Object.hasOwn(element.dataset, 'filter')) {
console.warn('TaxonomySelector: No hidden input found for field', element);
return;
}
@@ -505,7 +507,8 @@
let selectors = this.selectors.field;
- let button = element.querySelector('button.taxonomy-toggle');
+ const isFilter = Object.hasOwn(element.dataset,'filter') && element.dataset.filter === 'taxonomy';
+ let button = (isFilter) ? element : element.querySelector('button.taxonomy-toggle');
if (Object.keys(options).length === 0){
if (!button) return;
@@ -536,6 +539,7 @@
hasAutocomplete: options.autocomplete??false,
canCreate: options.creatable??false,
isRequired: options.required??false,
+ isFilter: isFilter,
toggle: button,
create: {
button: null,
@@ -545,6 +549,10 @@
ui: window.uiFromSelectors(selectors, element),
checked: false,
};
+
+ if (isFilter && !config.ui.toggle) {
+ config.ui.toggle = element;
+ }
if (!config.taxonomy) {
console.error('TaxonomySelector: Field missing taxonomy', element);
return;
@@ -569,17 +577,19 @@
}
setSelectedFromValue(fieldId, input) {
- if (!input) return;
if (!fieldId) return;
let field = this.fields.get(fieldId);
if (!field) return;
+ if (!input && !field.isFilter) return;
let selected = new Set();
- input.value.trim()
- .split(',')
- .map(id => parseInt(id.trim()))
- .filter(id => !isNaN(id))
- .forEach(id => selected.add(id));
+ if (input) {
+ input.value.trim()
+ .split(',')
+ .map(id => parseInt(id.trim()))
+ .filter(id => !isNaN(id))
+ .forEach(id => selected.add(id));
+ }
this.selectedTerms.set(fieldId, selected);
}
@@ -594,7 +604,7 @@
if (field.limit !== 0 && selected.size >= field.limit) return;
selected.add(parseInt(termId));
- if (!this.container.open) {
+ if (!this.container.open && !field.isFilter) {
this.updateFieldValue(fieldId);
}
this.addTermToDisplay(termId, fieldId);
@@ -607,17 +617,17 @@
if (!field || !term) return;
this.selectedTerms.get(fieldId).delete(parseInt(termId));
- const selectedItem = field.ui.selected.querySelector(`[data-id="${termId}"]`);
+ const selectedItem = (field.ui.selected) ? field.ui.selected.querySelector(`[data-id="${termId}"]`) : false;
if (selectedItem) selectedItem.remove();
if (this.container.open) {
- let item = this.ui.selected.querySelector(`[data-id="${termId}"]`);
+ let item = (this.ui.selected) ? this.ui.selected.querySelector(`[data-id="${termId}"]`) : false;
if (item) item.remove();
let checkbox = this.ui.terms.list.querySelector(`[type=checkbox][data-id="${termId}"]`);
if (checkbox) {
checkbox.checked = false;
}
}
- if (!this.container.open) {
+ if (!this.container.open && !field.isFilter) {
this.updateFieldValue(fieldId);
}
@@ -633,7 +643,7 @@
checkLimits(fieldId) {
if (!this.container.open) return;
const field = this.fields.get(fieldId);
- if (!field || field.limit === 0) return;
+ if (!field || !field.isFilter || field.limit === 0) return;
const disabled = this.selectedTerms.get(fieldId).size >= field.limit;
this.setCheckboxes(disabled);
}
@@ -651,7 +661,7 @@
updateFieldUI(fieldId) {
const field = this.fields.get(fieldId);
let selected = this.selectedTerms.get(fieldId)??new Set();
- if (!field || selected.size === 0) return;
+ if (!field || field.isFilter || selected.size === 0) return;
Array.from(selected).forEach(termId => {
this.addTermToDisplay(termId, fieldId);
@@ -665,8 +675,8 @@
.some(term=>term.taxonomy === taxonomy);
fields.forEach(field => {
- field.ui.toggle.disabled = !hasItems && !field.canCreate;
- field.ui.toggle.title = !hasItems
+ field.toggle.disabled = !hasItems && !field.canCreate;
+ field.toggle.title = !hasItems
? `No ${field.singular} available`
: `Select ${field.plural}`;
@@ -690,6 +700,8 @@
}
}
+ this.setCreateButton(true);
+
if (this.ui.terms.sentinel) {
if (this.store.lastResponse?.has_more) {
this.observer.observe(this.ui.terms.sentinel);
@@ -712,7 +724,9 @@
}
});
- this.setMessage(false);
+ if (terms.length > 0) {
+ this.setMessage(false);
+ }
this.ui.terms.list.append(fragment);
}
@@ -793,7 +807,7 @@
}
this.setCreateButton(true);
- if (field.ui.dropdown?.wrapper) {
+ if (field.ui.dropdown.wrapper) {
field.ui.dropdown.wrapper.hidden = false;
}
}
@@ -803,6 +817,7 @@
item.dataset.id = term.id;
item.textContent = term.path || term.name;
+
return item;
}
/******************************************************************
@@ -813,7 +828,7 @@
const field = this.fields.get(fieldId);
if (!term || !field) return;
//if the term already exists in the selected items, bail early
- if (field.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
+ if (field.ui.selected && field.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
const item = window.getTemplate('selectedTerm');
if (!item) return;
@@ -823,7 +838,9 @@
item.querySelector('.item-name').textContent = term.path;
item.querySelector('button').title = `Remove ${term.name}`;
- field.ui.selected.append(item);
+ if (field.ui.selected) {
+ field.ui.selected.append(item);
+ }
if (this.container.open) {
this.addTermToModal(termId);
@@ -929,7 +946,9 @@
}
handleDataLoaded() {
const taxonomy = this.store.filters.taxonomy;
- if (taxonomy?.includes(',')) {
+
+ // Always update fields for loaded taxonomies (handles both single and batch)
+ if (taxonomy) {
const taxonomies = taxonomy.split(',').map(t => t.trim());
taxonomies.forEach(tax => this.updateFieldsForTaxonomy(tax));
}
@@ -1014,7 +1033,6 @@
if (!field || !field.canCreate || !this.creator) return;
const conf = (this.container.open) ? this.ui : field.ui;
-
if (!conf.create?.button || !conf.create?.span) return;
const createButton = conf.create.button;
@@ -1076,9 +1094,12 @@
setMessage(show = true, message = '', type = true) {
const field = this.currentField();
if (!field) return;
+
+ const conf = this.container.open||field.isFilter ? this.ui : (field.isFilter ? null : field.ui);
+ if (!conf?.message?.message) return;
+
message = (message === '') ? `No ${field.plural??'items'} found.` : message;
- const conf = (this.container.open) ? this.ui : field.ui;
const p = conf.message.message;
const pText = conf.message.text;
@@ -1176,4 +1197,3 @@
}
});
});
-
diff --git a/assets/js/min/creator.min.js b/assets/js/min/creator.min.js
index f2ddfd6..4d76819 100644
--- a/assets/js/min/creator.min.js
+++ b/assets/js/min/creator.min.js
@@ -1 +1 @@
-window.jvbTaxCreator=class{constructor(e){this.selector=e,this.queue=window.jvbQueue,this.initElements(),this.initListeners()}initElements(){this.selectors={details:"details.create-term",parent:"#select_parent",summary:".create-term summary",suggestion:".term-suggestions",name:"#term_name",button:".submit-term",form:"form.create-term",label:{name:'[for="term_name"]',parent:'[for="select_parent"]'},loading:".loading-message.create-term"},this.ui=window.uiFromSelectors(this.selectors,this.selector.container)}handleOpen(e){this.field=e,this.ui.details&&(this.ui.details.hidden=!e.canCreate,this.ui.summary&&(this.ui.summary.textContent=`Add new ${e.singular}`),this.ui.label.name&&(this.ui.label.name.textContent=`Name this ${e.singular}`),this.ui.label.parent&&(this.ui.label.parent.textContent="Nest it under"),this.ui.button&&(this.ui.button.textContent=`Add ${e.singular}`))}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler),this.ui.form&&this.ui.form.addEventListener("change",(e=>{console.log("Creator form change, prevents default"),e.preventDefault(),e.stopPropagation()}))}handleClick(e){if(window.targetCheck(e,this.selectors.summary))return this.ui.details.open&&this.ui.name?.focus(),void this.resetParentOptions()}async handleTermCreation(e){if(!e.name||e.name.length<2)return!1;try{const t=await this.createTerm(e);return t.success?t.term?.pending?(this.selector.setMessage(!0,`"${e.name}" submitted for approval`,!1),!1):(t.success&&t.term&&(await this.handleSuccessfulCreation(t.term,e),this.clearForm()),t.term):t.term&&t.term.id?(this.selector.setMessage(!0,`Using existing "${t.term.name}"`),t.term):(this.selector.setMessage(!0,t.message||"Creation failed",!1),!1)}catch(e){return console.error("Error creating term:",e),!1}}async handleSuccessfulCreation(e,t){this.ui.details.open=!1,this.selector.store.clearCache(),await this.selector.store.fetch()}resetParentOptions(){const e=this.selector.currentField();if(!e)return;const t=e.taxonomy;if(!t)return;if(!this.ui.parent)return;let i=this.ui.parent.querySelector("option");if(!i)return;window.removeChildren(this.ui.parent),this.ui.parent.append(i.cloneNode(!0));const r=this.selector.store.filters.parent||0;if(0!==r){const e=this.selector.store.get(r);if(e){let t=i.cloneNode(!0);t.value=e.id,t.textContent=e.name,this.ui.parent.append(t)}}const s=[];this.selector.store.getFiltered().forEach((e=>{e.taxonomy===t&&e.parent===r&&s.push(e)})),s.sort(((e,t)=>e.name.localeCompare(t.name))),s.forEach((e=>{let t=i.cloneNode(!0);t.id=`select-parent-${e.id}`,t.value=e.id,t.textContent=" — "+e.name,this.ui.parent.append(t)}))}async createTerm(e){if(e.name&&void 0!==e.parent&&e.taxonomy)try{const t=await fetch(`${jvbSettings.api}terms`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify(e)});if(!t.ok)throw new Error(`Server error: ${t.status}`);return await t.json()}catch(e){throw console.error("Error creating term:",e),e}}clearForm(){this.ui.name&&(this.ui.name.value=""),this.selector.ui.search.input&&(this.selector.ui.search.input.value="")}destroy(){this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.ui.loading&&(this.ui.loading.hidden=!0)}};
\ No newline at end of file
+window.jvbTaxCreator=class{constructor(e){this.selector=e,this.queue=window.jvbQueue,this.initElements(),this.initListeners()}initElements(){this.selectors={details:"details.create-term",parent:"#select_parent",summary:".create-term summary",suggestion:".term-suggestions",name:"#term_name",button:".submit-term",form:"form.create-term",label:{name:'[for="term_name"]',parent:'[for="select_parent"]'},loading:".loading-message.create-term"},this.ui=window.uiFromSelectors(this.selectors,this.selector.container)}handleOpen(e){this.field=e,this.ui.details&&(this.ui.details.hidden=!e.canCreate,this.ui.summary&&(this.ui.summary.textContent=`Add new ${e.singular}`),this.ui.label.name&&(this.ui.label.name.textContent=`Name this ${e.singular}`),this.ui.label.parent&&(this.ui.label.parent.textContent="Nest it under"))}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler),this.ui.form&&this.ui.form.addEventListener("change",(e=>{console.log("Creator form change, prevents default"),e.preventDefault(),e.stopPropagation()}))}handleClick(e){if(window.targetCheck(e,this.selectors.summary))return this.ui.details.open&&this.ui.name?.focus(),void this.resetParentOptions()}async handleTermCreation(e){if(!e.name||e.name.length<2)return!1;try{const t=await this.createTerm(e);return t.success?t.term?.pending?(this.selector.setMessage(!0,`"${e.name}" submitted for approval`,!1),!1):(t.success&&t.term&&(await this.handleSuccessfulCreation(t.term,e),this.clearForm()),t.term):t.term&&t.term.id?(this.selector.setMessage(!0,`Using existing "${t.term.name}"`),t.term):(this.selector.setMessage(!0,t.message||"Creation failed",!1),!1)}catch(e){return console.error("Error creating term:",e),!1}}async handleSuccessfulCreation(e,t){this.ui.details.open=!1,this.selector.store.clearCache(),await this.selector.store.fetch()}resetParentOptions(){const e=this.selector.currentField();if(!e)return;const t=e.taxonomy;if(!t)return;if(!this.ui.parent)return;let i=this.ui.parent.querySelector("option");if(!i)return;window.removeChildren(this.ui.parent),this.ui.parent.append(i.cloneNode(!0));const r=this.selector.store.filters.parent||0;if(0!==r){const e=this.selector.store.get(r);if(e){let t=i.cloneNode(!0);t.value=e.id,t.textContent=e.name,this.ui.parent.append(t)}}const s=[];this.selector.store.getFiltered().forEach((e=>{e.taxonomy===t&&e.parent===r&&s.push(e)})),s.sort(((e,t)=>e.name.localeCompare(t.name))),s.forEach((e=>{let t=i.cloneNode(!0);t.id=`select-parent-${e.id}`,t.value=e.id,t.textContent=" — "+e.name,this.ui.parent.append(t)}))}async createTerm(e){if(e.name&&void 0!==e.parent&&e.taxonomy)try{const t=await fetch(`${jvbSettings.api}terms`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify(e)});if(!t.ok)throw new Error(`Server error: ${t.status}`);return await t.json()}catch(e){throw console.error("Error creating term:",e),e}}clearForm(){this.ui.name&&(this.ui.name.value=""),this.selector.ui.search.input&&(this.selector.ui.search.input.value="")}destroy(){this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.ui.loading&&(this.ui.loading.hidden=!0)}};
\ No newline at end of file
diff --git a/assets/js/min/dataStore.min.js b/assets/js/min/dataStore.min.js
index d92ca6d..a1cf5f4 100644
--- a/assets/js/min/dataStore.min.js
+++ b/assets/js/min/dataStore.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),delete:t=>this.delete(e,t),get:t=>this.get(e,t),getAll:()=>this.getAll(e),getAllByIndex:(t,s)=>this.getAllByIndex(e,t,s),filterByIndex:t=>this.filterByIndex(e,t),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach((e=>t.append(s,e))):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{i.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}i.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const r=this.stores.get(e);return r?.db&&r.db.objectStoreNames.contains(t)?new Promise((e=>{const i=r.db.transaction([t],"readonly").objectStore(t).getAll();i.onsuccess=t=>{const r=t.target.result||[];r.forEach(s),e(r)},i.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,r){const i=this.stores.get(e);return i?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=i.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>a(o.error);try{h=r(c,o)}catch(e){a(e)}}))):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL))return this.notify(e,"data-loaded",{cached:!0,items:r.items||[]}),r;t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&r&&(r.etag&&(a["If-None-Match"]=r.etag),r.lastModified&&(a["If-Modified-Since"]=r.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(i,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){throw"AbortError"!==t.name&&(console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t})),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s,r){const i=this.stores.get(e),a=t.items||[],o=[];i.db&&a.length>0&&await this.withTransaction(e,i.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const r=this._saveItem(e,s);o.push(r),t.put(r.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,i.config.keyPath))),timestamp:Date.now(),endpoint:i.config.endpoint,filters:{...i.filters},etag:r.headers.get("ETag"),lastModified:r.headers.get("Last-Modified"),has_more:t.has_more||!1};i.cache.set(s,n),i.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),i.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}},o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}_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 t?{valid:!1,error:`FormData at ${s}`}:(console.debug(`[DataStore] Converted 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])))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},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 t?{valid:!1,error:`FormData at ${s}`}:(console.debug(`[DataStore] Converted FormData at ${s}`),{valid:!0,data:this.formDataToObject(e)});if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0!==e.data&&(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:(console.debug(`[DataStore] Stripped unknown type at ${s}`),{valid:!0,data:void 0})}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}get(e,t){return this.stores.get(e).data.get(t)}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter((e=>Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&r.items)return r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]);if(t.filters.search&&t.filters.search.trim()){const e=t.filters.search.toLowerCase().trim();let s=Array.from(t.data.values()).filter((e=>{for(const[s,r]of Object.entries(t.filters))if("search"!==s&&"page"!==s&&null!=r&&""!==r&&e[s]!==r)return!1;return!0}));return s=s.filter((t=>["name","title","path","description","slug"].some((s=>{const r=t[s];return!!r&&r.toLowerCase().includes(e)})))),s}return this.getAll(e)}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t}));const a=await this.shouldFetchWithFilters(e,t,i);this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t}),r.config.endpoint&&a?await this.fetch(e):r.config.endpoint&&this.notify(e,"data-loaded")}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e]))||Object.keys(s.filters).some((e=>!(e in t)&&t!==s.config.filters)))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/selector.min.js b/assets/js/min/selector.min.js
index 8e5e21a..1714dda 100644
--- a/assets/js/min/selector.min.js
+++ b/assets/js/min/selector.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector("dialog#jvb-selector"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.subscribers=new Set,this.fields=new Map,this.selectedTerms=new Map,this.batchFetch=new Set,this.activeField=null,this.isInitializing=!0,this.messageText={},this.init())}init(){this.initStore(),this.initElements(),this.initModal(),this.scanExistingFields(),this.initListeners(),this.needsCreator()&&window.jvbTaxCreator&&(this.creator=new window.jvbTaxCreator(this)),this.isInitializing=!1,this.batchFetchTaxonomies().then((()=>{}))}initStore(){const e=window.jvbStore.register("taxonomies",{storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:12e4,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy",delayFetch:!0});this.store=e.terms,this.store.subscribe(this.handleStoreEvent.bind(this))}initElements(){this.selectors={search:{input:"[type=search]",clear:".clear-search",container:".search-wrapper",results:".search-results"},create:{button:"button.submit-term",span:".submit-term span"},terms:{list:".items-container",wrap:".items-wrap",sentinel:".scroll-sentinel"},nav:{nav:"nav.term-navigation",back:".back-to-parent",child:".toggle-children",pathLevel:".path-level"},message:{message:"p.message",text:"p.message span"},selected:".selected-items",modal:{title:"#modal-title",content:".modal-content",count:".selection-count"},favourites:".favourite-terms",field:{toggle:"button.taxonomy-toggle",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.container.open?this.activeField:this.getFieldId(e.target),s=this.fields.get(t);if(!t||!s)return;const i=window.targetCheck(e,"[data-autocomplete-select]");if(i){let e=parseInt(i.dataset.id);this.addSelected(e,t),s.ui.dropdown.wrapper&&(s.ui.dropdown.wrapper.hidden=!0),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"!==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"===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&&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)=>{e}))}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=`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.terms.list),this.modal.handleOpen(),this.a11y.announce(s)}closeModal(){this.modal.handleClose();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)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,a=e.querySelector("button.taxonomy-toggle");if(0===Object.keys(t).length){if(!a)return;if(t={taxonomy:a.dataset.taxonomy,single:a.dataset.single,plural:a.dataset.plural,search:Object.hasOwn(a.dataset,"search"),autocomplete:Object.hasOwn(a.dataset,"autocomplete"),creatable:Object.hasOwn(a.dataset,"creatable")},0===Object.keys(t).length)return}else Object.hasOwn(t,"toggle")&&(a=document.querySelector(t.toggle),r.toggle=t.toggle);const n={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,toggle:a,create:{button:null,span:null},selectors:r,ui:window.uiFromSelectors(r,e),checked:!1};if(n.taxonomy)return n.singular&&n.plural||(console.warn("TaxonomySelector: Field missing singular/plural labels",e),n.singular=n.taxonomy.replace("jvb_",""),n.plural=n.singular+"s"),this.fields.set(i,n),this.setSelectedFromValue(i,s),this.isInitializing&&this.batchFetch.add(n.taxonomy),this.updateFieldUI(i),i;console.error("TaxonomySelector: Field missing taxonomy",e)}setSelectedFromValue(e,t){if(!t)return;if(!e)return;if(!this.fields.get(e))return;let s=new Set;t.value.trim().split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>s.add(e))),this.selectedTerms.set(e,s)}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||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.querySelector(`[data-id="${e}"]`);if(r&&r.remove(),this.container.open){let t=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||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||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&&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.ui.toggle.disabled=!s&&!e.canCreate,e.ui.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.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)})),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.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.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?.includes(",")){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;t=""===t?`No ${i.plural??"items"} found.`:t;const r=this.container.open?this.ui:i.ui,a=r.message.message,n=r.message.text;a.hidden=!e,e?t&&n&&(s&&window.typeLoop&&n?(this.messageText[i.id]&&(this.messageText[i.id](),delete this.messageText[i.id]),this.messageText[i.id]=window.typeLoop(n,t)):n.textContent=t):this.messageText[i.id]&&(this.messageText[i.id](),delete this.messageText[i.id])}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.fields.forEach(((e,t)=>{window.debouncer.cancel(`${t}-search`),window.debouncer.cancel(`${t}-search-results`)})),Object.keys(this.messageText).forEach((e=>{this.messageText[e]&&this.messageText[e]()})),this.messageText={},this.ui.terms?.sentinel&&this.observer?.unobserve(this.ui.terms.sentinel),this.observer?.disconnect(),document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("input",this.inputHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),this.subscribers.clear(),this.fields.clear(),this.selectedTerms.clear(),this.batchFetch.clear(),this.creator&&(this.creator.destroy(),this.creator=null),this.store&&(this.store=null)}}document.addEventListener("DOMContentLoaded",(function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbSelector=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector("dialog#jvb-selector"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.subscribers=new Set,this.fields=new Map,this.selectedTerms=new Map,this.batchFetch=new Set,this.activeField=null,this.isInitializing=!0,this.messageText={},this.init())}init(){this.initStore(),this.initElements(),this.initModal(),this.scanExistingFields(),this.initListeners(),this.needsCreator()&&window.jvbTaxCreator&&(this.creator=new window.jvbTaxCreator(this)),this.isInitializing=!1,this.batchFetchTaxonomies().then((()=>{}))}initStore(){const e=window.jvbStore.register("taxonomies",{storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:12e4,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy",delayFetch:!0});this.store=e.terms,this.store.subscribe(this.handleStoreEvent.bind(this))}initElements(){this.selectors={search:{input:'[type="search"]',clear:".clear-search",container:".search-wrapper",results:".search-results"},create:{button:"button.submit-term",span:".submit-term span"},terms:{list:".items-container",wrap:".items-wrap",sentinel:".scroll-sentinel"},nav:{nav:"nav.term-navigation",back:".back-to-parent",child:".toggle-children",pathLevel:".path-level"},message:{message:"p.message",text:"p.message span"},selected:".selected-items",modal:{title:"#modal-title",content:".modal-content",count:".selection-count"},favourites:".favourite-terms",field:{toggle:'button.taxonomy-toggle, [data-filter="taxonomy"]',value:'input[type="hidden"]',selected:".selected-items",dropdown:{list:".search-results",wrapper:".auto-wrapper"},create:{button:".auto-wrapper .submit-term",span:".auto-wrapper button span"},search:"input[data-autocomplete]",message:{message:"p.message",text:"p.message span"}}},this.ui=window.uiFromSelectors(this.selectors,this.container)}initListeners(){this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.nextPage()}))}),{root:this.ui.terms.sentinel,threshold:.5}),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("input",this.inputHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0)}handleClick(e){const t=this.getFieldId(e.target)||this.activeField,s=this.fields.get(t);if(!t||!s)return;const i=window.targetCheck(e,".item.autocomplete");if(i){let e=parseInt(i.dataset.id);this.addSelected(e,t),this.scheduleHideDropdown(t),s.ui.search&&(s.ui.search.value="")}if(window.targetCheck(e,this.selectors.field.toggle))return e.preventDefault(),void this.openModal(t);const r=window.targetCheck(e,".remove-term");if(r){const e=r.closest("[data-id]").dataset.id??!1;return void(t&&e&&this.removeSelected(parseInt(e),t))}if(e.target.matches(".modal-close"))return this.updateFieldValue(t),void this.modal?.handleClose();if(window.targetCheck(e,this.selectors.nav.back))return void this.navigateToParent();if(window.targetCheck(e,this.selectors.nav.child)){const t=e.target.closest("li"),s=parseInt(t.dataset.id);return void(s&&this.navigateTo(s))}const a=window.targetCheck(e,this.selectors.nav.pathLevel);if(a){const e=parseInt(a.dataset.id)??0;this.navigateTo(e)}window.targetCheck(e,this.selectors.field.dropdown)&&this.scheduleHideDropdown(t);if(window.targetCheck(e,this.selectors.search.clear)){const e=this.currentField();e&&e.ui.search&&(e.ui.search.value="",this.store.setFilters({search:"",page:1,parent:this.store.filters.parent||0})),this.ui.search.input&&(this.ui.search.input.value="")}if(this.creator){window.targetCheck(e,this.selectors.create.button)&&this.maybeCreateTerm(e).then((()=>{}))}}handleChange(e){if(!this.container.contains(e.target))return;if(!["checkbox","button"].includes(e.target.type))return;e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.dataset.id);let s=this.getFieldId(e.target);e.target.checked?this.addSelected(t,s):this.removeSelected(t,s)}handleInput(e){let t=this.getFieldId(e.target)??this.activeField;if(!t)return;const s=this.fields.get(t);if(!s)return;if(["checkbox","button"].includes(e.target.type))return;e.preventDefault(),e.stopPropagation(),this.container.open||this.setField(t);let i=e.target.value.trim();this.setMessage(!0,`Searching for "${i}" in ${s.plural??"items"}`),window.debouncer.schedule(`${t}-search`,(async()=>{this.container.open&&window.removeChildren(this.ui.terms.list),await this.store.setFilters({taxonomy:s.taxonomy,search:i,page:1,parent:i?0:this.store.filters.parent||0})}),100)}setField(e){const t=this.fields.get(e);t?(this.activeField=e,this.setMessage(!0,`Loading ${t.plural}...`),this.resetFilters({taxonomy:t.taxonomy})):console.error("No field found...")}resetFilters(e){Object.hasOwn(e,"taxonomy")&&(e={page:1,search:"",parent:0,...e},this.store.setFilters(e))}handleFocus(e){const t=this.getFieldId(e.target),s=this.fields.get(t);t&&s&&(s.hasAutocomplete||s.hasSearch)&&(window.debouncer.cancel(`${t}-search-results`),this.container.open||this.setField(t))}handleBlur(e){const t=this.getFieldId(e.target),s=this.fields.get(t);t&&s&&s.hasAutocomplete&&!this.container.open&&(e.relatedTarget&&s.ui.dropdown.wrapper?.contains(e.relatedTarget)||this.scheduleHideDropdown(t))}scheduleHideDropdown(e){const t=this.fields.get(e);t&&window.debouncer.schedule(`${e}-search-results`,(()=>{this.container.open||(this.activeField=null),t.ui.dropdown.wrapper&&(t.ui.dropdown.wrapper.hidden=!0)}),1500)}initModal(){this.modalID="dialog#jvb-selector",this.container=document.querySelector(this.modalID),this.modal=new window.jvbModal(this.container,{handleForm:!1,open:null}),this.modal.subscribe(((e,t)=>{if("modal-close"===e)this.closeModal()}))}toggleModal(e,t=!0){this.fields.get(e)&&(t?this.openModal(e):this.closeModal())}openModal(e){const t=this.fields.get(e);if(!t)return;this.setField(e),this.ui.modal.title.textContent=t.isFilter?`Filter by ${t.singular}`:`Select ${t.plural}`,this.ui.search.container&&(this.ui.search.container.hidden=!t.canSearch),this.creator&&this.creator.handleOpen(t);let s=`Opened ${t.singular} selection. Choose from checkboxes, or search to filter results.`;window.removeChildren(this.ui.selected),window.removeChildren(this.ui.terms.list),this.modal.handleOpen(),this.a11y.announce(s)}closeModal(){const e=this.fields.get(this.activeField);if(!e)return;this.observer.unobserve(this.ui.terms.sentinel),window.removeChildren(this.ui.terms.list),this.notify("selected-terms",{terms:this.selectedTerms.get(this.activeField),taxonomy:e.taxonomy}),this.activeField=null;let t=`Closed ${e.singular} selector.`;this.a11y.announce(t)}navigateToParent(){const e=this.store.filters.parent;if(0===e)return;let t=this.store.get(parseInt(e));if(!t)return void this.navigateTo(0);let s=t.parent;this.navigateTo(parseInt(s))}navigateTo(e=0){e=parseInt(e)??0,this.store.setFilters({parent:e,page:1}),window.removeChildren(this.ui.terms.list),this.updateBreadcrumbs(e)}nextPage(){let e=this.store.filters.page,t=Math.min(e++,this.store.lastResponse.total);this.store.setFilters({page:t})}prevPage(){let e=this.store.filters.page,t=Math.max(e-1,1);this.store.setFilters({page:t})}addTermToModal(e){const t=this.store.get(e);if(!t)return;const s=this.currentField();if(!s)return;if(this.ui.selected.querySelector(`[data-id="${e}"]`))return;const i=window.getTemplate("selectedTerm");i&&(i.dataset.id=e,i.dataset.taxonomy=s.taxonomy,i.querySelector(".item-name").textContent=t.path,i.querySelector("button").title=`Remove ${t.name}`,this.ui.selected.append(i))}scanExistingFields(e=document.body){e.querySelectorAll('[data-type="selector"]').forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let s=e.querySelector('input[type="hidden"]');if(!s&&!Object.hasOwn(e.dataset,"filter"))return void console.warn("TaxonomySelector: No hidden input found for field",e);"fieldId"in e.dataset||(e.dataset.fieldId=window.generateID("selector"));const i=e.dataset.fieldId;let r=this.selectors.field;const a=Object.hasOwn(e.dataset,"filter")&&"taxonomy"===e.dataset.filter;let n=a?e:e.querySelector("button.taxonomy-toggle");if(0===Object.keys(t).length){if(!n)return;if(t={taxonomy:n.dataset.taxonomy,single:n.dataset.single,plural:n.dataset.plural,search:Object.hasOwn(n.dataset,"search"),autocomplete:Object.hasOwn(n.dataset,"autocomplete"),creatable:Object.hasOwn(n.dataset,"creatable")},0===Object.keys(t).length)return}else Object.hasOwn(t,"toggle")&&(n=document.querySelector(t.toggle),r.toggle=t.toggle);const o={id:i,value:s,element:e,taxonomy:t.taxonomy??!1,singular:t.single??"",plural:t.plural??"",name:e.dataset.field,canSearch:t.search??!1,limit:t.limit??0,hasAutocomplete:t.autocomplete??!1,canCreate:t.creatable??!1,isRequired:t.required??!1,isFilter:a,toggle:n,create:{button:null,span:null},selectors:r,ui:window.uiFromSelectors(r,e),checked:!1};if(a&&!o.ui.toggle&&(o.ui.toggle=e),o.taxonomy)return o.singular&&o.plural||(console.warn("TaxonomySelector: Field missing singular/plural labels",e),o.singular=o.taxonomy.replace("jvb_",""),o.plural=o.singular+"s"),this.fields.set(i,o),this.setSelectedFromValue(i,s),this.isInitializing&&this.batchFetch.add(o.taxonomy),this.updateFieldUI(i),i;console.error("TaxonomySelector: Field missing taxonomy",e)}setSelectedFromValue(e,t){if(!e)return;let s=this.fields.get(e);if(!s)return;if(!t&&!s.isFilter)return;let i=new Set;t&&t.value.trim().split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>i.add(e))),this.selectedTerms.set(e,i)}addSelected(e,t=null){t||(t=this.activeField);const s=this.fields.get(t),i=this.store.get(e);if(!s||!i)return;const r=this.selectedTerms.get(t);0!==s.limit&&r.size>=s.limit||(r.add(parseInt(e)),this.container.open||s.isFilter||this.updateFieldValue(t),this.addTermToDisplay(e,t),this.checkLimits(t))}removeSelected(e,t=null){t||(t=this.activeField);const s=this.fields.get(t),i=this.store.get(e);if(!s||!i)return;this.selectedTerms.get(t).delete(parseInt(e));const r=!!s.ui.selected&&s.ui.selected.querySelector(`[data-id="${e}"]`);if(r&&r.remove(),this.container.open){let t=!!this.ui.selected&&this.ui.selected.querySelector(`[data-id="${e}"]`);t&&t.remove();let s=this.ui.terms.list.querySelector(`[type=checkbox][data-id="${e}"]`);s&&(s.checked=!1)}this.container.open||s.isFilter||this.updateFieldValue(t),this.checkLimits(t)}updateFieldValue(e){const t=this.fields.get(e);if(!t)return;let s=Array.from(this.selectedTerms.get(e));t.ui.value.value=s.join(",")}checkLimits(e){if(!this.container.open)return;const t=this.fields.get(e);if(!t||!t.isFilter||0===t.limit)return;const s=this.selectedTerms.get(e).size>=t.limit;this.setCheckboxes(s)}updateFieldFromInput(e){const t=this.getFieldId(e);if(!t)return;this.fields.get(t)&&(this.setSelectedFromValue(t,e),this.updateFieldUI(t))}updateFieldUI(e){const t=this.fields.get(e);let s=this.selectedTerms.get(e)??new Set;t&&!t.isFilter&&0!==s.size&&Array.from(s).forEach((t=>{this.addTermToDisplay(t,e)}))}updateFieldsForTaxonomy(e){let t=Array.from(this.fields.values()).filter((t=>!t.checked&&t.taxonomy===e));const s=Array.from(this.store.data.values()).some((t=>t.taxonomy===e));t.forEach((e=>{e.toggle.disabled=!s&&!e.canCreate,e.toggle.title=s?`Select ${e.plural}`:`No ${e.singular} available`,e.checked=!0}))}showModalTerms(e=!1){const t=this.currentField(),s=this.store.getFiltered();0===s.length&&((this.store.filters.page??1)&&window.removeChildren(this.ui.terms.list),this.setMessage(!0,""===this.store.filters.search?`No matching ${t.plural}.`:`No ${t.plural} found.`,!1),this.ui.terms.sentinel&&this.observer.unobserve(this.ui.terms.sentinel)),this.setCreateButton(!0),this.ui.terms.sentinel&&(this.store.lastResponse?.has_more?this.observer.observe(this.ui.terms.sentinel):this.observer.unobserve(this.ui.terms.sentinel));const i=this.store.filters.parent??0;this.ui.nav.back.hidden=0===i;const r=document.createDocumentFragment();s.forEach((t=>{const s=this.createTermElement({show:e,...t});s&&r.append(s)})),s.length>0&&this.setMessage(!1),this.ui.terms.list.append(r)}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem");t.dataset.id=e.id;const s=this.selectedTerms.get(this.activeField).has(e.id);let[i,r,a]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("span, .term-name")],n=this.currentField(),o=n.limit>0&&this.selectedTerms.get(this.activeField).size>=n.limit;if(i&&r&&a&&([i.dataset.id,i.id,i.name,i.value,i.disabled,i.checked,r.htmlFor,r.title,r.dataset.path,a.textContent]=[e.id,`${n.element.id}-${e.id}`,`${n.element.id}-${n.taxonomy}-select`,e.id,!s&&o,s,`${n.element.id}-${e.id}`,e.path??e.name,e.path,e.show?e.path:e.name],e.hasChildren)){const s=window.getTemplate("termChildrenToggle");s&&(s.ariaLabel=`View ${n.plural} nested under ${e.name}`,t.append(s))}return t}showAutocompleteTerms(){const e=this.currentField(),t=this.currentTerms();if(!e)return;const s=e.ui.dropdown.list;s&&(window.removeChildren(s),0===t.length?this.setMessage(!0,`No ${e.plural} found.`,!1):(t.forEach((e=>{const t=this.createAutocompleteTerm(e);t&&s.append(t)})),this.setMessage(!1)),this.setCreateButton(!0),e.ui.dropdown.wrapper&&(e.ui.dropdown.wrapper.hidden=!1))}createAutocompleteTerm(e){const t=window.getTemplate("autocompleteItem");if(t)return t.dataset.id=e.id,t.textContent=e.path||e.name,t}addTermToDisplay(e,t){const s=this.store.get(e),i=this.fields.get(t);if(!s||!i)return;if(i.ui.selected&&i.ui.selected.querySelector(`[data-id="${e}"]`))return;const r=window.getTemplate("selectedTerm");if(r&&(r.dataset.id=e,r.dataset.taxonomy=i.taxonomy,r.querySelector(".item-name").textContent=s.path,r.querySelector("button").title=`Remove ${s.name}`,i.ui.selected&&i.ui.selected.append(r),this.container.open)){this.addTermToModal(e);const t=this.ui.terms.list.querySelector(`input[value="${e}"]`);t&&(t.checked=!0)}}updateBreadcrumbs(e){const t=this.ui.nav.nav;if(!t)return;const s=Array.from(t.children).find((t=>parseInt(t.dataset.id)===e));if(s){let e=s.nextElementSibling;for(;e;){const t=e;e=e.nextElementSibling,t.remove()}}else{const s=this.store.get(e);if(!s)return;const i=window.getTemplate("termBreadcrumb");if(!i)return;i.dataset.id=e,i.textContent=s.name,i.title=s.name,t.append(i)}}updateSelectionCount(){if(!this.container.open)return;const e=this.fields.get(this.activeField);if(e&&this.ui.modal.count){const t=this.selectedTerms.get(this.activeField).size;this.ui.modal.count.textContent=e.limit>0?`${t} of ${e.limit} ${e.plural} selected`:`${t} ${e.plural} selected`}}currentField(){return this.fields.get(this.activeField)??!1}currentTerms(){return this.store.getFiltered()}needsCreator(){return Array.from(this.fields.values()).some((e=>e.canCreate||e.hasAutocomplete))}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?.dataset.fieldId||null}setCheckboxes(e){this.ui.terms.list.querySelectorAll("input[type=checkbox]").forEach((t=>{t.checked||(t.disabled=e)}))}handleStoreEvent(e,t){const s={"data-loaded":()=>this.handleDataLoaded(),"filters-changed":()=>this.handleFiltersChanged(t),"fetch-error":()=>this.handleFetchError()};try{s[e]?.(t)}catch(t){console.error(`Error handling store event "${e}":`,t),this.setMessage(!0,"An error occurred loading data",!1)}}handleDataLoaded(){const e=this.store.filters.taxonomy;if(e){e.split(",").map((e=>e.trim())).forEach((e=>this.updateFieldsForTaxonomy(e)))}this.container.open?this.showResults():this.activeField?this.showResults(!0):this.setMessage(!1)}showResults(e=!1){this.setMessage(!1);const t=this.store.getFiltered(),s=this.store.filters,i=s.search&&s.search.length>0;this.notify("terms-loaded",{terms:t,filters:s}),e?this.showAutocompleteTerms():this.showModalTerms(i),this.a11y.announce(t.length)}handleFiltersChanged(e){}handleFetchError(e){const t=this.currentField(),s=t?`Failed to load ${t.plural}`:"Failed to load data";this.setMessage(!0,s,!1),console.error("Store fetch error:",e)}async batchFetchTaxonomies(){if(0===this.batchFetch.size)return;const e=Array.from(this.batchFetch);this.batchFetch.clear();try{await this.store.setFilters({taxonomy:e.join(","),page:1,search:"",parent:0})}catch(e){console.error("Failed to batch fetch taxonomies:",e)}}preloadTaxonomy(e){this.store.setFilters({taxonomy:e,page:1,search:"",parent:0})}setCreateButton(e=!0){const t=this.currentField();if(!t||!t.canCreate||!this.creator)return;const s=this.container.open?this.ui:t.ui;if(!s.create?.button||!s.create?.span)return;const i=s.create.button,r=s.create.span,a=this.container.open?s.search.input:s.search;if(!a)return;let n=(this.currentTerms()??[]).map((e=>e.name)),o=a.value;const l=e&&o.length>=2&&!n.includes(o);i.hidden=!l,l&&(r.textContent=a.value??"")}async maybeCreateTerm(e){const t=this.currentField();if(!t)return;window.debouncer.cancel(`${t.id}-search-results`);let s={taxonomy:t.taxonomy,parent:this.store.filters.parent??0};if(this.container.open&&""===this.ui.search.input.value?(s.parent=this.creator.ui.parent.value??s.parent,s.name=this.creator.ui.name.value??!1):s.name=this.container.open?this.ui.search.input.value:t.ui.search.value,void 0!==s.parent&&s.name){this.setMessage(!0,`Creating "${s.name}"...`),this.setCreateButton(!1),this.container.open?window.removeChildren(this.ui.terms.list):(t.ui.search.disabled=!0,window.removeChildren(t.ui.dropdown.list),t.ui.dropdown.wrapper&&(t.ui.dropdown.wrapper.hidden=!1));let e=await this.creator.handleTermCreation(s);e&&this.addSelected(e.id,t.id),this.container.open||(t.ui.search.disabled=!1,t.ui.search.value=""),this.scheduleHideDropdown(t.id),this.setMessage(!1)}}setMessage(e=!0,t="",s=!0){const i=this.currentField();if(!i)return;const r=this.container.open||i.isFilter?this.ui:i.isFilter?null:i.ui;if(!r?.message?.message)return;t=""===t?`No ${i.plural??"items"} found.`:t;const a=r.message.message,n=r.message.text;a.hidden=!e,e?t&&n&&(s&&window.typeLoop&&n?(this.messageText[i.id]&&(this.messageText[i.id](),delete this.messageText[i.id]),this.messageText[i.id]=window.typeLoop(n,t)):n.textContent=t):this.messageText[i.id]&&(this.messageText[i.id](),delete this.messageText[i.id])}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.fields.forEach(((e,t)=>{window.debouncer.cancel(`${t}-search`),window.debouncer.cancel(`${t}-search-results`)})),Object.keys(this.messageText).forEach((e=>{this.messageText[e]&&this.messageText[e]()})),this.messageText={},this.ui.terms?.sentinel&&this.observer?.unobserve(this.ui.terms.sentinel),this.observer?.disconnect(),document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("input",this.inputHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),this.subscribers.clear(),this.fields.clear(),this.selectedTerms.clear(),this.batchFetch.clear(),this.creator&&(this.creator.destroy(),this.creator=null),this.store&&(this.store=null)}}document.addEventListener("DOMContentLoaded",(function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbSelector=new e)}))}))})();
\ No newline at end of file
diff --git a/build/feed/style-index-rtl.css b/build/feed/style-index-rtl.css
index ba5c5ec..7a0ed7f 100644
--- a/build/feed/style-index-rtl.css
+++ b/build/feed/style-index-rtl.css
@@ -1 +1 @@
-.feed-block{grid-column:full}.feed-block .filters{margin:0 auto;max-width:var(--wide);padding:1rem 0}.feed-block .filter-group{padding:2rem 0;position:relative}.feed-block .filter-group .label{right:0;position:absolute}.feed-block .filter-group>.label{top:0}.feed-block .filter-group [type=radio]{right:var(--offScreen);position:absolute}.feed-block .filter-group button,.feed-block .filter-group label{height:-moz-max-content;height:max-content;padding:.5rem;position:relative}.feed-block .filter-group button:hover,.feed-block .filter-group label:hover{color:var(--action-contrast)}.feed-block .filter-group :checked+label .label,.feed-block .filter-group button:hover .label,.feed-block .filter-group label:hover .label{opacity:1;visibility:visible}.feed-block .filter-group button .label,.feed-block .filter-group label .label,.feed-block .filter-group:has(label:hover) :checked+label .label{--height:max-content;bottom:-2rem;font-weight:var(--fw-b);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.feed-block h3{font-size:var(--medium);margin:0 0 .25rem}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder i.icon{--w:50%;animation:dance 2.5s ease-in-out infinite;color:var(--base-200)}.item-grid{max-width:none;padding:0 var(--chip)}.feed.item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0;position:relative}.feed.item details{padding:0;position:relative;width:100%;z-index:var(--z-2)}.feed.item details summary{backdrop-filter:blur(5px);background-color:rgba(var(--base-rgb),var(--op-2));right:0;position:absolute;top:-3rem;width:100%}.feed.item details summary:hover{background-color:rgba(var(--action-rgb),var(--op-45))}.feed.item details[open]{padding:.25rem .5rem}.feed.item details[open] summary .icon{opacity:0}.feed.item img{-o-object-fit:cover;object-fit:cover}.feed.item img:hover{opacity:.8}.feed.item[data-timeline] .images{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] .images span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] .images span:first-of-type{bottom:0;left:50%;text-align:left}.feed.item[data-timeline] .images span:last-of-type{right:50%;top:0}.feed.item[data-timeline] .images>a{display:flex;flex-wrap:nowrap;height:100%;position:relative;width:100%}.feed.item[data-timeline] img{height:100%;-o-object-fit:cover;object-fit:cover;width:50%}.feed.item[data-timeline] img:first-of-type{border-left:1px solid var(--action-0)}.feed.item a:after,.feed.item a:before{display:none}.feed.item label{font-weight:400;text-transform:none}.feed.item label .icon{--w:1.5em}.item-grid:has([data-timeline]){grid-template-columns:repeat(1,1fr)}@media(min-width:768px){.item-grid:has([data-timeline]){grid-template-columns:repeat(2,1fr)}}
+.feed-block{grid-column:full}.feed-block .filters{margin:0 auto;max-width:var(--wide);padding:1rem 0}.feed-block .filters .remove-term.remove-term{height:-moz-max-content;height:max-content;width:-moz-max-content;width:max-content}.feed-block .filter-group{padding:2rem 0;position:relative}.feed-block .filter-group .label{right:0;position:absolute}.feed-block .filter-group>.label{top:0}.feed-block .filter-group [type=radio]{right:var(--offScreen);position:absolute}.feed-block .filter-group button,.feed-block .filter-group label{height:-moz-max-content;height:max-content;padding:.5rem;position:relative}.feed-block .filter-group button:hover,.feed-block .filter-group label:hover{color:var(--action-contrast)}.feed-block .filter-group :checked+label .label,.feed-block .filter-group button:hover .label,.feed-block .filter-group label:hover .label{opacity:1;visibility:visible}.feed-block .filter-group button .label,.feed-block .filter-group label .label,.feed-block .filter-group:has(label:hover) :checked+label .label{--height:max-content;bottom:-2rem;font-weight:var(--fw-b);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.feed-block h3{font-size:var(--medium);margin:0 0 .25rem}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder i.icon{--w:50%;animation:dance 2.5s ease-in-out infinite;color:var(--base-200)}.item-grid{max-width:none;padding:0 var(--chip)}.feed.item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0;position:relative}.feed.item details{padding:0;position:relative;width:100%;z-index:var(--z-2)}.feed.item details summary{backdrop-filter:blur(5px);background-color:rgba(var(--base-rgb),var(--op-2));right:0;position:absolute;top:-3rem;width:100%}.feed.item details summary:hover{background-color:rgba(var(--action-rgb),var(--op-45))}.feed.item details[open]{padding:.25rem .5rem}.feed.item details[open] summary .icon{opacity:0}.feed.item img{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.feed.item img:hover{opacity:.8}.feed.item[data-timeline] .images{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] .images span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] .images span:first-of-type{bottom:0;left:50%;text-align:left}.feed.item[data-timeline] .images span:last-of-type{right:50%;top:0}.feed.item[data-timeline] .images>a{display:flex;flex-wrap:nowrap;height:100%;position:relative;width:100%}.feed.item[data-timeline] img{height:100%;-o-object-fit:cover;object-fit:cover;width:50%}.feed.item[data-timeline] img:first-of-type{border-left:1px solid var(--action-0)}.feed.item a:after,.feed.item a:before{display:none}.feed.item label{font-weight:400;text-transform:none}.feed.item label .icon{--w:1.5em}.item-grid:has([data-timeline]){grid-template-columns:repeat(1,1fr)}@media(min-width:768px){.item-grid:has([data-timeline]){grid-template-columns:repeat(2,1fr)}}.items-wrap [type=checkbox],.items-wrap [type=radio]{right:-200vw;opacity:0;position:absolute}.items-wrap [type=checkbox]+label,.items-wrap [type=radio]+label{cursor:pointer;position:relative}.items-wrap [type=checkbox]+label:hover,.items-wrap [type=radio]+label:hover{color:var(--action-0)}.items-wrap [type=checkbox]+label:after,.items-wrap [type=checkbox]+label:before,.items-wrap [type=radio]+label:after,.items-wrap [type=radio]+label:before{content:"";position:absolute;top:50%}.items-wrap [type=checkbox]+label:after,.items-wrap [type=radio]+label:after{border:solid var(--light-0);border-width:0 0 2px 2px;display:none;height:10px;right:5px;transform:translateY(-70%) rotate(-45deg);width:5px}.items-wrap [type=checkbox]+label:before,.items-wrap [type=radio]+label:before{background-color:var(--base);border:2px solid var(--contrast-200);border-radius:var(--radius);height:1rem;right:0;transform:translateY(-50%);width:1rem}.items-wrap [type=checkbox]:hover+label:before,.items-wrap [type=radio]:hover+label:before{border-color:var(--action-200)}.items-wrap [type=checkbox]:checked+label:before,.items-wrap [type=radio]:checked+label:before{background-color:var(--action-0);border-color:var(--action-100)}.items-wrap [type=radio]:checked+label:before{border-radius:50%}.items-wrap [type=checkbox]:checked+label:after{border:solid var(--light-0);border-width:0 0 2px 2px;display:block;height:.66rem;right:5px;top:50%;transform:translateY(-70%) rotate(-45deg);width:.35rem}.items-wrap :disabled+label{cursor:not-allowed}.items-wrap :disabled+label,.items-wrap :disabled+label:hover{background-color:var(--base-50);border-color:var(--base-200);color:var(--base-200)}.items-wrap :disabled+label:before{border-color:var(--base-200)}#jvb-selector .items-wrap [type=checkbox]+label,#jvb-selector .items-wrap [type=radio]+label{flex:1;padding-right:2rem!important;transform-origin:top center;will-change:transform}
diff --git a/build/feed/style-index.css b/build/feed/style-index.css
index 7429b47..273f7f5 100644
--- a/build/feed/style-index.css
+++ b/build/feed/style-index.css
@@ -1 +1 @@
-.feed-block{grid-column:full}.feed-block .filters{margin:0 auto;max-width:var(--wide);padding:1rem 0}.feed-block .filter-group{padding:2rem 0;position:relative}.feed-block .filter-group .label{left:0;position:absolute}.feed-block .filter-group>.label{top:0}.feed-block .filter-group [type=radio]{left:var(--offScreen);position:absolute}.feed-block .filter-group button,.feed-block .filter-group label{height:-moz-max-content;height:max-content;padding:.5rem;position:relative}.feed-block .filter-group button:hover,.feed-block .filter-group label:hover{color:var(--action-contrast)}.feed-block .filter-group :checked+label .label,.feed-block .filter-group button:hover .label,.feed-block .filter-group label:hover .label{opacity:1;visibility:visible}.feed-block .filter-group button .label,.feed-block .filter-group label .label,.feed-block .filter-group:has(label:hover) :checked+label .label{--height:max-content;bottom:-2rem;font-weight:var(--fw-b);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.feed-block h3{font-size:var(--medium);margin:0 0 .25rem}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder i.icon{--w:50%;animation:dance 2.5s ease-in-out infinite;color:var(--base-200)}.item-grid{max-width:none;padding:0 var(--chip)}.feed.item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0;position:relative}.feed.item details{padding:0;position:relative;width:100%;z-index:var(--z-2)}.feed.item details summary{backdrop-filter:blur(5px);background-color:rgba(var(--base-rgb),var(--op-2));left:0;position:absolute;top:-3rem;width:100%}.feed.item details summary:hover{background-color:rgba(var(--action-rgb),var(--op-45))}.feed.item details[open]{padding:.25rem .5rem}.feed.item details[open] summary .icon{opacity:0}.feed.item img{-o-object-fit:cover;object-fit:cover}.feed.item img:hover{opacity:.8}.feed.item[data-timeline] .images{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] .images span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] .images span:first-of-type{bottom:0;right:50%;text-align:right}.feed.item[data-timeline] .images span:last-of-type{left:50%;top:0}.feed.item[data-timeline] .images>a{display:flex;flex-wrap:nowrap;height:100%;position:relative;width:100%}.feed.item[data-timeline] img{height:100%;-o-object-fit:cover;object-fit:cover;width:50%}.feed.item[data-timeline] img:first-of-type{border-right:1px solid var(--action-0)}.feed.item a:after,.feed.item a:before{display:none}.feed.item label{font-weight:400;text-transform:none}.feed.item label .icon{--w:1.5em}.item-grid:has([data-timeline]){grid-template-columns:repeat(1,1fr)}@media(min-width:768px){.item-grid:has([data-timeline]){grid-template-columns:repeat(2,1fr)}}
+.feed-block{grid-column:full}.feed-block .filters{margin:0 auto;max-width:var(--wide);padding:1rem 0}.feed-block .filters .remove-term.remove-term{height:-moz-max-content;height:max-content;width:-moz-max-content;width:max-content}.feed-block .filter-group{padding:2rem 0;position:relative}.feed-block .filter-group .label{left:0;position:absolute}.feed-block .filter-group>.label{top:0}.feed-block .filter-group [type=radio]{left:var(--offScreen);position:absolute}.feed-block .filter-group button,.feed-block .filter-group label{height:-moz-max-content;height:max-content;padding:.5rem;position:relative}.feed-block .filter-group button:hover,.feed-block .filter-group label:hover{color:var(--action-contrast)}.feed-block .filter-group :checked+label .label,.feed-block .filter-group button:hover .label,.feed-block .filter-group label:hover .label{opacity:1;visibility:visible}.feed-block .filter-group button .label,.feed-block .filter-group label .label,.feed-block .filter-group:has(label:hover) :checked+label .label{--height:max-content;bottom:-2rem;font-weight:var(--fw-b);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.feed-block h3{font-size:var(--medium);margin:0 0 .25rem}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder i.icon{--w:50%;animation:dance 2.5s ease-in-out infinite;color:var(--base-200)}.item-grid{max-width:none;padding:0 var(--chip)}.feed.item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0;position:relative}.feed.item details{padding:0;position:relative;width:100%;z-index:var(--z-2)}.feed.item details summary{backdrop-filter:blur(5px);background-color:rgba(var(--base-rgb),var(--op-2));left:0;position:absolute;top:-3rem;width:100%}.feed.item details summary:hover{background-color:rgba(var(--action-rgb),var(--op-45))}.feed.item details[open]{padding:.25rem .5rem}.feed.item details[open] summary .icon{opacity:0}.feed.item img{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.feed.item img:hover{opacity:.8}.feed.item[data-timeline] .images{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] .images span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] .images span:first-of-type{bottom:0;right:50%;text-align:right}.feed.item[data-timeline] .images span:last-of-type{left:50%;top:0}.feed.item[data-timeline] .images>a{display:flex;flex-wrap:nowrap;height:100%;position:relative;width:100%}.feed.item[data-timeline] img{height:100%;-o-object-fit:cover;object-fit:cover;width:50%}.feed.item[data-timeline] img:first-of-type{border-right:1px solid var(--action-0)}.feed.item a:after,.feed.item a:before{display:none}.feed.item label{font-weight:400;text-transform:none}.feed.item label .icon{--w:1.5em}.item-grid:has([data-timeline]){grid-template-columns:repeat(1,1fr)}@media(min-width:768px){.item-grid:has([data-timeline]){grid-template-columns:repeat(2,1fr)}}.items-wrap [type=checkbox],.items-wrap [type=radio]{left:-200vw;opacity:0;position:absolute}.items-wrap [type=checkbox]+label,.items-wrap [type=radio]+label{cursor:pointer;position:relative}.items-wrap [type=checkbox]+label:hover,.items-wrap [type=radio]+label:hover{color:var(--action-0)}.items-wrap [type=checkbox]+label:after,.items-wrap [type=checkbox]+label:before,.items-wrap [type=radio]+label:after,.items-wrap [type=radio]+label:before{content:"";position:absolute;top:50%}.items-wrap [type=checkbox]+label:after,.items-wrap [type=radio]+label:after{border:solid var(--light-0);border-width:0 2px 2px 0;display:none;height:10px;left:5px;transform:translateY(-70%) rotate(45deg);width:5px}.items-wrap [type=checkbox]+label:before,.items-wrap [type=radio]+label:before{background-color:var(--base);border:2px solid var(--contrast-200);border-radius:var(--radius);height:1rem;left:0;transform:translateY(-50%);width:1rem}.items-wrap [type=checkbox]:hover+label:before,.items-wrap [type=radio]:hover+label:before{border-color:var(--action-200)}.items-wrap [type=checkbox]:checked+label:before,.items-wrap [type=radio]:checked+label:before{background-color:var(--action-0);border-color:var(--action-100)}.items-wrap [type=radio]:checked+label:before{border-radius:50%}.items-wrap [type=checkbox]:checked+label:after{border:solid var(--light-0);border-width:0 2px 2px 0;display:block;height:.66rem;left:5px;top:50%;transform:translateY(-70%) rotate(45deg);width:.35rem}.items-wrap :disabled+label{cursor:not-allowed}.items-wrap :disabled+label,.items-wrap :disabled+label:hover{background-color:var(--base-50);border-color:var(--base-200);color:var(--base-200)}.items-wrap :disabled+label:before{border-color:var(--base-200)}#jvb-selector .items-wrap [type=checkbox]+label,#jvb-selector .items-wrap [type=radio]+label{flex:1;padding-left:2rem!important;transform-origin:top center;will-change:transform}
diff --git a/build/feed/view.asset.php b/build/feed/view.asset.php
index ef74b8c..dd296be 100644
--- a/build/feed/view.asset.php
+++ b/build/feed/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '1469437e42d138dbcd90');
+<?php return array('dependencies' => array(), 'version' => 'af81f16ca1cc19d9a8db');
diff --git a/build/feed/view.js b/build/feed/view.js
index b4b82e2..c38537e 100644
--- a/build/feed/view.js
+++ b/build/feed/view.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.cache=new window.jvbCache("feed"),this.error=window.jvbError,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.initElements(),this.initFilters(),this.loadWhenAble())}loadWhenAble(){"requestIdleCallback"in window?requestIdleCallback((()=>{this.initTaxonomies(),this.initStore(),this.initListeners(),this.initGallery()}),{timeout:2e3}):setTimeout((()=>{this.initTaxonomies(),this.initStore(),this.initListeners(),this.initGallery()}),100)}initElements(){var e;this.currentTaxonomies=new Set,this.taxonomyFilters={},this.elements={filterTrigger:"[data-filter]",filters:{content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},selectedTax:".selected-items",clearFilter:"button.clear-filters",loadMore:"button.load-more",filterContainer:".filters",grid:".item-grid"},this.ui=window.uiFromSelectors(this.elements),this.ui.content=null!==(e=this.ui.filterContainer.querySelectorAll('[name="content"]'))&&void 0!==e&&e,this.ui.taxonomies=this.ui.filterContainer.querySelectorAll("[data-taxonomy]"),this.ui.content&&this.ui.content.length>0?this.contentTypes=Array.from(this.ui.content).map((e=>e.value)):this.contentTypes=[this.container.dataset.content],this.ui.taxonomies.length>0?this.taxonomies=Array.from(this.ui.taxonomies).map((e=>e.dataset.taxonomy)):this.taxonomies=[]}async initTaxonomies(){this.selector=window.jvbSelector;const e=document.querySelectorAll('[data-filter="taxonomy"]');this.selector.isInitializing=!0,e.forEach((e=>{const t=e.dataset.taxonomy;this.currentTaxonomies.add(t),this.selector.registerFilterButton(e,{button:e,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax}),this.addTaxonomyPreloadListeners(e,t)})),this.selector.isInitializing=!1,this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}addTaxonomyPreloadListeners(e,t){const i=()=>{this.selector.preloadTaxonomy(t)};e.addEventListener("mouseenter",i,{once:!0}),e.addEventListener("pointerdown",i,{once:!0}),e.addEventListener("focus",i,{once:!0})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;t.size>0?this.taxonomyFilters[i]=Array.from(t.keys()):delete this.taxonomyFilters[i];let s={page:1};Object.keys(this.taxonomyFilters).length>0&&(s.taxonomy=this.taxonomyFilters),this.updateFilter(s)}clearAllTaxonomies(){this.taxonomyFilters={},window.removeChildren(this.ui.selectedTax),this.updateFilter({taxonomy:null,page:1})}initFilters(){this.filters={content:this.contentTypes[0],orderby:"date",order:"desc",page:1},this.config.context&&(this.filters.context=this.config.context),this.config.source&&(this.filters.source=this.config.source),this.processCachedFilters(),this.processURLFilters(),this.syncUIToFilters()}syncUIToFilters(){this.ui.filterContainer&&Object.entries(this.filters).forEach((([e,t])=>{const i=this.ui.filterContainer.querySelector(`[data-filter="${e}"][value="${t}"]`);i&&(i.checked=!0)})),this.updateContentFor(this.filters.content)}nextPage(){this.store.setFilter("page",this.store.filters.page++)}initStore(){const e=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:this.filters,TTL:216e5,showLoading:!0,required:"content",delayFetch:!0});this.store=e.feed,this.store.subscribe(((e,t)=>{"data-loaded"===e&&(this.renderItems(),this.ui.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse.has_more&&(this.ui.loadMore.hidden=!this.store.lastResponse.has_more))}))}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe(((e,t)=>{"load-more"===e&&this.store.lastResponse&&this.store.lastResponse.has_more&&this.nextPage()}))}processCachedFilters(){Object.keys(this.filters).forEach((e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)}))}processURLFilters(){if(this.filters.page>1)return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;["content","order","orderby","favourites","match"].forEach((t=>{let i=e.get(`f_${t}`);if(i){this.filters[t]=i;let e=this.ui.filters[t];e&&(e.checked=!0)}}));let t=!1;if(e.forEach(((e,i)=>{if(i.startsWith("f_tax_")){t=!0;const s=i.replace("f_tax_","");this.taxonomyFilters[s]||(this.taxonomyFilters[s]=[]),this.taxonomyFilters[s]=e.split(",").map(Number)}})),this.ui.filterContainer&&t)for(let[e,t]in Object.entries(this.taxonomyFilters)){let i=this.ui.filterContainer.querySelector(`[data-taxonomy="${e}"]`);i&&(i.dataset.fieldId?(this.selector.get(i.dataset.fieldId).selectedTerms=new Set(t),this.selector.initFieldDisplay(i.dataset.fieldId)):this.selector.registerField(i,{button:i,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax,selectedItems:t}))}return!0}updateURL(){const e=new URLSearchParams;["content","order","orderby","match"].forEach((t=>{this.filters[t]&&e.set(`f_${t}`,this.filters[t])})),Object.entries(this.taxonomyFilters).forEach((([t,i])=>{i.length>0&&e.set(`f_tax_${t}`,i.join(","))}));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;window.history.pushState({filters:this.filters},"",t)}renderItems(){let e=this.store.getFiltered();if(1===this.store.filters.page&&window.removeChildren(this.ui.grid),0===e.length)return void this.a11y.announceItems(0,this.store.filters.page>0);const t=document.createDocumentFragment(),i=s=>{const r=Math.min(s+10,e.length);for(let i=s;i<r;i++){const s=e[i],r=this.createItemElement(s);t.appendChild(r)}r<e.length?requestAnimationFrame((()=>i(r))):(this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.updateGalleryItems(this.gallery.getGalleryItems()),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,this.store.filters.page>1,this.store.hasMore))};e.length>0?i(0):this.a11y.announceItems(0,this.store.filters.page>1,!1),this.ui.filters.match&&(this.ui.filters.match.hidden=0===Object.keys(this.taxonomyFilters).length),this.ui.clearFilter&&(this.ui.clearFilter.hidden=0===Object.keys(this.taxonomyFilters).length)}createItemElement(e){let t=window.getTemplate(`feedItem${window.uppercaseFirst(e.content)}`);const i=Object.hasOwn(t.dataset,"timeline");for(let[s,r]of Object.entries(e.fields)){if(i&&["timeline","number"].includes(s))continue;let o=t.querySelector(`[data-field="${s}"]`);o&&(""!==r?this.isImageField(e,r)?this.formatImageFields(o,r,e):this.isTaxonomyField(e,s)?this.formatTaxonomyField(o,e,s,r):this.isTimeField(o)?this.formatTimeField(o,r):this.formatField(o,r):o.remove())}let s=t.querySelector("a");var r;return s&&""!==e.url&&([s.href,s.title]=[e.url,`View ${null!==(r=e.fields.post_title)&&void 0!==r?r:"Item"}`]),i&&this.addTimelineElements(e,t),t}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>e))}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some((t=>Object.keys(e.images).map((e=>parseInt(e))).includes(parseInt(t))))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach((s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)})),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){var s;let r=null!==(s=i.images[t])&&void 0!==s&&s;r&&([e.src,e.srcset,e.alt]=[r.tiny,`${r.tiny} 50w, ${r.small} 300w, ${r.medium} 1024w`,r["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){var n;let r=null!==(n=t.taxonomies[i][s])&&void 0!==n&&n;if(!r)continue;let a=o.cloneNode(!0),l=a.querySelector("a");l&&([l.href,l.title,l.textContent]=[r.url,`See more ${r.title}`,r.title],e.append(a))}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=t}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.fields.number} Tx`),s&&(s.textContent=e.fields.number),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach((e=>e.remove()))}addPlaceholders(){let e=this.contentTypes.length;const t=document.createDocumentFragment();for(let i=0;i<12;i++){let i,s=window.getTemplate("placeholderTemplate"),r=Math.floor(Math.random()*e);i=this.ui.content&&this.ui.content.length>0?this.ui.content.filter((e=>e.value===this.contentTypes[r])).querySelector(".icon").cloneNode(!0):window.getIcon(this.container.dataset.icon),s.append(i),t.append(s)}this.ui.grid.append(t)}updateFilter(e){let t=["taxonomy","favourites","match",...Object.keys(this.filters)];e=Object.keys(e).filter((e=>t.includes(e))).reduce(((t,i)=>(t[i]=e[i],t)),{}),window.getDifferences.map(this.filters,e)&&(this.filters={...this.filters,...e},this.updateURL(),this.store.setFilters(e))}updateContentFor(e){this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]').forEach((t=>{const i=t.dataset.for?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)})),this.ui.filterContainer.querySelectorAll("[data-for]").forEach((t=>{const i=t.dataset.for?.split(",")||[];i.length>0&&(t.hidden=!i.includes(e),t.hidden&&t.checked&&(t.checked=!1))}));const t=this.ui.filterContainer.querySelector('[name="orderby"]:checked');this.updateOrderDirectionVisibility(t?.value)}updateOrderDirectionVisibility(e){const t=this.ui.filterContainer.querySelector(".order-direction");if(t){const i=t.dataset.forOrder?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)}}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.imageObserver=null,this.resizeObserver=null,"IntersectionObserver"in window&&(this.imageObserver=new IntersectionObserver((e=>{e.forEach((e=>{this.loadImage(e.target),this.imageObserver.unobserve(e.target)}))}),{rootMargin:"100px",threshold:.1})),"ResizeObserver"in window?this.resizeObserver=new ResizeObserver((()=>{window.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})):window.addEventListener("resize",(()=>{window.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.elements.loadMore)?this.nextPage():window.targetCheck(e,this.elements.clearFilter)?this.clearAllTaxonomies():window.targetCheck(e,".remove-item")&&this.handleRemoveSelectedTerm(e)}handleRemoveSelectedTerm(e){const t=e.target.closest(".selected-item");if(!t)return;const i=parseInt(t.dataset.id),s=t.dataset.taxonomy;this.taxonomyFilters[s]&&(this.taxonomyFilters[s]=this.taxonomyFilters[s].filter((e=>e!==i)),0===this.taxonomyFilters[s].length&&delete this.taxonomyFilters[s]),t.remove(),this.updateFilter({taxonomy:Object.keys(this.taxonomyFilters).length>0?this.taxonomyFilters:null,page:1})}handleChange(e){let t=e.target;Object.hasOwn(t.dataset,"filter")&&("content"===t.dataset.filter?(this.updateContentFor(t.value),this.updateFilter({content:t.value,page:1})):"orderby"===t.dataset.filter?(this.updateOrderDirectionVisibility(t.value),this.updateFilter({orderby:t.value,page:1})):"order"===t.dataset.filter?this.updateFilter({order:t.value,page:1}):"match"===t.dataset.filter?this.updateFilter({match:t.checked?"all":"any",page:1}):"favourites"===t.dataset.filter&&this.updateFilter({favourites:t.checked,page:1}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.feedBlock=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.initListeners(),this.initFilters(),"requestIdleCallback"in window?requestIdleCallback((()=>{this.initStore(),this.initTaxonomies(),this.initGallery()}),{timeout:2e3}):setTimeout((()=>{this.initStore(),this.initTaxonomies(),this.initGallery()}),100)}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".filters",content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters"}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.content=this.ui.filters.container.querySelectorAll('[name="content"]'),0===this.ui.content.length&&(this.ui.content=!1),this.ui.taxonomies=this.ui.filters.container.querySelectorAll("[data-taxonomy]"),0===this.ui.taxonomies.length&&(this.ui.content=!1),this.ui.orderbyWrap=this.ui.filters.container.querySelector("[data-for-order]"),0===this.ui.orderbyWrap.length&&(this.ui.content=!1),this.ui.order=this.ui.filters.container.querySelectorAll('[data-filter="order"]'),0===this.ui.order.length&&(this.ui.content=!1),this.ui.orderby=this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'),0===this.ui.orderby.length&&(this.ui.content=!1),this.contentTypes=this.ui.content?Array.from(this.ui.content).map((e=>e.value)):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map((e=>e.dataset.taxonomy)):[]}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.contentTypes[0],orderby:"date",order:"desc",page:1};this.config.context&&(e.context=this.config.context),this.config.source&&(e.source=this.config.source),this.filters=e,this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach((e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.filters,e))break;let s=this.filters[e]===i;if(s){t.checked=s;break}}})),Object.hasOwn(this.filters,"taxonomy")))for(let[e,t]of Object.entries(this.filters.taxonomy))t.forEach((e=>{e=parseInt(e);const t=this.selector.store.get(e);t&&this.createTermElement(t)}))}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t)}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach((e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()})),this.store.setFilters({...this.defaults,taxonomy:null})}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e)}getFieldId(e){var t;return this.selector.getFieldId(null!==(t=Array.from(this.ui.taxonomies).filter((t=>t.dataset.taxonomy===e))[0])&&void 0!==t?t:null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter((e=>e!==t)),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){[this.ui.taxonomies,this.ui.orderby].forEach((t=>{t&&t.forEach((t=>{var i;const s=null!==(i=t.dataset.for?.split(","))&&void 0!==i?i:[];t.hidden=s.length>0&&!s.includes(e),t.hidden&&t.checked&&(t.checked=!1)}))}))}updateOrderOptions(e){if(this.ui.orderbyWrap){var t;let i=null!==(t=this.ui.orderbyWrap.dataset.forOrder.split(","))&&void 0!==t?t:[];this.ui.orderbyWrap.hidden=!i.includes(e)}}updateFilterControls(){const e=0===Object.keys(this.taxFilters).length;this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=e),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach((e=>{this.createTermElement(e)})),this.updateFilterControls())}getTaxonomyIcon(e){let t=Array.from(this.ui.taxonomies).find((t=>t.dataset.taxonomy===e));return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);if(!t)return;if(this.ui.selected.querySelector(`[data-id="${e}"]`))return;let i=this.getTaxonomyIcon(t.taxonomy),s=window.getTemplate("feedTerm");if(!s)return;let[r,o]=[s.querySelector(".icon"),s.querySelector("span")];r&&o&&(s.dataset.id=t.id,s.dataset.taxonomy=t.taxonomy,r.className=`icon icon-${i}`,o.textContent=t.name,this.ui.selected.append(s))}processCachedFilters(){Object.keys(this.filters).forEach((e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)}))}processURLFilters(){if(this.filters.page>1)return!1;const e=new URLSearchParams(window.location.search);return!!e.toString()&&(this.allowedFilters.forEach((t=>{let i=e.get(`f_${t}`);i&&(this.filters[t]=i)})),e.forEach(((e,t)=>{if(t.startsWith("f_tax_")){const i=t.replace("f_tax_","");this.taxFilters[i]=e.split(",").map(Number)}})),!0)}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach((t=>{Object.hasOwn(this.filters,t)&&this.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.filters[t])}));for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname&&window.history.pushState({filters:this.filters},"",t)}saveToCacheFilters(){Object.keys(this.filters).forEach((e=>{this.filters[e]!==this.defaults[e]&&this.cache.set(`${this.config.source}_${this.config.context}_${e}`,this.filters[e])})),Object.keys(this.taxFilters).length>0&&this.cache.set(`${this.config.source}_${this.config.context}_taxonomy`,this.taxFilters)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe(((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()}))}initStore(){const e=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"});this.store=e.feed,this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":var i;this.renderItems(),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=null===(i=!this.store.lastResponse?.has_more)||void 0===i||i);break;case"filters-changed":this.updateURL(),this.saveToCacheFilters()}}))}isFirstPage(){return 1===this.store.filters.page}renderItems(){let e=this.store.getFiltered();if(this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length)this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage());else{const t=document.createDocumentFragment(),i=s=>{const r=Math.min(s+10,e.length);for(let i=s;i<r;i++){const s=e[i],r=this.createItemElement(s);t.append(r)}var o;r<e.length?requestAnimationFrame((()=>i(r))):(this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),null!==(o=this.store.lastResponse?.has_more)&&void 0!==o&&o))};i(0)}this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid);let e=window.getTemplate("emptyState");e&&this.ui.grid.append(e)}createItemElement(e){let t=window.getTemplate(`feedItem${window.uppercaseFirst(e.content)}`);const i=Object.hasOwn(t.dataset,"timeline");for(let[s,r]of Object.entries(e.fields)){if(i&&["timeline","number"].includes(s))continue;let o=t.querySelector(`[data-field="${s}"]`);o&&(""!==r?this.isImageField(e,r)?this.formatImageField(o,r,e):this.isTaxonomyField(e,s)?this.formatTaxonomyField(o,e,s,r):this.isTimeField(o)?this.formatTimeField(o,r):this.formatField(o,r):o.remove())}let s=t.querySelector("a");var r;return s&&""!==e.url&&([s.href,s.title]=[e.url,`View ${null!==(r=e.fields.post_title)&&void 0!==r?r:"Item"}`]),i&&this.addTimelineElements(e,t),t}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>e))}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some((t=>Object.keys(e.images).map((e=>parseInt(e))).includes(parseInt(t))))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach((s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)})),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){var s;let r=null!==(s=i.images[t])&&void 0!==s&&s;r&&([e.src,e.srcset,e.alt]=[r.tiny,`${r.tiny} 50w, ${r.small} 300w, ${r.medium} 1024w`,r["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){var a;let r=null!==(a=t.taxonomies[i][s])&&void 0!==a&&a;if(!r)continue;let n=o.cloneNode(!0),l=n.querySelector("a");l&&([l.href,l.title,l.textContent]=[r.url,`See more ${r.title}`,r.title],e.append(n))}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=t}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.fields.number} Tx`),s&&(s.textContent=e.fields.number),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach((e=>e.remove()))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.feedBlock=new e)}))}))})();
\ No newline at end of file
diff --git a/inc/blocks/FeedBlock.php b/inc/blocks/FeedBlock.php
index 8c630dd..71f30c9 100644
--- a/inc/blocks/FeedBlock.php
+++ b/inc/blocks/FeedBlock.php
@@ -292,8 +292,8 @@
<div class="selected-items-section">
<div class="selected-items row"></div>
<div class="filter-actions row">
- <?= jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', false, ['filter' => 'match']) ?>
- <button type="button" class="clear-filters row">
+ <?= str_replace('class="toggle-text"', 'class="toggle-text" hidden', jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', false, ['filter' => 'match'])) ?>
+ <button type="button" class="clear-filters row" hidden>
<?= jvbIcon('x') ?>
Clear All Filters
</button>
@@ -391,7 +391,7 @@
}
}
-
+ echo '<template class="feedTerm"><button class="remove-term">'.jvbIcon(jvbDefaultIcon()).'<span></span>'.jvbIcon('x').'</button></template>';
echo '<template class="emptyState">'.apply_filters('jvbFeedEmptyState', '<div class="empty-state">
<h3>'.jvbIcon($this->getIcon()).'NOTHING HERE'.jvbIcon($this->getIcon()).'</h3>
<p>Try tweaking those filters a bit.</p>
diff --git a/inc/forms/TaxonomySelector.php b/inc/forms/TaxonomySelector.php
index f9d252b..eccabc2 100644
--- a/inc/forms/TaxonomySelector.php
+++ b/inc/forms/TaxonomySelector.php
@@ -135,7 +135,7 @@
<!-- Search section -->
<div class="search-wrapper">
<div class="search-bar">
- <?= jvbSearch('Search terms') ?>
+ <?= jvbSearch('Search terms', 'search-terms') ?>
</div>
</div>
@@ -155,8 +155,6 @@
<option value="0">None (Top Level)</option>
</select>
</div>
-
- <button type="button" class="submit-term">Add Term</button>
</form>
</div>
@@ -168,14 +166,14 @@
<p>{ <span>loading items</span> }</p>
</template>
<template class="autocompleteItem">
- <button class="autocomplete item" type="button" data-autocomplete-select></button>
+ <li class="autocomplete item btn"></li>
</template>
<template class="noTermResults">
<p>{ <span>nothing found</span> }</p>
</template>
<template class="termListItem">
<li>
- <input type ="checkbox">
+ <input type="checkbox">
<label>
<span class="term-name"></span>
</label>
@@ -247,7 +245,7 @@
<p class="message" hidden aria-live="polite">
{ <span>loading items</span> }
</p>
- <button class="submit-term" hidden data-ignore><strong>Create: </strong>"<span></span>"</button>
+ <button class="submit-term" hidden data-ignore><strong>Create: </strong> "<span></span>"</button>
</div>
<?php } ?>
@@ -271,7 +269,16 @@
protected function renderTaxonomyToggle(array $selected = [], string $extra = ''): string
{
- return '<button type="button" data-filter="taxonomy" data-taxonomy="'.$this->name.'" title="Filter by '.$this->singular.'">'.jvbIcon($this->config['icon']).'<span class="label">'.$this->singular.'</span></button>';
+ return sprintf(
+ '<button type="button" data-icon="%s" data-filter="taxonomy" data-taxonomy="%s" data-type="selector" data-single="%s" data-plural="%s" title="Filter by %s">%s<span class="label">%s</span></button>',
+ JVB_TAXONOMY[$this->name]['icon'],
+ $this->name,
+ $this->singular,
+ $this->plural,
+ $this->singular,
+ jvbIcon($this->config['icon']),
+ $this->singular
+ );
}
/**
diff --git a/inc/helpers/ui.php b/inc/helpers/ui.php
index bbd289a..256d908 100644
--- a/inc/helpers/ui.php
+++ b/inc/helpers/ui.php
@@ -205,19 +205,26 @@
* Outputs the search bar (likely don't need anymore)
* @return string
*/
-function jvbSearch(string $placeholder = 'Search...'):string
+function jvbSearch(string $placeholder = 'Search...', string $id = 'search'):string
{
- return '<div class="search-container row start nowrap">
- <input type="search" id="search" placeholder="'.$placeholder.'">
- <button
- title="Clear Search"
- type="button"
- class="clear-search"
- aria-label="Clear search"
- onclick="this.previousElementSibling.value = \'\'; this.previousElementSibling.focus();"
- >'.jvbIcon('x', ['title'=> 'Clear Search']).'</button>
- <button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">'.jvbIcon('magnifying-glass').'</button>
-</div>';
+ $id = sanitize_title($id);
+ return sprintf(
+ '<div class="search-container row start nowrap">
+ <input type="search" id="%s" placeholder="%s">
+ <button
+ title="Clear Search"
+ type="button"
+ class="clear-search"
+ aria-label="Clear search"
+ onclick="this.previousElementSibling.value = \'\'; this.previousElementSibling.focus();"
+ >%s</button>
+ <button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">%s</button>
+ </div>',
+ $id,
+ $placeholder,
+ jvbIcon('x', ['title'=> 'Clear Search']),
+ jvbIcon('magnifying-glass')
+ );
}
diff --git a/inc/rest/routes/TermRoutes.php b/inc/rest/routes/TermRoutes.php
index fe5de06..ec029c8 100644
--- a/inc/rest/routes/TermRoutes.php
+++ b/inc/rest/routes/TermRoutes.php
@@ -24,7 +24,7 @@
parent::__construct();
// $this->cache->invalidateGroup('terms');
$this->per_page = 20;
- $this->cache->clear();
+
add_action('edited_term', [$this, 'deleteTermPath']);
add_action('wp_login', [$this, 'clearUserTaxonomyCache'], 10, 2);
}
@@ -182,8 +182,7 @@
public function handleTermSelectionRequest(WP_REST_Request $request):WP_REST_Response
{
$data = $request->get_params();
- $taxonomy = jvbCheckBase($data['taxonomy'])??'';
-
+ $taxonomy = sanitize_text_field($data['taxonomy'])??'';
// Check HTTP cache headers
$cache_check = $this->checkHeaders($request, $taxonomy);
if ($cache_check) {
@@ -195,6 +194,7 @@
if (str_contains($taxonomy, ',')) {
return $this->handleBatchTermRequest($taxonomy, $data, $request);
}
+ $taxonomy = jvbCheckBase($taxonomy);
if (array_key_exists('termIDs', $data)) {
$args = [
diff --git a/src/feed/style.scss b/src/feed/style.scss
index b126889..937092f 100644
--- a/src/feed/style.scss
+++ b/src/feed/style.scss
@@ -647,6 +647,11 @@
padding: 1rem 0;
max-width:var(--wide);
margin: 0 auto;
+
+ .remove-term.remove-term {
+ width: max-content;
+ height: max-content
+ }
}
.filter-group {
position: relative;
@@ -752,6 +757,8 @@
img {
object-fit: cover;
+ width: 100%;
+ height: 100%;
//opacity: .7;
//filter: grayscale(.5) sepia(.3);
&:hover {
@@ -824,3 +831,103 @@
grid-template-columns: repeat(2, 1fr);
}
}
+
+.items-wrap [type=radio],
+.items-wrap [type=checkbox] {
+ position: absolute;
+ opacity: 0;
+ left: -200vw;
+}
+
+.items-wrap [type=radio] + label,
+.items-wrap [type=checkbox] + label {
+ position: relative;
+ cursor: pointer;
+}
+
+.items-wrap [type=radio] + label:hover,
+.items-wrap [type=checkbox] + label:hover {
+ color: var(--action-0);
+}
+
+.items-wrap [type=radio] + label::before,
+.items-wrap [type=checkbox] + label::before,
+.items-wrap [type=radio] + label::after,
+.items-wrap [type=checkbox] + label::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+}
+
+.items-wrap [type=radio] + label::after,
+.items-wrap [type=checkbox] + label::after {
+ left: 5px;
+ transform: translateY(-70%) rotate(45deg);
+ width: 5px;
+ height: 10px;
+ border: solid var(--light-0);
+ border-width: 0 2px 2px 0;
+ display: none;
+}
+
+.items-wrap [type=radio] + label::before,
+.items-wrap [type=checkbox] + label::before {
+ left: 0;
+ transform: translateY(-50%);
+ width: 1rem;
+ height: 1rem;
+ border: 2px solid var(--contrast-200);
+ background-color: var(--base);
+ border-radius: var(--radius);
+}
+
+.items-wrap [type=radio]:hover + label::before,
+.items-wrap [type=checkbox]:hover + label::before {
+ border-color: var(--action-200);
+}
+
+.items-wrap [type=radio]:checked + label::before,
+.items-wrap [type=checkbox]:checked + label::before{
+ background-color: var(--action-0);
+ border-color: var(--action-100);
+}
+
+.items-wrap [type=radio]:checked + label::before {
+ border-radius: 50%;
+}
+
+.items-wrap [type=checkbox]:checked + label::after {
+ display: block;
+ left: 5px;
+ top: 50%;
+ transform: translateY(-70%) rotate(45deg);
+ width: .35rem;
+ height: .66rem;
+ border: solid var(--light-0);
+ border-width: 0 2px 2px 0;
+}
+
+.items-wrap :disabled + label {
+ cursor: not-allowed;
+ background-color: var(--base-50);
+ color: var(--base-200);
+ border-color: var(--base-200);
+}
+
+.items-wrap :disabled + label:hover {
+ background-color: var(--base-50);
+ color: var(--base-200);
+ border-color: var(--base-200);
+}
+
+.items-wrap :disabled + label::before {
+ border-color: var(--base-200);
+}
+
+#jvb-selector .items-wrap [type=radio] + label,
+#jvb-selector .items-wrap [type=checkbox] + label{
+ flex: 1;
+ padding-left: 2rem!important;
+ transform-origin: top center;
+ will-change: transform;
+}
diff --git a/src/feed/view.js b/src/feed/view.js
index 94da05e..4e118c4 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -1,13 +1,11 @@
class FeedBlock {
constructor() {
this.container = document.querySelector('section.feed-block');
- if (!this.container) {
- return;
- }
+ if(!this.container) return;
this.a11y = window.jvbA11y;
- this.cache = new window.jvbCache('feed');
this.error = window.jvbError;
+ this.cache = new window.jvbCache('feed');
this.config = {
source: '',
@@ -17,186 +15,398 @@
view: this.cache.get('feedView') || 'grid',
... this.container.dataset
};
+
+ this.init();
+ }
+ init() {
this.initElements();
+ this.initListeners();
this.initFilters();
-
- this.loadWhenAble();
- }
-
- loadWhenAble() {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
- this.initTaxonomies();
this.initStore();
- this.initListeners();
+ this.initTaxonomies();
this.initGallery();
}, { timeout: 2000 });
} else {
setTimeout(() => {
- this.initTaxonomies();
this.initStore();
- this.initListeners();
+ this.initTaxonomies();
this.initGallery();
}, 100);
}
}
initElements() {
- this.currentTaxonomies = new Set(); // Allowed Taxonomies, grabbed from active buttons
- this.taxonomyFilters = {};
- this.elements = {
+ this.selectors = {
filterTrigger: '[data-filter]',
filters: {
- content: '[data-filter="content"]',
- orderby: '[data-filter="orderby"]',
- order: '[data-filter="order"]',
- match: '[data-filter="match"]',
+ actions: '.filter-actions .toggle-text',
+ container: '.filters',
+ content: '[data-filter="content"]',
+ orderby: '[data-filter="orderby"]',
+ order: '[data-filter="order"]',
+ match: '[data-filter="match"]',
favourites: '[data-filter="favourites"]',
- taxonomy: '[data-filter^="taxonomy"]'
+ taxonomy: '[data-filter^="taxonomy"]',
},
- selectedTax: '.selected-items',
- clearFilter: 'button.clear-filters',
- loadMore: 'button.load-more',
- filterContainer: '.filters',
- grid: '.item-grid',
+ grid: '.item-grid',
+ selected: '.selected-items',
+ buttons: {
+ loadMore: 'button.load-more',
+ remove: '.remove-term',
+ clearFilters: 'button.clear-filters'
+ }
};
- this.ui = window.uiFromSelectors(this.elements);
+ this.ui = window.uiFromSelectors(this.selectors, this.container);
+ //Add content and taxonomies
+ this.ui.content = this.ui.filters.container.querySelectorAll('[name="content"]');
+ if (this.ui.content.length === 0) this.ui.content = false;
+ this.ui.taxonomies = this.ui.filters.container.querySelectorAll('[data-taxonomy]');
+ if (this.ui.taxonomies.length === 0) this.ui.content = false;
+ this.ui.orderbyWrap = this.ui.filters.container.querySelector('[data-for-order]');
+ if (this.ui.orderbyWrap.length === 0) this.ui.content = false;
+ this.ui.order = this.ui.filters.container.querySelectorAll('[data-filter="order"]');
+ if (this.ui.order.length === 0) this.ui.content = false;
+ this.ui.orderby = this.ui.filters.container.querySelectorAll('[data-filter="orderby"]');
+ if (this.ui.orderby.length === 0) this.ui.content = false;
- this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false;
- this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
- if (this.ui.content && this.ui.content.length > 0) {
- this.contentTypes = Array.from(
- this.ui.content
- ).map(content => content.value);
- } else {
- this.contentTypes = [this.container.dataset['content']];
- }
-
- if (this.ui.taxonomies.length>0) {
- this.taxonomies = Array.from(
- this.ui.taxonomies,
- ).map(content => content.dataset.taxonomy);
- } else {
- this.taxonomies = [];
- }
-
-
+ this.contentTypes = (this.ui.content)
+ ? Array.from(this.ui.content).map(c => c.value)
+ : [this.container.dataset.content];
+ this.taxonomies = (this.ui.taxonomies?.length > 0)
+ ? Array.from(this.ui.taxonomies).map(t => t.dataset.taxonomy)
+ : [];
}
- async initTaxonomies() {
- this.selector = window.jvbSelector;
- const buttons = document.querySelectorAll('[data-filter="taxonomy"]');
+ initListeners() {
+ this.popStateHandler = this.handlePopState.bind(this);
+ this.clickHandler = this.handleClick.bind(this);
+ this.changeHandler = this.handleChange.bind(this);
- this.selector.isInitializing = true;
- buttons.forEach((button) => {
- const taxonomy = button.dataset.taxonomy;
- this.currentTaxonomies.add(taxonomy);
-
- this.selector.registerFilterButton(button, {
- button: button,
- buttonSelector: '[data-filter="taxonomy"]',
- selected: this.ui.selectedTax
- });
-
- // Add preload listeners
- this.addTaxonomyPreloadListeners(button, taxonomy);
- });
-
- this.selector.isInitializing = false;
-
- this.selector.subscribe((event, data) => {
- if (event === 'selected-terms') this.handleTaxonomyChange(data);
- });
- }
-
- addTaxonomyPreloadListeners(button, taxonomy) {
- const preload = () => {
- this.selector.preloadTaxonomy(taxonomy);
- };
-
- // Desktop hover
- button.addEventListener('mouseenter', preload, { once: true });
-
- // Touch/keyboard (fires before click)
- button.addEventListener('pointerdown', preload, { once: true });
-
- // Keyboard focus
- button.addEventListener('focus', preload, { once: true });
- }
-
- handleTaxonomyChange(data) {
- const { terms, taxonomy } = data;
-
- // Update only the current taxonomy's terms
- if (terms.size > 0) {
- this.taxonomyFilters[taxonomy] = Array.from(terms.keys());
- } else {
- // Remove taxonomy if no terms selected
- delete this.taxonomyFilters[taxonomy];
- }
-
- // Build filters object with all taxonomies
- let filters = {
- page: 1
- };
-
- // Add taxonomy filters if any exist
- if (Object.keys(this.taxonomyFilters).length > 0) {
- filters.taxonomy = this.taxonomyFilters;
- }
-
- this.updateFilter(filters);
- }
-
- clearAllTaxonomies() {
- this.taxonomyFilters = {};
- window.removeChildren(this.ui.selectedTax);
-
- this.updateFilter({
- taxonomy: null,
- page: 1
- });
+ window.addEventListener('popstate', this.popStateHandler);
+ document.addEventListener('click', this.clickHandler);
+ document.addEventListener('change', this.changeHandler);
}
initFilters() {
- //defaults
- this.filters = {
- content: this.contentTypes[0],
- orderby: 'date',
- order: 'desc',
- page: 1
+ this.allowedFilters = ['content', 'order', 'orderby', 'favourites', 'match'];
+ let defaults = {
+ content: this.contentTypes[0],
+ orderby: 'date',
+ order: 'desc',
+ page: 1,
};
- if (this.config.context) this.filters.context = this.config.context;
- if (this.config.source) this.filters.source = this.config.source;
+ if (this.config.context) defaults.context = this.config.context;
+ if (this.config.source) defaults.source = this.config.source;
- //check the cache
+ this.filters = defaults;
this.processCachedFilters();
- //check url
this.processURLFilters();
-
- // Set initial UI state
- this.syncUIToFilters();
+ this.updateFilterUI();
+ this.defaults = {...defaults};
}
- syncUIToFilters() {
- if (this.ui.filterContainer) {
- // Check radio buttons
- Object.entries(this.filters).forEach(([key, value]) => {
- const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`);
- if (input) {
- input.checked = true;
+ updateFilterUI() {
+ if (this.ui.filters.container) {
+ //Get cached inputs
+ let groups = [
+ this.ui.content,
+ this.ui.orderby,
+ this.ui.order
+ ];
+
+ groups.forEach(group => {
+ if(group) {
+ for (let input of group) {
+ let [filter, value] = [input.dataset.filter, input.value];
+ if (!Object.hasOwn(this.filters, filter)) break;
+ let doit = this.filters[filter] === value;
+ if (doit) {
+ input.checked = doit;
+ break;
+ }
+ }
+ }
+ });
+
+ if (Object.hasOwn(this.filters, 'taxonomy')) {
+ for (let [taxonomy, terms] of Object.entries(this.filters.taxonomy)) {
+ terms.forEach(termId => {
+ termId = parseInt(termId);
+ const term = this.selector.store.get(termId);
+ if (term) {
+ this.createTermElement(term);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ handlePopState(e) {
+ if (e.state?.filters) {
+ if (this.processURLFilters()) {
+ this.store.setFilters(this.filters);
+ this.a11y.announce('Feed filters updated from browser history');
+ }
+ }
+ }
+
+ handleClick(e) {
+ if (window.targetCheck(e, this.selectors.buttons.loadMore)) {
+ this.nextPage();
+ } else if (window.targetCheck(e, this.selectors.buttons.clearFilters)) {
+ this.clearFilters();
+ }
+ let remove = window.targetCheck(e, this.selectors.buttons.remove);
+ if (remove) {
+ this.removeSelectedTerm(remove);
+ }
+ }
+
+ nextPage() {
+ const nextPage = (this.store.filters.page || 1) + 1;
+ const maxPage = this.store.lastResponse?.pages || nextPage;
+ this.store.setFilters({ page: Math.min(nextPage, maxPage) });
+ }
+
+ handleChange(e) {
+ const target = e.target;
+ if (Object.hasOwn(target.dataset, 'filter')) {
+ if (this.allowedFilters.includes(target.dataset.filter)) {
+ let filters = {};
+ filters[target.dataset.filter] = target.value;
+ this.resetFilters(filters);
+ }
+ switch (target.dataset.filter) {
+ case 'content':
+ this.updateContentFor(target.value);
+ break;
+ case 'orderby':
+ this.updateOrderOptions(target.value);
+ break;
+ }
+ }
+ }
+
+ clearFilters() {
+ this.taxFilters = {};
+ window.removeChildren(this.ui.selected);
+
+ this.taxonomies.forEach(tax => {
+ let fieldId = this.getFieldId(tax);
+ this.selector.selectedTerms.get(fieldId)?.clear();
+ });
+
+ this.store.setFilters({
+ ...this.defaults,
+ taxonomy: null
+ });
+ }
+
+ resetFilters(filters) {
+ filters = {
+ ...this.store.filters,
+ page: 1,
+ ... filters
+ }
+ this.store.setFilters(filters);
+ }
+
+ getFieldId(taxonomy) {
+ return this.selector.getFieldId(Array.from(this.ui.taxonomies).filter(tax => tax.dataset.taxonomy === taxonomy)[0]??null);
+ }
+ removeSelectedTerm(button) {
+ const termId = parseInt(button.dataset.id);
+ const taxonomy = button.dataset.taxonomy;
+
+ if (Object.hasOwn(this.taxFilters, taxonomy)){
+ this.taxFilters[taxonomy] = this.taxFilters[taxonomy]
+ .filter(id => id !== termId);
+ if (this.taxFilters[taxonomy].length === 0) {
+ delete this.taxFilters[taxonomy];
+ }
+ }
+ button.remove();
+
+ // Find the fieldId for this taxonomy
+ const field = this.getFieldId(taxonomy);
+ if (field) {
+ this.selector.activeField = field;
+ // Notify selector to remove from its selectedTerms
+ this.selector.removeSelected(termId, field);
+ }
+
+ this.resetFilters({
+ taxonomy: Object.keys(this.taxFilters).length > 0
+ ? this.taxFilters
+ : null
+ });
+ }
+
+ updateContentFor(content) {
+ let checkIt = [
+ this.ui.taxonomies,
+ this.ui.orderby
+ ];
+ checkIt.forEach(check => {
+ if (!check) return;
+ check.forEach(button => {
+ const forTypes = button.dataset.for?.split(',')??[];
+ button.hidden = forTypes.length > 0 && !forTypes.includes(content);
+ if (button.hidden && button.checked) {
+ button.checked = false;
+ }
+ });
+ });
+ }
+ updateOrderOptions(order) {
+ if (this.ui.orderbyWrap) {
+ let options = this.ui.orderbyWrap.dataset.forOrder.split(',')??[];
+ this.ui.orderbyWrap.hidden = !options.includes(order);
+ }
+ }
+
+ updateFilterControls() {
+ const isHidden = Object.keys(this.taxFilters).length === 0;
+ if (this.ui.buttons.clearFilters) {
+ this.ui.buttons.clearFilters.hidden = isHidden;
+ }
+ if (this.ui.filters.actions) {
+ this.ui.filters.actions.hidden = isHidden;
+ }
+ }
+
+ async initTaxonomies() {
+ this.taxFilters = {};
+ this.selector = window.jvbSelector;
+ // this.selector.scanExistingFields(this.ui.filters.container);
+ // this.taxonomies.map(tax => this.selector.batchFetch.add(tax));
+ // this.selector.batchFetchTaxonomies();
+ this.selector.subscribe((event, data) => {
+ switch (event) {
+ case 'selected-terms':
+
+ this.handleTaxonomyChange(data);
+ break;
+
+ }
+ });
+ }
+ handleTaxonomyChange(data) {
+ const {terms, taxonomy } = data;
+ if (terms.size === 0) return;
+ this.taxFilters[taxonomy] = Array.from(terms);
+ this.resetFilters({ taxonomy: this.taxFilters });
+
+ terms.forEach(t => {
+ this.createTermElement(t);
+ });
+ this.updateFilterControls();
+ }
+ getTaxonomyIcon(taxonomy) {
+ let iconButton = Array.from(this.ui.taxonomies)
+ .find(t => t.dataset.taxonomy === taxonomy);
+ return iconButton?.dataset.icon.trim() || 'tag';
+ }
+ createTermElement(termId) {
+ const term = this.selector.store.get(termId);
+ if (!term) return;
+ if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
+ let icon = this.getTaxonomyIcon(term.taxonomy);
+ let template = window.getTemplate('feedTerm');
+ if (!template) return;
+ let [iconEl,span] = [template.querySelector('.icon'), template.querySelector('span')];
+ if (!iconEl || !span) return;
+ template.dataset.id = term.id;
+ template.dataset.taxonomy = term.taxonomy;
+ iconEl.className = `icon icon-${icon}`;
+ span.textContent = term.name;
+
+ this.ui.selected.append(template);
+ }
+
+ processCachedFilters() {
+ Object.keys(this.filters).forEach(filter => {
+ let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`);
+ if (cached && cached !== this.filters[filter]) {
+ this.filters[filter] = cached;
+ }
+ });
+ }
+
+ processURLFilters() {
+ if (this.filters.page > 1) return false;
+ const params = new URLSearchParams(window.location.search);
+ if (!params.toString()) return false;
+
+ this.allowedFilters.forEach(filter => {
+ let value = params.get(`f_${filter}`);
+ if (value) {
+ this.filters[filter] = value;
+ }
+ });
+
+ params.forEach((value, key) => {
+ if (key.startsWith('f_tax_')) {
+ const taxonomy = key.replace('f_tax_','');
+ this.taxFilters[taxonomy] = value.split(',').map(Number);
+ }
+ });
+ return true;
+ }
+
+ updateURL() {
+ const params = new URLSearchParams();
+ this.allowedFilters.forEach(key => {
+ if (Object.hasOwn(this.filters, key) && this.filters[key] !== this.defaults[key]) {
+ params.set(`f_${key}`, this.filters[key]);
+ }
+ });
+
+ for (let [tax, terms] of Object.entries(this.taxFilters)) {
+ if (terms.length > 0) {
+ params.set(`f_tax_${tax}`, terms.join(','));
+ }
+ }
+ const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
+ if (newURL !== window.location.pathname) {
+ window.history.pushState({filters:this.filters}, '', newURL)
+ }
+ }
+
+ saveToCacheFilters() {
+ // Save each filter that's changed from defaults
+ Object.keys(this.filters).forEach(filter => {
+ if (this.filters[filter] !== this.defaults[filter]) {
+ this.cache.set(
+ `${this.config.source}_${this.config.context}_${filter}`,
+ this.filters[filter]
+ );
+ }
+ });
+
+ // Also save taxonomy filters
+ if (Object.keys(this.taxFilters).length > 0) {
+ this.cache.set(
+ `${this.config.source}_${this.config.context}_taxonomy`,
+ this.taxFilters
+ );
+ }
+ }
+
+ initGallery() {
+ this.gallery = (this.config.gallery) ? window.jvbGallery : false;
+ if (this.gallery) {
+ this.gallery.subscribe((event, data) => {
+ if (event === 'load-more' && this.store.lastResponse?.has_more) {
+ this.nextPage();
}
});
}
-
- // Update content-specific visibility
- this.updateContentFor(this.filters.content);
}
- nextPage() {
- this.store.setFilter('page', this.store.filters.page++);
- }
-
initStore() {
const store = window.jvbStore.register(
'feed',
@@ -212,10 +422,9 @@
{ name: 'title', keyPath: 'title'}
],
filters: this.filters,
- TTL: 6 * 60 * 60 * 1000,
+ TTL: 6 * 60 * 60 * 1000, //6 hours
showLoading: true,
required: 'content',
- delayFetch: true
}
);
this.store = store.feed;
@@ -224,192 +433,81 @@
switch (event) {
case 'data-loaded':
this.renderItems();
- this.ui.loadMore.hidden = true;
- if (this.store.lastResponse && this.store.lastResponse['has_more']) {
- this.ui.loadMore.hidden = !this.store.lastResponse['has_more'];
+ this.ui.buttons.loadMore.hidden = true;
+ if (this.store.lastResponse && this.store.lastResponse?.has_more) {
+ this.ui.buttons.loadMore.hidden = !this.store.lastResponse?.has_more??true;
}
break;
+ case 'filters-changed':
+ this.updateURL();
+ this.saveToCacheFilters();
+ break;
}
});
}
- initGallery() {
- this.gallery = (this.config.gallery) ? window.jvbGallery : false;
- if (this.gallery) {
- this.gallery.subscribe((event, data) => {
- if (event === 'load-more' && this.store.lastResponse) {
- if (this.store.lastResponse['has_more']) {
- this.nextPage();
- }
- }
- });
- }
- }
-
- processCachedFilters() {
- Object.keys(this.filters).forEach(filter => {
- let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`);
- if (cached && cached !== this.filters[filter]){
- this.filters[filter] = cached;
- }
- });
- }
-
- processURLFilters() {
- if (this.filters.page > 1) {
- return false;
- }
- const params = new URLSearchParams(window.location.search);
-
- if (!params.toString()) {
- return false;
- }
- let filters = ['content', 'order', 'orderby', 'favourites', 'match'];
- filters.forEach(filter => {
- let value = params.get(`f_${filter}`);
- if (value) {
- this.filters[filter] = value;
- let input = this.ui.filters[filter];
- if (input) {
- input.checked = true;
- }
- }
- });
-
- let hasTaxonomy = false;
- // Load taxonomy filters from URL
- params.forEach((value, key) => {
- if (key.startsWith('f_tax_')) {
- hasTaxonomy = true;
- const taxonomy = key.replace('f_tax_', '');
- if (!this.taxonomyFilters[taxonomy]) {
- this.taxonomyFilters[taxonomy] = [];
- }
- this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
- }
- });
- if (this.ui.filterContainer && hasTaxonomy) {
- for (let [tax, ids] in Object.entries(this.taxonomyFilters)) {
- let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`);
- if (button) {
- if (button.dataset.fieldId) {
- let field = this.selector.get(button.dataset.fieldId);
- field.selectedTerms = new Set(ids);
- this.selector.initFieldDisplay(button.dataset.fieldId);
- } else {
- this.selector.registerField(button, {
- button: button,
- buttonSelector: '[data-filter="taxonomy"]',
- selected: this.ui.selectedTax,
- selectedItems: ids
- });
- }
- }
- }
- }
- return true;
- }
-
- /**
- * Update URL with current filters (for sharing/bookmarking)
- */
- updateURL() {
- const params = new URLSearchParams();
-
- // Add simple filters
- ['content', 'order', 'orderby', 'match'].forEach(key => {
- if (this.filters[key]) {
- params.set(`f_${key}`, this.filters[key]);
- }
- });
-
- // Add taxonomy filters
- Object.entries(this.taxonomyFilters).forEach(([taxonomy, terms]) => {
- if (terms.length > 0) {
- params.set(`f_tax_${taxonomy}`, terms.join(','));
- }
- });
-
- // Update URL without reload
- const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
- window.history.pushState({ filters: this.filters }, '', newURL);
+ isFirstPage() {
+ return this.store.filters.page === 1;
}
renderItems() {
let items = this.store.getFiltered();
- if (this.store.filters['page'] === 1) {
+ if (this.isFirstPage()) {
window.removeChildren(this.ui.grid);
}
-
if (items.length === 0) {
- this.a11y.announceItems(0, this.store.filters['page'] > 0);
- return;
- }
-
- const fragment = document.createDocumentFragment();
- const batchSize = 10;
-
- const processBatch = (startIndex) => {
- const endIndex = Math.min(startIndex + batchSize, items.length);
-
- for (let i = startIndex; i < endIndex; i++) {
- const item = items[i];
- const element = this.createItemElement(item);
-
- fragment.appendChild(element);
- }
-
- if (endIndex < items.length) {
- requestAnimationFrame(() => processBatch(endIndex));
- } else {
- this.removePlaceholders();
- this.ui.grid.append(fragment);
-
- if (this.config.gallery) {
- this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
- }
-
- this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
- this.a11y.announceItems(items.length, this.store.filters['page'] > 1, this.store.hasMore);
- }
- };
-
- if (items.length > 0) {
- processBatch(0);
+ this.showEmptyState();
+ this.a11y.announceItems(0, this.isFirstPage());
} else {
- this.a11y.announceItems(0, this.store.filters['page'] >1, false);
+ const fragment = document.createDocumentFragment();
+ const processBatch = (startIndex) => {
+ const endIndex = Math.min(startIndex + 10, items.length);
+ for (let i = startIndex; i < endIndex; i++) {
+ const item = items[i];
+ const element = this.createItemElement(item);
+ fragment.append(element);
+ }
+ if (endIndex < items.length) {
+ requestAnimationFrame(() => processBatch(endIndex));
+ } else {
+ this.removePlaceholders();
+ this.ui.grid.append(fragment);
+
+ if (this.config.gallery) this.gallery.buildGalleryItems('.item img');
+ this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
+ this.a11y.announceItems(items.length, !this.isFirstPage(), this.store.lastResponse?.has_more??false);
+ }
+ };
+ processBatch(0);
}
- if (this.ui.filters.match) {
- this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
- }
- if (this.ui.clearFilter) {
- this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
- }
+ this.updateFilterControls();
}
- /**
- *
- * @param {object} item
- */
+ showEmptyState() {
+ window.removeChildren(this.ui.grid);
+ let template = window.getTemplate('emptyState');
+ if (!template) return;
+ this.ui.grid.append(template);
+
+ }
+
createItemElement(item) {
let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
-
const isTimeline = Object.hasOwn(template.dataset, 'timeline');
- // Format fields using helpers
for (let [fieldName, value] of Object.entries(item.fields)) {
if (isTimeline && ['timeline', 'number'].includes(fieldName)) continue;
+
let el = template.querySelector(`[data-field="${fieldName}"]`);
if (!el) continue;
-
if (value === '') {
el.remove();
continue;
}
if (this.isImageField(item, value)) {
- this.formatImageFields(el, value, item);
+ this.formatImageField(el, value, item);
} else if (this.isTaxonomyField(item, fieldName)) {
this.formatTaxonomyField(el, item, fieldName, value);
} else if (this.isTimeField(el)) {
@@ -419,16 +517,10 @@
}
}
- // Handle link
let link = template.querySelector('a');
if (link && item.url !== '') {
- [
- link.href,
- link.title
- ] = [
- item.url,
- `View ${item.fields['post_title']??'Item'}`
- ];
+ [link.href, link.title] =
+ [item.url, `View ${item.fields['post_title']??'Item'}`];
}
if (isTimeline) {
@@ -440,6 +532,7 @@
splitIDs(value) {
return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value);
}
+
isImageField(item, value) {
if (!Object.hasOwn(item, 'images') || Object.keys(item.images).length === 0) {
return false;
@@ -471,19 +564,19 @@
this.formatImageField(element, values[0], item);
}
}
- formatImageField(element, value, item) {
- let imgData = item.images[value]??false;
- if (!imgData) return;
- [
- element.src,
- element.srcset,
- element.alt
- ] = [
- imgData.tiny,
- `${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`,
- imgData['image-alt-text']
- ]
- }
+ formatImageField(element, value, item) {
+ let imgData = item.images[value]??false;
+ if (!imgData) return;
+ [
+ element.src,
+ element.srcset,
+ element.alt
+ ] = [
+ imgData.tiny,
+ `${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`,
+ imgData['image-alt-text']
+ ]
+ }
isTaxonomyField(item, field) {
if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) {
return false;
@@ -567,198 +660,24 @@
}
}
-
- addPlaceholders() {
- let total = this.contentTypes.length;
- const fragment = document.createDocumentFragment();
- for (let i = 0; i < 12; i++) {
- let template = window.getTemplate('placeholderTemplate');
-
- let rand = Math.floor(Math.random() * total);
- let icon;
- if (this.ui.content && this.ui.content.length > 0) {
- icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true);
- } else {
- icon = window.getIcon(this.container.dataset.icon);
- }
- template.append(icon);
- fragment.append(template);
- }
- this.ui.grid.append(fragment);
- }
-
-
-
- /**
- *
- * @param {object} filters {name: value}
- */
- updateFilter(filters) {
- //double check filters are what we're expecting
- let allowed = ['taxonomy','favourites','match', ... Object.keys(this.filters)];
-
- filters = Object.keys(filters)
- .filter(key => allowed.includes(key))
- .reduce((obj, key) => {
- obj[key] = filters[key];
- return obj;
- }, {});
-
- if (window.getDifferences.map(this.filters, filters)) {
- this.filters = { ...this.filters, ...filters }; // Merge instead of replace
- this.updateURL();
- this.store.setFilters(filters);
- }
- }
- /**
- * Update visible filters based on selected content type
- */
- updateContentFor(contentType) {
- // Update taxonomy filter visibility
- const taxonomyButtons = this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]');
- taxonomyButtons.forEach(button => {
- const forTypes = button.dataset.for?.split(',') || [];
- button.hidden = forTypes.length > 0 && !forTypes.includes(contentType);
- });
-
- // Update ordering options
- const orderButtons = this.ui.filterContainer.querySelectorAll('[data-for]');
- orderButtons.forEach(button => {
- const forTypes = button.dataset.for?.split(',') || [];
- if (forTypes.length > 0) {
- button.hidden = !forTypes.includes(contentType);
- // Uncheck if hiding
- if (button.hidden && button.checked) {
- button.checked = false;
- }
- }
- });
-
- // Update order direction visibility based on selected orderby
- const orderBy = this.ui.filterContainer.querySelector('[name="orderby"]:checked');
- this.updateOrderDirectionVisibility(orderBy?.value);
- }
-
- /**
- * Show/hide order direction based on orderby selection
- */
- updateOrderDirectionVisibility(orderBy) {
- const orderDirection = this.ui.filterContainer.querySelector('.order-direction');
- if (orderDirection) {
- const forOrders = orderDirection.dataset.forOrder?.split(',') || [];
- orderDirection.hidden = forOrders.length > 0 && !forOrders.includes(orderBy);
- }
- }
- /*********************************************************************
- LISTENERS
- *********************************************************************/
- initListeners() {
- this.popStateHandler = this.handlePopState.bind(this);
- this.clickHandler = this.handleClick.bind(this);
- this.changeHandler = this.handleChange.bind(this);
- this.imageObserver = null;
- this.resizeObserver = null;
- if ('IntersectionObserver' in window) {
- this.imageObserver = new IntersectionObserver(entries => {
- entries.forEach(entry => {
- this.loadImage(entry.target);
- this.imageObserver.unobserve(entry.target);
- });
- }, {
- rootMargin: '100px',
- threshold: .1
- });
- }
-
- if ('ResizeObserver' in window) {
- this.resizeObserver = new ResizeObserver(() => {
- window.debouncer.schedule(
- 'feed-update-images',
- () => this.updateImageSizes(),
- 250
- );
- });
- } else {
- window.addEventListener('resize', () => {
- window.debouncer.schedule(
- 'feed-update-images',
- () => this.updateImageSizes(),
- 250
- );
- });
- }
-
- window.addEventListener('popstate', this.popStateHandler);
- document.addEventListener('click', this.clickHandler);
- document.addEventListener('change', this.changeHandler);
- }
-
- handlePopState(e) {
- if (e.state?.filters) {
- if (this.processURLFilters()) {
- this.store.setFilters(this.filters);
- this.a11y.announce('Feed filters updated from browser history');
- }
- }
- }
-
- handleClick(e) {
- if (window.targetCheck(e, this.elements.loadMore)) {
- this.nextPage();
- } else if (window.targetCheck(e, this.elements.clearFilter)) {
- this.clearAllTaxonomies();
- } else if (window.targetCheck(e, '.remove-item')) {
- this.handleRemoveSelectedTerm(e);
- }
- }
-
- handleRemoveSelectedTerm(e) {
- const selectedItem = e.target.closest('.selected-item');
- if (!selectedItem) return;
-
- const termId = parseInt(selectedItem.dataset.id);
- const taxonomy = selectedItem.dataset.taxonomy;
-
- // Remove from filters
- if (this.taxonomyFilters[taxonomy]) {
- this.taxonomyFilters[taxonomy] = this.taxonomyFilters[taxonomy]
- .filter(id => id !== termId);
-
- if (this.taxonomyFilters[taxonomy].length === 0) {
- delete this.taxonomyFilters[taxonomy];
- }
- }
-
- // Remove from UI
- selectedItem.remove();
-
- // Update filters
- this.updateFilter({
- taxonomy: Object.keys(this.taxonomyFilters).length > 0
- ? this.taxonomyFilters
- : null,
- page: 1
- });
- }
-
- handleChange(e) {
- let target = e.target;
- if (Object.hasOwn(target.dataset, 'filter')) {
- if (target.dataset.filter === 'content') {
- this.updateContentFor(target.value);
- this.updateFilter({ content: target.value, page: 1 });
- } else if (target.dataset.filter === 'orderby') {
- this.updateOrderDirectionVisibility(target.value);
- this.updateFilter({ orderby: target.value, page: 1 });
- } else if (target.dataset.filter === 'order') {
- this.updateFilter({ order: target.value, page: 1 });
- } else if (target.dataset.filter === 'match') {
- this.updateFilter({ match: target.checked ? 'all' : 'any', page: 1 });
- } else if (target.dataset.filter === 'favourites') {
- this.updateFilter({ favourites: target.checked, page: 1 });
- }
- }
- }
+ // addPlaceholders() {
+ // let total = this.contentTypes.length;
+ // const fragment = document.createDocumentFragment();
+ // for (let i = 0; i < 12; i++) {
+ // let template = window.getTemplate('placeholderTemplate');
+ //
+ // let rand = Math.floor(Math.random() * total);
+ // let icon;
+ // if (this.ui.content && this.ui.content.length > 0) {
+ // icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true);
+ // } else {
+ // icon = window.getIcon(this.container.dataset.icon);
+ // }
+ // template.append(icon);
+ // fragment.append(template);
+ // }
+ // this.ui.grid.append(fragment);
+ // }
}
document.addEventListener('DOMContentLoaded', async function() {
@@ -768,5 +687,3 @@
}
});
});
-
-
diff --git a/src/feed/viewOld.js b/src/feed/viewOld.js
index 29859f9..c65be60 100644
--- a/src/feed/viewOld.js
+++ b/src/feed/viewOld.js
@@ -1,745 +1,770 @@
-class FeedBlock {
+class FeedBlockOld {
constructor() {
- this.cache = window.jvbCache;
- this.a11y = window.jvbA11y;
- this.loading = window.jvbLoading;
- this.error = window.jvbError;
-
-
this.container = document.querySelector('section.feed-block');
if (!this.container) {
return;
}
- this.openGallery = false;
+ this.a11y = window.jvbA11y;
+ this.cache = new window.jvbCache('feed');
+ this.error = window.jvbError;
- this.initElements();
- this.addPlaceholders();
this.config = {
- api: feedSettings.apiUrl,
- nonce: feedSettings.nonce,
- user: jvbSettings.currentUser || null,
source: '',
context: '',
highlight: null,
gallery: false,
- showAuthor: true,
- showDate: false,
- view: localStorage.getItem('feedViewMode') || 'grid',
+ view: this.cache.get('feedView') || 'grid',
... this.container.dataset
};
- this.taxonomies = {};
- this.rendered = {};
+ this.initElements();
+ this.initFilters();
- this.feed = {
- imageLoadThreshold: 5,
- lazyLoadOffset: '100px',
- gallery: [],
- loaded: 0,
- intsersectionObserver: null,
- templates: new Map()
- };
- this.isLoading = false;
- this.hasMore = true;
- this.retries = {
- count: 0,
- max: 3,
- delay: 1000
- };
- this.page = 1;
- this.order = 'DESC';
- this.orderby = 'date';
-
- this.gallery = (this.config.gallery) ? new window.jvbGallery(document.querySelector('dialog.gallery'), {
- imageWrapper: '.item',
- loadMore: ()=>this.fetchFeed.bind(this)
- }) : false;
- this.initListeners();
- if (this.page === 1) {
- this.processURLFilters();
- } else {
- this.updateFilters();
- }
-
+ this.loadWhenAble();
}
+
+ loadWhenAble() {
+ if ('requestIdleCallback' in window) {
+ requestIdleCallback(() => {
+ this.initTaxonomies();
+ this.initStore();
+ this.initListeners();
+ this.initGallery();
+ }, { timeout: 2000 });
+ } else {
+ setTimeout(() => {
+ this.initTaxonomies();
+ this.initStore();
+ this.initListeners();
+ this.initGallery();
+ }, 100);
+ }
+ }
+
initElements() {
- this.filterSelector = 'form.feed-filters';
- this.filterForm = this.container.querySelector(this.filterSelector);
- this.grid = this.container.querySelector('.item-grid');
- this.loadMore = this.container.querySelector('.load-more');
- this.filterControls = this.container.querySelector('.filter-actions');
- this.contentTypes = Array.from(this.filterForm.querySelectorAll('input[name="content"]')).map(
- content => {
- return content.value;
- });
- this.selectedTerms = this.container.querySelector('.selected-items-section .selected-items');
+ this.currentTaxonomies = new Set(); // Allowed Taxonomies, grabbed from active buttons
+ this.taxonomyFilters = {};
+ this.elements = {
+ filterTrigger: '[data-filter]',
+ filters: {
+ content: '[data-filter="content"]',
+ orderby: '[data-filter="orderby"]',
+ order: '[data-filter="order"]',
+ match: '[data-filter="match"]',
+ favourites: '[data-filter="favourites"]',
+ taxonomy: '[data-filter^="taxonomy"]'
+ },
+ selectedTax: '.selected-items',
+ clearFilter: 'button.clear-filters',
+ loadMore: 'button.load-more',
+ filterContainer: '.filters',
+ grid: '.item-grid',
+ };
+ this.ui = window.uiFromSelectors(this.elements);
+
+
+ this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false;
+ this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
+ if (this.ui.content && this.ui.content.length > 0) {
+ this.contentTypes = Array.from(
+ this.ui.content
+ ).map(content => content.value);
+ } else {
+ this.contentTypes = [this.container.dataset['content']];
+ }
+
+ if (this.ui.taxonomies.length>0) {
+ this.taxonomies = Array.from(
+ this.ui.taxonomies,
+ ).map(content => content.dataset.taxonomy);
+ } else {
+ this.taxonomies = [];
+ }
+
+
}
- initListeners() {
- window.addEventListener('popstate', this.handlePopState.bind(this));
- document.addEventListener('click', this.handleClick.bind(this));
- document.addEventListener('change', this.handleChange.bind(this));
+ async initTaxonomies() {
+ this.selector = window.jvbSelector;
+ const buttons = document.querySelectorAll('[data-filter="taxonomy"]');
+ this.selector.isInitializing = true;
+ buttons.forEach((button) => {
+ const taxonomy = button.dataset.taxonomy;
+ this.currentTaxonomies.add(taxonomy);
- // Intersection observer for lazy loading
- if ('IntersectionObserver' in window) {
- this.imageObserver = new IntersectionObserver(entries => {
- entries.forEach(entry => {
- if (entry.isIntersecting) {
- this.loadImage(entry.target);
- this.imageObserver.unobserve(entry.target);
- }
- });
- }, {
- rootMargin: '100px',
- threshold: 0.1
+ this.selector.registerFilterButton(button, {
+ button: button,
+ buttonSelector: '[data-filter="taxonomy"]',
+ selected: this.ui.selectedTax
+ });
+
+ // Add preload listeners
+ this.addTaxonomyPreloadListeners(button, taxonomy);
+ });
+
+ this.selector.isInitializing = false;
+
+ this.selector.subscribe((event, data) => {
+ if (event === 'selected-terms') this.handleTaxonomyChange(data);
+ });
+ }
+
+ addTaxonomyPreloadListeners(button, taxonomy) {
+ const preload = () => {
+ this.selector.preloadTaxonomy(taxonomy);
+ };
+
+ // Desktop hover
+ button.addEventListener('mouseenter', preload, { once: true });
+
+ // Touch/keyboard (fires before click)
+ button.addEventListener('pointerdown', preload, { once: true });
+
+ // Keyboard focus
+ button.addEventListener('focus', preload, { once: true });
+ }
+
+ handleTaxonomyChange(data) {
+ const { terms, taxonomy } = data;
+
+ // Update only the current taxonomy's terms
+ if (terms.size > 0) {
+ this.taxonomyFilters[taxonomy] = Array.from(terms.keys());
+ } else {
+ // Remove taxonomy if no terms selected
+ delete this.taxonomyFilters[taxonomy];
+ }
+
+ // Build filters object with all taxonomies
+ let filters = {
+ page: 1
+ };
+
+ // Add taxonomy filters if any exist
+ if (Object.keys(this.taxonomyFilters).length > 0) {
+ filters.taxonomy = this.taxonomyFilters;
+ }
+
+ this.updateFilter(filters);
+ }
+
+ clearAllTaxonomies() {
+ this.taxonomyFilters = {};
+ window.removeChildren(this.ui.selectedTax);
+
+ this.updateFilter({
+ taxonomy: null,
+ page: 1
+ });
+ }
+
+ initFilters() {
+ //defaults
+ this.filters = {
+ content: this.contentTypes[0],
+ orderby: 'date',
+ order: 'desc',
+ page: 1
+ };
+ if (this.config.context) this.filters.context = this.config.context;
+ if (this.config.source) this.filters.source = this.config.source;
+
+ //check the cache
+ this.processCachedFilters();
+ //check url
+ this.processURLFilters();
+
+ // Set initial UI state
+ this.syncUIToFilters();
+ }
+ syncUIToFilters() {
+ if (this.ui.filterContainer) {
+ // Check radio buttons
+ Object.entries(this.filters).forEach(([key, value]) => {
+ const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`);
+ if (input) {
+ input.checked = true;
+ }
});
}
- // Resize observer for responsive images
- if ('ResizeObserver' in window) {
- this.resizeObserver = new ResizeObserver(window.debounce(() => {
- this.updateImageSizes();
- }, 250));
- // Observe the container
- this.resizeObserver.observe(this.container);
- } else {
- // Fallback to window resize
- window.addEventListener('resize', window.debounce(() => {
- this.updateImageSizes();
- }, 250));
- }
+ // Update content-specific visibility
+ this.updateContentFor(this.filters.content);
+ }
+ nextPage() {
+ this.store.setFilter('page', this.store.filters.page++);
+ }
- this.taxonomies = {};
- this.container.querySelectorAll('.jvb-selector:not([hidden])').forEach(selector => {
- let taxonomy = selector.dataset.taxonomy;
- if (!Object.hasOwn(this.taxonomies, taxonomy)) {
- this.taxonomies[taxonomy] = new window.jvbTaxonomySelector(
- selector,
- {
- multiple: true,
- feed: true,
- selected: {},
- onClose: () => this.setSelectedTerms(taxonomy),
+ initStore() {
+ const store = window.jvbStore.register(
+ 'feed',
+ {
+ storeName: 'feed',
+ endpoint: 'feed',
+ keyPath: 'id',
+ indexes: [
+ { name: 'content', keyPath: 'content'},
+ { name: 'taxonomy', keyPath: 'taxonomy'},
+ { name: 'user', keyPath: 'user'},
+ { name: 'date', keyPath: 'modified'},
+ { name: 'title', keyPath: 'title'}
+ ],
+ filters: this.filters,
+ TTL: 6 * 60 * 60 * 1000,
+ showLoading: true,
+ required: 'content',
+ delayFetch: true
+ }
+ );
+ this.store = store.feed;
+
+ this.store.subscribe((event, data) => {
+ switch (event) {
+ case 'data-loaded':
+ this.renderItems();
+ this.ui.loadMore.hidden = true;
+ if (this.store.lastResponse && this.store.lastResponse['has_more']) {
+ this.ui.loadMore.hidden = !this.store.lastResponse['has_more'];
}
- );
+ break;
}
});
}
-
- /**
- * Handle browser history navigation
- */
- handlePopState(e) {
- if (e.state && e.state.filters) {
- if(this.processURLFilters()){
- // Load items with updated filters
- this.resetPage();
- this.fetchFeed();
-
- // Announce to screen readers
- this.a11y.announce('Feed filters updated from browser history.');
- }
+ initGallery() {
+ this.gallery = (this.config.gallery) ? window.jvbGallery : false;
+ if (this.gallery) {
+ this.gallery.subscribe((event, data) => {
+ if (event === 'load-more' && this.store.lastResponse) {
+ if (this.store.lastResponse['has_more']) {
+ this.nextPage();
+ }
+ }
+ });
}
}
+ processCachedFilters() {
+ Object.keys(this.filters).forEach(filter => {
+ let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`);
+ if (cached && cached !== this.filters[filter]){
+ this.filters[filter] = cached;
+ }
+ });
+ }
+
processURLFilters() {
- const params = new URLSearchParams(window.location.search);
- //No parameters to process
- if (!params.toString()) {
- this.updateFilters();
- return;
+ if (this.filters.page > 1) {
+ return false;
}
+ const params = new URLSearchParams(window.location.search);
- let filters = ['content', 'order', 'orderby', 'favourites','match'];
-
+ if (!params.toString()) {
+ return false;
+ }
+ let filters = ['content', 'order', 'orderby', 'favourites', 'match'];
filters.forEach(filter => {
- let value = params.get('f_'+filter);
- params.delete('f_'+filter);
- if (value && this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`)) {
- this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`).checked = true;
+ let value = params.get(`f_${filter}`);
+ if (value) {
+ this.filters[filter] = value;
+ let input = this.ui.filters[filter];
+ if (input) {
+ input.checked = true;
+ }
}
});
- let unprocessed = {};
- for (var [key, value] of Object.entries(Object.fromEntries(params))) {
-
- key = key.replace('f_','');
- if (this.contentTypes.includes(key)) {
- this.openGallery = value;
- } else {
- this.taxonomies[key].addTermsFromURL(value);
- this.setSelectedTerms(key);
+ let hasTaxonomy = false;
+ // Load taxonomy filters from URL
+ params.forEach((value, key) => {
+ if (key.startsWith('f_tax_')) {
+ hasTaxonomy = true;
+ const taxonomy = key.replace('f_tax_', '');
+ if (!this.taxonomyFilters[taxonomy]) {
+ this.taxonomyFilters[taxonomy] = [];
+ }
+ this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
}
- }
-
- this.updateFilters();
- }
-
- handleClick(e) {
- if (e.target.classList.contains('load-more') || e.target.closest('.load-more')) {
- this.fetchFeed(false);
- e.target.disabled = true;
- } else if (e.target.classList.contains('clear-filters') || e.target.closest('.clear-filters')) {
- this.resetFilters();
- } else if (this.config.gallery && e.target.closest('.feed-image')) {
- this.gallery.handleGalleryOpen(e);
- } else if (e.target.classList.contains('.remove-item') || e.target.closest('.remove-item')) {
- let tag = e.target.closest('.selected-item');
- let taxonomy = tag.dataset.taxonomy;
- this.taxonomies[taxonomy].removeSelectedTerm(tag.dataset.id);
- this.setSelectedTerms(taxonomy);
- this.updateFilters();
- }
- }
- handleChange(e) {
- if (e.target.closest(this.filterSelector)) {
- this.resetPage();
- window.removeChildren(this.grid);
- this.addPlaceholders();
- //update filters
- this.updateFilters();
- }
- }
-
- updateFilters() {
- this.page = 1;
- const params = new URLSearchParams(window.location.search);
-
- let filters = Object.fromEntries(new FormData(this.filterForm));
-
- let contents = [];
- for (let [key, value] of Object.entries(filters)) {
- let set = false;
- switch (key) {
- case 'content':
- if (value !== this.contentTypes[0]) {
- set = true;
+ });
+ if (this.ui.filterContainer && hasTaxonomy) {
+ for (let [tax, ids] in Object.entries(this.taxonomyFilters)) {
+ let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`);
+ if (button) {
+ if (button.dataset.fieldId) {
+ let field = this.selector.get(button.dataset.fieldId);
+ field.selectedTerms = new Set(ids);
+ this.selector.initFieldDisplay(button.dataset.fieldId);
} else {
- params.delete('f_'+key);
+ this.selector.registerField(button, {
+ button: button,
+ buttonSelector: '[data-filter="taxonomy"]',
+ selected: this.ui.selectedTax,
+ selectedItems: ids
+ });
}
- break;
- case 'orderby':
- if (value !== 'date') {
- set = true;
- }
- break;
- case 'order':
- if (value !== 'desc') {
- set = true;
- }
- break;
- default:
- set = true;
+ }
}
- if (!set) {
- params.delete('f_'+key);
- }
-
-
- if (set && value !== false && value !== '') {
- params.set('f_'+key, value);
- }
- if (value !== '') {
- contents.push(value);
- }
-
- const newURL = `${window.location.pathname}?${params.toString()}`;
- history.pushState(filters, '', newURL);
-
}
-
- this.filters = filters;
- this.updateContentFor(filters.content);
-
- this.updateFilterControls();
-
- this.loading.setContent(contents);
- this.fetchFeed(true);
- }
-
- updateFilterControls() {
- this.filterControls.hidden = this.selectedTerms.children.length < 2;
+ return true;
}
/**
- * Toggles taxonomy selectors and certain order/orderby options
- * depending on current content
- * @param content
+ * Update URL with current filters (for sharing/bookmarking)
*/
- updateContentFor(content) {
- this.filterForm.querySelectorAll('.jvb-selector').forEach(tax => {
- let hasContent = tax.dataset.for.includes(content);
- tax.hidden = !hasContent;
- if (!hasContent) {
- let t = tax.dataset.taxonomy;
- this.clearSelectedTerms(t);
+ updateURL() {
+ const params = new URLSearchParams();
+
+ // Add simple filters
+ ['content', 'order', 'orderby', 'match'].forEach(key => {
+ if (this.filters[key]) {
+ params.set(`f_${key}`, this.filters[key]);
}
});
- this.filterForm.querySelectorAll('input[data-for]').forEach(toggle => {
- toggle.hidden = !toggle.dataset.for.includes(content);
+
+ // Add taxonomy filters
+ Object.entries(this.taxonomyFilters).forEach(([taxonomy, terms]) => {
+ if (terms.length > 0) {
+ params.set(`f_tax_${taxonomy}`, terms.join(','));
+ }
});
- this.filterForm.querySelectorAll('input[name="order"]').forEach(order => {
- order.hidden = this.filters.order === 'random';
- });
+
+ // Update URL without reload
+ const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
+ window.history.pushState({ filters: this.filters }, '', newURL);
}
- clearSelectedTerms(taxonomy) {
- this.filterForm.querySelector(`input[name="${taxonomy}"]`).value = '';
- if (Object.hasOwn(this.taxonomies, taxonomy)) {
- this.taxonomies[taxonomy].selectedItems = {};
- }
- }
-
- setSelectedTerms(taxonomy) {
- let input = this.filterForm.querySelector(`input[name="${taxonomy}"]`);
- input.value = '';
- let selected = this.taxonomies[taxonomy].selectedTerms;
- if (!window.isEmptyObject(selected)) {
- let ids = Object.keys(selected);
- input.value = ids.join(',');
- }
- this.updateFilters();
- }
-
-
- nextPage() {
- if (this.hasMore) {
- this.page++;
- }
- }
- resetPage() {
- this.page = 1;
- this.hasMore = true;
- }
- resetState() {
- this.resetPage(true);
- this.isLoading = false;
- this.retries = {
- count: 0,
- max: 3,
- delay: 1000
- };
- }
-
- resetFilters() {
- this.filterForm.reset();
- //check the first content
- this.filterForm.querySelector('input[name="content"]').checked = true;
- this.filterForm.querySelector('input[name="orderby"][value="date"]').checked = true;
- this.page = 1;
- this.updateFilters();
- }
-
-
- buildFilterRequest() {
-
- let filters = {};
-
- for (let [filter, value] of Object.entries(this.filters)) {
- if (value !== false && value !== '') {
- filters[filter] = value;
- }
- }
- filters.page = parseInt(this.page);
- if (this.container.dataset.context) {
- filters.context = this.container.dataset.context;
- }
- if (this.container.dataset.source) {
- filters.source = this.container.dataset.source;
- }
- return new URLSearchParams(filters).toString();
- }
-
- async fetchFeed(reset = false, force = false) {
- if (this.isLoading) {
- return false;
- }
- this.loading.showLoading(this.filters);
- try {
- if (this.page === 1) {
- window.removeChildren(this.grid);
- this.addPlaceholders();
- }
-
- const data = await this.cache.fetchWithCache(
- `${this.config.api}feed?${this.buildFilterRequest()}`,
- {
- method: 'GET',
- },
- {
- context: 'feed',
- forceRefresh: true
- // forceRefresh: force
- }
- );
-
- //Handle empty results
- if (!data || !data.items || data.items.length === 0) {
- if (this.page === 1) {
- this.showEmptyState();
- }
- this.hasMore = false;
- return false;
- } else {
- this.hasMore = data['has_more'];
-
- this.renderItems(data.items, this.page > 1);
-
- if (this.hasMore) {
- this.nextPage();
- }
- return true;
- }
- } catch (error) {
- this.handleError(error);
- } finally {
- this.loading.hideLoading();
- if (this.openGallery !== false) {
- this.gallery.openWhenReady = this.openGallery;
- this.openGallery = false;
- }
- this.loadMore.disabled = false;
- this.loadMore.hidden = !this.hasMore;
- }
- }
-
- removePlaceholders() {
- if (this.grid.querySelector('.placeholder')) {
- window.removeChildren(this.grid);
- }
- }
- showEmptyState() {
- window.removeChildren(this.grid);
- let template = window.getTemplate('emptyState');
- let isFavourite = Object.hasOwn(this.filters, 'favourites') && this.filters.favourites === true;
- if (isFavourite) {
- [
- template.querySelector('h3').textContent,
- template.querySelector('p:first-of-type').textContent,
- template.querySelector('p:last-of-type').textContent,
- ] = [
- '♡ BLANK CANVAS ♡',
- 'You haven\'t fallen in love with any pieces... yet!',
- 'Hit that heart icon when something stops your scroll — your dream collection is waiting to start.'
- ];
- }
- this.grid.append(template);
- this.a11y.announceEmpty(isFavourite);
-
- }
- handleError(error){
- return this.error.handleApiError(
- error,
- {
- component: 'Feed Block',
- action: 'loaditems'
- },
- () => this.fetchFeed()
- );
- }
-
- addPlaceholders() {
- let total = this.contentTypes.length - 1;
- for (let i = 0; i < 9; i++) {
- let template = window.getTemplate('placeholderTemplate');
- let rand = Math.floor(Math.random()*total+1);
- let icon = window.getIcon(this.contentTypes[rand]).cloneNode(true);
-
- template.append(icon);
- this.grid.append(template);
- }
- }
- renderItems(items, append = false) {
- //Clear the grid if we aren't appending
- if (!append) {
- window.removeChildren(this.grid);
- this.addPlaceholders();
+ renderItems() {
+ let items = this.store.getFiltered();
+ if (this.store.filters['page'] === 1) {
+ window.removeChildren(this.ui.grid);
}
-
- //Bail early if no items
if (items.length === 0) {
- this.a11y.announceUpdate(0, append);
+ this.a11y.announceItems(0, this.store.filters['page'] > 0);
return;
}
- //Use DocumentFragment for better performance
const fragment = document.createDocumentFragment();
-
const batchSize = 10;
+
const processBatch = (startIndex) => {
const endIndex = Math.min(startIndex + batchSize, items.length);
for (let i = startIndex; i < endIndex; i++) {
const item = items[i];
const element = this.createItemElement(item);
- fragment.appendChild(element);
- this.imageObserver.observe(element);
+ fragment.appendChild(element);
}
if (endIndex < items.length) {
- requestAnimationFrame(() => {
- processBatch(endIndex);
- });
+ requestAnimationFrame(() => processBatch(endIndex));
} else {
this.removePlaceholders();
- //all batches are processed, append fragment
- this.grid.appendChild(fragment);
+ this.ui.grid.append(fragment);
+
if (this.config.gallery) {
this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
}
- this.a11y.makeNavigable(this.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
- this.a11y.announceItems(items.length, append, this.hasMore);
+
+ this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
+ this.a11y.announceItems(items.length, this.store.filters['page'] > 1, this.store.hasMore);
}
};
if (items.length > 0) {
processBatch(0);
} else {
- this.a11y.announceUpdate(0, append);
+ this.a11y.announceItems(0, this.store.filters['page'] >1, false);
+ }
+
+ if (this.ui.filters.match) {
+ this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
+ }
+ if (this.ui.clearFilter) {
+ this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
}
}
/**
- * Creates a feed-item. Used by RenderItems
+ *
+ * @param {object} item
*/
createItemElement(item) {
- if(!this.rendered[item.icon]) {
- this.rendered[item.icon] = new Map();
- }
- if (this.rendered[item.icon].has(item.id)) {
- return this.rendered[item.icon].get(item.id);
- }
+ let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
- const favourited = window.isFavourited(item.icon, item.id)??false;
- const template = window.getTemplate('feed-item');
+ const isTimeline = Object.hasOwn(template.dataset, 'timeline');
- template.id = `${item.icon}-${item.id}`;
- template.dataset.id = item.id;
- template.classList.add(item.icon);
+ // Format fields using helpers
+ for (let [fieldName, value] of Object.entries(item.fields)) {
+ if (isTimeline && ['timeline', 'number'].includes(fieldName)) continue;
+ let el = template.querySelector(`[data-field="${fieldName}"]`);
+ if (!el) continue;
- if (item['umami_view']) {
- this.buildUmamiData(template, item['umami_view']);
- }
+ if (value === '') {
+ el.remove();
+ continue;
+ }
- let favouriteButton = template.querySelector('button.favourite');
- [
- favouriteButton.dataset.id,
- favouriteButton.dataset.type,
- favouriteButton.dataset.artist,
- favouriteButton.title
- ] = [
- item.id,
- item.icon,
- item['user_id'],
- (favourited) ? 'Remove from Favourites' : 'Add to Favourites'
- ];
-
- let order = item.order;
- let single = template.querySelector('.item');
- let list = template.querySelector('.item-list');
- let img = template.querySelector('.feed-images');
- let summary = template.querySelector('summary');
- let info = template.querySelector('.item-info');
-
- for (let [index, id] of Object.entries(order)) {
- let target;
- let config = item[id];
- if (id === 'title') {
- target = template.querySelector('h3 a');
- if (item.title !== '') {
- [
- target.textContent,
- target.href,
- target.url
- ] = [
- item.title,
- item.url,
- `Learn more about this ${item.icon}`
- ];
- if (item.icon !== '') {
- target.closest('h3').prepend(window.getIcon(item.icon));
- }
- if (item.umami_click) {
- this.buildUmamiData(target, item.umami_click);
- }
- } else {
- target.remove();
- }
- } else if (Object.hasOwn(config, 'terms')) {
- //Taxonomy list
- if (config.terms.length === 0) {
- continue;
- }
- let taxonomy = list.cloneNode(true);
- let label = taxonomy.querySelector('.label');
- let termList = taxonomy.querySelector('ul');
- let listItem = taxonomy.querySelector('li');
-
- if (config.label) {
- label.textContent = config.label;
- }
- if (config.icon) {
- label.prepend(window.getIcon(config.icon));
- }
- if (!config.label && !config.icon){
- label.remove();
- }
-
- config.terms.forEach(term => {
- let termItem = listItem.cloneNode(true);
- let link = termItem.querySelector('a');
- [
- link.href,
- link.title,
- link.textContent
- ] = [
- term.url,
- `Learn more about ${term.title}`,
- term.title
- ];
- if (term.umami_click.length > 0) {
- this.buildUmamiData(link, term.umami_click);
- }
- termList.append(termItem);
- });
-
- listItem.remove();
- info.appendChild(taxonomy);
- } else if (Object.hasOwn(config, 'value') && config.value !== '') {
- let itemInfo = single.cloneNode(true);
- let label = itemInfo.querySelector('.label');
- let link = itemInfo.querySelector('a');
- let p = itemInfo.querySelector('p');
- if (Object.hasOwn(config, 'label')) {
- label.textContent = config.label;
- }
- if (Object.hasOwn(config, 'icon')) {
- label.prepend(window.getIcon(config.icon));
- }
- if (!Object.hasOwn(config, 'icon') && !Object.hasOwn(config, 'label')) {
- label.remove();
- }
- if (Object.hasOwn(config, 'url')) {
- p.remove();
- [
- link.textContent,
- link.href,
- link.title
- ] = [
- config.value,
- config.url,
- `Learn more about ${config.value}`
- ];
- } else {
- link.remove();
- p.textContent = config.value;
- }
- info.appendChild(itemInfo);
- } else if (id === 'image') {
- let images = summary.querySelector('.feed-images');
- let img = images.querySelector('a');
-
- let main = img.cloneNode(true);
- if (!this.config.gallery) {
- main.href = item.url;
- }
-
- main.classList.add('feed-image');
- this.buildImageData(main.querySelector('img'), item.image);
- images.append(main);
-
- if (item.content?.length > 0) {
- images.classList.add('multi');
- item.content.forEach(c => {
- let image = img.cloneNode(true);
- if (!this.config.gallery) {
- image.href = c.url;
- }
- let itemImg = image.querySelector('img');
- itemImg.src = c.image.small;
- itemImg.alt = c.image.alt;
- images.append(image);
- });
- }
- img.remove();
+ if (this.isImageField(item, value)) {
+ this.formatImageFields(el, value, item);
+ } else if (this.isTaxonomyField(item, fieldName)) {
+ this.formatTaxonomyField(el, item, fieldName, value);
+ } else if (this.isTimeField(el)) {
+ this.formatTimeField(el, value);
+ } else {
+ this.formatField(el, value);
}
}
- single.remove();
- list.remove();
- this.rendered[item.icon].set(item.id, template);
+ // Handle link
+ let link = template.querySelector('a');
+ if (link && item.url !== '') {
+ [
+ link.href,
+ link.title
+ ] = [
+ item.url,
+ `View ${item.fields['post_title']??'Item'}`
+ ];
+ }
+
+ if (isTimeline) {
+ this.addTimelineElements(item, template);
+ }
return template;
}
-
- buildImageData(img, data){
- if (typeof data.tiny !== 'string') {
- return;
+ splitIDs(value) {
+ return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value);
+ }
+ isImageField(item, value) {
+ if (!Object.hasOwn(item, 'images') || Object.keys(item.images).length === 0) {
+ return false;
}
+ let values = this.splitIDs(value);
+
+ return values.some(v =>
+ Object.keys(item.images).map(k => parseInt(k)).includes(parseInt(v))
+ );
+ }
+ formatImageFields(element, value, item) {
+ let values = this.splitIDs(value); // Convert string to array first
+ if (values.length === 0) return;
+
+ if (values.length > 1) {
+ let image = element.querySelector('img');
+ if (!image) return;
+ values.forEach(imgID => {
+ let img = image.cloneNode(true);
+ this.formatImageField(img, imgID, item);
+ element.append(img);
+ });
+ image.remove();
+ } else {
+ if (element.tagName !== 'IMG') {
+ element = element.querySelector('img');
+ if (!element) return;
+ }
+ this.formatImageField(element, values[0], item);
+ }
+ }
+ formatImageField(element, value, item) {
+ let imgData = item.images[value]??false;
+ if (!imgData) return;
[
- img.src,
- img.dataset.small,
- img.dataset.medium,
- img.dataset.large,
- img.alt
- ] =
+ element.src,
+ element.srcset,
+ element.alt
+ ] = [
+ imgData.tiny,
+ `${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`,
+ imgData['image-alt-text']
+ ]
+ }
+ isTaxonomyField(item, field) {
+ if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) {
+ return false;
+ }
+
+ return Object.keys(item.taxonomies).includes(field);
+ }
+ formatTaxonomyField(element, item, field, value) {
+ if (element.tagName !== 'UL' || !element.querySelector('li')) return;
+ let values = this.splitIDs(value);
+ if (values.length === 0) {
+ element.remove();
+ }
+ let listItem = element.querySelector('li');
+ for (let termID of values) {
+ let term = item.taxonomies[field][termID]??false;
+ if (!term) continue;
+ let termItem = listItem.cloneNode(true);
+ let link = termItem.querySelector('a');
+ if (!link) continue;
+
[
- data.tiny,
- data.small,
- data.medium,
- data.large,
- data.alt
+ link.href,
+ link.title,
+ link.textContent
+ ] = [
+ term.url,
+ `See more ${term.title}`,
+ term.title
];
+ element.append(termItem);
+ }
+ listItem.remove();
+ }
+ isTimeField(el) {
+ return el.tagName === 'TIME' || el.querySelector('time') !== null;
+ }
+ formatTimeField(element, value) {
+ if (element.tagName !== 'TIME') {
+ element = element.querySelector('time');
+ if (!element) return;
+ }
+ element.setAttribute('datetime', value);
+ element.textContent = window.formatTimeAgo(value, 'F Y');
+ }
+ formatField(element, value) {
+ element.textContent = value;
}
- buildUmamiData(item, data){
- for(let [key, value] of Object.entries(data)){
- item.dataset[key] = value;
+ addTimelineElements(item, template) {
+ let [
+ afterEl,
+ number,
+ started,
+ last
+ ] = [
+ template.querySelector('span.after-text'),
+ template.querySelector('[data-field="number"] b'),
+ template.querySelector('[data-field="started"] time'),
+ template.querySelector('[data-field="updated"] time')
+ ];
+
+ if (afterEl) {
+ afterEl.textContent = `After ${item.fields.number} Tx`;
+ }
+ if (number) {
+ number.textContent = item.fields.number;
+ }
+ if (started) {
+ this.formatTimeField(started, item.fields.timeline[0]['post_date']);
+ }
+ if (last) {
+ this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']);
}
}
- /**
- * Load Image, used by renderItems
- * @param element
- */
- loadImage(element) {
- const img = element.querySelector('img');
- if (!img) return;
- const size = this.getImageSize();
-
- img.src = img.dataset[size] || img.dataset.src;
- element.setAttribute('data-loaded', 'true');
+ removePlaceholders() {
+ const placeholders = this.ui.grid.querySelectorAll('.placeholder');
+ if (placeholders.length > 0) {
+ placeholders.forEach(p => p.remove());
+ }
}
+
+ addPlaceholders() {
+ let total = this.contentTypes.length;
+ const fragment = document.createDocumentFragment();
+ for (let i = 0; i < 12; i++) {
+ let template = window.getTemplate('placeholderTemplate');
+
+ let rand = Math.floor(Math.random() * total);
+ let icon;
+ if (this.ui.content && this.ui.content.length > 0) {
+ icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true);
+ } else {
+ icon = window.getIcon(this.container.dataset.icon);
+ }
+ template.append(icon);
+ fragment.append(template);
+ }
+ this.ui.grid.append(fragment);
+ }
+
+
+
/**
- * Updates the image size according to screen size
+ *
+ * @param {object} filters {name: value}
*/
- updateImageSizes() {
- const size = this.getImageSize();
- const items = this.grid.querySelectorAll('.item');
- items.forEach(item => {
- const img = item.querySelector('img');
- if (img && img.dataset[size] && img.src !== img.dataset[size]) {
- img.src = img.dataset[size];
+ updateFilter(filters) {
+ //double check filters are what we're expecting
+ let allowed = ['taxonomy','favourites','match', ... Object.keys(this.filters)];
+
+ filters = Object.keys(filters)
+ .filter(key => allowed.includes(key))
+ .reduce((obj, key) => {
+ obj[key] = filters[key];
+ return obj;
+ }, {});
+
+ if (window.getDifferences.map(this.filters, filters)) {
+ this.filters = { ...this.filters, ...filters }; // Merge instead of replace
+ this.updateURL();
+ this.store.setFilters(filters);
+ }
+ }
+ /**
+ * Update visible filters based on selected content type
+ */
+ updateContentFor(contentType) {
+ // Update taxonomy filter visibility
+ const taxonomyButtons = this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]');
+ taxonomyButtons.forEach(button => {
+ const forTypes = button.dataset.for?.split(',') || [];
+ button.hidden = forTypes.length > 0 && !forTypes.includes(contentType);
+ });
+
+ // Update ordering options
+ const orderButtons = this.ui.filterContainer.querySelectorAll('[data-for]');
+ orderButtons.forEach(button => {
+ const forTypes = button.dataset.for?.split(',') || [];
+ if (forTypes.length > 0) {
+ button.hidden = !forTypes.includes(contentType);
+ // Uncheck if hiding
+ if (button.hidden && button.checked) {
+ button.checked = false;
+ }
}
});
- }
- /**
- * Get image size based on screen width
- */
- getImageSize() {
- const width = window.innerWidth;
- if (width > 1024) return 'medium';
- if (width > 500) return 'medium';
- return 'small';
+
+ // Update order direction visibility based on selected orderby
+ const orderBy = this.ui.filterContainer.querySelector('[name="orderby"]:checked');
+ this.updateOrderDirectionVisibility(orderBy?.value);
}
+ /**
+ * Show/hide order direction based on orderby selection
+ */
+ updateOrderDirectionVisibility(orderBy) {
+ const orderDirection = this.ui.filterContainer.querySelector('.order-direction');
+ if (orderDirection) {
+ const forOrders = orderDirection.dataset.forOrder?.split(',') || [];
+ orderDirection.hidden = forOrders.length > 0 && !forOrders.includes(orderBy);
+ }
+ }
+ /*********************************************************************
+ LISTENERS
+ *********************************************************************/
+ initListeners() {
+ this.popStateHandler = this.handlePopState.bind(this);
+ this.clickHandler = this.handleClick.bind(this);
+ this.changeHandler = this.handleChange.bind(this);
+ this.imageObserver = null;
+ this.resizeObserver = null;
+ if ('IntersectionObserver' in window) {
+ this.imageObserver = new IntersectionObserver(entries => {
+ entries.forEach(entry => {
+ this.loadImage(entry.target);
+ this.imageObserver.unobserve(entry.target);
+ });
+ }, {
+ rootMargin: '100px',
+ threshold: .1
+ });
+ }
+
+ if ('ResizeObserver' in window) {
+ this.resizeObserver = new ResizeObserver(() => {
+ window.debouncer.schedule(
+ 'feed-update-images',
+ () => this.updateImageSizes(),
+ 250
+ );
+ });
+ } else {
+ window.addEventListener('resize', () => {
+ window.debouncer.schedule(
+ 'feed-update-images',
+ () => this.updateImageSizes(),
+ 250
+ );
+ });
+ }
+
+ window.addEventListener('popstate', this.popStateHandler);
+ document.addEventListener('click', this.clickHandler);
+ document.addEventListener('change', this.changeHandler);
+ }
+
+ handlePopState(e) {
+ if (e.state?.filters) {
+ if (this.processURLFilters()) {
+ this.store.setFilters(this.filters);
+ this.a11y.announce('Feed filters updated from browser history');
+ }
+ }
+ }
+
+ handleClick(e) {
+ if (window.targetCheck(e, this.elements.loadMore)) {
+ this.nextPage();
+ } else if (window.targetCheck(e, this.elements.clearFilter)) {
+ this.clearAllTaxonomies();
+ } else if (window.targetCheck(e, '.remove-item')) {
+ this.handleRemoveSelectedTerm(e);
+ }
+ }
+
+ handleRemoveSelectedTerm(e) {
+ const selectedItem = e.target.closest('.selected-item');
+ if (!selectedItem) return;
+
+ const termId = parseInt(selectedItem.dataset.id);
+ const taxonomy = selectedItem.dataset.taxonomy;
+
+ // Remove from filters
+ if (this.taxonomyFilters[taxonomy]) {
+ this.taxonomyFilters[taxonomy] = this.taxonomyFilters[taxonomy]
+ .filter(id => id !== termId);
+
+ if (this.taxonomyFilters[taxonomy].length === 0) {
+ delete this.taxonomyFilters[taxonomy];
+ }
+ }
+
+ // Remove from UI
+ selectedItem.remove();
+
+ // Update filters
+ this.updateFilter({
+ taxonomy: Object.keys(this.taxonomyFilters).length > 0
+ ? this.taxonomyFilters
+ : null,
+ page: 1
+ });
+ }
+
+ handleChange(e) {
+ let target = e.target;
+ if (Object.hasOwn(target.dataset, 'filter')) {
+ if (target.dataset.filter === 'content') {
+ this.updateContentFor(target.value);
+ this.updateFilter({ content: target.value, page: 1 });
+ } else if (target.dataset.filter === 'orderby') {
+ this.updateOrderDirectionVisibility(target.value);
+ this.updateFilter({ orderby: target.value, page: 1 });
+ } else if (target.dataset.filter === 'order') {
+ this.updateFilter({ order: target.value, page: 1 });
+ } else if (target.dataset.filter === 'match') {
+ this.updateFilter({ match: target.checked ? 'all' : 'any', page: 1 });
+ } else if (target.dataset.filter === 'favourites') {
+ this.updateFilter({ favourites: target.checked, page: 1 });
+ }
+ }
+ }
}
-document.addEventListener('DOMContentLoaded', () => {
- window.feedBlock = new FeedBlock();
+
+document.addEventListener('DOMContentLoaded', async function() {
+ window.auth.subscribe(event => {
+ if (event === 'auth-loaded') {
+ window.feedBlock = new FeedBlock();
+ }
+ });
});
--
Gitblit v1.10.0