Jake Vanderwerf
2026-01-07 bad59c66549eb601fed963ed013f9b79305ca003
=Feedblock integrated with refactored taxonomy selector
18 files modified
2415 ■■■■ changed files
assets/css/forms.min.css 2 ●●● patch | view | raw | blame | history
assets/js/concise/DataStore.js 47 ●●●●● patch | view | raw | blame | history
assets/js/concise/TaxonomyCreator.js 3 ●●●●● patch | view | raw | blame | history
assets/js/concise/TaxonomySelector.js 80 ●●●●● patch | view | raw | blame | history
assets/js/min/creator.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/dataStore.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/selector.min.js 2 ●●● patch | view | raw | blame | history
build/feed/style-index-rtl.css 2 ●●● patch | view | raw | blame | history
build/feed/style-index.css 2 ●●● patch | view | raw | blame | history
build/feed/view.asset.php 2 ●●● patch | view | raw | blame | history
build/feed/view.js 2 ●●● patch | view | raw | blame | history
inc/blocks/FeedBlock.php 6 ●●●● patch | view | raw | blame | history
inc/forms/TaxonomySelector.php 17 ●●●● patch | view | raw | blame | history
inc/helpers/ui.php 19 ●●●●● patch | view | raw | blame | history
inc/rest/routes/TermRoutes.php 6 ●●●● patch | view | raw | blame | history
src/feed/style.scss 107 ●●●●● patch | view | raw | blame | history
src/feed/view.js 871 ●●●● patch | view | raw | blame | history
src/feed/viewOld.js 1243 ●●●● patch | view | raw | blame | history
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%}}
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%}}
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,8 +1008,8 @@
        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];
@@ -978,7 +1017,7 @@
                    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;
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}`;
            }
        }
    }
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();
        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 @@
            }
        });
        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}`;
        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 @@
        }
    });
});
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)}};
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)}};
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)}))}))})();
(()=>{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)}))}))})();
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)}))}))})();
(()=>{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)}))}))})();
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}
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}
build/feed/view.asset.php
@@ -1 +1 @@
<?php return array('dependencies' => array(), 'version' => '1469437e42d138dbcd90');
<?php return array('dependencies' => array(), 'version' => 'af81f16ca1cc19d9a8db');
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)}))}))})();
(()=>{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)}))}))})();
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>
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,7 +166,7 @@
            <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>
@@ -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
        );
    }
    /**
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.'">
    $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();"
  >'.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>';
          >%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')
    );
}
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 = [
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;
}
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: {
                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',
            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']];
        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)
            : [];
        }
        if (this.ui.taxonomies.length>0) {
            this.taxonomies = Array.from(
                this.ui.taxonomies,
            ).map(content => content.dataset.taxonomy);
        } else {
            this.taxonomies = [];
        }
    initListeners() {
        this.popStateHandler    = this.handlePopState.bind(this);
        this.clickHandler       = this.handleClick.bind(this);
        this.changeHandler      = 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);
            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 = {
        this.allowedFilters = ['content', 'order', 'orderby', 'favourites', 'match'];
        let defaults = {
            content:    this.contentTypes[0],
            orderby:    'date',
            order:      'desc',
            page:       1
            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;
                }
            });
        }
        // Update content-specific visibility
        this.updateContentFor(this.filters.content);
    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;
    }
    nextPage() {
        this.store.setFilter('page', this.store.filters.page++);
        });
        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();
                }
            });
        }
    }
    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;
        }
            this.showEmptyState();
            this.a11y.announceItems(0, this.isFirstPage());
        } else {
        const fragment = document.createDocumentFragment();
        const batchSize = 10;
        const processBatch = (startIndex) => {
            const endIndex = Math.min(startIndex + batchSize, items.length);
                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.appendChild(element);
                    fragment.append(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());
                }
                    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.store.filters['page'] > 1, this.store.hasMore);
                    this.a11y.announceItems(items.length, !this.isFirstPage(), this.store.lastResponse?.has_more??false);
            }
        };
        if (items.length > 0) {
            processBatch(0);
        } else {
            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;
        }
        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;
@@ -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 @@
        }
    });
});
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.loadWhenAble();
    }
        this.gallery = (this.config.gallery) ? new window.jvbGallery(document.querySelector('dialog.gallery'), {
            imageWrapper: '.item',
            loadMore: ()=>this.fetchFeed.bind(this)
        }) : false;
    loadWhenAble() {
        if ('requestIdleCallback' in window) {
            requestIdleCallback(() => {
                this.initTaxonomies();
                this.initStore();
        this.initListeners();
        if (this.page === 1) {
            this.processURLFilters();
                this.initGallery();
            }, { timeout: 2000 });
        } else {
            this.updateFilters();
            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');
    }
    initListeners() {
        window.addEventListener('popstate', this.handlePopState.bind(this));
        document.addEventListener('click', this.handleClick.bind(this));
        document.addEventListener('change', this.handleChange.bind(this));
        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);
        // 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
            });
        }
        // 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);
        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 {
            // Fallback to window resize
            window.addEventListener('resize', window.debounce(() => {
                this.updateImageSizes();
            }, 250));
            this.contentTypes = [this.container.dataset['content']];
        }
        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,
        if (this.ui.taxonomies.length>0) {
            this.taxonomies = Array.from(
                this.ui.taxonomies,
            ).map(content => content.dataset.taxonomy);
        } else {
            this.taxonomies = [];
        }
    }
    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);
            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;
                }
            });
        }
        // 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',
                    {
                        multiple: true,
                        feed: true,
                        selected: {},
                        onClose: () => this.setSelectedTerms(taxonomy),
                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() {
        if (this.filters.page > 1) {
            return false;
        }
        const params = new URLSearchParams(window.location.search);
        //No parameters to process
        if (!params.toString()) {
            this.updateFilters();
            return;
            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;
        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.taxonomies[key].addTermsFromURL(value);
                this.setSelectedTerms(key);
            }
        }
        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;
                    } else {
                        params.delete('f_'+key);
                    }
                    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;
    }
    /**
     * Toggles taxonomy selectors and certain order/orderby options
     *  depending on current content
     * @param content
     */
    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);
            }
        });
        this.filterForm.querySelectorAll('input[data-for]').forEach(toggle => {
            toggle.hidden = !toggle.dataset.for.includes(content);
        });
        this.filterForm.querySelectorAll('input[name="order"]').forEach(order => {
            order.hidden = this.filters.order === 'random';
                        this.selector.registerField(button, {
                            button: button,
                            buttonSelector: '[data-filter="taxonomy"]',
                            selected: this.ui.selectedTax,
                            selectedItems: ids
        });
    }
    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;
    /**
     * 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]);
            }
            this.loadMore.disabled = false;
            this.loadMore.hidden = !this.hasMore;
        });
        // 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);
    }
    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()
        );
    renderItems() {
        let items = this.store.getFiltered();
        if (this.store.filters['page'] === 1) {
            window.removeChildren(this.ui.grid);
    }
    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();
        }
        //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']);
        }
        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) {
            if (value === '') {
                el.remove();
                    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 (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);
                }
                if (config.icon) {
                    label.prepend(window.getIcon(config.icon));
                }
                if (!config.label && !config.icon){
                    label.remove();
                }
                config.terms.forEach(term => {
        // 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;
    }
    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;
        [
            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;
                    [
                        link.href,
                        link.title,
                        link.textContent
                    ] = [
                        term.url,
                        `Learn more about ${term.title}`,
                `See more ${term.title}`,
                        term.title
                    ];
                    if (term.umami_click.length > 0) {
                        this.buildUmamiData(link, term.umami_click);
            element.append(termItem);
                    }
                    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));
    isTimeField(el) {
        return el.tagName === 'TIME' || el.querySelector('time') !== null;
                }
                if (!Object.hasOwn(config, 'icon') && !Object.hasOwn(config, 'label')) {
                    label.remove();
    formatTimeField(element, value) {
        if (element.tagName !== 'TIME') {
            element = element.querySelector('time');
            if (!element) return;
                }
                if (Object.hasOwn(config, 'url')) {
                    p.remove();
                    [
                        link.textContent,
                        link.href,
                        link.title
        element.setAttribute('datetime', value);
        element.textContent = window.formatTimeAgo(value, 'F Y');
    }
    formatField(element, value) {
        element.textContent = value;
    }
    addTimelineElements(item, template) {
        let [
            afterEl,
            number,
            started,
            last
                    ] = [
                        config.value,
                        config.url,
                        `Learn more about ${config.value}`
            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']);
        }
    }
    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 {
                    link.remove();
                    p.textContent = config.value;
                icon = window.getIcon(this.container.dataset.icon);
                }
                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;
            template.append(icon);
            fragment.append(template);
        }
        this.ui.grid.append(fragment);
                }
                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;
    /**
     *
     * @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);
                        }
                        let itemImg = image.querySelector('img');
                        itemImg.src = c.image.small;
                        itemImg.alt = c.image.alt;
                        images.append(image);
    }
    /**
     * 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);
                    });
                }
                img.remove();
            }
        }
        single.remove();
        list.remove();
        this.rendered[item.icon].set(item.id, template);
        return template;
        // 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;
    }
    buildImageData(img, data){
        if (typeof data.tiny !== 'string') {
            return;
        }
        [
            img.src,
            img.dataset.small,
            img.dataset.medium,
            img.dataset.large,
            img.alt
        ] =
            [
                data.tiny,
                data.small,
                data.medium,
                data.large,
                data.alt
            ];
    }
    buildUmamiData(item, data){
        for(let [key, value] of Object.entries(data)){
            item.dataset[key] = value;
        }
    }
    /**
     * 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');
    }
    /**
     * Updates the image size according to screen size
     */
    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];
            }
        });
    }
    /**
     * 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);
}
document.addEventListener('DOMContentLoaded', () => {
    }
    /*********************************************************************
    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', async function() {
    window.auth.subscribe(event => {
        if (event === 'auth-loaded') {
    window.feedBlock = new FeedBlock();
        }
    });
});