Merge branch 'main' of https://github.com/jakevdwerf/jvb
35 files modified
1 files added
| | |
| | | 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{--justify:flex-start;--gap:.5rem;margin-bottom:.5rem}.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-item{background:0 0;border:none;padding:.25rem;cursor:pointer;color:#666;border-radius:var(--radius);width:1.5em;height:1.5em}.remove-item .close{width:.5em;height:.5em}.remove-item:hover{color:var(--action-0);background:#fee}.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:38px;height:38px;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%}.create-new-term{margin-top:1rem;width:100%}.create-new-term .field,.create-new-term[open] summary{margin-bottom:1rem}.create-new-term .field{max-width:100%}#jvb-selector>.wrap{--wrap:nowrap;--justify:flex-start}#jvb-selector .items-wrap{width:100%}#jvb-selector .items-container{display:grid;grid-template-columns:repeat(auto-fit,minmax(1fr,100%))}.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%}} |
| | |
| | | .icon-google-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMSwxLTIxLjQ5LTU5LjgyLDYsNiwwLDEsMS05LjI1LDcuNjRBODIsODIsMCwxLDAsMjA5Ljc4LDEzNEgxMjhhNiw2LDAsMCwxLDAtMTJoODhBNiw2LDAsMCwxLDIyMiwxMjhaIi8+PC9zdmc+');}.icon-apple-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTkuNCwxNjcuODRDMjAxLjcxLDE1NS42OSwxOTgsMTM1LjEyLDE5OCwxMjBjMC0xOC40MiwxMy44Ni0zNC4yOSwyMi4xMi00Mi4xMmE2LDYsMCwwLDAsMC04LjcxQzIwOCw1Ny43LDE4Ny4wNyw1MCwxNjgsNTBhNzAuMjMsNzAuMjMsMCwwLDAtNDAsMTIuNTUsNjkuNiw2OS42LDAsMCwwLTg5LjMxLDguMDhBNzIuNjMsNzIuNjMsMCwwLDAsMTgsMTIzLjM1YTEyNS4xMSwxMjUuMTEsMCwwLDAsMzkuNTMsODguMzNBMzcuODUsMzcuODUsMCwwLDAsODMuNiwyMjJoODcuN0EzNy44MywzNy44MywwLDAsMCwxOTksMjEwLjA3YTEyMi42LDEyMi42LDAsMCwwLDE3LjU0LTI0LjJjNi41NS0xMiw1Ljc3LTEzLjc1LDUtMTUuNDhBNi4wNyw2LjA3LDAsMCwwLDIxOS40LDE2Ny44NFptLTI5LjIzLDM0QTI1LjgyLDI1LjgyLDAsMCwxLDE3MS4zLDIxMEg4My42QTI1Ljg1LDI1Ljg1LDAsMCwxLDY1Ljc4LDIwMywxMTMuMjEsMTEzLjIxLDAsMCwxLDMwLDEyM2E2MC41NSw2MC41NSwwLDAsMSwxNy4yMS00NEE1Ni44Miw1Ni44MiwwLDAsMSw4OCw2MmguODFhNTcuMzUsNTcuMzUsMCwwLDEsMzUuNDQsMTIuNzEsNiw2LDAsMCwwLDcuNSwwQTU3LjM5LDU3LjM5LDAsMCwxLDE2OCw2MmMxMy44OSwwLDI4LjgxLDQuNjgsMzkuMTEsMTItOS40NCwxMC4xNC0yMS4xLDI2LjU5LTIxLjEsNDYsMCwyMy43OCw3LjgxLDQyLjYsMjIuNjYsNTQuNzdBMTA3LjMzLDEwNy4zMywwLDAsMSwxOTAuMTcsMjAxLjg5Wm0tNjAtMTcxLjM5QTM4LDM4LDAsMCwxLDE2NywyaDFhNiw2LDAsMCwxLDAsMTJoLTFhMjYsMjYsMCwwLDAtMjUuMTgsMTkuNSw2LDYsMCwxLDEtMTEuNjItM1oiLz48L3N2Zz4=');}.icon-check-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuMjQsOTkuNzZhNiw2LDAsMCwxLDAsOC40OGwtNTYsNTZhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxMTIsMTUxLjUxbDUxLjc2LTUxLjc1QTYsNiwwLDAsMSwxNzIuMjQsOTkuNzZaTTIzMCwxMjhBMTAyLDEwMiwwLDEsMSwxMjgsMjYsMTAyLjEyLDEwMi4xMiwwLDAsMSwyMzAsMTI4Wm0tMTIsMGE5MCw5MCwwLDEsMC05MCw5MEE5MC4xLDkwLjEsMCwwLDAsMjE4LDEyOFoiLz48L3N2Zz4=');}.icon-cloud-slash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik01Mi40NCwzNkE2LDYsMCwwLDAsNDMuNTYsNDRsNDAuMTgsNDQuMmMtLjQ1Ljg3LS45LDEuNzUtMS4zMiwyLjY0QTYyLDYyLDAsMSwwLDcyLDIxNGg4OGE4NS4yMyw4NS4yMywwLDAsMCwzMi4zNS02LjNMMjAzLjU2LDIyMGE2LDYsMCwwLDAsOC44OC04LjA4Wk0xNjAsMjAySDcyYTUwLDUwLDAsMSwxLDUuOS05OS42NEE4Ni4yNSw4Ni4yNSwwLDAsMCw3NCwxMjhhNiw2LDAsMCwwLDEyLDAsNzMuOTIsNzMuOTIsMCwwLDEsNi40NC0zMC4ybDkxLjIyLDEwMC4zNEE3My42NSw3My42NSwwLDAsMSwxNjAsMjAyWm04Ni03NGE4NS44NSw4NS44NSwwLDAsMS0yMS44NSw1Ny4yNyw2LDYsMCwwLDEtNC40NywyLDYsNiwwLDAsMS00LjQ3LTEwLDc0LDc0LDAsMCwwLTk5LTEwOC45Miw2LDYsMCwxLDEtNy4xMS05LjY3QTg2LDg2LDAsMCwxLDI0NiwxMjhaIi8+PC9zdmc+');}.icon-exclamation-mark{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDIsMjAwYTE0LDE0LDAsMSwxLTE0LTE0QTE0LDE0LDAsMCwxLDE0MiwyMDBabS0xNC00MmE2LDYsMCwwLDAsNi02VjQ4YTYsNiwwLDAsMC0xMiwwVjE1MkE2LDYsMCwwLDAsMTI4LDE1OFoiLz48L3N2Zz4=');}.icon-cloud-arrow-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI4YTg1LjI3LDg1LjI3LDAsMCwxLTE3LjIsNTEuNiw2LDYsMCwxLDEtOS42LTcuMkE3NCw3NCwwLDEsMCw4NiwxMjhhNiw2LDAsMCwxLTEyLDAsODUuNTQsODUuNTQsMCwwLDEsMy45MS0yNS42NEE1MC42OCw1MC42OCwwLDAsMCw3MiwxMDJhNTAsNTAsMCwwLDAsMCwxMDBIOTZhNiw2LDAsMCwxLDAsMTJINzJBNjIsNjIsMCwxLDEsODIuNDMsOTAuODgsODYsODYsMCwwLDEsMjQ2LDEyOFptLTY2LjI0LDQzLjc2TDE1OCwxOTMuNTFWMTI4YTYsNiwwLDAsMC0xMiwwdjY1LjUxbC0yMS43Ni0yMS43NWE2LDYsMCwwLDAtOC40OCw4LjQ4bDMyLDMyYTYsNiwwLDAsMCw4LjQ4LDBsMzItMzJhNiw2LDAsMCwwLTguNDgtOC40OFoiLz48L3N2Zz4=');}.icon-caret-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTAwLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OCwwbC04MC04MGE2LDYsMCwwLDEsOC40OC04LjQ4TDEyOCwxNjcuNTFsNzUuNzYtNzUuNzVhNiw2LDAsMCwxLDguNDgsOC40OFoiLz48L3N2Zz4=');}.icon-cloud-arrow-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xODguMjQsMTY0LjI0YTYsNiwwLDAsMS04LjQ4LDBMMTU4LDE0Mi40OVYyMDhhNiw2LDAsMCwxLTEyLDBWMTQyLjQ5bC0yMS43NiwyMS43NWE2LDYsMCwwLDEtOC40OC04LjQ4bDMyLTMyYTYsNiwwLDAsMSw4LjQ4LDBsMzIsMzJBNiw2LDAsMCwxLDE4OC4yNCwxNjQuMjRaTTE2MCw0MkE4Ni4xLDg2LjEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDQwYTYsNiwwLDAsMCwwLTEySDcyYTUwLDUwLDAsMCwxLDAtMTAwLDUwLjY4LDUwLjY4LDAsMCwxLDUuOTEuMzZBODUuNTQsODUuNTQsMCwwLDAsNzQsMTI4YTYsNiwwLDAsMCwxMiwwLDc0LDc0LDAsMSwxLDEwMy42LDY3Ljg1LDYsNiwwLDAsMCw0LjgsMTFBODYsODYsMCwwLDAsMTYwLDQyWiIvPjwvc3ZnPg==');}.icon-cloud-check{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptMzYuMjQtOTQuMjRhNiw2LDAsMCwxLDAsOC40OGwtNDgsNDhhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxNDQsMTUxLjUxbDQzLjc2LTQzLjc1QTYsNiwwLDAsMSwxOTYuMjQsMTA3Ljc2WiIvPjwvc3ZnPg==');}.icon-cloud-warning{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptLTYtNzRWODhhNiw2LDAsMCwxLDEyLDB2NDBhNiw2LDAsMCwxLTEyLDBabTE2LDM2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE3MCwxNjRaIi8+PC9zdmc+');}.icon-syncing{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iY3VycmVudENvbG9yIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PHBhdGggaWQ9InJlZnJlc2giIGQ9Ik0xNjAuMDQ3IDEyMi44NzVhMzAuNzg0IDMwLjc4NCAwIDAgMC0yMS43NSA4Ljc5N2MtMi44NDIgMy4wMDMtLjQ2NyA0Ljk3MSAxLjMxMiAzLjE1NiAxMS4wNDMtMTAuNzg2IDI4LjcxLTEwLjY4IDM5LjYyNS4yMzRsNy4yMDMgNy4yMDRoLTEyLjg3NWMtMy4zNDcuMDA4LTMuMTY1IDMuODc1IDAgMy44NzVoMTYuMTFjMi4wNjIgMCAyLjU0LTEuNDE4IDIuNTYyLTQuOTdsLjA5NC0xNC45MjFjLjAyLTMuMjktMy40MzctMy4xNjUtMy40MzcgMHYxMi44NmwtNy4yMDMtNy4xODhhMzAuNzY4IDMwLjc2OCAwIDAgMC0yMS42NDEtOS4wNDd6bS0yOS41OTQgMzkuNzk3Yy0yLjA2MiAwLTIuNTI0IDEuNDAyLTIuNTQ3IDQuOTUzbC0uMDk0IDE0LjkyMmMtLjAyIDMuMjkgMy40MjIgMy4xNjQgMy40MjIgMHYtMTIuODZsNy4yMDMgNy4yMDRjMTEuOTU2IDExLjk1NSAzMS4zMTIgMTIuMDY0IDQzLjQwNy4yNSAyLjg0Mi0zLjAwMy40NTEtNC45ODgtMS4zMjgtMy4xNzItMTEuMDQzIDEwLjc4Ni0yOC43MSAxMC42OC0zOS42MjUtLjIzNWwtNy4xODgtNy4yMDNoMTIuODZjMy4zNDctLjAwOCAzLjE2NS0zLjg2IDAtMy44NmgtMTYuMTF6Ii8+PHBhdGggZD0iTTE2MCA0NGE4NC4xMSA4NC4xMSAwIDAgMC03Ni40MSA0OS4xMkE2MC43MSA2MC43MSAwIDAgMCA3MiA5MmE2MCA2MCAwIDAgMCAwIDEyMGg4OGE4NCA4NCAwIDAgMCAwLTE2OFptMCAxNjBINzJhNTIgNTIgMCAxIDEgOC41NS0xMDMuM0E4My42NiA4My42NiAwIDAgMCA3NiAxMjhhNCA0IDAgMCAwIDggMCA3NiA3NiAwIDEgMSA3NiA3NloiLz48L3N2Zz4=');}.icon-cloud-x{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptMjguMjQtODUuNzZMMTY4LjQ4LDEzNmwxOS43NiwxOS43NmE2LDYsMCwxLDEtOC40OCw4LjQ4TDE2MCwxNDQuNDhsLTE5Ljc2LDE5Ljc2YTYsNiwwLDAsMS04LjQ4LTguNDhMMTUxLjUyLDEzNmwtMTkuNzYtMTkuNzZhNiw2LDAsMCwxLDguNDgtOC40OEwxNjAsMTI3LjUybDE5Ljc2LTE5Ljc2YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+');}.icon-arrows-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDhWOTZhNiw2LDAsMCwxLTYsNkgxNjhhNiw2LDAsMCwxLDAtMTJoMzMuNTJMMTgzLjQ3LDcyYTgxLjUxLDgxLjUxLDAsMCwwLTU3LjUzLTI0aC0uNDZBODEuNSw4MS41LDAsMCwwLDY4LjE5LDcxLjI4YTYsNiwwLDEsMS04LjM4LTguNTgsOTMuMzgsOTMuMzgsMCwwLDEsNjUuNjctMjYuNzZIMTI2YTkzLjQ1LDkzLjQ1LDAsMCwxLDY2LDI3LjUzbDE4LDE4VjQ4YTYsNiwwLDAsMSwxMiwwWk0xODcuODEsMTg0LjcyYTgxLjUsODEuNSwwLDAsMS01Ny4yOSwyMy4zNGgtLjQ2YTgxLjUxLDgxLjUxLDAsMCwxLTU3LjUzLTI0TDU0LjQ4LDE2Nkg4OGE2LDYsMCwwLDAsMC0xMkg0MGE2LDYsMCwwLDAtNiw2djQ4YTYsNiwwLDAsMCwxMiwwVjE3NC40OGwxOCwxOC4wNWE5My40NSw5My40NSwwLDAsMCw2NiwyNy41M2guNTJhOTMuMzgsOTMuMzgsMCwwLDAsNjUuNjctMjYuNzYsNiw2LDAsMSwwLTguMzgtOC41OFoiLz48L3N2Zz4=');}.icon-share-fat{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTA3Ljc2bC04MC04MEE2LDYsMCwwLDAsMTQ2LDMyVjc0LjJjLTU0LjQ4LDMuNTktMTIwLjM5LDU1LTEyNy45MywxMjAuNjZhMTAsMTAsMCwwLDAsMTcuMjMsOGgwQzQ2LjU2LDE5MC44NSw4NywxNTIuNiwxNDYsMTUwLjEzVjE5MmE2LDYsMCwwLDAsMTAuMjQsNC4yNGw4MC04MEE2LDYsMCwwLDAsMjM2LjI0LDEwNy43NlpNMTU4LDE3Ny41MlYxNDRhNiw2LDAsMCwwLTYtNmMtMjcuNzMsMC01NC43Niw3LjI1LTgwLjMyLDIxLjU1YTE5My4zOCwxOTMuMzgsMCwwLDAtNDAuODEsMzAuNjVjNC43LTI2LjU2LDIwLjE2LTUyLDQ0LTcyLjI3Qzk4LjQ3LDk3Ljk0LDEyNy4yOSw4NiwxNTIsODZhNiw2LDAsMCwwLDYtNlY0Ni40OUwyMjMuNTEsMTEyWiIvPjwvc3ZnPg==');}.icon-trash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBIMTc0VjQwYTIyLDIyLDAsMCwwLTIyLTIySDEwNEEyMiwyMiwwLDAsMCw4Miw0MFY1MEg0MGE2LDYsMCwwLDAsMCwxMkg1MFYyMDhhMTQsMTQsMCwwLDAsMTQsMTRIMTkyYTE0LDE0LDAsMCwwLDE0LTE0VjYyaDEwYTYsNiwwLDAsMCwwLTEyWk05NCw0MGExMCwxMCwwLDAsMSwxMC0xMGg0OGExMCwxMCwwLDAsMSwxMCwxMFY1MEg5NFpNMTk0LDIwOGEyLDIsMCwwLDEtMiwySDY0YTIsMiwwLDAsMS0yLTJWNjJIMTk0Wk0xMTAsMTA0djY0YTYsNiwwLDAsMS0xMiwwVjEwNGE2LDYsMCwwLDEsMTIsMFptNDgsMHY2NGE2LDYsMCwwLDEtMTIsMFYxMDRhNiw2LDAsMCwxLDEyLDBaIi8+PC9zdmc+');}.icon-star{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzcuMjgsOTcuODdBMTQuMTgsMTQuMTgsMCwwLDAsMjI0Ljc2LDg4bC02MC4yNS00Ljg3LTIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDAtMjYuNTgsMEw5MS40OSw4My4xMSwzMS4yNCw4OGExNC4xOCwxNC4xOCwwLDAsMC0xMi41Miw5Ljg5QTE0LjQzLDE0LjQzLDAsMCwwLDIzLDExMy4zMkw2OSwxNTIuOTNsLTE0LDU5LjI1YTE0LjQsMTQuNCwwLDAsMCw1LjU5LDE1LDE0LjEsMTQuMSwwLDAsMCwxNS45MS42TDEyOCwxOTYuMTJsNTEuNTgsMzEuNzFhMTQuMSwxNC4xLDAsMCwwLDE1LjkxLS42LDE0LjQsMTQuNCwwLDAsMCw1LjU5LTE1bC0xNC01OS4yNUwyMzMsMTEzLjMyQTE0LjQzLDE0LjQzLDAsMCwwLDIzNy4yOCw5Ny44N1ptLTEyLjE0LDYuMzctNDguNjksNDJhNiw2LDAsMCwwLTEuOTIsNS45MmwxNC44OCw2Mi43OWEyLjM1LDIuMzUsMCwwLDEtLjk1LDIuNTcsMi4yNCwyLjI0LDAsMCwxLTIuNi4xTDEzMS4xNCwxODRhNiw2LDAsMCwwLTYuMjgsMEw3MC4xNCwyMTcuNjFhMi4yNCwyLjI0LDAsMCwxLTIuNi0uMSwyLjM1LDIuMzUsMCwwLDEtMS0yLjU3bDE0Ljg4LTYyLjc5YTYsNiwwLDAsMC0xLjkyLTUuOTJsLTQ4LjY5LTQyYTIuMzcsMi4zNywwLDAsMS0uNzMtMi42NSwyLjI4LDIuMjgsMCwwLDEsMi4wNy0xLjY1bDYzLjkyLTUuMTZhNiw2LDAsMCwwLDUuMDYtMy42OWwyNC42My01OS42YTIuMzUsMi4zNSwwLDAsMSw0LjM4LDBsMjQuNjMsNTkuNmE2LDYsMCwwLDAsNS4wNiwzLjY5bDYzLjkyLDUuMTZhMi4yOCwyLjI4LDAsMCwxLDIuMDcsMS42NUEyLjM3LDIuMzcsMCwwLDEsMjI1LjE0LDEwNC4yNFoiLz48L3N2Zz4=');}.icon-alphabetical{--icon:url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9ImN1cnJlbnRDb2xvciIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTgzLjc4IDE4NC4wNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNTkuNTg2IDY5Ljc0MmMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAyMjktMC41Mjk1OCAxLjk0OTNzMC4xNzkwMyAxLjU5MzcgMC41Mjk1OCAxLjk5NDRjMC4zNzU1OCAwLjM3NTU4IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGg3LjAxOTdsLTEyLjQyOCAzNC4zNjZoLTIuMTA3Yy0wLjg1MTMgMC0xLjQ2MSAwLjE5NjU2LTEuODM2NiAwLjU5NzE4LTAuMzUwNTQgMC4zNzU1OC0wLjUyOTU3IDEuMDM0MS0wLjUyOTU3IDEuOTYwNiAwIDAuOTI2NDQgMC4xNzkwMyAxLjU4MjUgMC41Mjk1NyAxLjk4MyAwLjM3NTU4IDAuMzc1NTkgMC45ODUyOSAwLjU2MzM4IDEuODM2NiAwLjU2MzM4aDEyLjU1MmMwLjg1MTMgMCAxLjQ1MjItMC4xODc3OSAxLjgwMjgtMC41NjMzOCAwLjM3NTU4LTAuNDAwNjIgMC41NjMzNy0xLjA1NjYgMC41NjMzNy0xLjk4MyAwLTAuOTI2NDUtMC4xODc3OS0xLjU4NS0wLjU2MzM3LTEuOTYwNi0wLjM1MDU0LTAuNDAwNjItMC45NTE0Ny0wLjU5NzE4LTEuODAyOC0wLjU5NzE4aC00LjU1MjFsMy4xMjExLTguOTM0OWgxOC4yMmwzLjA3NiA4LjkzNDloLTUuMDcwNGMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAzNDEtMC41Mjk1OCAxLjk2MDYgMCAwLjkyNjQ0IDAuMTc5MDMgMS41ODI1IDAuNTI5NTggMS45ODMgMC4zNzU1OCAwLjM3NTU5IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGgxMy4yOTZjMC44NTEzIDAgMS40NTIyLTAuMTg3NzkgMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzctMS4wNTY2IDAuNTYzMzctMS45ODMgMC0wLjkyNjQ1LTAuMTg3NzktMS41ODUtMC41NjMzNy0xLjk2MDYtMC4zNTA1NC0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOGgtMi4yODczbC0xMy4yNjItMzcuMDM2Yy0wLjMwMDQ3LTAuODUxMy0wLjc1OTk0LTEuNDYxLTEuMzg1OS0xLjgzNjYtMC42MDA5My0wLjQwMDYyLTEuNDA5Ny0wLjU5NzE4LTIuNDExMy0wLjU5NzE4em00NC4xNDYgMGMtMC44NTEzIDAtMS40NzIzIDAuMTk2NTYtMS44NDc4IDAuNTk3MTgtMC4zNTA1NSAwLjM3NTU4LTAuNTE4MyAxLjAyMjktMC41MTgzIDEuOTQ5M3YxMS45MWMwIDAuODc2MzMgMC4yMDUzMiAxLjUwNjEgMC42MzA5OCAxLjg4MTcgMC40MjU2NiAwLjM3NTU4IDEuMTU5MyAwLjU2MzM3IDIuMTg1OSAwLjU2MzM3czEuNzQ5LTAuMTg3NzkgMi4xNzQ3LTAuNTYzMzdjMC40MjU2OS0wLjM3NTU4IDAuNjQyMjYtMS4wMDUzIDAuNjQyMjYtMS44ODE3di05LjM1MTdoMTguODUxbC0yNC43NTQgMzUuMzAxYy0wLjM1MDU0IDAuNTI1ODItMC41MTgzMSAxLjA3MTctMC41MTgzMSAxLjYyMjYgMCAwLjkyNjQ1IDAuMTY3NzcgMS41ODI1IDAuNTE4MzEgMS45ODMxIDAuMzc1NTggMC4zNzU1OCAwLjk5NjU0IDAuNTYzMzggMS44NDc4IDAuNTYzMzhoMjguNzY2YzAuODUxMyAwIDEuNDUyMi0wLjE4NzggMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzgtMS4wNTY2IDAuNTYzMzgtMS45ODMxdi0xMi42NjVjMC0wLjg3NjMzLTAuMjE2NTgtMS40OTQ4LTAuNjQyMjUtMS44NzA0LTAuNDI1NjYtMC4zNzU1OC0xLjE0OC0wLjU2MzM4LTIuMTc0Ny0wLjU2MzM4LTEuMDI2NiAwLTEuNzQ5IDAuMTg3NzktMi4xNzQ3IDAuNTYzMzgtMC40MjU2NiAwLjM3NTU4LTAuNjQyMjQgMC45OTQwMi0wLjY0MjI0IDEuODcwNHYxMC4xMDdoLTE5Ljk3OGwyNC45MDEtMzUuNDU5YzAuMjUwMzktMC4zNTA1NCAwLjM3MTgzLTAuODM4ODMgMC4zNzE4My0xLjQ2NDggMC0wLjkyNjQ1LTAuMTg3OC0xLjU3MzctMC41NjMzOC0xLjk0OTMtMC4zNTA1NS0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOHptLTMxLjc1MiA1LjEwNDJoMC43MDk4NWw2Ljk4NTkgMjAuMzE1aC0xNC43MTZ6bS0zNy43MjMtNDkuMTgzYy00LjczNDIgMC04LjYzMTMgMy44OTctOC42MzEzIDguNjMxM3YxMTUuNDdjMCA0LjczNDIgMy44OTcgOC42MzEzIDguNjMxMyA4LjYzMTNoMTE1LjI2YzQuNzM0MiAwIDguNjQyMS0zLjg5NyA4LjY0MjEtOC42MzEzdi0xMTUuNDdjMC00LjczNDItMy45MDgyLTguNjMxMy04LjY0MjEtOC42MzEzem0wIDUuNzI0aDExNS4yNmMxLjY1OCAwIDIuOTA3IDEuMjQ5MSAyLjkwNyAyLjkwNzF2MTE1LjQ3YzAgMS42NTgtMS4yNDkxIDIuOTA3LTIuOTA3IDIuOTA3aC0xMTUuMjZjLTEuNjU4IDAtMi44OTU4LTEuMjQ5MS0yLjg5NTgtMi45MDd2LTExNS40N2MwLTEuNjU4IDEuMjM3OC0yLjkwNzEgMi44OTU4LTIuOTA3MXoiIGZpbGw9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIuNzIxMTQiLz48L3N2Zz4=');}.icon-scribble{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjUsMTg4LjI0YTE2LjYzLDE2LjYzLDAsMCwwLDAsMjMuNTIsNiw2LDAsMSwxLTguNDgsOC40OCwyOC42MSwyOC42MSwwLDAsMSwwLTQwLjQ4bDkuMzctOS4zOGExNi42MywxNi42MywwLDAsMC0yMy41Mi0yMy41MWwtNjYuNzUsNjYuNzVhMjguNjMsMjguNjMsMCwwLDEtNDAuNDktNDAuNDlsOTguNzYtOTguNzVhMTYuNjMsMTYuNjMsMCwwLDAtMjMuNTItMjMuNTFMODIuODYsMTE3LjYyQTI4LjYzLDI4LjYzLDAsMCwxLDQyLjM3LDc3LjEzTDgzLjc1LDM1Ljc2YTYsNiwwLDEsMSw4LjQ5LDguNDhMNTAuODYsODUuNjJhMTYuNjMsMTYuNjMsMCwwLDAsMjMuNTIsMjMuNTFsNjYuNzUtNjYuNzVhMjguNjMsMjguNjMsMCwwLDEsNDAuNDksNDAuNDlMODIuODYsMTgxLjYyYTE2LjYzLDE2LjYzLDAsMCwwLDIzLjUyLDIzLjUxbDY2Ljc2LTY2Ljc1YTI4LjYzLDI4LjYzLDAsMCwxLDQwLjQ5LDQwLjQ5WiIvPjwvc3ZnPg==');}.icon-brackets-angle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik04NS4wNiw0My4yMiwzMS4xMSwxMjhsNTQsODQuNzhhNiw2LDAsMCwxLTEuODQsOC4yOCw2LDYsMCwwLDEtOC4yOC0xLjg0bC01Ni04OGE2LDYsMCwwLDEsMC02LjQ0bDU2LTg4YTYsNiwwLDAsMSwxMC4xMiw2LjQ0Wm0xNTIsODEuNTYtNTYtODhhNiw2LDAsMSwwLTEwLjEyLDYuNDRMMjI0Ljg5LDEyOGwtNTMuOTUsODQuNzhhNiw2LDAsMCwwLDEuODQsOC4yOCw2LDYsMCwwLDAsOC4yOC0xLjg0bDU2LTg4QTYsNiwwLDAsMCwyMzcuMDYsMTI0Ljc4WiIvPjwvc3ZnPg==');}.icon-brain{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI0YTU0LjEzLDU0LjEzLDAsMCwwLTMyLTQ5LjMzVjcyYTQ2LDQ2LDAsMCwwLTg2LTIyLjY3QTQ2LDQ2LDAsMCwwLDQyLDcydjIuNjdhNTQsNTQsMCwwLDAsMCw5OC42M1YxNzZhNDYsNDYsMCwwLDAsODYsMjIuNjdBNDYsNDYsMCwwLDAsMjE0LDE3NnYtMi43QTU0LjA3LDU0LjA3LDAsMCwwLDI0NiwxMjRaTTg4LDIxMGEzNCwzNCwwLDAsMS0zNC0zMi45NEE1My42Nyw1My42NywwLDAsMCw2NCwxNzhoOGE2LDYsMCwwLDAsMC0xMkg2NEE0Miw0MiwwLDAsMSw1MCw4NC4zOWE2LDYsMCwwLDAsNC01LjY2VjcyYTM0LDM0LDAsMCwxLDY4LDB2NzMuMDVBNDUuODksNDUuODksMCwwLDAsODgsMTMwYTYsNiwwLDAsMCwwLDEyLDM0LDM0LDAsMCwxLDAsNjhabTEwNC00NGgtOGE2LDYsMCwwLDAsMCwxMmg4YTUzLjY3LDUzLjY3LDAsMCwwLDEwLS45NEEzNCwzNCwwLDEsMSwxNjgsMTQyYTYsNiwwLDAsMCwwLTEyLDQ1Ljg5LDQ1Ljg5LDAsMCwwLTM0LDE1LjA1VjcyYTM0LDM0LDAsMCwxLDY4LDB2Ni43M2E2LDYsMCwwLDAsNCw1LjY2QTQyLDQyLDAsMCwxLDE5MiwxNjZabTE0LTU0YTYsNiwwLDAsMS02LDZoLTRhMzQsMzQsMCwwLDEtMzQtMzRWODBhNiw2LDAsMCwxLDEyLDB2NGEyMiwyMiwwLDAsMCwyMiwyMmg0QTYsNiwwLDAsMSwyMDYsMTEyWk02MCwxMThINTZhNiw2LDAsMCwxLDAtMTJoNEEyMiwyMiwwLDAsMCw4Miw4NFY4MGE2LDYsMCwwLDEsMTIsMHY0QTM0LDM0LDAsMCwxLDYwLDExOFoiLz48L3N2Zz4=');}.icon-palette{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTkuMzcsNTUuMzFBMTAxLjMyLDEwMS4zMiwwLDAsMCwxMjgsMjZoLTFBMTAyLDEwMiwwLDAsMCwyNiwxMjhjMCw0Mi4wOSwyNi4wNyw3Ny40NCw2OCw5Mi4yNkEzMC4yMSwzMC4yMSwwLDAsMCwxMDQuMTEsMjIyLDMwLjA2LDMwLjA2LDAsMCwwLDEzNCwxOTJhMTgsMTgsMCwwLDEsMTgtMThoNDYuMjFhMjkuODIsMjkuODIsMCwwLDAsMjkuMjUtMjMuMzFBMTAyLjcxLDEwMi43MSwwLDAsMCwyMzAsMTI3LjExLDEwMS4yNSwxMDEuMjUsMCwwLDAsMTk5LjM3LDU1LjMxWk0yMTUuNzYsMTQ4YTE3Ljg5LDE3Ljg5LDAsMCwxLTE3LjU1LDE0SDE1MmEzMCwzMCwwLDAsMC0zMCwzMCwxOCwxOCwwLDAsMS0yNCwxN0M2MSwxOTUuODYsMzgsMTY0Ljg1LDM4LDEyOGE5MCw5MCwwLDAsMSw4OS4wNy05MEgxMjhhOTAuMzQsOTAuMzQsMCwwLDEsOTAsODkuMjJBOTAuNDYsOTAuNDYsMCwwLDEsMjE1Ljc2LDE0OFpNMTM4LDc2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCw3NlpNOTQsMTAwQTEwLDEwLDAsMSwxLDg0LDkwLDEwLDEwLDAsMCwxLDk0LDEwMFptMCw1NmExMCwxMCwwLDEsMS0xMC0xMEExMCwxMCwwLDAsMSw5NCwxNTZabTg4LTU2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE4MiwxMDBaIi8+PC9zdmc+');}.icon-pen-nib{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsOTIuNjhhMTMuOTQsMTMuOTQsMCwwLDAtNC4xLTkuOUwxNzMuMjEsMTQuMWExNCwxNCwwLDAsMC0xOS44LDBMMTI0LjY4LDQyLjgzLDY2LjIyLDY0Ljc2YTE0LDE0LDAsMCwwLTguOSwxMC44TDM0LjA4LDIxNUE2LDYsMCwwLDAsNDAsMjIyYTYuNjEsNi42MSwwLDAsMCwxLS4wOGwxMzkuNDQtMjMuMjRhMTQsMTQsMCwwLDAsMTAuODEtOC45bDIxLjkyLTU4LjQ2LDI4Ljc0LTI4Ljc0QTEzLjkyLDEzLjkyLDAsMCwwLDI0Niw5Mi42OFptLTY2LDkyLjg5YTIsMiwwLDAsMS0xLjU0LDEuMjdMNTcuNDksMjA3bDUyLjg3LTUyLjg4YTI2LDI2LDAsMSwwLTguNDgtOC40OEw0OSwxOTguNTNsMjAuMTctMTIxQTIsMiwwLDAsMSw3MC40Myw3Nmw1Ni4wNi0yMUwyMDEsMTI5LjUxWk0xMTAsMTMyYTE0LDE0LDAsMSwxLDE0LDE0QTE0LDE0LDAsMCwxLDExMCwxMzJaTTIzMy40MSw5NC4xLDIwOCwxMTkuNTEsMTM2LjQ4LDQ4LDE2MS45LDIyLjU4YTIsMiwwLDAsMSwyLjgzLDBsNjguNjgsNjguNjlhMiwyLDAsMCwxLDAsMi44M1oiLz48L3N2Zz4=');}.icon-question{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTgwYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxODBaTTEyOCw3NGMtMjEsMC0zOCwxNS4yNS0zOCwzNHY0YTYsNiwwLDAsMCwxMiwwdi00YzAtMTIuMTMsMTEuNjYtMjIsMjYtMjJzMjYsOS44NywyNiwyMi0xMS42NiwyMi0yNiwyMmE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LTIuNDJjMTguMTEtMi41OCwzMi0xNi42NiwzMi0zMy41OEMxNjYsODkuMjUsMTQ5LDc0LDEyOCw3NFptMTAyLDU0QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+');}.icon-city{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsMjEwSDIzMFY4OGE2LDYsMCwwLDAtNi02SDE2MGE2LDYsMCwwLDAtNiw2djQySDEwMlY0MGE2LDYsMCwwLDAtNi02SDMyYTYsNiwwLDAsMC02LDZWMjEwSDE2YTYsNiwwLDAsMCwwLDEySDI0MGE2LDYsMCwwLDAsMC0xMlpNMTY2LDk0aDUyVjIxMEgxNjZabS0xMiw0OHY2OEgxMDJWMTQyWk0zOCw0Nkg5MFYyMTBIMzhaTTcwLDcyVjg4YTYsNiwwLDAsMS0xMiwwVjcyYTYsNiwwLDAsMSwxMiwwWm0wLDQ4djE2YTYsNiwwLDAsMS0xMiwwVjEyMGE2LDYsMCwwLDEsMTIsMFptMCw0OHYxNmE2LDYsMCwwLDEtMTIsMFYxNjhhNiw2LDAsMCwxLDEyLDBabTUyLDE2VjE2OGE2LDYsMCwwLDEsMTIsMHYxNmE2LDYsMCwwLDEtMTIsMFptNjQsMFYxNjhhNiw2LDAsMCwxLDEyLDB2MTZhNiw2LDAsMCwxLTEyLDBabTAtNDhWMTIwYTYsNiwwLDAsMSwxMiwwdjE2YTYsNiwwLDAsMS0xMiwwWiIvPjwvc3ZnPg==');}.icon-folder{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzRIMTMwLjQ5bC0yNy45LTI3LjlhMTMuOTQsMTMuOTQsMCwwLDAtOS45LTQuMUg0MEExNCwxNCwwLDAsMCwyNiw1NlYyMDAuNjJBMTMuMzksMTMuMzksMCwwLDAsMzkuMzgsMjE0SDIxNi44OUExMy4xMiwxMy4xMiwwLDAsMCwyMzAsMjAwLjg5Vjg4QTE0LDE0LDAsMCwwLDIxNiw3NFpNNDAsNTRIOTIuNjlhMiwyLDAsMCwxLDEuNDEuNTlMMTEzLjUxLDc0SDM4VjU2QTIsMiwwLDAsMSw0MCw1NFpNMjE4LDIwMC44OWExLjExLDEuMTEsMCwwLDEtMS4xMSwxLjExSDM5LjM4QTEuNCwxLjQsMCwwLDEsMzgsMjAwLjYyVjg2SDIxNmEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==');}.icon-hash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsOTBIMTczbDguODktNDguOTNhNiw2LDAsMSwwLTExLjgtMi4xNEwxNjAuODEsOTBIMTA5bDguODktNDguOTNhNiw2LDAsMCwwLTExLjgtMi4xNEw5Ni44MSw5MEg0OGE2LDYsMCwwLDAsMCwxMkg5NC42M2wtOS40Niw1MkgzMmE2LDYsMCwwLDAsMCwxMkg4M0w3NC4xLDIxNC45M2E2LDYsMCwwLDAsNC44Myw3QTUuNjQsNS42NCwwLDAsMCw4MCwyMjJhNiw2LDAsMCwwLDUuODktNC45M0w5NS4xOSwxNjZIMTQ3bC04Ljg5LDQ4LjkzYTYsNiwwLDAsMCw0LjgzLDcsNS42NCw1LjY0LDAsMCwwLDEuMDguMSw2LDYsMCwwLDAsNS44OS00LjkzTDE1OS4xOSwxNjZIMjA4YTYsNiwwLDAsMCwwLTEySDE2MS4zN2w5LjQ2LTUySDIyNGE2LDYsMCwwLDAsMC0xMlptLTc0LjgzLDY0SDk3LjM3bDkuNDYtNTJoNTEuOFoiLz48L3N2Zz4=');}.icon-shapes{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik02OS42OSw2Mi4xYTYsNiwwLDAsMC0xMS4zOCwwbC00MCwxMjBBNiw2LDAsMCwwLDI0LDE5MGg4MGE2LDYsMCwwLDAsNS42OS03LjlaTTMyLjMyLDE3OCw2NCw4M2wzMS42OCw5NVpNMjA2LDc2YTUwLDUwLDAsMSwwLTUwLDUwQTUwLjA2LDUwLjA2LDAsMCwwLDIwNiw3NlptLTg4LDBhMzgsMzgsMCwxLDEsMzgsMzhBMzgsMzgsMCwwLDEsMTE4LDc2Wm0xMDYsNzBIMTM2YTYsNiwwLDAsMC02LDZ2NTZhNiw2LDAsMCwwLDYsNmg4OGE2LDYsMCwwLDAsNi02VjE1MkE2LDYsMCwwLDAsMjI0LDE0NlptLTYsNTZIMTQyVjE1OGg3NloiLz48L3N2Zz4=');}.icon-diamonds-four{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjMuNzYsMTA4LjI0YTYsNiwwLDAsMCw4LjQ4LDBsNDAtNDBhNiw2LDAsMCwwLDAtOC40OGwtNDAtNDBhNiw2LDAsMCwwLTguNDgsMGwtNDAsNDBhNiw2LDAsMCwwLDAsOC40OFpNMTI4LDMyLjQ5LDE1OS41MSw2NCwxMjgsOTUuNTEsOTYuNDksNjRabTQuMjQsMTE1LjI3YTYsNiwwLDAsMC04LjQ4LDBsLTQwLDQwYTYsNiwwLDAsMCwwLDguNDhsNDAsNDBhNiw2LDAsMCwwLDguNDgsMGw0MC00MGE2LDYsMCwwLDAsMC04LjQ4Wk0xMjgsMjIzLjUxLDk2LjQ5LDE5MiwxMjgsMTYwLjQ5LDE1OS41MSwxOTJabTEwOC4yNC05OS43NS00MC00MGE2LDYsMCwwLDAtOC40OCwwbC00MCw0MGE2LDYsMCwwLDAsMCw4LjQ4bDQwLDQwYTYsNiwwLDAsMCw4LjQ4LDBsNDAtNDBBNiw2LDAsMCwwLDIzNi4yNCwxMjMuNzZaTTE5MiwxNTkuNTEsMTYwLjQ5LDEyOCwxOTIsOTYuNDksMjIzLjUxLDEyOFptLTgzLjc2LTM1Ljc1LTQwLTQwYTYsNiwwLDAsMC04LjQ4LDBsLTQwLDQwYTYsNiwwLDAsMCwwLDguNDhsNDAsNDBhNiw2LDAsMCwwLDguNDgsMGw0MC00MEE2LDYsMCwwLDAsMTA4LjI0LDEyMy43NlpNNjQsMTU5LjUxLDMyLjQ5LDEyOCw2NCw5Ni40OSw5NS41MSwxMjhaIi8+PC9zdmc+');}.icon-crosshair-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm02LDE5MS44VjE4NGE2LDYsMCwwLDAtMTIsMHYzMy44QTkwLjE1LDkwLjE1LDAsMCwxLDM4LjIsMTM0SDcyYTYsNiwwLDAsMCwwLTEySDM4LjJBOTAuMTUsOTAuMTUsMCwwLDEsMTIyLDM4LjJWNzJhNiw2LDAsMCwwLDEyLDBWMzguMkE5MC4xNSw5MC4xNSwwLDAsMSwyMTcuOCwxMjJIMTg0YTYsNiwwLDAsMCwwLDEyaDMzLjhBOTAuMTUsOTAuMTUsMCwwLDEsMTM0LDIxNy44WiIvPjwvc3ZnPg==');}.icon-circle-notch{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAsMTI4YTEwMiwxMDIsMCwwLDEtMjA0LDBjMC00MC4xOCwyMy4zNS03Ni44Niw1OS41LTkzLjQ1YTYsNiwwLDAsMSw1LDEwLjlDNTguNjEsNjAuMDksMzgsOTIuNDksMzgsMTI4YTkwLDkwLDAsMCwwLDE4MCwwYzAtMzUuNTEtMjAuNjEtNjcuOTEtNTIuNS04Mi41NWE2LDYsMCwwLDEsNS0xMC45QzIwNi42NSw1MS4xNCwyMzAsODcuODIsMjMwLDEyOFoiLz48L3N2Zz4=');}.icon-cards-three{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsOTBINDhhMTQsMTQsMCwwLDAtMTQsMTR2OTZhMTQsMTQsMCwwLDAsMTQsMTRIMjA4YTE0LDE0LDAsMCwwLDE0LTE0VjEwNEExNCwxNCwwLDAsMCwyMDgsOTBabTIsMTEwYTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlYxMDRhMiwyLDAsMCwxLDItMkgyMDhhMiwyLDAsMCwxLDIsMlpNNTAsNjRhNiw2LDAsMCwxLDYtNkgyMDBhNiw2LDAsMCwxLDAsMTJINTZBNiw2LDAsMCwxLDUwLDY0Wk02NiwzMmE2LDYsMCwwLDEsNi02SDE4NGE2LDYsMCwwLDEsMCwxMkg3MkE2LDYsMCwwLDEsNjYsMzJaIi8+PC9zdmc+');}.icon-sun-dim{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjIsNDBWMzJhNiw2LDAsMCwxLDEyLDB2OGE2LDYsMCwwLDEtMTIsMFptNjgsODhhNjIsNjIsMCwxLDEtNjItNjJBNjIuMDcsNjIuMDcsMCwwLDEsMTkwLDEyOFptLTEyLDBhNTAsNTAsMCwxLDAtNTAsNTBBNTAuMDYsNTAuMDYsMCwwLDAsMTc4LDEyOFpNNTkuNzYsNjguMjRhNiw2LDAsMSwwLDguNDgtOC40OGwtOC04YTYsNiwwLDAsMC04LjQ4LDguNDhabTAsMTE5LjUyLTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwxLDAtOC40OC04LjQ4Wm0xMzYtMTM2LTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwwLDAtOC40OC04LjQ4Wm0uNDgsMTM2YTYsNiwwLDAsMC04LjQ4LDguNDhsOCw4YTYsNiwwLDAsMCw4LjQ4LTguNDhaTTQwLDEyMkgzMmE2LDYsMCwwLDAsMCwxMmg4YTYsNiwwLDAsMCwwLTEyWm04OCw4OGE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LThBNiw2LDAsMCwwLDEyOCwyMTBabTk2LTg4aC04YTYsNiwwLDAsMCwwLDEyaDhhNiw2LDAsMCwwLDAtMTJaIi8+PC9zdmc+');}.icon-moon{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuMTMsMTQzLjY0YTYsNiwwLDAsMC02LTEuNDlBOTAuMDcsOTAuMDcsMCwwLDEsMTEzLjg2LDI5Ljg1YTYsNiwwLDAsMC03LjQ5LTcuNDhBMTAyLjg4LDEwMi44OCwwLDAsMCw1NC40OCw1OC42OCwxMDIsMTAyLDAsMCwwLDE5Ny4zMiwyMDEuNTJhMTAyLjg4LDEwMi44OCwwLDAsMCwzNi4zMS01MS44OUE2LDYsMCwwLDAsMjMyLjEzLDE0My42NFptLTQyLDQ4LjI5YTkwLDkwLDAsMCwxLTEyNi0xMjZBOTAuOSw5MC45LDAsMCwxLDk5LjY1LDM3LjY2LDEwMi4wNiwxMDIuMDYsMCwwLDAsMjE4LjM0LDE1Ni4zNSw5MC45LDkwLjksMCwwLDEsMTkwLjEsMTkxLjkzWiIvPjwvc3ZnPg==');}.icon-plus-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRabTIsMTc0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWm0tMzYtODBhNiw2LDAsMCwxLTYsNkgxMzR2MzRhNiw2LDAsMCwxLTEyLDBWMTM0SDg4YTYsNiwwLDAsMSwwLTEyaDM0Vjg4YTYsNiwwLDAsMSwxMiwwdjM0aDM0QTYsNiwwLDAsMSwxNzQsMTI4WiIvPjwvc3ZnPg==');}.icon-arrow-elbow-left-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTkyYTYsNiwwLDAsMS02LDZIODhhNiw2LDAsMCwxLTYtNlY2Mi40OUw0NC4yNCwxMDAuMjRhNiw2LDAsMCwxLTguNDgtOC40OGw0OC00OGE2LDYsMCwwLDEsOC40OCwwbDQ4LDQ4YTYsNiwwLDEsMS04LjQ4LDguNDhMOTQsNjIuNDlWMTg2SDIzMkE2LDYsMCwwLDEsMjM4LDE5MloiLz48L3N2Zz4=');}.icon-arrow-elbow-right-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjAuMjQsMTAwLjI0YTYsNiwwLDAsMS04LjQ4LDBMMTc0LDYyLjQ5VjE5MmE2LDYsMCwwLDEtNiw2SDI0YTYsNiwwLDAsMSwwLTEySDE2MlY2Mi40OWwtMzcuNzYsMzcuNzVhNiw2LDAsMCwxLTguNDgtOC40OGw0OC00OGE2LDYsMCwwLDEsOC40OCwwbDQ4LDQ4QTYsNiwwLDAsMSwyMjAuMjQsMTAwLjI0WiIvPjwvc3ZnPg==');}.icon-x{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjQsMTk1Ljc2YTYsNiwwLDEsMS04LjQ4LDguNDhMMTI4LDEzNi40OSw2MC4yNCwyMDQuMjRhNiw2LDAsMCwxLTguNDgtOC40OEwxMTkuNTEsMTI4LDUxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MWw2Ny43Ni02Ny43NWE2LDYsMCwwLDEsOC40OCw4LjQ4TDEzNi40OSwxMjhaIi8+PC9zdmc+');}.icon-floppy-disk{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSw3My40MiwxODIuNTgsMzguMWExMy45LDEzLjksMCwwLDAtOS44OS00LjFINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY4My4zMUExMy45LDEzLjksMCwwLDAsMjE3LjksNzMuNDJaTTE3MCwyMTBIODZWMTUyYTIsMiwwLDAsMSwyLTJoODBhMiwyLDAsMCwxLDIsMlptNDAtMmEyLDIsMCwwLDEtMiwySDE4MlYxNTJhMTQsMTQsMCwwLDAtMTQtMTRIODhhMTQsMTQsMCwwLDAtMTQsMTR2NThINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDE3Mi42OWEyLDIsMCwwLDEsMS40MS41OEwyMDkuNDIsODEuOWEyLDIsMCwwLDEsLjU4LDEuNDFaTTE1OCw3MmE2LDYsMCwwLDEtNiw2SDk2YTYsNiwwLDAsMSwwLTEyaDU2QTYsNiwwLDAsMSwxNTgsNzJaIi8+PC9zdmc+');}.icon-x-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMTAwLjI0LDEzNi40OCwxMjhsMjcuNzYsMjcuNzZhNiw2LDAsMSwxLTguNDgsOC40OEwxMjgsMTM2LjQ4bC0yNy43NiwyNy43NmE2LDYsMCwwLDEtOC40OC04LjQ4TDExOS41MiwxMjgsOTEuNzYsMTAwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MmwyNy43Ni0yNy43NmE2LDYsMCwwLDEsOC40OCw4LjQ4Wk0yMzAsMTI4QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+');}.icon-minus-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRabTIsMTc0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWm0tMzYtODBhNiw2LDAsMCwxLTYsNkg4OGE2LDYsMCwwLDEsMC0xMmg4MEE2LDYsMCwwLDEsMTc0LDEyOFoiLz48L3N2Zz4=');}.icon-pencil-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjUuOSw3NC43OCwxODEuMjEsMzAuMDlhMTQsMTQsMCwwLDAtMTkuOCwwTDM4LjEsMTUzLjQxYTEzLjk0LDEzLjk0LDAsMCwwLTQuMSw5LjlWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDkyLjY5YTEzLjk0LDEzLjk0LDAsMCwwLDkuOS00LjFMMjI1LjksOTQuNThhMTQsMTQsMCwwLDAsMC0xOS44Wk05NC4xLDIwOS40MWEyLDIsMCwwLDEtMS40MS41OUg0OGEyLDIsMCwwLDEtMi0yVjE2My4zMWEyLDIsMCwwLDEsLjU5LTEuNDFMMTM2LDcyLjQ4LDE4My41MSwxMjBaTTIxNy40MSw4Ni4xLDE5MiwxMTEuNTEsMTQ0LjQ5LDY0LDE2OS45LDM4LjU4YTIsMiwwLDAsMSwyLjgzLDBsNDQuNjgsNDQuNjlhMiwyLDAsMCwxLDAsMi44M1oiLz48L3N2Zz4=');}.icon-dots-six-vertical{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDIsNjBBMTAsMTAsMCwxLDEsOTIsNTAsMTAsMTAsMCwwLDEsMTAyLDYwWm02MiwxMGExMCwxMCwwLDEsMC0xMC0xMEExMCwxMCwwLDAsMCwxNjQsNzBaTTkyLDExOGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCw5MiwxMThabTcyLDBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTY0LDExOFpNOTIsMTg2YTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDkyLDE4NlptNzIsMGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCwxNjQsMTg2WiIvPjwvc3ZnPg==');}.icon-list{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4Wk00MCw3MEgyMTZhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==');}.icon-loading{--icon:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgICB3aWR0aD0iMzIiICAgIGhlaWdodD0iMzIiICAgIHZpZXdCb3g9IjAgMCAzMiAzMiIgICAgdmVyc2lvbj0iMS4xIiAgICB4bWw6c3BhY2U9InByZXNlcnZlIiAgICBzdHlsZT0iY2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNSIgICAgaWQ9InN2ZzEwIiAgICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxkZWZzICAgIGlkPSJkZWZzMTAiIC8+PHBhdGggICAgaWQ9InBhdGgxMSIgICAgc3R5bGU9ImJhc2VsaW5lLXNoaWZ0OmJhc2VsaW5lO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmVjdG9yLWVmZmVjdDpub25lO2ZpbGw6IzIyMjIyMjtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlO3N0b3AtY29sb3I6IzAwMDAwMDtzdG9wLW9wYWNpdHk6MSIgICAgZD0ibSAxNi42MjEwOTQsMS4xNDI1NzgxIGMgLTguMjY2MzIzMiwwIC0xNi4yMjA4NjczOCw2LjQ0MjgwOTUgLTE1LjU4NTkzNzgsMTQuNjg1NTQ2OSAwLjYwMTM0NTUsNy44MDczMDggNy40MzQxMjY0LDE0LjEyNjk4IDE0LjkzMzU5MzgsMTQuOTQzMzU5IDguODM5ODQ1LDAuOTYyMjgzIDE1LjUwNTQ2OSwtNi4zNzY5MTkgMTUuMDA1ODU5LC0xNC45ODYzMjggQyAzMC40OTU5LDcuNTM2MjY4NCAyNC44ODMzOTcsMS4xNDI1NzgxIDE2LjYyMTA5NCwxLjE0MjU3ODEgWiBtIDAsMC42NTAzOTA3IEMgMjYuNDg4Nzg2LDEuODAzODY0NSAyOS43MTQ1MTgsOS41OTM1ODMzIDMwLjMwMjczNCwxNS44MDQ2ODggMzEuMTQxOTgyLDI0LjY2NjM2NSAyMi4xNjA0NTksMzEuMTY4MDc3IDE2LjAzOTA2MiwzMC4xMjUgOC44OTUxMzI3LDI4LjkwNzY4MSAyLjI2MTMxNDIsMjMuMjc5Mzc2IDEuNjgzNTkzOCwxNS43NzkyOTcgMS4wNzY5MzM4LDcuOTAzMjc1NCA4LjcyMjU0NTEsMS43ODQyNjk5IDE2LjYyMTA5NCwxLjc5Mjk2ODggWiBtIC0wLjA2NDQ1LDEuMjE4NzUgYyAtMy42MTAwODMsMCAtNy4xNTQ3OTk1LDEuNDAxMDY4NyAtOS43MzA0NjkxLDMuNzAzMTI1IEMgNC4yNTA1MDIzLDkuMDE2OTAwMiAyLjY0MjAzNzIsMTIuMjI2Mjk1IDIuOTE5OTIxOSwxNS44MzM5ODQgMy40NDY5MzUsMjIuNjc1NzEyIDkuNDI4OTY0OSwyOC4xOTg5ODUgMTUuOTk4MDQ3LDI4LjkxNDA2MiAyMy43MTQyNTYsMjkuNzU0MDIzIDI5LjUzMTYwMywyMy4zMzE3IDI5LjA5NTcwMywxNS44MjAzMTIgMjguNjc3OTQ4LDguNjIxMzk1MyAyMy43NzY2ODYsMy4wMTE3MTg4IDE2LjU1NjY0MSwzLjAxMTcxODggWiBtIDAsMC4xOTUzMTI0IGMgNy4xMTkxMzQsMCAxMS45MzI3MSw1LjUwODEzNzMgMTIuMzQ1NzAzLDEyLjYyNDk5OTggQyAyOS4zMzIwNjIsMjMuMjM2ODk2IDIzLjYxODk1OCwyOS41NDU5OTggMTYuMDE5NTMxLDI4LjcxODc1IDkuNTQ1NDMyMSwyOC4wMTQwMTIgMy42MzQxNjM3LDIyLjU1NTE0MyAzLjExNTIzNDQsMTUuODE4MzU5IDIuODQyNDU2MywxMi4yNzY5NjcgNC40MTg0MTA5LDkuMTI4MzE2OSA2Ljk1NzAzMTIsNi44NTkzNzUgOS40OTU2NTE2LDQuNTkwNDMzMSAxMi45OTcwOTMsMy4yMDcwMzEyIDE2LjU1NjY0MSwzLjIwNzAzMTIgWiBtIC0wLjA3MDMxLDEuNDE2MDE1NyBjIC0zLjE2MTk3MywwIC02LjI2MzUwOSwxLjIyNTgxMzkgLTguNTE5NTMxMSwzLjI0MjE4NzUgQyA1LjcxMDc2OTEsOS44ODE2MDggNC4zMDE0NTQyLDEyLjY5NDU4OSA0LjU0NDkyMTksMTUuODU1NDY5IDUuMDA2NTYyNCwyMS44NDg1NTQgMTAuMjQ0MTc4LDI2LjY4NjE1OSAxNS45OTgwNDcsMjcuMzEyNSAyMi43NTcwMTMsMjguMDQ4MjYxIDI3Ljg1NDQ1MSwyMi40MjA5MzYgMjcuNDcyNjU2LDE1Ljg0MTc5NyAyNy4xMDY4MjQsOS41Mzc2MDI1IDIyLjgxMDE2LDQuNjIzMDQ2OSAxNi40ODYzMjgsNC42MjMwNDY5IFogbSAwLDAuMTk1MzEyNSBjIDYuMjIyOTIsMCAxMC40Mjk5NDYsNC44MTMwMTM4IDEwLjc5MTAxNiwxMS4wMzUxNTY2IDAuMzc1NjEzLDYuNDcyNjE1IC00LjYxNzU4NCwxMS45ODY3MiAtMTEuMjU5NzY2LDExLjI2MzY3MiBDIDEwLjM1ODY4NSwyNi41MDExODYgNS4xOTE4MzgxLDIxLjcyNzk4NSA0LjczODI4MTIsMTUuODM5ODQ0IDQuNDk5OTIwMSwxMi43NDUyNjIgNS44NzY3MzE1LDkuOTk0OTc3OCA4LjA5NTcwMzEsOC4wMTE3MTg4IDEwLjMxNDY3NSw2LjAyODQ1OTUgMTMuMzc0ODksNC44MTgzNTk0IDE2LjQ4NjMyOCw0LjgxODM1OTQgWiBtIC0wLjA2ODM2LDEuNDE2MDE1NiBjIC0yLjcxMzg3NywwIC01LjM3NjExOCwxLjA1MjUxNjQgLTcuMzEyNTAwMiwyLjc4MzIwMzEgLTEuOTM2MzgyOCwxLjczMDY4NjkgLTMuMTQ2NTUxNyw0LjE0NTMxMTkgLTIuOTM3NSw2Ljg1OTM3NDkgMC4zOTYyNjk5LDUuMTQ0NDMgNC44ODk0NDQyLDkuMjk0NDI5IDkuODI4MTI1Miw5LjgzMjAzMSA1LjgwMTc0OSwwLjYzMTU2MiAxMC4xNzkyNTcsLTQuMTk4ODI4IDkuODUxNTYyLC05Ljg0NTcwMyBDIDI1LjUzMzc1LDEwLjQ1MzgyMiAyMS44NDU2MTYsNi4yMzQzNzUgMTYuNDE3OTc0LDYuMjM0Mzc1IFogbSAwLDAuMTk1MzEyNSBjIDUuMzI2NzMsMCA4LjkyNTIyNiw0LjExNzkwNTUgOS4yMzQzNzUsOS40NDUzMTI1IDAuMzIxNTEzLDUuNTQwMzUxIC0zLjk0OTgwMSwxMC4yNTk0NzQgLTkuNjM0NzY2LDkuNjQwNjI1IEMgMTEuMTczODc1LDI0Ljk4ODM2MiA2Ljc0OTUxNDMsMjAuOTAwODE0IDYuMzYxMzI4MSwxNS44NjEzMjggNi4xNTczODMxLDEzLjIxMzU2MyA3LjMzNTA0MzEsMTAuODU5NjgyIDkuMjM0Mzc1LDkuMTYyMTA5NCAxMS4xMzM3MDcsNy40NjQ1MzcyIDEzLjc1NDYyOCw2LjQyOTY4NzUgMTYuNDE3OTY5LDYuNDI5Njg3NSBaIG0gLTAuMDY4MzYsMS40MTYwMTU2IGMgLTIuMjY1Nzc1LDAgLTQuNDg4NzI5LDAuODc5MjE5NiAtNi4xMDU0NjgsMi4zMjQyMTg5IC0xLjYxNjc0MDgsMS40NDQ5OTkgLTIuNjI3NzYwNywzLjQ2MTI2OSAtMi40NTMxMjU0LDUuNzI4NTE2IDAuMzMwODk4Niw0LjI5NTc2OCA0LjA4MTU5NjQsNy43NjAxMiA4LjIwNTA3ODQsOC4yMDg5ODQgNC44NDQ1MjUsMC41MjczNiA4LjUwMDE1NiwtMy41MDYwOTcgOC4yMjY1NjIsLTguMjIwNzAzIEMgMjMuOTYwNjcyLDExLjM3MTk5NiAyMC44ODEwNiw3Ljg0NTcwMzEgMTYuMzQ5NjE0LDcuODQ1NzAzMSBaIG0gMCwwLjE5NTMxMjUgYyA0LjQzMDUzNCwwIDcuNDIyNDYxLDMuNDIyNzk5NCA3LjY3OTY4OCw3Ljg1NTQ2ODQgMC4yNjc0MTIsNC42MDgwODIgLTMuMjgzOTc4LDguNTMyMjI2IC04LjAxMTcxOSw4LjAxNzU3OCBDIDExLjk4OTA3NSwyMy40NzU1MzggOC4zMDcxODk5LDIwLjA3NTU5MyA3Ljk4NDM3NSwxNS44ODQ3NjYgNy44MTQ4NDYzLDEzLjY4MzgxOSA4Ljc5NTMxMDUsMTEuNzI2MzM4IDEwLjM3NSwxMC4zMTQ0NTMgMTEuOTU0Njg5LDguOTAyNTY4OSAxNC4xMzQzNyw4LjA0MTAxNTYgMTYuMzQ5NjA5LDguMDQxMDE1NiBaIG0gLTAuMDY4MzYsMS40MTYwMTU2IGMgLTEuODE3NjcyLDAgLTMuNjAxMzQyLDAuNzAzOTY4OCAtNC44OTg0MzgsMS44NjMyODA4IC0xLjI5NzA5NSwxLjE1OTMxIC0yLjEwODk2ODMsMi43NzkxODUgLTEuOTY4NzQ5NSw0LjU5OTYxIDAuMjY1NTI2OSwzLjQ0NzExMSAzLjI3Mzc1MDUsNi4yMjU4MTMgNi41ODIwMzE1LDYuNTg1OTM3IDMuODg3Mjk1LDAuNDIzMTYgNi44MjMwMDgsLTIuODE1MzE4IDYuNjAzNTE1LC02LjU5NzY1NiBDIDIyLjM4OTU0MSwxMi4yODgyMjIgMTkuOTE2NDk1LDkuNDU3MDMxMSAxNi4yODEyNSw5LjQ1NzAzMTIgWiBtIDAsMC4xOTUzMTI2IGMgMy41MzQzMzMsMCA1LjkxNzc0MiwyLjcyNzY5NjIgNi4xMjMwNDcsNi4yNjU2MjUyIDAuMjEzMzExLDMuNjc1ODE0IC0yLjYxNjIwOCw2LjgwMzAyNSAtNi4zODY3MTksNi4zOTI1NzggLTMuMjEzMjk4LC0wLjM0OTc4NSAtNi4xNTA3NTk3LC0zLjA2MjEzIC02LjQwODIwMywtNi40MDQyOTcgLTAuMTM1MTEyMiwtMS43NTQxMjcgMC42NDQyNTIsLTMuMzEzMjU3IDEuOTA0Mjk3LC00LjQzOTQ1MyAxLjI2MDA0NSwtMS4xMjYxOTYgMy4wMDA0NDEsLTEuODE0NDUzMyA0Ljc2NzU3OCwtMS44MTQ0NTMyIHogbSAtMC4wNzAzMSwxLjQxNjAxNTIgYyAtMS4zNjk1NzIsMCAtMi43MTIsMC41MzA2NzUgLTMuNjg5NDU0LDEuNDA0Mjk3IC0wLjk3NzQ1MywwLjg3MzYyMiAtMS41OTAxNzcsMi4wOTUxNDUgLTEuNDg0Mzc1LDMuNDY4NzUgMC4yMDAxNTYsMi41OTg0NTIgMi40NjU5LDQuNjg5NTUxIDQuOTU4OTg1LDQuOTYwOTM4IDIuOTMwMDcsMC4zMTg5NTggNS4xNDM5MDgsLTIuMTIyNTg3IDQuOTc4NTE1LC00Ljk3MjY1NiAtMC4xNTgxNDUsLTIuNzI1MjQ0IC0yLjAyNDYyMiwtNC44NjEzMjkgLTQuNzYzNjcxLC00Ljg2MTMyOSB6IG0gMCwwLjE5NTMxMyBjIDIuNjM4MTM1LDAgNC40MTQ5NzUsMi4wMzQ1NDQgNC41NjgzNTksNC42Nzc3MzQgMC4xNTkyMTEsMi43NDM1NDYgLTEuOTUwMzg2LDUuMDczODI0IC00Ljc2MzY3Miw0Ljc2NzU3OCAtMi4zOTgxMDIsLTAuMjYxMDQ3IC00LjU5MTEzMSwtMi4yODc3NDEgLTQuNzgzMjAzLC00Ljc4MTI1IC0wLjEwMDY5NiwtMS4zMDczMDggMC40Nzk1MTksLTIuNDcwMDM5IDEuNDE5OTIyLC0zLjMxMDU0NiAwLjk0MDQwMywtMC44NDA1MDggMi4yMzk1NTcsLTEuMzUzNTE2IDMuNTU4NTk0LC0xLjM1MzUxNiB6IG0gLTAuMDY4MzYsMS40MTYwMTYgYyAtMC45MjE0NzIsMCAtMS44MjI2NTcsMC4zNTU0MjUgLTIuNDgwNDY5LDAuOTQzMzU5IC0wLjY1NzgxMSwwLjU4NzkzNCAtMS4wNzMzMzksMS40MTUwMSAtMS4wMDE5NTMsMi4zNDE3OTcgMC4xMzQ3ODUsMS43NDk3OTIgMS42NTYwOTUsMy4xNTMyOTEgMy4zMzM5ODUsMy4zMzU5MzcgMS45NzI4NDYsMC4yMTQ3NTkgMy40NjY3NiwtMS40MzE4MDkgMy4zNTU0NjgsLTMuMzQ5NjA5IC0wLjEwNjIyNCwtMS44MzA1MDMgLTEuMzY0MTc3LC0zLjI3MTQ4NyAtMy4yMDcwMzEsLTMuMjcxNDg0IHogbSAwLDAuMTk1MzEyIGMgMS43NDE5NDIsMCAyLjkxMjIwOSwxLjMzOTQ0IDMuMDEzNjcyLDMuMDg3ODkxIDAuMTA1MTEsMS44MTEyNzYgLTEuMjg0NTYyLDMuMzQ2NTc3IC0zLjE0MDYyNSwzLjE0NDUzMSAtMS41ODI5MDcsLTAuMTcyMzA3IC0zLjAzMzQ1NSwtMS41MTMzNTUgLTMuMTYwMTU2LC0zLjE1ODIwMyAtMC4wNjYyOCwtMC44NjA0OSAwLjMxNDc4NSwtMS42MjQ4NjggMC45MzU1NDcsLTIuMTc5Njg4IDAuNjIwNzQ5LC0wLjU1NDgxOSAxLjQ4MDYyLC0wLjg5NDUzMSAyLjM1MTU1NiwtMC44OTQ1MzEgeiBtIC0wLjA2ODM2LDEuNDE2MDE2IGMgLTAuNDczMzY5LDAgLTAuOTM1MjcxLDAuMTgyMTI5IC0xLjI3MzQzOCwwLjQ4NDM3NSAtMC4zMzgxNjcsMC4zMDIyNDYgLTAuNTU0NTQ2LDAuNzMwOTY5IC0wLjUxNzU3OCwxLjIxMDkzNyAwLjA2OTQxLDAuOTAxMTMzIDAuODQ4MjQ5LDEuNjE4OTgxIDEuNzEwOTM4LDEuNzEyODkxIDEuMDE1NjE2LDAuMTEwNTU3IDEuNzg5NjE0LC0wLjc0MTAzMSAxLjczMjQyMSwtMS43MjY1NjMgLTAuMDU0MywtMC45MzU3NjYgLTAuNzA1NjkxLC0xLjY4MTY0IC0xLjY1MjM0MywtMS42ODE2NCB6IG0gMCwwLjE5NTMxMiBjIDAuODQ1NzQsMCAxLjQwNzQ5LDAuNjQ0MzMzIDEuNDU3MDMxLDEuNDk4MDQ3IDAuMDUxMDEsMC44NzkwMDggLTAuNjE2NzkzLDEuNjE5MzI5IC0xLjUxNTYyNSwxLjUyMTQ4NCAtMC43Njc3MDYsLTAuMDgzNTcgLTEuNDc1NzgsLTAuNzM4OTY3IC0xLjUzNzEwOSwtMS41MzUxNTYgLTAuMDMxODYsLTAuNDEzNjcxIDAuMTUwMDU1LC0wLjc3OTY5NyAwLjQ1MTE3MiwtMS4wNDg4MjggMC4zMDExMTYsLTAuMjY5MTMxIDAuNzIxNjk4LC0wLjQzNTU0NyAxLjE0NDUzMSwtMC40MzU1NDcgeiIgLz48L3N2Zz4=');}.icon-magnifying-glass{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMjE5Ljc2bC01MS4zOC01MS4zOGE4Ni4xNSw4Ni4xNSwwLDEsMC04LjQ4LDguNDhsNTEuMzgsNTEuMzhhNiw2LDAsMCwwLDguNDgtOC40OFpNMzgsMTEyYTc0LDc0LDAsMSwxLDc0LDc0QTc0LjA5LDc0LjA5LDAsMCwxLDM4LDExMloiLz48L3N2Zz4=');}.icon-infinity{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI4YTU0LDU0LDAsMCwxLTkyLjE4LDM4LjE4LDMuMDcsMy4wNywwLDAsMS0uMjUtLjI2bC02MC02Ny43NGE0Miw0MiwwLDEsMCwwLDU5LjY0bDguNTctOS42N2E2LDYsMCwxLDEsOSw4bC04LjY5LDkuODFhMy4wNywzLjA3LDAsMCwxLS4yNS4yNiw1NCw1NCwwLDEsMSwwLTc2LjM2LDMuMDcsMy4wNywwLDAsMSwuMjUuMjZsNjAsNjcuNzRhNDIsNDIsMCwxLDAsMC01OS42NGwtOC41Nyw5LjY3YTYsNiwwLDEsMS05LThsOC42OS05LjgxYTMuMDcsMy4wNywwLDAsMSwuMjUtLjI2QTU0LDU0LDAsMCwxLDI0NiwxMjhaIi8+PC9zdmc+');}.icon-arrow-counter-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMCwxLTkyLjc0LDk0SDEyOGE5My40Myw5My40MywwLDAsMS02NC41LTI1LjY1LDYsNiwwLDEsMSw4LjI0LTguNzJBODIsODIsMCwxLDAsNzAsNzBsLS4xOS4xOUwzOS40NCw5OEg3MmE2LDYsMCwwLDEsMCwxMkgyNGE2LDYsMCwwLDEtNi02VjU2YTYsNiwwLDAsMSwxMiwwVjkwLjM0TDYxLjYzLDYxLjRBOTQsOTQsMCwwLDEsMjIyLDEyOFoiLz48L3N2Zz4=');}.icon-clock{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm0wLDE5MmE5MCw5MCwwLDEsMSw5MC05MEE5MC4xLDkwLjEsMCwwLDEsMTI4LDIxOFptNjItOTBhNiw2LDAsMCwxLTYsNkgxMjhhNiw2LDAsMCwxLTYtNlY3MmE2LDYsMCwwLDEsMTIsMHY1MGg1MEE2LDYsMCwwLDEsMTkwLDEyOFoiLz48L3N2Zz4=');}.icon-house{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSwxMTAuMWwtODAtODBhMTQsMTQsMCwwLDAtMTkuOCwwbC04MCw4MEExMy45MiwxMy45MiwwLDAsMCwzNCwxMjB2OTZhNiw2LDAsMCwwLDYsNmg2NGE2LDYsMCwwLDAsNi02VjE1OGgzNnY1OGE2LDYsMCwwLDAsNiw2aDY0YTYsNiwwLDAsMCw2LTZWMTIwQTEzLjkyLDEzLjkyLDAsMCwwLDIxNy45LDExMC4xWk0yMTAsMjEwSDE1OFYxNTJhNiw2LDAsMCwwLTYtNkgxMDRhNiw2LDAsMCwwLTYsNnY1OEg0NlYxMjBhMiwyLDAsMCwxLC41OC0xLjQybDgwLTgwYTIsMiwwLDAsMSwyLjg0LDBsODAsODBBMiwyLDAsMCwxLDIxMCwxMjBaIi8+PC9zdmc+');}.icon-logo{--icon:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgICB3aWR0aD0iMzIiICAgIGhlaWdodD0iMzIiICAgIHZpZXdCb3g9IjAgMCAzMiAzMiIgICAgdmVyc2lvbj0iMS4xIiAgICB4bWw6c3BhY2U9InByZXNlcnZlIiAgICBzdHlsZT0iY2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlO3N0cm9rZS1saW5lam9pbjpiZXZlbDtzdHJva2UtbWl0ZXJsaW1pdDoxLjUiICAgIGlkPSJzdmcxMiIgICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcyAgICBpZD0iZGVmczEyIiAvPjxwYXRoICAgIGQ9Ik0gMTYsMi4wOTE3OTY5IEMgOC4yODYyNTM4LDIuMDkxNzk2OSAyLjAyNTM5MDcsOC4zMjE0NzYzIDIuMDI1MzkwNiwxNiAyLjAyNTM5MDYsMjMuNjc4NTI0IDguMjg2MjUzOCwyOS45MDgyMDMgMTYsMjkuOTA4MjAzIDIzLjcxMzc0NiwyOS45MDgyMDMgMjkuOTc0NjA5LDIzLjY3ODUyNCAyOS45NzQ2MDksMTYgMjkuOTc0NjA5LDguMzIxNDc2MyAyMy43MTM3NDYsMi4wOTE3OTY5IDE2LDIuMDkxNzk2OSBaIG0gMCwwLjYwNTQ2ODcgYyAwLjIwMzk4LDAgMC40MDU2ODMsMC4wMDY2NCAwLjYwNzQyMiwwLjAxNTYyNSAwLjE5NjUxNiwwLjAwODc1IDAuMzkxNzI4LDAuMDIxOTcyIDAuNTg1OTM3LDAuMDM5MDYzIC0wLjE5NDE2NiwtMC4wMTcwODEgLTAuMzg5NDY3LC0wLjAzMDMxNiAtMC41ODU5MzcsLTAuMDM5MDYzIFYgMjkuMjg3MTA5IGMgMC4xOTY0NywtMC4wMDg3IDAuMzkxNzcxLC0wLjAyMTk4IDAuNTg1OTM3LC0wLjAzOTA2IC0wLjE5NDIwOSwwLjAxNzA5IC0wLjM4OTQyMSwwLjAzMDMxIC0wLjU4NTkzNywwLjAzOTA2IEMgMTYuNDA1NjgzLDI5LjI5NjA5IDE2LjIwMzk4LDI5LjMwMjczNCAxNiwyOS4zMDI3MzQgOC42MTIxNjcsMjkuMzAyNzM0IDIuNjMwODU5NCwyMy4zNDk0MDggMi42MzA4NTk0LDE2IDIuNjMwODU5NCw4LjY1MDU5MTggOC42MTIxNjcxLDIuNjk3MjY1NiAxNiwyLjY5NzI2NTYgWiBtIDEuMjgxMjUsMC4wNjI1IGMgMC4yMTQ5ODYsMC4wMjAzMjkgMC40Mjg3NCwwLjA0NTg0NSAwLjY0MDYyNSwwLjA3NjE3MiAwLjIxMTQxMiwwLjAzMDI1OSAwLjQyMDgyMywwLjA2NTQ5NyAwLjYyODkwNiwwLjEwNTQ2ODggQyAxOC4zNDI4MiwyLjkwMTQ3NDYgMTguMTMzMTU5LDIuODY2MTcxOCAxNy45MjE4NzUsMi44MzU5Mzc1IDE3LjcwOTkxNSwyLjgwNTYwNjYgMTcuNDk2MzEsMi43ODAwOTU1IDE3LjI4MTI1LDIuNzU5NzY1NiBaIG0gMS4yODcxMDksMC4xODM1OTM4IGMgMC4yMTExMDcsMC4wNDA4NDEgMC40MTk1MzMsMC4wODgwODEgMC42MjY5NTMsMC4xMzg2NzE5IDAuMTc2MDc3LDAuMDQyOTQ2IDAuMzUwMTg2LDAuMDkwODIgMC41MjM0MzgsMC4xNDA2MjUgLTAuMTczMDk1LC0wLjA0OTczMSAtMC4zNDc1MjUsLTAuMDk3NzM4IC0wLjUyMzQzOCwtMC4xNDA2MjUgLTAuMjA3NDI4LC0wLjA1MDU3MSAtMC40MTU4NCwtMC4wOTc4NDcgLTAuNjI2OTUzLC0wLjEzODY3MTkgeiBtIDEuMjg3MTEsMC4zMTgzNTk0IGMgMC4xNDQ0MTEsMC4wNDMxODUgMC4yODczODQsMC4wODg4NTMgMC40Mjk2ODcsMC4xMzY3MTg3IC0wLjE0MjM0OCwtMC4wNDc4NTcgLTAuMjg1MjMxLC0wLjA5MzU0MyAtMC40Mjk2ODcsLTAuMTM2NzE4NyB6IG0gMC42MTcxODcsMC4yMDExNzE4IGMgMC4xNjc2NzIsMC4wNTkxOTEgMC4zMzM1MzcsMC4xMjE5MTcxIDAuNDk4MDQ3LDAuMTg3NSAtMC4xNjQ1NjEsLTAuMDY1NTc0IC0wLjMzMDMyNCwtMC4xMjgzMTg3IC0wLjQ5ODA0NywtMC4xODc1IHogbSAwLjU2MjUsMC4yMTI4OTA3IGMgMC4xNzYxNzgsMC4wNzEyOTcgMC4zNTExMDcsMC4xNDYxNDY0IDAuNTIzNDM4LDAuMjI0NjA5MyAtMC4xNzIyMTgsLTAuMDc4MzcxIC0wLjM0NzM4LC0wLjE1MzM5MTYgLTAuNTIzNDM4LC0wLjIyNDYwOTMgeiBtIC01LjUyNzM0MywwLjExOTE0MDYgLTAuMTIxMDk0LDAuMDA1ODYgQyA4Ljg5ODA2NTIsNC4xMTkzODMgMy43MjY1NjI1LDkuNDY0MjE4NSAzLjcyNjU2MjUsMTYgYyAwLDYuNTM1ODE3IDUuMTcxNTAyNSwxMS44ODA2NTIgMTEuNjYwMTU2NSwxMi4xOTkyMTkgbCAwLjEyMTA5NCwwLjAwNTkgMC4wMTE3MiwtMC4yNDIxODcgLTAuMTIxMDkzLC0wLjAwNTkgQyA5LjAzNjA5MjgsMjcuNjQ0NjY0IDMuOTY4NzUsMjIuNDA3OTY1IDMuOTY4NzUsMTYgMy45Njg3NSw5LjU5MjA3MjEgOS4wMzYwOTY0LDQuMzU1MzY5OCAxNS4zOTg0MzgsNC4wNDI5Njg3IGwgMC4xMjEwOTMsLTAuMDA1ODYgeiBtIDYuMTA5Mzc0LDAuMTMwODU5NCBjIDAuMTY0NjM1LDAuMDc1OTExIDAuMzI3MzY0LDAuMTU1OTgyOCAwLjQ4ODI4MiwwLjIzODI4MTIgLTAuMTYxNTE3LC0wLjA4MjU5NCAtMC4zMjMwMjEsLTAuMTYyMTIwMyAtMC40ODgyODIsLTAuMjM4MjgxMiB6IG0gMC41ODM5ODUsMC4yODcxMDkzIGMgMC4xNTIwMjYsMC4wNzkzIDAuMzAwNzE1LDAuMTYzMTY1MyAwLjQ0OTIxOSwwLjI0ODA0NjkgLTAuMTQ4MjE1LC0wLjA4NDY5MiAtMC4yOTc0OTcsLTAuMTY4OTEzIC0wLjQ0OTIxOSwtMC4yNDgwNDY5IHogbSAwLjU1NjY0LDAuMzA4NTkzOCBjIDAuMTM4NDkzLDAuMDgwODY2IDAuMjc0OTEzLDAuMTY0MzcwOCAwLjQxMDE1NywwLjI1IC0wLjEzNTA3OSwtMC4wODU0ODYgLTAuMjcxODM3LC0wLjE2OTI2MjkgLTAuNDEwMTU3LC0wLjI1IHogbSAwLjU0ODgyOSwwLjMzOTg0MzcgYyAwLjEyMzIwMSwwLjA4MDE1MSAwLjI0NDkwMywwLjE2MjA0OTcgMC4zNjUyMzQsMC4yNDYwOTM4IC0wLjEyMDM0OSwtMC4wODQwMiAtMC4yNDIwMTUsLTAuMTY1OTY2MyAtMC4zNjUyMzQsLTAuMjQ2MDkzOCB6IG0gMC41NDY4NzUsMC4zNzUgYyAwLjEwMzM0OCwwLjA3NDc3MSAwLjIwNTU2LDAuMTUwODk4NiAwLjMwNjY0LDAuMjI4NTE1NiAtMC4xMDEwNzEsLTAuMDc3NTc1IC0wLjIwMzMwMiwtMC4xNTM3ODUgLTAuMzA2NjQsLTAuMjI4NTE1NiB6IG0gLTguMzQ3NjU3LDAuMDcwMzEyIC0wLjEyMTA5MywwLjAwNzgxIEMgOS43MzUzODExLDUuNjMxODMwOCA1LjI0NDE0MDYsMTAuMjk4OTk4IDUuMjQ0MTQwNiwxNiBjIDAsNS43MDEwMzMgNC40OTExNDg1LDEwLjM2ODE3NCAxMC4xNDA2MjU0LDEwLjY4NTU0NyBsIDAuMTIxMDkzLDAuMDA3OCAwLjAxMzY3LC0wLjI0MjE4NyAtMC4xMjEwOTMsLTAuMDA3OCBDIDkuODc0NjI3MiwyNi4xMzMwNDQgNS40ODgyODE0LDIxLjU3Mzc1NyA1LjQ4ODI4MTIsMTYgYyAwLC01LjU3MzcyNCA0LjM4NjM0MiwtMTAuMTMzMDQ2MSA5LjkxMDE1NjgsLTEwLjQ0MzM1OTQgbCAwLjEyMTA5MywtMC4wMDc4MSB6IG0gOC44NDk2MSwwLjMxMDU0NjkgYyAwLjEwMDk1NiwwLjA4MDUyNyAwLjIwMDMwNCwwLjE2Mjc0MTEgMC4yOTg4MjgsMC4yNDYwOTM3IC0wLjA5ODcsLTAuMDgzNDc1IC0wLjE5NzY5MiwtMC4xNjU0NTM1IC0wLjI5ODgyOCwtMC4yNDYwOTM3IHogbSAwLjQ5MDIzNCwwLjQxMDE1NjIgYyAwLjA5NDk3LDAuMDgzNDYyIDAuMTg4NzEzLDAuMTY3ODI3MiAwLjI4MTI1LDAuMjUzOTA2MyAtMC4wOTIzLC0wLjA4NTgxMyAtMC4xODY1MzUsLTAuMTcwNjk0OSAtMC4yODEyNSwtMC4yNTM5MDYzIHogbSAwLjQ0MzM1OSwwLjQwODIwMzIgQyAyNS4zOTM4NzcsNi41MzYzNjA5IDI1LjQ5ODE0Nyw2LjYzODAzNCAyNS41OTk2MDksNi43NDIxODcgMjUuNDk3OTc5LDYuNjM3OTAyIDI1LjM5NDA1NCw2LjUzNjQ4MjggMjUuMjg5MDYyLDYuNDM1NTQ2NCBaIE0gMTUuNTA1ODU5LDYuODIwMzEyIDE1LjM4NDc2Niw2LjgyODEyMiBDIDEwLjU3NDc5Niw3LjE0Mzc4OTUgNi43NjM2NzE5LDExLjEzMzk2MyA2Ljc2MzY3MTksMTYgYyAwLDQuODY2MDM4IDMuODExMDMyMSw4Ljg1NjE4OCA4LjYyMTA5NDEsOS4xNzE4NzUgbCAwLjEyMTA5MywwLjAwNzggMC4wMTU2MywtMC4yNDIxODcgLTAuMTIxMDkzLC0wLjAwNzggQyAxMC43MTUxLDI0LjYyMjE4OCA3LjAwNTg1OTQsMjAuNzM5NTMgNy4wMDU4NTk0LDE2IGMgMCwtNC43Mzk1MjkgMy43MDkyNTE2LC04LjYyMjIxNjggOC4zOTQ1MzE2LC04LjkyOTY4NzUgTCAxNS41MjE0ODQsNy4wNjI1IFogbSAxMC4yMTg3NSwwLjA1MjczNCBjIDAuMDkzNzcsMC4wOTg4ODYgMC4xODY2MjgsMC4xOTkwNjk0IDAuMjc3MzQ0LDAuMzAwNzgxMiBDIDI1LjkxMTE0Myw3LjA3MjAzNDUgMjUuODE4NDc5LDYuOTcyMDA3OSAyNS43MjQ2MDksNi44NzMwNDYgWiBtIDAuNDM5NDUzLDAuNDg2MzI4MSBjIDAuMDk0MzksMC4xMDk4MzY1IDAuMTg2NTM0LDAuMjIxMDkyNCAwLjI3NzM0NCwwLjMzMzk4NDQgLTAuMDkwNjksLTAuMTEyNzA0NCAtMC4xODMwODEsLTAuMjI0MzI0OSAtMC4yNzczNDQsLTAuMzMzOTg0NCB6IG0gMC4zNTM1MTYsMC40Mjk2ODc1IGMgMC4xMDE5NDcsMC4xMjkxNDA4IDAuMjAxNjQ3LDAuMjU5NjQ5NiAwLjI5ODgyOCwwLjM5MjU3ODEgQyAyNi43MTkwODUsOC4wNDg1NTI4IDI2LjYxOTY3OSw3LjkxODM1MiAyNi41MTc1NzgsNy43ODkwNjE2IFogTSAyNi45MTAxNTYsOC4zMTI1IGMgMC4wODU5NywwLjEyMDYyOTIgMC4xNzE5MTgsMC4yNDE2NzMzIDAuMjUzOTA2LDAuMzY1MjM0NCBDIDI3LjA4MjE2MSw4LjU1NDM0MzYgMjYuOTk2MDM1LDguNDMyOTY1NSAyNi45MTAxNTYsOC4zMTI1IFogbSAtMTEuNDA2MjUsMC4wMjM0MzcgLTAuMTIxMDkzLDAuMDA3ODEgQyAxMS40MTI3OTgsOC42NTcxMTA1IDguMjgzMjAzMSwxMS45NjkxODcgOC4yODMyMDMxLDE2IGMgMCw0LjAzMDgzNSAzLjEyOTQwNzksNy4zNDI5MTggNy4wOTk2MDk5LDcuNjU2MjUgbCAwLjEyMTA5MywwLjAwNzggMC4wMTk1MywtMC4yNDAyMzQgLTAuMTIxMDk0LC0wLjAwOTggQyAxMS41NTU2NjUsMjMuMTEwNTcyIDguNTI1MzkwNiwxOS45MDU0MDkgOC41MjUzOTA2LDE2IGMgMCwtMy45MDUzODcgMy4wMzAyNzA0LC03LjExMDQ1NTggNi44NzY5NTM0LC03LjQxNDA2MjUgbCAwLjEyMTA5NCwtMC4wMDk3NyB6IG0gMTEuNzg3MTEsMC41NDI5Njg4IGMgMC4wNjkwMywwLjEwODE3MDQgMC4xMzkxMzMsMC4yMTU5MTA0IDAuMjA1MDc4LDAuMzI2MTcxOCAtMC4wNjU5LC0wLjExMDE1MTMgLTAuMTM2MDk5LC0wLjIxODEwODQgLTAuMjA1MDc4LC0wLjMyNjE3MTggeiBtIDAuMzQ1NzAzLDAuNTcyMjY1NiBjIDAuMDUzMjcsMC4wOTM1NTkgMC4xMDcxNjIsMC4xODYyOTU0IDAuMTU4MjAzLDAuMjgxMjUgLTAuMDUxMTIsLTAuMDk1MDgyIC0wLjEwNDg0NCwtMC4xODc1Njg1IC0wLjE1ODIwMywtMC4yODEyNSB6IG0gLTEyLjEzNDc2NiwwLjM5ODQzOCAtMC4xMjEwOTQsMC4wMTM2NzIgQyAxMi4yNTE1MjcsMTAuMTczMDgxIDkuODAyNzM0NCwxMi44MDQ4NzMgOS44MDI3MzQ0LDE2IGMgMCwzLjE5NTE0NCAyLjQ0ODUxMjYsNS44MjY5MiA1LjU3ODEyNDYsNi4xMzY3MTkgbCAwLjEyMTA5NCwwLjAxMzY3IDAuMDIzNDQsLTAuMjQyMTg4IC0wLjEyMTA5NCwtMC4wMTE3MiBDIDEyLjM5NjI0NiwyMS41OTg3MjUgMTAuMDQ0OTIyLDE5LjA3MTM0OCAxMC4wNDQ5MjIsMTYgYyAwLC0zLjA3MTMyOSAyLjM1MTMyOCwtNS41OTg3MTkgNS4zNTkzNzUsLTUuODk2NDg0IGwgMC4xMjEwOTQsLTAuMDExNzIgeiBtIDEyLjQ3NDYwOSwwLjI0MDIzNDYgYyAwLjAzMDk1LDAuMDYyMDIgMC4wNjM3NSwwLjEyMjk4NiAwLjA5Mzc1LDAuMTg1NTQ3IC0wLjAyOTk1LC0wLjA2MjQzIC0wLjA2Mjg1LC0wLjEyMzY2NCAtMC4wOTM3NSwtMC4xODU1NDcgeiBtIDAuMjgzMjA0LDAuNTk5NjA5IGMgMC4wMjU0LDAuMDU4MDIgMC4wNTE1OCwwLjExNTM4NCAwLjA3NjE3LDAuMTczODI4IC0wLjAyNDU4LC0wLjA1ODQxIC0wLjA1MDc4LC0wLjExNTg0OSAtMC4wNzYxNywtMC4xNzM4MjggeiBtIDAuMjM0Mzc1LDAuNTc0MjE5IGMgMC4wMjY0MiwwLjA2OTAyIDAuMDU0NzgsMC4xMzc0NyAwLjA4MDA4LDAuMjA3MDMxIC0wLjAyNTM2LC0wLjA2OTcyIC0wLjA1MzU5LC0wLjEzNzg2MSAtMC4wODAwOCwtMC4yMDcwMzEgeiBtIC0xMi45OTgwNDcsMC4xMDU0NjkgLTAuMTE5MTQxLDAuMDE1NjMgQyAxMy4wODk4MDUsMTEuNjg4NjA0IDExLjMyMjI2NiwxMy42NDE0OTMgMTEuMzIyMjY2LDE2IGMgMCwyLjM1ODUyMSAxLjc2NzE3Nyw0LjMxMTM4NyA0LjA1NDY4Nyw0LjYxNTIzNCBsIDAuMTE5MTQxLDAuMDE1NjMgMC4wMzMyLC0wLjI0MDIzNCBMIDE1LjQwODIsMjAuMzc1IEMgMTMuMjM4ODQ1LDIwLjA4Njg0NyAxMS41NjQ0NSwxOC4yMzc0NjMgMTEuNTY0NDUsMTYgYyAwLC0yLjIzNzQ0NyAxLjY3NDM5NSwtNC4wODY4NDYgMy44NDM3NSwtNC4zNzUgbCAwLjEyMTA5NCwtMC4wMTU2MyB6IE0gMjguODc1LDEyLjQxNjAxNiBjIDAuMDMwOCwwLjEwOTg3NSAwLjA2Mzc4LDAuMjE5MDczIDAuMDkxOCwwLjMzMDA3OCAtMC4wMjc5OSwtMC4xMTA4NDYgLTAuMDYxMDMsLTAuMjIwMzYgLTAuMDkxOCwtMC4zMzAwNzggeiBtIC0xMy4zODg2NzIsMC40Nzg1MTUgLTAuMTE3MTg3LDAuMDIzNDQgQyAxMy45MjgwNzYsMTMuMjA5NjMyIDEyLjgzOTg0NCwxNC40ODA2NTEgMTIuODM5ODQ0LDE2IGMgMCwxLjUxOTM1IDEuMDg3ODAyLDIuNzkwMzY1IDIuNTI5Mjk3LDMuMDgyMDMxIGwgMC4xMTcxODcsMC4wMjM0NCAwLjA0ODgzLC0wLjIzODI4MiAtMC4xMTkxNCwtMC4wMjM0NCBDIDE0LjA4NTM0MiwxOC41NzQ1MSAxMy4wODM5ODQsMTcuNDAzODUyIDEzLjA4Mzk4NCwxNiBjIDAsLTEuNDAzODUxIDEuMDAxMzYyLC0yLjU3NDUwNyAyLjMzMjAzMiwtMi44NDM3NSBsIDAuMTE5MTQsLTAuMDIzNDQgeiBNIDI5LDEyLjg5NjQ4NCBDIDI5LjAzODA2LDEzLjA1NTA3IDI5LjA3NTEyLDEzLjIxNDI2MiAyOS4xMDc0MjIsMTMuMzc1IDI5LjA3NTA2NywxMy4yMTQwNTQgMjkuMDM4MTMyLDEzLjA1NTI3MSAyOSwxMi44OTY0ODQgWiBtIDAuMTM4NjcyLDAuNjQ2NDg1IGMgMC4wMjk1NywwLjE1NzcwOCAwLjA1ODEsMC4zMTQ5OTEgMC4wODIwMywwLjQ3NDYwOSAtMC4wMjM5MSwtMC4xNTkyNjIgLTAuMDUyNTEsLTAuMzE3MjUgLTAuMDgyMDMsLTAuNDc0NjA5IHogbSAtMTMuNjgzNTk0LDAuOTEyMTA5IC0wLjExMTMyOCwwLjA0ODgzIEMgMTQuNzY0OTMyLDE0Ljc1NjA0MyAxNC4zNTkzNzUsMTUuMzMxNjcyIDE0LjM1OTM3NSwxNiBjIDAsMC42NjgzMjkgMC40MDUxMjcsMS4yNDM5NTIgMC45ODQzNzUsMS40OTYwOTQgbCAwLjExMTMyOCwwLjA0ODgzIDAuMDk3NjYsLTAuMjIyNjU2IC0wLjExMTMyOCwtMC4wNDY4NyBDIDE0Ljk0ODAwMywxNy4wNjA2MDYgMTQuNjAxNTYsMTYuNTcwNDM5IDE0LjYwMTU2MywxNiBjIDAsLTAuNTcwNDM4IDAuMzQ2NDQ3LC0xLjA2MDYyIDAuODM5ODQzLC0xLjI3NTM5MSBsIDAuMTExMzI4LC0wLjA0Njg3IHogbSAxMy43NjU2MjUsMy41MjczNDQgYyAtMC4wMjM5MywwLjE1OTYxOCAtMC4wNTI0NiwwLjMxNjkwMSAtMC4wODIwMywwLjQ3NDYwOSAwLjAyOTUyLC0wLjE1NzM1OSAwLjA1ODEzLC0wLjMxNTM0NyAwLjA4MjAzLC0wLjQ3NDYwOSB6IE0gMjkuMTA3NDIyLDE4LjYyNSBjIC0wLjAzMjMsMC4xNjA3MzggLTAuMDY5MzYsMC4zMTk5MyAtMC4xMDc0MjIsMC40Nzg1MTYgMC4wMzgxMywtMC4xNTg3ODcgMC4wNzUwNywtMC4zMTc1NyAwLjEwNzQyMiwtMC40Nzg1MTYgeiBtIC0wLjE0MDYyNSwwLjYyODkwNiBjIC0wLjAyODAyLDAuMTExMDA1IC0wLjA2MSwwLjIyMDIwMyAtMC4wOTE4LDAuMzMwMDc4IDAuMDMwNzcsLTAuMTA5NzE4IDAuMDYzOCwtMC4yMTkyMzIgMC4wOTE4LC0wLjMzMDA3OCB6IG0gLTAuMzkyNTc4LDEuMjc1MzkxIGMgLTAuMDI1MywwLjA2OTU2IC0wLjA1MzY2LDAuMTM4MDE2IC0wLjA4MDA4LDAuMjA3MDMxIDAuMDI2NDksLTAuMDY5MTcgMC4wNTQ3MiwtMC4xMzczMTIgMC4wODAwOCwtMC4yMDcwMzEgeiBtIC0wLjIzODI4MiwwLjYwNzQyMiBjIC0wLjAyNDU5LDAuMDU4NDQgLTAuMDUwNzcsMC4xMTU4MTEgLTAuMDc2MTcsMC4xNzM4MjggMC4wMjUzOSwtMC4wNTc5OCAwLjA1MTU5LC0wLjExNTQyMiAwLjA3NjE3LC0wLjE3MzgyOCB6IG0gLTAuMjY1NjI1LDAuNTg3ODkgYyAtMC4wMywwLjA2MjU2IC0wLjA2MjgsMC4xMjM1MzEgLTAuMDkzNzUsMC4xODU1NDcgMC4wMzA5LC0wLjA2MTg4IDAuMDYzOCwtMC4xMjMxMiAwLjA5Mzc1LC0wLjE4NTU0NyB6IG0gLTAuMjc1MzksMC41NDI5NjkgYyAtMC4wNTEwNCwwLjA5NDk1IC0wLjEwNDkzMiwwLjE4NzY5MSAtMC4xNTgyMDMsMC4yODEyNSAwLjA1MzM2LC0wLjA5MzY4IDAuMTA3MDgxLC0wLjE4NjE2OCAwLjE1ODIwMywtMC4yODEyNSB6IG0gLTAuMjk4ODI4LDAuNTI3MzQ0IGMgLTAuMDY1OTQsMC4xMTAyNjEgLTAuMTM2MDUxLDAuMjE4MDAxIC0wLjIwNTA3OCwwLjMyNjE3MiAwLjA2ODk4LC0wLjEwODA2NCAwLjEzOTE3NiwtMC4yMTYwMjEgMC4yMDUwNzgsLTAuMzI2MTcyIHogbSAtMC4zMzIwMzIsMC41MjczNDQgYyAtMC4wODE5OSwwLjEyMzU2MSAtMC4xNjc5MzEsMC4yNDQ2MDUgLTAuMjUzOTA2LDAuMzY1MjM0IDAuMDg1ODgsLTAuMTIwNDY2IDAuMTcyMDA1LC0wLjI0MTg0NCAwLjI1MzkwNiwtMC4zNjUyMzQgeiBtIC0wLjM0NzY1NiwwLjQ5NjA5MyBjIC0wLjA5NzE4LDAuMTMyOTI5IC0wLjE5Njg4MSwwLjI2MzQzOCAtMC4yOTg4MjgsMC4zOTI1NzggMC4xMDIxMDEsLTAuMTI5Mjg5IDAuMjAxNTA3LC0wLjI1OTQ5IDAuMjk4ODI4LC0wLjM5MjU3OCB6IG0gLTAuMzc1LDAuNDg4MjgyIGMgLTAuMDkwODEsMC4xMTI4OTIgLTAuMTgyOTU0LDAuMjI0MTQ4IC0wLjI3NzM0NCwwLjMzMzk4NCAwLjA5NDI2LC0wLjEwOTY1OSAwLjE4NjY1MSwtMC4yMjEyNzkgMC4yNzczNDQsLTAuMzMzOTg0IHogbSAtMC40Mzk0NTMsMC41MTk1MzEgYyAtMC4wOTA3MiwwLjEwMTcxMiAtMC4xODM1NzUsMC4yMDE4OTUgLTAuMjc3MzQ0LDAuMzAwNzgxIDAuMDkzODcsLTAuMDk4OTYgMC4xODY1MzMsLTAuMTk4OTg4IDAuMjc3MzQ0LC0wLjMwMDc4MSB6IG0gLTAuNDAyMzQ0LDAuNDMxNjQgYyAtMC4xMDE0NjIsMC4xMDQxNTMgLTAuMjA1NzMyLDAuMjA1ODI3IC0wLjMxMDU0NywwLjMwNjY0MSAwLjEwNDk5MiwtMC4xMDA5MzYgMC4yMDg5MTcsLTAuMjAyMzU1IDAuMzEwNTQ3LC0wLjMwNjY0MSB6IG0gLTAuNDcyNjU2LDAuNDYwOTM4IGMgLTAuMDkyNTQsMC4wODYwOCAtMC4xODYyNzksMC4xNzA0NDQgLTAuMjgxMjUsMC4yNTM5MDYgMC4wOTQ3MiwtMC4wODMyMSAwLjE4ODk1NSwtMC4xNjgwOTMgMC4yODEyNSwtMC4yNTM5MDYgeiBtIC0wLjQ3MjY1NiwwLjQxNzk2OSBjIC0wLjA5ODUyLDAuMDgzMzUgLTAuMTk3ODcyLDAuMTY1NTY2IC0wLjI5ODgyOCwwLjI0NjA5MyAwLjEwMTEzNiwtMC4wODA2NCAwLjIwMDEzMiwtMC4xNjI2MTkgMC4yOTg4MjgsLTAuMjQ2MDkzIHogbSAtMC40OTQxNDEsMC4zOTg0MzcgYyAtMC4xMDEwOCwwLjA3NzYyIC0wLjIwMzI5MiwwLjE1Mzc0NSAtMC4zMDY2NCwwLjIyODUxNiAwLjEwMzMzOCwtMC4wNzQ3MyAwLjIwNTU2OSwtMC4xNTA5NCAwLjMwNjY0LC0wLjIyODUxNiB6IG0gLTAuNDg4MjgxLDAuMzU3NDIyIGMgLTAuMTIwMzMxLDAuMDg0MDQgLTAuMjQyMDMzLDAuMTY1OTQzIC0wLjM2NTIzNCwwLjI0NjA5NCAwLjEyMzIxOSwtMC4wODAxMyAwLjI0NDg4NSwtMC4xNjIwNzQgMC4zNjUyMzQsLTAuMjQ2MDk0IHogbSAtMC41MDM5MDYsMC4zMzU5MzggYyAtMC4xMzUyNDQsMC4wODU2MyAtMC4yNzE2NjQsMC4xNjkxMzMgLTAuNDEwMTU3LDAuMjUgMC4xMzgzMiwtMC4wODA3NCAwLjI3NTA3OCwtMC4xNjQ1MTQgMC40MTAxNTcsLTAuMjUgeiBtIC0wLjUxNzU3OCwwLjMxMDU0NiBjIC0wLjE0ODUwNCwwLjA4NDg4IC0wLjI5NzE5MywwLjE2ODc0NyAtMC40NDkyMTksMC4yNDgwNDcgMC4xNTE3MjIsLTAuMDc5MTMgMC4zMDEwMDQsLTAuMTYzMzU1IDAuNDQ5MjE5LC0wLjI0ODA0NyB6IG0gLTAuNTQ0OTIyLDAuMjk2ODc1IGMgLTAuMTYwOTE4LDAuMDgyMyAtMC4zMjM2NDcsMC4xNjIzNyAtMC40ODgyODIsMC4yMzgyODIgMC4xNjUyNjEsLTAuMDc2MTYgMC4zMjY3NjUsLTAuMTU1Njg4IDAuNDg4MjgyLC0wLjIzODI4MiB6IG0gLTAuNTQ2ODc1LDAuMjYzNjcyIGMgLTAuMTcyMzMxLDAuMDc4NDYgLTAuMzQ3MjYsMC4xNTMzMTIgLTAuNTIzNDM4LDAuMjI0NjEgMC4xNzYwNTgsLTAuMDcxMjIgMC4zNTEyMiwtMC4xNDYyMzkgMC41MjM0MzgsLTAuMjI0NjEgeiBtIC0wLjU4Nzg5MSwwLjI1IGMgLTAuMTY0NTEsMC4wNjU1OCAtMC4zMzAzNzUsMC4xMjgzMDkgLTAuNDk4MDQ3LDAuMTg3NSAwLjE2NzcyMywtMC4wNTkxOCAwLjMzMzQ4NiwtMC4xMjE5MjUgMC40OTgwNDcsLTAuMTg3NSB6IG0gLTAuNjg1NTQ3LDAuMjUxOTUzIGMgLTAuMTQyMzAzLDAuMDQ3ODcgLTAuMjg1Mjc2LDAuMDkzNTMgLTAuNDI5Njg3LDAuMTM2NzE5IDAuMTQ0NDU2LC0wLjA0MzE4IDAuMjg3MzM5LC0wLjA4ODg2IDAuNDI5Njg3LC0wLjEzNjcxOSB6IG0gLTAuNTY2NDA2LDAuMTc1NzgyIGMgLTAuMTczMjUyLDAuMDQ5OCAtMC4zNDczNjEsMC4wOTc2OCAtMC41MjM0MzgsMC4xNDA2MjUgLTAuMjA3NDIsMC4wNTA1OSAtMC40MTU4NDYsMC4wOTc4MyAtMC42MjY5NTMsMC4xMzg2NzIgMC4yMTExMTMsLTAuMDQwODIgMC40MTk1MjUsLTAuMDg4MSAwLjYyNjk1MywtMC4xMzg2NzIgMC4xNzU5MTMsLTAuMDQyODkgMC4zNTAzNDMsLTAuMDkwODkgMC41MjM0MzgsLTAuMTQwNjI1IHogbSAtMS4xNjc5NjksMC4yODEyNSBjIC0wLjIwODA4MywwLjAzOTk3IC0wLjQxNzQ5NCwwLjA3NTIxIC0wLjYyODkwNiwwLjEwNTQ2OCAtMC4yMTE4ODUsMC4wMzAzMyAtMC40MjU2MzksMC4wNTU4NCAtMC42NDA2MjUsMC4wNzYxNyAwLjIxNTA2LC0wLjAyMDMzIDAuNDI4NjY1LC0wLjA0NTg0IDAuNjQwNjI1LC0wLjA3NjE3IDAuMjExMjg0LC0wLjAzMDIzIDAuNDIwOTQ1LC0wLjA2NTU0IDAuNjI4OTA2LC0wLjEwNTQ2OCB6IiAgICBzdHlsZT0iYmFzZWxpbmUtc2hpZnQ6YmFzZWxpbmU7Y2xpcC1ydWxlOm5vbnplcm87ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2ZWN0b3ItZWZmZWN0Om5vbmU7ZmlsbDojMjIyMjIyO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlO3N0b3AtY29sb3I6IzAwMDAwMDtzdG9wLW9wYWNpdHk6MSIgICAgaWQ9InBhdGgxNCIgLz48L3N2Zz4=');}.icon-jakevan{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0eWxlPSJjbGlwLXJ1bGU6ZXZlbm9kZDtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MiIgdmlld0JveD0iMCAwIDMyIDMyIj48cGF0aCBkPSJNMTcuODggMTQuNjhIMTIuOWwtLjQzLTEuNjNIOS41OGwtLjQ1IDEuNjNINi41bDIuODktOC43NGgzLjJsMi44OSA4LjY0di04LjZoMi40djMuNzhjLjEtLjIuMjItLjM4LjM1LS41Ny4xMy0uMi4yNi0uMzcuMzktLjU0bDEuODYtMi42N2g3Ljh2MS44OUgyNS40djEuMzdoMi42NXYxLjg4SDI1LjR2MS42NWgyLjg2djEuOTFoLTcuOTNsLTEuNzUtMy4zMi0uNy40MXptNS4xMy04LjU5LTIuNyAzLjc5IDIuNyA0Ljc0em0tMTEuMDUgNS4wMy0uMzgtMS40M2ExMzYuODYgMTM2Ljg2IDAgMCAwLS40LTEuNTVMMTEgNy4zOGExNy43NiAxNy43NiAwIDAgMS0uMzYgMS42bC0uMTguNzEtLjM5IDEuNDN6bS04LjU4IDYuM2E1Ljc0IDUuNzQgMCAwIDEtMS4yNC0uMTN2LTEuODNsLjQxLjA4Yy4xNS4wMy4zLjA1LjQ3LjA1LjMgMCAuNTEtLjA2LjY3LS4xN2EuOTIuOTIgMCAwIDAgLjM0LS41MmMuMDYtLjIzLjEtLjUyLjEtLjg2VjUuOThoMi40djcuODVjMCAuODgtLjEzIDEuNTctLjQgMi4xLS4yNi41Mi0uNjMuOS0xLjEgMS4xNC0uNDguMjMtMS4wMy4zNS0xLjY1LjM1WiIgc3R5bGU9ImZpbGw6Y3VycmVudENvbG9yO3N0cm9rZS13aWR0aDouMDE4NDM5MiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS40IC42Nikgc2NhbGUoLjk2MDUwMTM0KSIvPjxwYXRoIGQ9Ik0yMi44MiAyMi4yN2gtNC4wNmwtLjM3LTEuNEgxNS45bC0uMzkgMS40aC0yLjI2bDIuNDktNy41M2gyLjc1bDIuNDkgNy40NHYtNy40MWgyLjdsMi43NyA1LjIxaC4wM2E0MS4xIDQxLjEgMCAwIDEtLjA3LTEuODJ2LTMuMzloMS44M3Y3LjVoLTIuN2wtMi43OS01LjI4aC0uMDRhMTIuODMgMTIuODMgMCAwIDEgLjA4IDEuMjZsLjAyLjY0em0tNC44Ni0zLjA3LS4zMy0xLjIzYTg5LjA3IDg5LjA3IDAgMCAwLS4zNS0xLjM0bC0uMTQtLjY1YTE1LjA0IDE1LjA0IDAgMCAxLS4zMSAxLjM3bC0uMTYuNjItLjMzIDEuMjN6bS0zLjg1LTQuNDMtMi41IDcuNUg5LjJsLTIuNS03LjVoMi4zMmwxLjA0IDMuOGExNS4wMyAxNS4wMyAwIDAgMSAuMzYgMS43NiA3LjYxIDcuNjEgMCAwIDEgLjItMS4ybC4xNC0uNTQgMS4wNi0zLjgyeiIgc3R5bGU9ImZpbGw6Y3VycmVudENvbG9yO3N0cm9rZS13aWR0aDouMDE1OTg4NCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS40IC42Nikgc2NhbGUoLjk2MDUwMTM0KSIvPjxwYXRoIGQ9Ik0xMS45IDI0LjIxYzAgLjQtLjA3LjcyLS4yLjk5LS4xNS4yNi0uMzYuNDYtLjYzLjYtLjI4LjEzLS42Mi4yLTEuMDMuMkg5LjJ2LTMuNWguOTdjLjM4IDAgLjcuMDYuOTYuMTkuMjUuMTMuNDUuMzIuNTguNTguMTQuMjUuMi41Ny4yLjk0em0tLjI2LjAxYzAtLjMzLS4wNS0uNjEtLjE2LS44M2ExLjEgMS4xIDAgMCAwLS41MS0uNTEgMS45NSAxLjk1IDAgMCAwLS44Ny0uMTdoLS42NnYzLjA3aC42Yy41MyAwIC45My0uMTMgMS4yLS4zOS4yNy0uMjYuNC0uNjUuNC0xLjE3ek0xNC4yNyAyNmgtMS45NXYtMy41aDEuOTV2LjIyaC0xLjd2MS4zMmgxLjZ2LjIzaC0xLjZ2MS41aDEuN3ptMS4yOC0zLjVjLjI4IDAgLjUyLjAyLjcuMDhhLjguOCAwIDAgMSAuNDQuM2MuMS4xNC4xNC4zMy4xNC41N2EuOS45IDAgMCAxLS4xLjQ1Ljg3Ljg3IDAgMCAxLS4yNy4zMmMtLjEyLjA4LS4yNS4xNC0uNC4xOGwuOTggMS42aC0uM2wtLjkyLTEuNTNoLS44OVYyNmgtLjI1di0zLjV6bS0uMDMuMjFoLS41OXYxLjU1aC43MWMuMyAwIC41Mi0uMDcuNjktLjIxLjE2LS4xNC4yNC0uMzQuMjQtLjYgMC0uMjgtLjA5LS40Ny0uMjYtLjU4LS4xNy0uMS0uNDMtLjE2LS43OS0uMTZ6bTUuNTctLjIyTDIwLjEyIDI2aC0uMjVsLS43Ni0yLjY1LS4wNS0uMTYtLjA0LS4xNGExOC44IDE4LjggMCAwIDEtLjA2LS4yNCAyMC42IDIwLjYgMCAwIDEtLjExLjQ4TDE4LjA5IDI2aC0uMjVsLS45Ni0zLjVoLjI2bC42NyAyLjQ3YTI3LjM2IDI3LjM2IDAgMCAxIC4wOS4zNWwuMDQuMTcuMDMuMTUuMDMtLjE2YTQuODMgNC44MyAwIDAgMSAuMTQtLjUzbC43LTIuNDZoLjI1bC43MyAyLjQ4YTExLjk4IDExLjk4IDAgMCAxIC4xMy41M2wuMDQuMTVhMTEuMDIgMTEuMDIgMCAwIDEgLjE1LS42OGwuNjktMi40OHpNMjMuMjYgMjZoLTEuOTV2LTMuNWgxLjk1di4yMmgtMS43djEuMzJoMS42di4yM2gtMS42djEuNWgxLjd6bTEuMjgtMy41Yy4yOCAwIC41Mi4wMi43MS4wOGEuOC44IDAgMCAxIC40My4zYy4xLjE0LjE0LjMzLjE0LjU3YS45LjkgMCAwIDEtLjEuNDUuODcuODcgMCAwIDEtLjI3LjMyYy0uMTEuMDgtLjI1LjE0LS40LjE4bC45OCAxLjZoLS4zbC0uOTItMS41M2gtLjg4VjI2aC0uMjZ2LTMuNXptLS4wMi4yMWgtLjZ2MS41NWguNzJjLjI5IDAgLjUxLS4wNy42OC0uMjEuMTYtLjE0LjI0LS4zNC4yNC0uNiAwLS4yOC0uMDgtLjQ3LS4yNi0uNTgtLjE3LS4xLS40My0uMTYtLjc4LS4xNnpNMjYuNSAyNmgtLjI1di0zLjVoMS45NXYuMjJoLTEuN3YxLjQ5aDEuNnYuMjJoLTEuNnoiIHN0eWxlPSJmaWxsOmN1cnJlbnRDb2xvcjtzdHJva2Utd2lkdGg6LjAxMDEwNjgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEuNCAuNjYpIHNjYWxlKC45NjA1MDEzNCkiLz48L3N2Zz4=');}.icon-user-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRaTTk0LDEyMGEzNCwzNCwwLDEsMSwzNCwzNEEzNCwzNCwwLDAsMSw5NCwxMjBaTTY1Ljc3LDIxMGE2Ni40Myw2Ni40MywwLDAsMSwyMC43Ny0yOS4zNiw2Niw2NiwwLDAsMSw4Mi45MiwwQTY2LjQzLDY2LjQzLDAsMCwxLDE5MC4yMywyMTBaTTIxMCwyMDhhMiwyLDAsMCwxLTIsMmgtNS4xN2E3Ny44NSw3Ny44NSwwLDAsMC00OS4zOC01MS43MSw0Niw0NiwwLDEsMC01MC45LDBBNzcuODUsNzcuODUsMCwwLDAsNTMuMTcsMjEwSDQ4YTIsMiwwLDAsMS0yLTJWNDhhMiwyLDAsMCwxLDItMkgyMDhhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-chat-teardrop{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzIsMjZhOTguMTEsOTguMTEsMCwwLDAtOTgsOTh2ODRhMTQsMTQsMCwwLDAsMTQsMTRoODRhOTgsOTgsMCwwLDAsMC0xOTZabTAsMTg0SDQ4YTIsMiwwLDAsMS0yLTJWMTI0YTg2LDg2LDAsMSwxLDg2LDg2WiIvPjwvc3ZnPg==');}.icon-house-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSwxMTAuMWwtODAtODBhMTQsMTQsMCwwLDAtMTkuOCwwbC04MCw4MEExMy45MiwxMy45MiwwLDAsMCwzNCwxMjB2OTZhNiw2LDAsMCwwLDYsNkgyMTZhNiw2LDAsMCwwLDYtNlYxMjBBMTMuOTIsMTMuOTIsMCwwLDAsMjE3LjksMTEwLjFaTTIxMCwyMTBINDZWMTIwYTIsMiwwLDAsMSwuNTgtMS40Mmw4MC04MGEyLDIsMCwwLDEsMi44NCwwbDgwLDgwQTIsMiwwLDAsMSwyMTAsMTIwWiIvPjwvc3ZnPg==');}.icon-caret-left{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMjAzLjc2YTYsNiwwLDEsMS04LjQ4LDguNDhsLTgwLTgwYTYsNiwwLDAsMSwwLTguNDhsODAtODBhNiw2LDAsMCwxLDguNDgsOC40OEw4OC40OSwxMjhaIi8+PC9zdmc+');}.icon-chat{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBINDBBMTQsMTQsMCwwLDAsMjYsNjRWMjI0YTEzLjg4LDEzLjg4LDAsMCwwLDguMDksMTIuNjlBMTQuMTEsMTQuMTEsMCwwLDAsNDAsMjM4YTEzLjg3LDEzLjg3LDAsMCwwLDktMy4zMWwuMDYtLjA1TDgyLjIzLDIwNkgyMTZhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjE2LDUwWm0yLDE0MmEyLDIsMCwwLDEtMiwySDgwYTYsNiwwLDAsMC0zLjkyLDEuNDZMNDEuMjYsMjI1LjUzQTIsMiwwLDAsMSwzOCwyMjRWNjRhMiwyLDAsMCwxLDItMkgyMTZhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-envelope{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBabS05Niw4NS44Nkw0Ny40Miw2MkgyMDguNThaTTEwMS42NywxMjgsMzgsMTg2LjM2VjY5LjY0Wm04Ljg4LDguMTRMMTI0LDE0OC40MmE2LDYsMCwwLDAsOC4xLDBsMTMuNC0xMi4yOEwyMDguNTgsMTk0SDQ3LjQzWk0xNTQuMzMsMTI4LDIxOCw2OS42NFYxODYuMzZaIi8+PC9zdmc+');}.icon-caret-right{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xODAuMjQsMTMyLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OC04LjQ4TDE2Ny41MSwxMjgsOTEuNzYsNTIuMjRhNiw2LDAsMCwxLDguNDgtOC40OGw4MCw4MEE2LDYsMCwwLDEsMTgwLjI0LDEzMi4yNFoiLz48L3N2Zz4=');}.icon-calendar{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRIMTgyVjI0YTYsNiwwLDAsMC0xMiwwVjM0SDg2VjI0YTYsNiwwLDAsMC0xMiwwVjM0SDQ4QTE0LDE0LDAsMCwwLDM0LDQ4VjIwOGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMjA4LDM0Wk00OCw0Nkg3NFY1NmE2LDYsMCwwLDAsMTIsMFY0Nmg4NFY1NmE2LDYsMCwwLDAsMTIsMFY0NmgyNmEyLDIsMCwwLDEsMiwyVjgySDQ2VjQ4QTIsMiwwLDAsMSw0OCw0NlpNMjA4LDIxMEg0OGEyLDIsMCwwLDEtMi0yVjk0SDIxMFYyMDhBMiwyLDAsMCwxLDIwOCwyMTBabS05OC05MHY2NGE2LDYsMCwwLDEtMTIsMFYxMjkuNzFsLTcuMzIsMy42NmE2LDYsMCwxLDEtNS4zNi0xMC43NGwxNi04QTYsNiwwLDAsMSwxMTAsMTIwWm01OS41NywyOS4yNUwxNDgsMTc4aDIwYTYsNiwwLDAsMSwwLDEySDEzNmE2LDYsMCwwLDEtNC44LTkuNkwxNjAsMTQyYTEwLDEwLDAsMSwwLTE2LjY1LTExQTYsNiwwLDEsMSwxMzMsMTI1YTIyLDIyLDAsMSwxLDM2LjYyLDI0LjI2WiIvPjwvc3ZnPg==');}.icon-shuffle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTc5Ljc2YTYsNiwwLDAsMSwwLDguNDhsLTI0LDI0YTYsNiwwLDAsMS04LjQ4LTguNDhMMjE3LjUyLDE5MEgyMDAuOTRhNzAuMTYsNzAuMTYsMCwwLDEtNTctMjkuMzFsLTQxLjcxLTU4LjRBNTguMTEsNTguMTEsMCwwLDAsNTUuMDYsNzhIMzJhNiw2LDAsMCwxLDAtMTJINTUuMDZhNzAuMTYsNzAuMTYsMCwwLDEsNTcsMjkuMzFsNDEuNzEsNTguNEE1OC4xMSw1OC4xMSwwLDAsMCwyMDAuOTQsMTc4aDE2LjU4bC0xMy43Ni0xMy43NmE2LDYsMCwwLDEsOC40OC04LjQ4Wm0tOTIuMDYtNzQuNDFhNS45MSw1LjkxLDAsMCwwLDMuNDgsMS4xMiw2LDYsMCwwLDAsNC44OS0yLjUxbDEuMTktMS42N0E1OC4xMSw1OC4xMSwwLDAsMSwyMDAuOTQsNzhoMTYuNThMMjAzLjc2LDkxLjc2YTYsNiwwLDEsMCw4LjQ4LDguNDhsMjQtMjRhNiw2LDAsMCwwLDAtOC40OGwtMjQtMjRhNiw2LDAsMCwwLTguNDgsOC40OEwyMTcuNTIsNjZIMjAwLjk0YTcwLjE2LDcwLjE2LDAsMCwwLTU3LDI5LjMxTDE0Mi43OCw5N0E2LDYsMCwwLDAsMTQ0LjE4LDEwNS4zNVptLTMyLjM2LDQ1LjNhNiw2LDAsMCwwLTguMzcsMS4zOWwtMS4xOSwxLjY3QTU4LjExLDU4LjExLDAsMCwxLDU1LjA2LDE3OEgzMmE2LDYsMCwwLDAsMCwxMkg1NS4wNmE3MC4xNiw3MC4xNiwwLDAsMCw1Ny0yOS4zMWwxLjE5LTEuNjdBNiw2LDAsMCwwLDExMS44MiwxNTAuNjVaIi8+PC9zdmc+');}.icon-sort-descending{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00MiwxMjhhNiw2LDAsMCwxLDYtNmg3MmE2LDYsMCwwLDEsMCwxMkg0OEE2LDYsMCwwLDEsNDIsMTI4Wm02LTU4aDU2YTYsNiwwLDAsMCwwLTEySDQ4YTYsNiwwLDAsMCwwLDEyWk0xODQsMTg2SDQ4YTYsNiwwLDAsMCwwLDEySDE4NGE2LDYsMCwwLDAsMC0xMlpNMjI4LjI0LDgzLjc2bC00MC00MGE2LDYsMCwwLDAtOC40OCwwbC00MCw0MGE2LDYsMCwwLDAsOC40OCw4LjQ4TDE3OCw2Mi40OVYxNDRhNiw2LDAsMCwwLDEyLDBWNjIuNDlsMjkuNzYsMjkuNzVhNiw2LDAsMCwwLDguNDgtOC40OFoiLz48L3N2Zz4=');}.icon-sort-ascending{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjYsMTI4YTYsNiwwLDAsMS02LDZINDhhNiw2LDAsMCwxLDAtMTJoNzJBNiw2LDAsMCwxLDEyNiwxMjhaTTQ4LDcwSDE4NGE2LDYsMCwwLDAsMC0xMkg0OGE2LDYsMCwwLDAsMCwxMlptNTYsMTE2SDQ4YTYsNiwwLDAsMCwwLDEyaDU2YTYsNiwwLDAsMCwwLTEyWm0xMjQuMjQtMjIuMjRhNiw2LDAsMCwwLTguNDgsMEwxOTAsMTkzLjUxVjExMmE2LDYsMCwwLDAtMTIsMHY4MS41MWwtMjkuNzYtMjkuNzVhNiw2LDAsMCwwLTguNDgsOC40OGw0MCw0MGE2LDYsMCwwLDAsOC40OCwwbDQwLTQwQTYsNiwwLDAsMCwyMjguMjQsMTYzLjc2WiIvPjwvc3ZnPg==');}.icon-arrow-elbow-left-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsNzJhNiw2LDAsMCwxLTYsNkg5NFYyMDEuNTFsMzcuNzYtMzcuNzVhNiw2LDAsMCwxLDguNDgsOC40OGwtNDgsNDhhNiw2LDAsMCwxLTguNDgsMGwtNDgtNDhhNiw2LDAsMCwxLDguNDgtOC40OEw4MiwyMDEuNTFWNzJhNiw2LDAsMCwxLDYtNkgyMzJBNiw2LDAsMCwxLDIzOCw3MloiLz48L3N2Zz4=');}.icon-arrow-elbow-right-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMTY0LjI0bC00OCw0OGE2LDYsMCwwLDEtOC40OCwwbC00OC00OGE2LDYsMCwxLDEsOC40OC04LjQ4TDE3MCwxOTMuNTFWNzBIMzJhNiw2LDAsMCwxLDAtMTJIMTc2YTYsNiwwLDAsMSw2LDZWMTkzLjUxbDM3Ljc2LTM3Ljc1YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+');}.icon-heart{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzgsNDJjLTIxLDAtMzkuMjYsOS40Ny01MCwyNS4zNEMxMTcuMjYsNTEuNDcsOTksNDIsNzgsNDJhNjAuMDcsNjAuMDcsMCwwLDAtNjAsNjBjMCwyOS4yLDE4LjIsNTkuNTksNTQuMSw5MC4zMWEzMzQuNjgsMzM0LjY4LDAsMCwwLDUzLjA2LDM3LDYsNiwwLDAsMCw1LjY4LDAsMzM0LjY4LDMzNC42OCwwLDAsMCw1My4wNi0zN0MyMTkuOCwxNjEuNTksMjM4LDEzMS4yLDIzOCwxMDJBNjAuMDcsNjAuMDcsMCwwLDAsMTc4LDQyWk0xMjgsMjE3LjExQzExMS41OSwyMDcuNjQsMzAsMTU3LjcyLDMwLDEwMkE0OC4wNSw0OC4wNSwwLDAsMSw3OCw1NGMyMC4yOCwwLDM3LjMxLDEwLjgzLDQ0LjQ1LDI4LjI3YTYsNiwwLDAsMCwxMS4xLDBDMTQwLjY5LDY0LjgzLDE1Ny43Miw1NCwxNzgsNTRhNDguMDUsNDguMDUsMCwwLDEsNDgsNDhDMjI2LDE1Ny43MiwxNDQuNDEsMjA3LjY0LDEyOCwyMTcuMTFaIi8+PC9zdmc+');}.icon-dots-three{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTI4YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxMjhaTTYwLDExOGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCw2MCwxMThabTEzNiwwYTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDE5NiwxMThaIi8+PC9zdmc+');}.icon-logo-jakevan{--icon:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgICB3aWR0aD0iMzIiICAgIGhlaWdodD0iMzIiICAgIHZpZXdCb3g9IjAgMCAzMiAzMiIgICAgdmVyc2lvbj0iMS4xIiAgICB4bWw6c3BhY2U9InByZXNlcnZlIiAgICBzdHlsZT0iY2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlO3N0cm9rZS1saW5lam9pbjpiZXZlbDtzdHJva2UtbWl0ZXJsaW1pdDoxLjUiICAgIGlkPSJzdmcxMCIgICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcyAgICBpZD0iZGVmczEwIiAvPjxwYXRoICAgIGQ9Ik0gMTUuNTA3ODEzLDMuNzEyODkwNiAxNS4zODQ3NjYsMy43MTg3NSBDIDguODg0Mzg3NCw0LjAzOTQ5NDEgMy43MDUwNzgxLDkuNDIxMjM3MiAzLjcwNTA3ODEsMTYgYyAwLDYuNTc4Nzk5IDUuMTc5MzA4OSwxMS45NjA1NDMgMTEuNjc5Njg3OSwxMi4yODEyNSBsIDAuMTIzMDQ3LDAuMDA1OSAwLjAxMTcyLC0wLjI0NDE0IC0wLjEyMzA0NywtMC4wMDU5IEMgOS4wMjM1NjQ2LDI3LjcyMjY5IDMuOTQ3MjY1NiwyMi40NTEwMzcgMy45NDcyNjU2LDE2IGMgMCwtNi40NTEwMDA2IDUuMDc2MzAwNiwtMTEuNzIyNjU0NCAxMS40NDkyMTg0LC0xMi4wMzcxMDk0IGwgMC4xMjMwNDcsLTAuMDA1ODYgeiBtIC0wLjAwMiwxLjUyMzQzNzUgLTAuMTIxMDkzLDAuMDA3ODEgQyA5LjcyNTA3MDMsNS41NjM2NTE2IDUuMjI2NTYyNSwxMC4yNjE1NjYgNS4yMjY1NjI1LDE2IGMgMCw1LjczODQ2NSA0LjQ5ODQ2MTgsMTAuNDM2MzUgMTAuMTU4MjAzNSwxMC43NTU4NTkgbCAwLjEyMTA5MywwLjAwNzggMC4wMTM2NywtMC4yNDIxODggLTAuMTIxMDkzLC0wLjAwNzggQyA5Ljg2NTQ5OTYsMjYuMjAxMzIyIDUuNDY4NzUsMjEuNjExMjc5IDUuNDY4NzUsMTYgYyAwLC01LjYxMTI0OCA0LjM5Njc0NzYsLTEwLjIwMTMyMTcgOS45Mjk2ODgsLTEwLjUxMzY3MTkgbCAwLjEyMTA5MywtMC4wMDU4NiB6IG0gLTAuMDAyLDEuNTIzNDM3NSAtMC4xMjEwOTMsMC4wMDc4MSBDIDEwLjU2NDAzOCw3LjA4NTM2ODggNi43NDgwNDY5LDExLjEwMjA3NiA2Ljc0ODA0NjksMTYgYyAwLDQuODk3OTIzIDMuODE1ODk5MSw4LjkxNDYwNiA4LjYzNDc2NjEsOS4yMzI0MjIgbCAwLjEyMTA5MywwLjAwNzggMC4wMTc1OCwtMC4yNDQxNCAtMC4xMjMwNDYsLTAuMDA3OCBDIDEwLjcwNTQ3NywyNC42Nzg3NjggNi45OTIxODc1LDIwLjc3MTUwNSA2Ljk5MjE4NzUsMTYgYyAwLC00Ljc3MTUwNiAzLjcxMzI5MzUsLTguNjc4Nzk2NCA4LjQwNjI1MDUsLTguOTg4MjgxMyBsIDAuMTIzMDQ2LC0wLjAwNzgxIHogbSAwLDEuNTI1MzkwNyAtMC4xMjMwNDcsMC4wMDk3NyBDIDExLjQwMzQxMiw4LjYxMDM5MTkgOC4yNjk1MzExLDExLjk0Mjg1MiA4LjI2OTUzMTMsMTYgYyAwLDQuMDU3MTcgMy4xMzM3ODY3LDcuMzg5NjI3IDcuMTExMzI3Nyw3LjcwNTA3OCBsIDAuMTIzMDQ3LDAuMDA5OCAwLjAxNzU4LC0wLjI0MjE4OCAtMC4xMjEwOTMsLTAuMDA5OCBDIDExLjU0NzQ4LDIzLjE1NzMyNSA4LjUxMzY3MTksMTkuOTMxODM0IDguNTEzNjcxOSwxNiBjIDAsLTMuOTMxODEyIDMuMDMzODA5MSwtNy4xNTczMDEyIDYuODg2NzE5MSwtNy40NjI4OTA2IGwgMC4xMjEwOTMsLTAuMDA5NzcgeiBNIDE1LjUsOS44MTA1NDY5IDE1LjM3ODkwNiw5LjgyMjI2NTYgQyAxMi4yNDM0MjMsMTAuMTM0MTY4IDkuNzkxMDE1NiwxMi43ODQwODcgOS43OTEwMTU2LDE2IGMgMCwzLjIxNTkzIDIuNDUyNDA3NCw1Ljg2NTgzMiA1LjU4Nzg5MDQsNi4xNzc3MzQgTCAxNS41LDIyLjE4OTQ1NCAxNS41MjUzOSwyMS45NDcyNjcgMTUuNDA0Mjk2LDIxLjkzNTU0NyBDIDEyLjM5MTQ3LDIxLjYzNTg0NSAxMC4wMzUxNTYsMTkuMDkyMjIyIDEwLjAzNTE1NiwxNiBjIDAsLTMuMDkyMjAzIDIuMzU2MzEzLC01LjYzNTg0NiA1LjM2OTE0MSwtNS45MzU1NDcgbCAwLjEyMTA5NCwtMC4wMTE3MiB6IG0gLTAuMDAzOSwxLjUyNzM0NDEgLTAuMTIxMDk0LDAuMDE1NjMgQyAxMy4wODMwOTMsMTEuNjU5NDM1IDExLjMxMjUsMTMuNjI2MjU5IDExLjMxMjUsMTYgYyAwLDIuMzczNzU0IDEuNzcwNTk5LDQuMzQwNTYxIDQuMDYyNSw0LjY0NjQ4NCBsIDAuMTIxMDk0LDAuMDE1NjMgMC4wMzEyNSwtMC4yNDIxODcgLTAuMTE5MTQxLC0wLjAxNTYzIEMgMTMuMjM1NTA0LDIwLjExNDI4NCAxMS41NTY2NDEsMTguMjUyNzgyIDExLjU1NjY0MSwxNiBjIDAsLTIuMjUyNzY5IDEuNjc4ODYzLC00LjExNDI4MyAzLjg1MTU2MiwtNC40MDQyOTcgbCAwLjExOTE0MSwtMC4wMTU2MyB6IG0gLTAuMDA5OCwxLjUzNTE1NiAtMC4xMTkxNCwwLjAyNTM5IEMgMTMuOTIyODQzLDEzLjE5MjExOSAxMi44MzM5ODQsMTQuNDcwOTc1IDEyLjgzMzk4NCwxNiBjIDAsMS41MjkwMjUgMS4wODg4MywyLjgwNzg4IDIuNTMzMjA0LDMuMTAxNTYyIGwgMC4xMTkxNCwwLjAyNTM5IDAuMDQ4ODMsLTAuMjQwMjM0IC0wLjExOTE0LC0wLjAyMzQ0IEMgMTQuMDgzNDExLDE4LjU5MjMyOCAxMy4wNzgxMjUsMTcuNDEzNjA5IDEzLjA3ODEyNSwxNiBjIDAsLTEuNDEzNjA5IDEuMDA1Mjg3LC0yLjU5MjMyNSAyLjMzNzg5MSwtMi44NjMyODEgbCAwLjExOTE0LC0wLjAyMzQ0IHogbSAtMC4wMzMyLDEuNTcyMjY2IC0wLjExMTMyOCwwLjA0ODgzIEMgMTQuNzYxMzU0LDE0Ljc0ODA1IDE0LjM1NzQyMiwxNS4zMjc1ODggMTQuMzU3NDIyLDE2IGMgMCwwLjY3MjQxMSAwLjQwMzkwNiwxLjI1MTk0OCAwLjk4NDM3NSwxLjUwNTg1OSBsIDAuMTExMzI4LDAuMDQ4ODMgMC4wOTc2NiwtMC4yMjI2NTYgLTAuMTExMzI4LC0wLjA0ODgzIEMgMTQuOTQ1NDEyLDE3LjA2NzA5OSAxNC41OTk2MDksMTYuNTc0NTkxIDE0LjU5OTYwOSwxNiBjIDAsLTAuNTc0NTkyIDAuMzQ1ODA2LC0xLjA2NzA5OSAwLjgzOTg0NCwtMS4yODMyMDMgbCAwLjExMTMyOCwtMC4wNDg4MyB6IiAgICBzdHlsZT0iZmlsbDojMjIyMjIyIiAgICBpZD0icGF0aDExIiAvPjxwYXRoICAgIGQ9Ik0gMTYsMiBDIDguMjczMTU0NiwyIDIsOC4yNzMxNjYzIDIsMTYgMiwyMy43MjY4NzQgOC4yNzMxNTQ2LDMwIDE2LDMwIDIzLjcyNjgwMywzMCAzMCwyMy43MjY4NzQgMzAsMTYgMzAsOC4yNzMxNjYzIDIzLjcyNjgwMywyIDE2LDIgWiBtIDAsMC42MDkzNzUgYyAwLjIwMzk4OCwwIDAuNDA3NiwwLjAwNDYzIDAuNjA5Mzc1LDAuMDEzNjcyIFYgMjkuMzc2OTUzIEMgMTYuNDA3NiwyOS4zODU5NTMgMTYuMjAzOTg4LDI5LjM5MDYyNSAxNiwyOS4zOTA2MjUgOC42MDkxMjYxLDI5LjM5MDYyNSAyLjYwOTM3NSwyMy4zOTA5IDIuNjA5Mzc1LDE2IDIuNjA5Mzc1LDguNjA5MTQwNyA4LjYwOTEyNjEsMi42MDkzNzUgMTYsMi42MDkzNzUgWiBtIDQuNTcyMjY2LDguOTQzMzU5IGggMS40MTc5NjggbCAxLjI4MTI1LDMuODMwMDc5IHYgLTMuODE0NDU0IGggMS4wNjgzNiB2IDEuNjc1NzgyIGMgMC4wNDQ4NiwtMC4wODQ0OCAwLjA5NjA0LC0wLjE2NzM5MyAwLjE1NDI5NywtMC4yNTE5NTMgMC4wNTgyNiwtMC4wODQ1MiAwLjExNDQxMSwtMC4xNjQ1NTUgMC4xNjk5MjEsLTAuMjQwMjM1IGwgMC44MjYxNzIsLTEuMTgzNTk0IGggMy40NTcwMzIgdiAwLjgzNTkzOCBoIC0xLjI3MTQ4NSB2IDAuNjA3NDIyIGggMS4xNzc3MzUgdiAwLjgzNzg5IGggLTEuMTc3NzM1IHYgMC43Mjg1MTYgaCAxLjI3MTQ4NSB2IDAuODUxNTYzIGggLTMuNTE1NjI1IGwgLTAuNzc3MzQ0LC0xLjQ3NjU2MyAtMC4zMTQ0NTMsMC4xODU1NDcgdiAxLjI5MTAxNiBoIC0yLjIxMjg5MSBsIC0wLjE5MzM1OSwtMC43MjQ2MSBIIDIwLjY1NjI1IGwgLTAuMTk5MjE5LDAuNzI0NjEgaCAtMS4xNjYwMTUgeiBtIC0yLjMzMzk4NSwwLjAxNTYzIGggMS4wNjgzNiB2IDMuNDgyNDIyIGMgMCwwLjM4NzQxNiAtMC4wNTk0NSwwLjY5NzI1NSAtMC4xNzc3MzUsMC45Mjk2ODggLTAuMTE4MjgzLDAuMjMyNDMzIC0wLjI4MjUxOCwwLjQwMjAzIC0wLjQ5MjE4NywwLjUwNTg1OSAtMC4yMDk3MTEsMC4xMDM5MDcgLTAuNDUyNTIyLDAuMTU2MjUgLTAuNzI4NTE2LDAuMTU2MjUgLTAuMTIwMDgsMCAtMC4yMjU4OTYsLTAuMDA2MSAtMC4zMTY0MDYsLTAuMDE3NTggLTAuMDkwNTEsLTAuMDExMzggLTAuMTY4MDk1LC0wLjAyNDk3IC0wLjIzNDM3NSwtMC4wMzkwNiB2IC0wLjgxMDU0NiBjIDAuMDU3MzUsMC4wMTA1MSAwLjExNzE1LDAuMDIxOTMgMC4xODE2NCwwLjAzNTE2IDAuMDY0NTMsMC4wMTMyIDAuMTM1Njc4LDAuMDE5NTMgMC4yMTA5MzgsMC4wMTk1MyAwLjEyNzI2MywwIDAuMjI2MDc1LC0wLjAyNTEgMC4yOTY4NzUsLTAuMDc2MTcgMC4wNzA3NSwtMC4wNTEwNyAwLjExOTc0NywtMC4xMjYzNzEgMC4xNDg0MzcsLTAuMjI4NTE1IDAuMDI4NjUsLTAuMTAyMDYxIDAuMDQyOTcsLTAuMjMxNTUgMC4wNDI5NywtMC4zODQ3NjYgeiBtIDguMzc1LDAuMDUwNzggLTEuMjAxMTcyLDEuNjc3NzM0IDEuMjAxMTcyLDIuMTA1NDY5IHogTSAyMS4yODcxMDksMTIuMTg3NSBjIC0wLjAxNjEyLDAuMDg4MDUgLTAuMDM4NzksMC4xOTg2OTggLTAuMDY4MzYsMC4zMjgxMjUgLTAuMDI5NTcsMC4xMjk0MjYgLTAuMDYwMzksMC4yNTYwNTggLTAuMDkxOCwwLjM4MjgxMyAtMC4wMzEzNywwLjEyNjc5NiAtMC4wNTg3MiwwLjIzMzg3OSAtMC4wODIwMywwLjMxODM1OSBsIC0wLjE2Nzk2OSwwLjYzMjgxMiBoIDAuODM1OTM4IGwgLTAuMTY5OTIyLC0wLjYzMjgxMiBjIC0wLjAxNzkyLC0wLjA2ODY2IC0wLjA0NDA1LC0wLjE2NzI2MiAtMC4wNzgxMywtMC4yOTQ5MjIgLTAuMDM0MDQsLTAuMTI3NjU5IC0wLjA2ODQ0LC0wLjI2MDAzMSAtMC4xMDE1NjMsLTAuMzk2NDg0IC0wLjAzMzE2LC0wLjEzNjQ1MiAtMC4wNTgyNSwtMC4yNDk4NDEgLTAuMDc2MTcsLTAuMzM3ODkxIHogbSAyLjEwNTQ2OSwzLjI2NTYyNSBoIDEuMjIwNzAzIGwgMS4xMDM1MTYsMy4yOTg4MjggdiAtMy4yODUxNTYgaCAxLjE5NTMxMiBsIDEuMjMwNDY5LDIuMzEwNTQ3IGggMC4wMTM2NyBjIC0wLjAwNDUsLTAuMDcyODEgLTAuMDA5MSwtMC4xNTU2NDMgLTAuMDEzNjcsLTAuMjUxOTUzIC0wLjAwNDcsLTAuMDk2MjcgLTAuMDA3OCwtMC4xOTQzOTkgLTAuMDExNzIsLTAuMjkyOTY5IC0wLjAwMzgsLTAuMDk4NTcgLTAuMDA1OSwtMC4xODU4NjkgLTAuMDA1OSwtMC4yNjE3MTkgdiAtMS41MDM5MDYgaCAwLjgxNDQ1MyB2IDMuMzI2MTcyIGggLTEuMTk5MjE5IGwgLTEuMjM2MzI4LC0yLjM0Mzc1IGggLTAuMDE5NTMgYyAwLjAwNzgsMC4wNzEyOSAwLjAxMzMzLDAuMTU3NzIyIDAuMDE5NTMsMC4yNTc4MTIgMC4wMDYyLDAuMTAwMDkgMC4wMTI5OCwwLjIwMjc3MSAwLjAxNzU4LDAuMzA2NjQxIDAuMDA0NywwLjEwMzg3IDAuMDA1OSwwLjE5NTg4NyAwLjAwNTksMC4yNzkyOTcgdiAxLjUgaCAtMS43OTY4NzUgbCAtMC4xNjYwMTYsLTAuNjIzMDQ3IGggLTEuMDk5NjA5IGwgLTAuMTcxODc1LDAuNjIzMDQ3IGggLTEuMDAzOTA3IHogbSAtNC4wMTM2NzIsMC4wMTM2NyBoIDEuMDI3MzQ0IGwgMC40NjA5MzcsMS42ODU1NDcgYyAwLjAxNjk2LDAuMDYzNjUgMC4wMzYyLDAuMTQ0NzkgMC4wNTg1OSwwLjI0NDE0IDAuMDIyMzgsMC4wOTkzMSAwLjA0MzI1LDAuMTk4MzA1IDAuMDYyNSwwLjI5Njg3NSAwLjAxOTMsMC4wOTg1NyAwLjAzMDU2LDAuMTgwMDE4IDAuMDM1MTYsMC4yNDIxODggMC4wMDYyLC0wLjA2MjE3IDAuMDE4MiwtMC4xNDEyNzEgMC4wMzUxNiwtMC4yMzgyODEgMC4wMTY5NiwtMC4wOTcwNSAwLjAzNTgsLTAuMTk1MTc5IDAuMDU2NjQsLTAuMjkyOTY5IDAuMDIwODQsLTAuMDk3ODMgMC4wNDAwOSwtMC4xNzg0OTggMC4wNTg1OSwtMC4yNDIxODggbCAwLjQ3MDcwMywtMS42OTUzMTIgaCAxLjAyMzQzOCBsIC0xLjEwOTM3NSwzLjMyNjE3MiBoIC0xLjA3NjE3MiB6IG0gNC42Mjg5MDYsMC41MzUxNTYgYyAtMC4wMTM3OCwwLjA3NTgxIC0wLjAzMzExLDAuMTY5Nzc5IC0wLjA1ODU5LDAuMjgxMjUgLTAuMDI1NDQsMC4xMTE0MyAtMC4wNTExNCwwLjIyMDkwOCAtMC4wNzgxMywwLjMzMDA3OCAtMC4wMjcwMywwLjEwOTE3IC0wLjA1MDIyLDAuMjAwNjI4IC0wLjA3MDMxLDAuMjczNDM4IGwgLTAuMTQ2NDg0LDAuNTQ0OTIyIEggMjQuMzc1IGwgLTAuMTQ2NDg0LC0wLjU0NDkyMiBjIC0wLjAxNTM3LC0wLjA1OTE3IC0wLjAzNzA1LC0wLjE0Mzk5NyAtMC4wNjY0MSwtMC4yNTM5MDcgLTAuMDI5MjgsLTAuMTA5OTUgLTAuMDU5MzIsLTAuMjI0Mjg1IC0wLjA4Nzg5LC0wLjM0MTc5NiAtMC4wMjg1NywtMC4xMTc1MTEgLTAuMDUwOTEsLTAuMjEzMjUzIC0wLjA2NjQxLC0wLjI4OTA2MyB6IG0gLTMuNTIzNDM3LDIuODkwNjI1IGggMC40Mjc3MzQgYyAwLjE2ODkwNSwwIDAuMzExMDA3LDAuMDI5NzUgMC40MjU3ODIsMC4wODc4OSAwLjExNDc3NSwwLjA1ODE0IDAuMjAyNTM4LDAuMTQyNzQ1IDAuMjYxNzE4LDAuMjU1ODU5IDAuMDU5MTksMC4xMTMxMTUgMC4wODc4OSwwLjI1Mzk2OSAwLjA4Nzg5LDAuNDE5OTIyIDAsMC4xNzM3NiAtMC4wMzEzMSwwLjMxNzc4OSAtMC4wOTM3NSwwLjQzNTU0NyAtMC4wNjI0NCwwLjExNzcxNiAtMC4xNTUwMDksMC4yMDc5OTggLTAuMjc3MzQ0LDAuMjY3NTc4IC0wLjEyMjM3NiwwLjA1OTU4IC0wLjI3MjY1MSwwLjA4Nzg5IC0wLjQ1MzEyNSwwLjA4Nzg5IGggLTAuMzc4OTA2IHogbSAxLjM4ODY3MiwwIGggMC44NjUyMzQgdiAwLjA5OTYxIGggLTAuNzUzOTA2IHYgMC41ODc4OTEgaCAwLjcxMDkzNyB2IDAuMDk5NjEgaCAtMC43MTA5MzcgdiAwLjY2OTkyMiBoIDAuNzUzOTA2IHYgMC4wOTc2NiBoIC0wLjg2NTIzNCB6IG0gMS4wNDY4NzUsMCBoIDAuMzg0NzY1IGMgMC4xMjU1OTMsMCAwLjIzMDM3NCwwLjAxNDA3IDAuMzE0NDU0LDAuMDQxMDIgMC4wODQxMiwwLjAyNjk1IDAuMTQ2ODEzLDAuMDcxMTQgMC4xODk0NTMsMC4xMzI4MTIgMC4wNDI1MiwwLjA2MTcxIDAuMDY0NDUsMC4xNDUwMjEgMC4wNjQ0NSwwLjI1IDAsMC4wNzg3MiAtMC4wMTU2NCwwLjE0NjcxNSAtMC4wNDQ5MiwwLjIwMzEyNSAtMC4wMjkyMywwLjA1NjM3IC0wLjA2OTkyLDAuMTAyNTEyIC0wLjEyMTA5NCwwLjEzODY3MiAtMC4wNTEyOSwwLjAzNjIgLTAuMTA4NzAxLDAuMDYyODggLTAuMTc1NzgxLDAuMDgyMDMgbCAwLjQzMzU5NCwwLjcwNzAzMiBIIDIzLjgzMjAzMSBMIDIzLjQyMzgyOCwxOS43NzE0ODQgSCAyMy4wMzEyNSB2IDAuNjc1NzgyIGggLTAuMTExMzI4IHogbSAwLjk3MjY1NiwwIGggMC4xMTUyMzQgbCAwLjI5ODgyOSwxLjA5NzY1NiBjIDAuMDA3OSwwLjAyOTA5IDAuMDE0NTgsMC4wNTU3OCAwLjAyMTQ4LDAuMDgyMDMgMC4wMDY4LDAuMDI2MjEgMC4wMTM3MywwLjA1MTcyIDAuMDE5NTMsMC4wNzYxNyAwLjAwNTcsMC4wMjQ0OSAwLjAxMDIyLDAuMDQ5MjIgMC4wMTU2MywwLjA3MjI3IDAuMDA1NCwwLjAyMzA0IDAuMDExMzIsMC4wNDQ5NCAwLjAxNTYzLDAuMDY4MzYgMC4wMDUxLC0wLjAyMzQyIDAuMDA4NywtMC4wNDYxOSAwLjAxMzY3LC0wLjA3MDMxIDAuMDA1MSwtMC4wMjQxMiAwLjAxMTQ4LC0wLjA0OTQgMC4wMTc1OCwtMC4wNzQyMiAwLjAwNjIsLTAuMDI0ODUgMC4wMTI2MywtMC4wNDk1NSAwLjAxOTUzLC0wLjA3NjE3IDAuMDA2OCwtMC4wMjY1OCAwLjAxNDg0LC0wLjA1NDkzIDAuMDIzNDQsLTAuMDgzOTggbCAwLjMxMDU0NywtMS4wOTE3OTcgSCAyNC44NzUgbCAwLjMyMjI2NiwxLjA5NzY1NiBjIDAuMDA4NywwLjAzMDQ5IDAuMDE4MTksMC4wNTg5OSAwLjAyNTM5LDAuMDg1OTQgMC4wMDcyLDAuMDI2OTUgMC4wMTM3MywwLjA1MzYzIDAuMDE5NTMsMC4wNzgxMyAwLjAwNTcsMC4wMjQ0NSAwLjAxMDUyLDAuMDQ3MjYgMC4wMTU2MywwLjA3MDMxIDAuMDA1LDAuMDIzMDUgMC4wMTA2MiwwLjA0Njg5IDAuMDE1NjMsMC4wNzAzMSAwLjAwNTgsLTAuMDMxODggMC4wMTEzOCwtMC4wNjI4OSAwLjAxNzU4LC0wLjA5Mzc1IDAuMDA2MSwtMC4wMzA4NiAwLjAxMjg4LC0wLjA2MzY0IDAuMDIxNDgsLTAuMDk3NjYgMC4wMDg3LC0wLjAzNDA2IDAuMDE4NDQsLTAuMDcwOSAwLjAyOTMsLTAuMTExMzI5IGwgMC4zMDQ2ODcsLTEuMDk5NjA5IGggMC4xMTUyMzUgbCAtMC40MzE2NDEsMS41NTQ2ODggaCAtMC4xMDc0MjIgbCAtMC4zMzk4NDQsLTEuMTczODI5IGMgLTAuMDA4LC0wLjAyNDgxIC0wLjAxNTI4LC0wLjA0OCAtMC4wMjE0OCwtMC4wNzAzMSAtMC4wMDYxLC0wLjAyMjM1IC0wLjAxMjA4LC0wLjA0NDIzIC0wLjAxNzU4LC0wLjA2NDQ1IC0wLjAwNTQsLTAuMDIwMjIgLTAuMDA5LC0wLjAzODU2IC0wLjAxMzY3LC0wLjA1NjY0IC0wLjAwNDcsLTAuMDE4MDggLTAuMDA4OCwtMC4wMzU5MSAtMC4wMTE3MiwtMC4wNTA3OCAtMC4wMDI5LDAuMDE0ODcgLTAuMDA2NSwwLjAzMjE1IC0wLjAwOTgsMC4wNDg4MyAtMC4wMDMzLDAuMDE2NjkgLTAuMDA3NCwwLjAzMzAzIC0wLjAxMTcyLDAuMDUwNzggLTAuMDA0NCwwLjAxNzcgLTAuMDA4NiwwLjAzNTU0IC0wLjAxMzY3LDAuMDU0NjkgLTAuMDA1MSwwLjAxOTE1IC0wLjAwOTgsMC4wNDAwMSAtMC4wMTU2MywwLjA2MDU1IGwgLTAuMzM3ODkxLDEuMjAxMTcyIGggLTAuMTA5Mzc1IHogbSAxLjk2ODc1LDAgaCAwLjg2NTIzNCB2IDAuMDk5NjEgaCAtMC43NTM5MDYgdiAwLjU4Nzg5MSBoIDAuNzEwOTM4IHYgMC4wOTk2MSBoIC0wLjcxMDkzOCB2IDAuNjY5OTIyIGggMC43NTM5MDYgdiAwLjA5NzY2IGggLTAuODY1MjM0IHogbSAxLjA0Njg3NSwwIGggMC4zODQ3NjYgYyAwLjEyNTU1MSwwIDAuMjMwMzczLDAuMDE0MDcgMC4zMTQ0NTMsMC4wNDEwMiAwLjA4NDEyLDAuMDI2OTUgMC4xNDY4NTMsMC4wNzExNCAwLjE4OTQ1MywwLjEzMjgxMiAwLjA0MjUyLDAuMDYxNzEgMC4wNjQ0NSwwLjE0NTAyMSAwLjA2NDQ1LDAuMjUgMCwwLjA3ODcyIC0wLjAxNTY0LDAuMTQ2NzE1IC0wLjA0NDkyLDAuMjAzMTI1IC0wLjAyOTIzLDAuMDU2MzcgLTAuMDY3OTgsMC4xMDI1MTIgLTAuMTE5MTQsMC4xMzg2NzIgLTAuMDUxMjksMC4wMzYyIC0wLjExMDYxNSwwLjA2Mjg4IC0wLjE3NzczNSwwLjA4MjAzIGwgMC40MzM1OTQsMC43MDcwMzIgSCAyNy44MjAzMTEgTCAyNy40MTIxMDgsMTkuNzcxNDg1IEggMjcuMDE5NTMgdiAwLjY3NTc4MiBoIC0wLjExMTMyOCB6IG0gMS4xNDA2MjUsMCBoIDAuODY1MjM0IHYgMC4wOTk2MSBoIC0wLjc1MzkwNiB2IDAuNjYyMTEgaCAwLjcxMjg5MSB2IDAuMDk3NjYgaCAtMC43MTI4OTEgdiAwLjY5NTMxMyBoIC0wLjExMTMyOCB6IG0gLTcuNDUzMTI1LDAuMDk3NjYgdiAxLjM2MTMyOCBoIDAuMjYxNzE5IGMgMC4yMzg5NDcsMWUtNiAwLjQxNjc4OSwtMC4wNTg1OCAwLjUzNTE1NiwtMC4xNzM4MjggMC4xMTg0MTIsLTAuMTE1MjUxIDAuMTc3NzM0LC0wLjI4NzAzNSAwLjE3NzczNCwtMC41MTc1NzggMCwtMC4xNDgyMDMgLTAuMDIzMTksLTAuMjcxNzg0IC0wLjA3MjI3LC0wLjM3MTA5NCAtMC4wNDkwOCwtMC4wOTkzMSAtMC4xMjU1MjksLTAuMTc0NjA5IC0wLjIyNjU2MywtMC4yMjQ2MDkgLTAuMTAxMDc1LC0wLjA0OTk3IC0wLjIyNzYwNiwtMC4wNzQyMiAtMC4zODI4MTIsLTAuMDc0MjIgeiBtIDIuNDM1NTQ3LDAgdiAwLjY4MzU5NCBoIDAuMzE0NDUzIGMgMC4xMjkyMjcsMCAwLjIzMDIyNCwtMC4wMjg4MiAwLjMwMjczNCwtMC4wODk4NCAwLjA3MjU5LC0wLjA2MDk4IDAuMTA5Mzc1LC0wLjE0OTQ5IDAuMTA5Mzc1LC0wLjI2MzY3MiAwLC0wLjEyNTU2NCAtMC4wMzg3NiwtMC4yMTIyNjUgLTAuMTE1MjM0LC0wLjI1OTc2NSAtMC4wNzY2LC0wLjA0NzU0IC0wLjE5Mjk0MiwtMC4wNzAzMSAtMC4zNDk2MDksLTAuMDcwMzEgeiBtIDMuOTg4MjgxLDAgdiAwLjY4MzU5NCBoIDAuMzE0NDUzIGMgMC4xMjkyNjgsMCAwLjIzMDIyNSwtMC4wMjg4MiAwLjMwMjczNSwtMC4wODk4NCAwLjA3MjU5LC0wLjA2MDk4IDAuMTA5Mzc1LC0wLjE0OTQ5IDAuMTA5Mzc1LC0wLjI2MzY3MiAwLC0wLjEyNTU2NCAtMC4wMzg3NSwtMC4yMTIyNjUgLTAuMTE1MjM1LC0wLjI1OTc2NSAtMC4wNzY2LC0wLjA0NzU0IC0wLjE5Mjk0MiwtMC4wNzAzMSAtMC4zNDk2MDksLTAuMDcwMzEgeiIgICAgc3R5bGU9ImJhc2VsaW5lLXNoaWZ0OmJhc2VsaW5lO2NsaXAtcnVsZTpub256ZXJvO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmVjdG9yLWVmZmVjdDpub25lO2ZpbGw6IzIyMjIyMjtmaWxsLXJ1bGU6bm9uemVybztzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlO3N0b3AtY29sb3I6IzAwMDAwMDtzdG9wLW9wYWNpdHk6MSIgICAgaWQ9InBhdGgxMCIgLz48L3N2Zz4=');}.icon-sign-out{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMTgsMjE2YTYsNiwwLDAsMS02LDZINDhhNiw2LDAsMCwxLTYtNlY0MGE2LDYsMCwwLDEsNi02aDY0YTYsNiwwLDAsMSwwLDEySDU0VjIxMGg1OEE2LDYsMCwwLDEsMTE4LDIxNlptMTEwLjI0LTkyLjI0LTQwLTQwYTYsNiwwLDAsMC04LjQ4LDguNDhMMjA5LjUxLDEyMkgxMTJhNiw2LDAsMCwwLDAsMTJoOTcuNTFsLTI5Ljc1LDI5Ljc2YTYsNiwwLDEsMCw4LjQ4LDguNDhsNDAtNDBBNiw2LDAsMCwwLDIyOC4yNCwxMjMuNzZaIi8+PC9zdmc+');}.icon-star-half-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzkuMTgsOTcuMjZBMTYuMzgsMTYuMzgsMCwwLDAsMjI0LjkyLDg2bC01OS00Ljc2TDE0My4xNCwyNi4xNWExNi4zNiwxNi4zNiwwLDAsMC0zMC4yNywwTDkwLjExLDgxLjIzLDMxLjA4LDg2YTE2LjQ2LDE2LjQ2LDAsMCwwLTkuMzcsMjguODZsNDUsMzguODNMNTMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMCwyNC41LDE3LjgyTDEyOCwxOTguNDlsNTAuNTMsMzEuMDhBMTYuNCwxNi40LDAsMCwwLDIwMywyMTEuNzVsLTEzLjc2LTU4LjA3LDQ1LTM4LjgzQTE2LjQzLDE2LjQzLDAsMCwwLDIzOS4xOCw5Ny4yNlptLTE1LjM0LDUuNDctNDguNyw0MmE4LDgsMCwwLDAtMi41Niw3LjkxbDE0Ljg4LDYyLjhhLjM3LjM3LDAsMCwxLS4xNy40OGMtLjE4LjE0LS4yMy4xMS0uMzgsMGwtNTQuNzItMzMuNjVBOCw4LDAsMCwwLDEyOCwxODEuMVYzMmMuMjQsMCwuMjcuMDguMzUuMjZMMTUzLDkxLjg2YTgsOCwwLDAsMCw2Ljc1LDQuOTJsNjMuOTEsNS4xNmMuMTYsMCwuMjUsMCwuMzQuMjlTMjI0LDEwMi42MywyMjMuODQsMTAyLjczWiIvPjwvc3ZnPg==');}.icon-star-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzQuMjksMTE0Ljg1bC00NSwzOC44M0wyMDMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMS0yNC41LDE3LjgyTDEyOCwxOTguNDksNzcuNDcsMjI5LjU3QTE2LjQsMTYuNCwwLDAsMSw1MywyMTEuNzVsMTMuNzYtNTguMDctNDUtMzguODNBMTYuNDYsMTYuNDYsMCwwLDEsMzEuMDgsODZsNTktNC43NiwyMi43Ni01NS4wOGExNi4zNiwxNi4zNiwwLDAsMSwzMC4yNywwbDIyLjc1LDU1LjA4LDU5LDQuNzZhMTYuNDYsMTYuNDYsMCwwLDEsOS4zNywyOC44NloiLz48L3N2Zz4=');}.icon-heart-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsMTAyYzAsNzAtMTAzLjc5LDEyNi42Ni0xMDguMjEsMTI5YTgsOCwwLDAsMS03LjU4LDBDMTE5Ljc5LDIyOC42NiwxNiwxNzIsMTYsMTAyQTYyLjA3LDYyLjA3LDAsMCwxLDc4LDQwYzIwLjY1LDAsMzguNzMsOC44OCw1MCwyMy44OUMxMzkuMjcsNDguODgsMTU3LjM1LDQwLDE3OCw0MEE2Mi4wNyw2Mi4wNywwLDAsMSwyNDAsMTAyWiIvPjwvc3ZnPg==');} |
| | | .icon-google-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMSwxLTIxLjQ5LTU5LjgyLDYsNiwwLDEsMS05LjI1LDcuNjRBODIsODIsMCwxLDAsMjA5Ljc4LDEzNEgxMjhhNiw2LDAsMCwxLDAtMTJoODhBNiw2LDAsMCwxLDIyMiwxMjhaIi8+PC9zdmc+');}.icon-apple-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTkuNCwxNjcuODRDMjAxLjcxLDE1NS42OSwxOTgsMTM1LjEyLDE5OCwxMjBjMC0xOC40MiwxMy44Ni0zNC4yOSwyMi4xMi00Mi4xMmE2LDYsMCwwLDAsMC04LjcxQzIwOCw1Ny43LDE4Ny4wNyw1MCwxNjgsNTBhNzAuMjMsNzAuMjMsMCwwLDAtNDAsMTIuNTUsNjkuNiw2OS42LDAsMCwwLTg5LjMxLDguMDhBNzIuNjMsNzIuNjMsMCwwLDAsMTgsMTIzLjM1YTEyNS4xMSwxMjUuMTEsMCwwLDAsMzkuNTMsODguMzNBMzcuODUsMzcuODUsMCwwLDAsODMuNiwyMjJoODcuN0EzNy44MywzNy44MywwLDAsMCwxOTksMjEwLjA3YTEyMi42LDEyMi42LDAsMCwwLDE3LjU0LTI0LjJjNi41NS0xMiw1Ljc3LTEzLjc1LDUtMTUuNDhBNi4wNyw2LjA3LDAsMCwwLDIxOS40LDE2Ny44NFptLTI5LjIzLDM0QTI1LjgyLDI1LjgyLDAsMCwxLDE3MS4zLDIxMEg4My42QTI1Ljg1LDI1Ljg1LDAsMCwxLDY1Ljc4LDIwMywxMTMuMjEsMTEzLjIxLDAsMCwxLDMwLDEyM2E2MC41NSw2MC41NSwwLDAsMSwxNy4yMS00NEE1Ni44Miw1Ni44MiwwLDAsMSw4OCw2MmguODFhNTcuMzUsNTcuMzUsMCwwLDEsMzUuNDQsMTIuNzEsNiw2LDAsMCwwLDcuNSwwQTU3LjM5LDU3LjM5LDAsMCwxLDE2OCw2MmMxMy44OSwwLDI4LjgxLDQuNjgsMzkuMTEsMTItOS40NCwxMC4xNC0yMS4xLDI2LjU5LTIxLjEsNDYsMCwyMy43OCw3LjgxLDQyLjYsMjIuNjYsNTQuNzdBMTA3LjMzLDEwNy4zMywwLDAsMSwxOTAuMTcsMjAxLjg5Wm0tNjAtMTcxLjM5QTM4LDM4LDAsMCwxLDE2NywyaDFhNiw2LDAsMCwxLDAsMTJoLTFhMjYsMjYsMCwwLDAtMjUuMTgsMTkuNSw2LDYsMCwxLDEtMTEuNjItM1oiLz48L3N2Zz4=');}.icon-check-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuMjQsOTkuNzZhNiw2LDAsMCwxLDAsOC40OGwtNTYsNTZhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxMTIsMTUxLjUxbDUxLjc2LTUxLjc1QTYsNiwwLDAsMSwxNzIuMjQsOTkuNzZaTTIzMCwxMjhBMTAyLDEwMiwwLDEsMSwxMjgsMjYsMTAyLjEyLDEwMi4xMiwwLDAsMSwyMzAsMTI4Wm0tMTIsMGE5MCw5MCwwLDEsMC05MCw5MEE5MC4xLDkwLjEsMCwwLDAsMjE4LDEyOFoiLz48L3N2Zz4=');}.icon-cloud-slash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik01Mi40NCwzNkE2LDYsMCwwLDAsNDMuNTYsNDRsNDAuMTgsNDQuMmMtLjQ1Ljg3LS45LDEuNzUtMS4zMiwyLjY0QTYyLDYyLDAsMSwwLDcyLDIxNGg4OGE4NS4yMyw4NS4yMywwLDAsMCwzMi4zNS02LjNMMjAzLjU2LDIyMGE2LDYsMCwwLDAsOC44OC04LjA4Wk0xNjAsMjAySDcyYTUwLDUwLDAsMSwxLDUuOS05OS42NEE4Ni4yNSw4Ni4yNSwwLDAsMCw3NCwxMjhhNiw2LDAsMCwwLDEyLDAsNzMuOTIsNzMuOTIsMCwwLDEsNi40NC0zMC4ybDkxLjIyLDEwMC4zNEE3My42NSw3My42NSwwLDAsMSwxNjAsMjAyWm04Ni03NGE4NS44NSw4NS44NSwwLDAsMS0yMS44NSw1Ny4yNyw2LDYsMCwwLDEtNC40NywyLDYsNiwwLDAsMS00LjQ3LTEwLDc0LDc0LDAsMCwwLTk5LTEwOC45Miw2LDYsMCwxLDEtNy4xMS05LjY3QTg2LDg2LDAsMCwxLDI0NiwxMjhaIi8+PC9zdmc+');}.icon-exclamation-mark{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDIsMjAwYTE0LDE0LDAsMSwxLTE0LTE0QTE0LDE0LDAsMCwxLDE0MiwyMDBabS0xNC00MmE2LDYsMCwwLDAsNi02VjQ4YTYsNiwwLDAsMC0xMiwwVjE1MkE2LDYsMCwwLDAsMTI4LDE1OFoiLz48L3N2Zz4=');}.icon-cloud-arrow-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI4YTg1LjI3LDg1LjI3LDAsMCwxLTE3LjIsNTEuNiw2LDYsMCwxLDEtOS42LTcuMkE3NCw3NCwwLDEsMCw4NiwxMjhhNiw2LDAsMCwxLTEyLDAsODUuNTQsODUuNTQsMCwwLDEsMy45MS0yNS42NEE1MC42OCw1MC42OCwwLDAsMCw3MiwxMDJhNTAsNTAsMCwwLDAsMCwxMDBIOTZhNiw2LDAsMCwxLDAsMTJINzJBNjIsNjIsMCwxLDEsODIuNDMsOTAuODgsODYsODYsMCwwLDEsMjQ2LDEyOFptLTY2LjI0LDQzLjc2TDE1OCwxOTMuNTFWMTI4YTYsNiwwLDAsMC0xMiwwdjY1LjUxbC0yMS43Ni0yMS43NWE2LDYsMCwwLDAtOC40OCw4LjQ4bDMyLDMyYTYsNiwwLDAsMCw4LjQ4LDBsMzItMzJhNiw2LDAsMCwwLTguNDgtOC40OFoiLz48L3N2Zz4=');}.icon-caret-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTAwLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OCwwbC04MC04MGE2LDYsMCwwLDEsOC40OC04LjQ4TDEyOCwxNjcuNTFsNzUuNzYtNzUuNzVhNiw2LDAsMCwxLDguNDgsOC40OFoiLz48L3N2Zz4=');}.icon-cloud-arrow-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xODguMjQsMTY0LjI0YTYsNiwwLDAsMS04LjQ4LDBMMTU4LDE0Mi40OVYyMDhhNiw2LDAsMCwxLTEyLDBWMTQyLjQ5bC0yMS43NiwyMS43NWE2LDYsMCwwLDEtOC40OC04LjQ4bDMyLTMyYTYsNiwwLDAsMSw4LjQ4LDBsMzIsMzJBNiw2LDAsMCwxLDE4OC4yNCwxNjQuMjRaTTE2MCw0MkE4Ni4xLDg2LjEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDQwYTYsNiwwLDAsMCwwLTEySDcyYTUwLDUwLDAsMCwxLDAtMTAwLDUwLjY4LDUwLjY4LDAsMCwxLDUuOTEuMzZBODUuNTQsODUuNTQsMCwwLDAsNzQsMTI4YTYsNiwwLDAsMCwxMiwwLDc0LDc0LDAsMSwxLDEwMy42LDY3Ljg1LDYsNiwwLDAsMCw0LjgsMTFBODYsODYsMCwwLDAsMTYwLDQyWiIvPjwvc3ZnPg==');}.icon-cloud-check{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptMzYuMjQtOTQuMjRhNiw2LDAsMCwxLDAsOC40OGwtNDgsNDhhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxNDQsMTUxLjUxbDQzLjc2LTQzLjc1QTYsNiwwLDAsMSwxOTYuMjQsMTA3Ljc2WiIvPjwvc3ZnPg==');}.icon-cloud-warning{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptLTYtNzRWODhhNiw2LDAsMCwxLDEyLDB2NDBhNiw2LDAsMCwxLTEyLDBabTE2LDM2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE3MCwxNjRaIi8+PC9zdmc+');}.icon-syncing{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iY3VycmVudENvbG9yIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PHBhdGggaWQ9InJlZnJlc2giIGQ9Ik0xNjAuMDQ3IDEyMi44NzVhMzAuNzg0IDMwLjc4NCAwIDAgMC0yMS43NSA4Ljc5N2MtMi44NDIgMy4wMDMtLjQ2NyA0Ljk3MSAxLjMxMiAzLjE1NiAxMS4wNDMtMTAuNzg2IDI4LjcxLTEwLjY4IDM5LjYyNS4yMzRsNy4yMDMgNy4yMDRoLTEyLjg3NWMtMy4zNDcuMDA4LTMuMTY1IDMuODc1IDAgMy44NzVoMTYuMTFjMi4wNjIgMCAyLjU0LTEuNDE4IDIuNTYyLTQuOTdsLjA5NC0xNC45MjFjLjAyLTMuMjktMy40MzctMy4xNjUtMy40MzcgMHYxMi44NmwtNy4yMDMtNy4xODhhMzAuNzY4IDMwLjc2OCAwIDAgMC0yMS42NDEtOS4wNDd6bS0yOS41OTQgMzkuNzk3Yy0yLjA2MiAwLTIuNTI0IDEuNDAyLTIuNTQ3IDQuOTUzbC0uMDk0IDE0LjkyMmMtLjAyIDMuMjkgMy40MjIgMy4xNjQgMy40MjIgMHYtMTIuODZsNy4yMDMgNy4yMDRjMTEuOTU2IDExLjk1NSAzMS4zMTIgMTIuMDY0IDQzLjQwNy4yNSAyLjg0Mi0zLjAwMy40NTEtNC45ODgtMS4zMjgtMy4xNzItMTEuMDQzIDEwLjc4Ni0yOC43MSAxMC42OC0zOS42MjUtLjIzNWwtNy4xODgtNy4yMDNoMTIuODZjMy4zNDctLjAwOCAzLjE2NS0zLjg2IDAtMy44NmgtMTYuMTF6Ii8+PHBhdGggZD0iTTE2MCA0NGE4NC4xMSA4NC4xMSAwIDAgMC03Ni40MSA0OS4xMkE2MC43MSA2MC43MSAwIDAgMCA3MiA5MmE2MCA2MCAwIDAgMCAwIDEyMGg4OGE4NCA4NCAwIDAgMCAwLTE2OFptMCAxNjBINzJhNTIgNTIgMCAxIDEgOC41NS0xMDMuM0E4My42NiA4My42NiAwIDAgMCA3NiAxMjhhNCA0IDAgMCAwIDggMCA3NiA3NiAwIDEgMSA3NiA3NloiLz48L3N2Zz4=');}.icon-cloud-x{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptMjguMjQtODUuNzZMMTY4LjQ4LDEzNmwxOS43NiwxOS43NmE2LDYsMCwxLDEtOC40OCw4LjQ4TDE2MCwxNDQuNDhsLTE5Ljc2LDE5Ljc2YTYsNiwwLDAsMS04LjQ4LTguNDhMMTUxLjUyLDEzNmwtMTkuNzYtMTkuNzZhNiw2LDAsMCwxLDguNDgtOC40OEwxNjAsMTI3LjUybDE5Ljc2LTE5Ljc2YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+');}.icon-arrows-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDhWOTZhNiw2LDAsMCwxLTYsNkgxNjhhNiw2LDAsMCwxLDAtMTJoMzMuNTJMMTgzLjQ3LDcyYTgxLjUxLDgxLjUxLDAsMCwwLTU3LjUzLTI0aC0uNDZBODEuNSw4MS41LDAsMCwwLDY4LjE5LDcxLjI4YTYsNiwwLDEsMS04LjM4LTguNTgsOTMuMzgsOTMuMzgsMCwwLDEsNjUuNjctMjYuNzZIMTI2YTkzLjQ1LDkzLjQ1LDAsMCwxLDY2LDI3LjUzbDE4LDE4VjQ4YTYsNiwwLDAsMSwxMiwwWk0xODcuODEsMTg0LjcyYTgxLjUsODEuNSwwLDAsMS01Ny4yOSwyMy4zNGgtLjQ2YTgxLjUxLDgxLjUxLDAsMCwxLTU3LjUzLTI0TDU0LjQ4LDE2Nkg4OGE2LDYsMCwwLDAsMC0xMkg0MGE2LDYsMCwwLDAtNiw2djQ4YTYsNiwwLDAsMCwxMiwwVjE3NC40OGwxOCwxOC4wNWE5My40NSw5My40NSwwLDAsMCw2NiwyNy41M2guNTJhOTMuMzgsOTMuMzgsMCwwLDAsNjUuNjctMjYuNzYsNiw2LDAsMSwwLTguMzgtOC41OFoiLz48L3N2Zz4=');}.icon-share-fat{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTA3Ljc2bC04MC04MEE2LDYsMCwwLDAsMTQ2LDMyVjc0LjJjLTU0LjQ4LDMuNTktMTIwLjM5LDU1LTEyNy45MywxMjAuNjZhMTAsMTAsMCwwLDAsMTcuMjMsOGgwQzQ2LjU2LDE5MC44NSw4NywxNTIuNiwxNDYsMTUwLjEzVjE5MmE2LDYsMCwwLDAsMTAuMjQsNC4yNGw4MC04MEE2LDYsMCwwLDAsMjM2LjI0LDEwNy43NlpNMTU4LDE3Ny41MlYxNDRhNiw2LDAsMCwwLTYtNmMtMjcuNzMsMC01NC43Niw3LjI1LTgwLjMyLDIxLjU1YTE5My4zOCwxOTMuMzgsMCwwLDAtNDAuODEsMzAuNjVjNC43LTI2LjU2LDIwLjE2LTUyLDQ0LTcyLjI3Qzk4LjQ3LDk3Ljk0LDEyNy4yOSw4NiwxNTIsODZhNiw2LDAsMCwwLDYtNlY0Ni40OUwyMjMuNTEsMTEyWiIvPjwvc3ZnPg==');}.icon-trash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBIMTc0VjQwYTIyLDIyLDAsMCwwLTIyLTIySDEwNEEyMiwyMiwwLDAsMCw4Miw0MFY1MEg0MGE2LDYsMCwwLDAsMCwxMkg1MFYyMDhhMTQsMTQsMCwwLDAsMTQsMTRIMTkyYTE0LDE0LDAsMCwwLDE0LTE0VjYyaDEwYTYsNiwwLDAsMCwwLTEyWk05NCw0MGExMCwxMCwwLDAsMSwxMC0xMGg0OGExMCwxMCwwLDAsMSwxMCwxMFY1MEg5NFpNMTk0LDIwOGEyLDIsMCwwLDEtMiwySDY0YTIsMiwwLDAsMS0yLTJWNjJIMTk0Wk0xMTAsMTA0djY0YTYsNiwwLDAsMS0xMiwwVjEwNGE2LDYsMCwwLDEsMTIsMFptNDgsMHY2NGE2LDYsMCwwLDEtMTIsMFYxMDRhNiw2LDAsMCwxLDEyLDBaIi8+PC9zdmc+');}.icon-star{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzcuMjgsOTcuODdBMTQuMTgsMTQuMTgsMCwwLDAsMjI0Ljc2LDg4bC02MC4yNS00Ljg3LTIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDAtMjYuNTgsMEw5MS40OSw4My4xMSwzMS4yNCw4OGExNC4xOCwxNC4xOCwwLDAsMC0xMi41Miw5Ljg5QTE0LjQzLDE0LjQzLDAsMCwwLDIzLDExMy4zMkw2OSwxNTIuOTNsLTE0LDU5LjI1YTE0LjQsMTQuNCwwLDAsMCw1LjU5LDE1LDE0LjEsMTQuMSwwLDAsMCwxNS45MS42TDEyOCwxOTYuMTJsNTEuNTgsMzEuNzFhMTQuMSwxNC4xLDAsMCwwLDE1LjkxLS42LDE0LjQsMTQuNCwwLDAsMCw1LjU5LTE1bC0xNC01OS4yNUwyMzMsMTEzLjMyQTE0LjQzLDE0LjQzLDAsMCwwLDIzNy4yOCw5Ny44N1ptLTEyLjE0LDYuMzctNDguNjksNDJhNiw2LDAsMCwwLTEuOTIsNS45MmwxNC44OCw2Mi43OWEyLjM1LDIuMzUsMCwwLDEtLjk1LDIuNTcsMi4yNCwyLjI0LDAsMCwxLTIuNi4xTDEzMS4xNCwxODRhNiw2LDAsMCwwLTYuMjgsMEw3MC4xNCwyMTcuNjFhMi4yNCwyLjI0LDAsMCwxLTIuNi0uMSwyLjM1LDIuMzUsMCwwLDEtMS0yLjU3bDE0Ljg4LTYyLjc5YTYsNiwwLDAsMC0xLjkyLTUuOTJsLTQ4LjY5LTQyYTIuMzcsMi4zNywwLDAsMS0uNzMtMi42NSwyLjI4LDIuMjgsMCwwLDEsMi4wNy0xLjY1bDYzLjkyLTUuMTZhNiw2LDAsMCwwLDUuMDYtMy42OWwyNC42My01OS42YTIuMzUsMi4zNSwwLDAsMSw0LjM4LDBsMjQuNjMsNTkuNmE2LDYsMCwwLDAsNS4wNiwzLjY5bDYzLjkyLDUuMTZhMi4yOCwyLjI4LDAsMCwxLDIuMDcsMS42NUEyLjM3LDIuMzcsMCwwLDEsMjI1LjE0LDEwNC4yNFoiLz48L3N2Zz4=');}.icon-alphabetical{--icon:url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9ImN1cnJlbnRDb2xvciIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTgzLjc4IDE4NC4wNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNTkuNTg2IDY5Ljc0MmMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAyMjktMC41Mjk1OCAxLjk0OTNzMC4xNzkwMyAxLjU5MzcgMC41Mjk1OCAxLjk5NDRjMC4zNzU1OCAwLjM3NTU4IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGg3LjAxOTdsLTEyLjQyOCAzNC4zNjZoLTIuMTA3Yy0wLjg1MTMgMC0xLjQ2MSAwLjE5NjU2LTEuODM2NiAwLjU5NzE4LTAuMzUwNTQgMC4zNzU1OC0wLjUyOTU3IDEuMDM0MS0wLjUyOTU3IDEuOTYwNiAwIDAuOTI2NDQgMC4xNzkwMyAxLjU4MjUgMC41Mjk1NyAxLjk4MyAwLjM3NTU4IDAuMzc1NTkgMC45ODUyOSAwLjU2MzM4IDEuODM2NiAwLjU2MzM4aDEyLjU1MmMwLjg1MTMgMCAxLjQ1MjItMC4xODc3OSAxLjgwMjgtMC41NjMzOCAwLjM3NTU4LTAuNDAwNjIgMC41NjMzNy0xLjA1NjYgMC41NjMzNy0xLjk4MyAwLTAuOTI2NDUtMC4xODc3OS0xLjU4NS0wLjU2MzM3LTEuOTYwNi0wLjM1MDU0LTAuNDAwNjItMC45NTE0Ny0wLjU5NzE4LTEuODAyOC0wLjU5NzE4aC00LjU1MjFsMy4xMjExLTguOTM0OWgxOC4yMmwzLjA3NiA4LjkzNDloLTUuMDcwNGMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAzNDEtMC41Mjk1OCAxLjk2MDYgMCAwLjkyNjQ0IDAuMTc5MDMgMS41ODI1IDAuNTI5NTggMS45ODMgMC4zNzU1OCAwLjM3NTU5IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGgxMy4yOTZjMC44NTEzIDAgMS40NTIyLTAuMTg3NzkgMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzctMS4wNTY2IDAuNTYzMzctMS45ODMgMC0wLjkyNjQ1LTAuMTg3NzktMS41ODUtMC41NjMzNy0xLjk2MDYtMC4zNTA1NC0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOGgtMi4yODczbC0xMy4yNjItMzcuMDM2Yy0wLjMwMDQ3LTAuODUxMy0wLjc1OTk0LTEuNDYxLTEuMzg1OS0xLjgzNjYtMC42MDA5My0wLjQwMDYyLTEuNDA5Ny0wLjU5NzE4LTIuNDExMy0wLjU5NzE4em00NC4xNDYgMGMtMC44NTEzIDAtMS40NzIzIDAuMTk2NTYtMS44NDc4IDAuNTk3MTgtMC4zNTA1NSAwLjM3NTU4LTAuNTE4MyAxLjAyMjktMC41MTgzIDEuOTQ5M3YxMS45MWMwIDAuODc2MzMgMC4yMDUzMiAxLjUwNjEgMC42MzA5OCAxLjg4MTcgMC40MjU2NiAwLjM3NTU4IDEuMTU5MyAwLjU2MzM3IDIuMTg1OSAwLjU2MzM3czEuNzQ5LTAuMTg3NzkgMi4xNzQ3LTAuNTYzMzdjMC40MjU2OS0wLjM3NTU4IDAuNjQyMjYtMS4wMDUzIDAuNjQyMjYtMS44ODE3di05LjM1MTdoMTguODUxbC0yNC43NTQgMzUuMzAxYy0wLjM1MDU0IDAuNTI1ODItMC41MTgzMSAxLjA3MTctMC41MTgzMSAxLjYyMjYgMCAwLjkyNjQ1IDAuMTY3NzcgMS41ODI1IDAuNTE4MzEgMS45ODMxIDAuMzc1NTggMC4zNzU1OCAwLjk5NjU0IDAuNTYzMzggMS44NDc4IDAuNTYzMzhoMjguNzY2YzAuODUxMyAwIDEuNDUyMi0wLjE4NzggMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzgtMS4wNTY2IDAuNTYzMzgtMS45ODMxdi0xMi42NjVjMC0wLjg3NjMzLTAuMjE2NTgtMS40OTQ4LTAuNjQyMjUtMS44NzA0LTAuNDI1NjYtMC4zNzU1OC0xLjE0OC0wLjU2MzM4LTIuMTc0Ny0wLjU2MzM4LTEuMDI2NiAwLTEuNzQ5IDAuMTg3NzktMi4xNzQ3IDAuNTYzMzgtMC40MjU2NiAwLjM3NTU4LTAuNjQyMjQgMC45OTQwMi0wLjY0MjI0IDEuODcwNHYxMC4xMDdoLTE5Ljk3OGwyNC45MDEtMzUuNDU5YzAuMjUwMzktMC4zNTA1NCAwLjM3MTgzLTAuODM4ODMgMC4zNzE4My0xLjQ2NDggMC0wLjkyNjQ1LTAuMTg3OC0xLjU3MzctMC41NjMzOC0xLjk0OTMtMC4zNTA1NS0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOHptLTMxLjc1MiA1LjEwNDJoMC43MDk4NWw2Ljk4NTkgMjAuMzE1aC0xNC43MTZ6bS0zNy43MjMtNDkuMTgzYy00LjczNDIgMC04LjYzMTMgMy44OTctOC42MzEzIDguNjMxM3YxMTUuNDdjMCA0LjczNDIgMy44OTcgOC42MzEzIDguNjMxMyA4LjYzMTNoMTE1LjI2YzQuNzM0MiAwIDguNjQyMS0zLjg5NyA4LjY0MjEtOC42MzEzdi0xMTUuNDdjMC00LjczNDItMy45MDgyLTguNjMxMy04LjY0MjEtOC42MzEzem0wIDUuNzI0aDExNS4yNmMxLjY1OCAwIDIuOTA3IDEuMjQ5MSAyLjkwNyAyLjkwNzF2MTE1LjQ3YzAgMS42NTgtMS4yNDkxIDIuOTA3LTIuOTA3IDIuOTA3aC0xMTUuMjZjLTEuNjU4IDAtMi44OTU4LTEuMjQ5MS0yLjg5NTgtMi45MDd2LTExNS40N2MwLTEuNjU4IDEuMjM3OC0yLjkwNzEgMi44OTU4LTIuOTA3MXoiIGZpbGw9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIuNzIxMTQiLz48L3N2Zz4=');}.icon-scribble{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjUsMTg4LjI0YTE2LjYzLDE2LjYzLDAsMCwwLDAsMjMuNTIsNiw2LDAsMSwxLTguNDgsOC40OCwyOC42MSwyOC42MSwwLDAsMSwwLTQwLjQ4bDkuMzctOS4zOGExNi42MywxNi42MywwLDAsMC0yMy41Mi0yMy41MWwtNjYuNzUsNjYuNzVhMjguNjMsMjguNjMsMCwwLDEtNDAuNDktNDAuNDlsOTguNzYtOTguNzVhMTYuNjMsMTYuNjMsMCwwLDAtMjMuNTItMjMuNTFMODIuODYsMTE3LjYyQTI4LjYzLDI4LjYzLDAsMCwxLDQyLjM3LDc3LjEzTDgzLjc1LDM1Ljc2YTYsNiwwLDEsMSw4LjQ5LDguNDhMNTAuODYsODUuNjJhMTYuNjMsMTYuNjMsMCwwLDAsMjMuNTIsMjMuNTFsNjYuNzUtNjYuNzVhMjguNjMsMjguNjMsMCwwLDEsNDAuNDksNDAuNDlMODIuODYsMTgxLjYyYTE2LjYzLDE2LjYzLDAsMCwwLDIzLjUyLDIzLjUxbDY2Ljc2LTY2Ljc1YTI4LjYzLDI4LjYzLDAsMCwxLDQwLjQ5LDQwLjQ5WiIvPjwvc3ZnPg==');}.icon-brackets-angle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik04NS4wNiw0My4yMiwzMS4xMSwxMjhsNTQsODQuNzhhNiw2LDAsMCwxLTEuODQsOC4yOCw2LDYsMCwwLDEtOC4yOC0xLjg0bC01Ni04OGE2LDYsMCwwLDEsMC02LjQ0bDU2LTg4YTYsNiwwLDAsMSwxMC4xMiw2LjQ0Wm0xNTIsODEuNTYtNTYtODhhNiw2LDAsMSwwLTEwLjEyLDYuNDRMMjI0Ljg5LDEyOGwtNTMuOTUsODQuNzhhNiw2LDAsMCwwLDEuODQsOC4yOCw2LDYsMCwwLDAsOC4yOC0xLjg0bDU2LTg4QTYsNiwwLDAsMCwyMzcuMDYsMTI0Ljc4WiIvPjwvc3ZnPg==');}.icon-brain{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI0YTU0LjEzLDU0LjEzLDAsMCwwLTMyLTQ5LjMzVjcyYTQ2LDQ2LDAsMCwwLTg2LTIyLjY3QTQ2LDQ2LDAsMCwwLDQyLDcydjIuNjdhNTQsNTQsMCwwLDAsMCw5OC42M1YxNzZhNDYsNDYsMCwwLDAsODYsMjIuNjdBNDYsNDYsMCwwLDAsMjE0LDE3NnYtMi43QTU0LjA3LDU0LjA3LDAsMCwwLDI0NiwxMjRaTTg4LDIxMGEzNCwzNCwwLDAsMS0zNC0zMi45NEE1My42Nyw1My42NywwLDAsMCw2NCwxNzhoOGE2LDYsMCwwLDAsMC0xMkg2NEE0Miw0MiwwLDAsMSw1MCw4NC4zOWE2LDYsMCwwLDAsNC01LjY2VjcyYTM0LDM0LDAsMCwxLDY4LDB2NzMuMDVBNDUuODksNDUuODksMCwwLDAsODgsMTMwYTYsNiwwLDAsMCwwLDEyLDM0LDM0LDAsMCwxLDAsNjhabTEwNC00NGgtOGE2LDYsMCwwLDAsMCwxMmg4YTUzLjY3LDUzLjY3LDAsMCwwLDEwLS45NEEzNCwzNCwwLDEsMSwxNjgsMTQyYTYsNiwwLDAsMCwwLTEyLDQ1Ljg5LDQ1Ljg5LDAsMCwwLTM0LDE1LjA1VjcyYTM0LDM0LDAsMCwxLDY4LDB2Ni43M2E2LDYsMCwwLDAsNCw1LjY2QTQyLDQyLDAsMCwxLDE5MiwxNjZabTE0LTU0YTYsNiwwLDAsMS02LDZoLTRhMzQsMzQsMCwwLDEtMzQtMzRWODBhNiw2LDAsMCwxLDEyLDB2NGEyMiwyMiwwLDAsMCwyMiwyMmg0QTYsNiwwLDAsMSwyMDYsMTEyWk02MCwxMThINTZhNiw2LDAsMCwxLDAtMTJoNEEyMiwyMiwwLDAsMCw4Miw4NFY4MGE2LDYsMCwwLDEsMTIsMHY0QTM0LDM0LDAsMCwxLDYwLDExOFoiLz48L3N2Zz4=');}.icon-palette{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTkuMzcsNTUuMzFBMTAxLjMyLDEwMS4zMiwwLDAsMCwxMjgsMjZoLTFBMTAyLDEwMiwwLDAsMCwyNiwxMjhjMCw0Mi4wOSwyNi4wNyw3Ny40NCw2OCw5Mi4yNkEzMC4yMSwzMC4yMSwwLDAsMCwxMDQuMTEsMjIyLDMwLjA2LDMwLjA2LDAsMCwwLDEzNCwxOTJhMTgsMTgsMCwwLDEsMTgtMThoNDYuMjFhMjkuODIsMjkuODIsMCwwLDAsMjkuMjUtMjMuMzFBMTAyLjcxLDEwMi43MSwwLDAsMCwyMzAsMTI3LjExLDEwMS4yNSwxMDEuMjUsMCwwLDAsMTk5LjM3LDU1LjMxWk0yMTUuNzYsMTQ4YTE3Ljg5LDE3Ljg5LDAsMCwxLTE3LjU1LDE0SDE1MmEzMCwzMCwwLDAsMC0zMCwzMCwxOCwxOCwwLDAsMS0yNCwxN0M2MSwxOTUuODYsMzgsMTY0Ljg1LDM4LDEyOGE5MCw5MCwwLDAsMSw4OS4wNy05MEgxMjhhOTAuMzQsOTAuMzQsMCwwLDEsOTAsODkuMjJBOTAuNDYsOTAuNDYsMCwwLDEsMjE1Ljc2LDE0OFpNMTM4LDc2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCw3NlpNOTQsMTAwQTEwLDEwLDAsMSwxLDg0LDkwLDEwLDEwLDAsMCwxLDk0LDEwMFptMCw1NmExMCwxMCwwLDEsMS0xMC0xMEExMCwxMCwwLDAsMSw5NCwxNTZabTg4LTU2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE4MiwxMDBaIi8+PC9zdmc+');}.icon-pen-nib{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsOTIuNjhhMTMuOTQsMTMuOTQsMCwwLDAtNC4xLTkuOUwxNzMuMjEsMTQuMWExNCwxNCwwLDAsMC0xOS44LDBMMTI0LjY4LDQyLjgzLDY2LjIyLDY0Ljc2YTE0LDE0LDAsMCwwLTguOSwxMC44TDM0LjA4LDIxNUE2LDYsMCwwLDAsNDAsMjIyYTYuNjEsNi42MSwwLDAsMCwxLS4wOGwxMzkuNDQtMjMuMjRhMTQsMTQsMCwwLDAsMTAuODEtOC45bDIxLjkyLTU4LjQ2LDI4Ljc0LTI4Ljc0QTEzLjkyLDEzLjkyLDAsMCwwLDI0Niw5Mi42OFptLTY2LDkyLjg5YTIsMiwwLDAsMS0xLjU0LDEuMjdMNTcuNDksMjA3bDUyLjg3LTUyLjg4YTI2LDI2LDAsMSwwLTguNDgtOC40OEw0OSwxOTguNTNsMjAuMTctMTIxQTIsMiwwLDAsMSw3MC40Myw3Nmw1Ni4wNi0yMUwyMDEsMTI5LjUxWk0xMTAsMTMyYTE0LDE0LDAsMSwxLDE0LDE0QTE0LDE0LDAsMCwxLDExMCwxMzJaTTIzMy40MSw5NC4xLDIwOCwxMTkuNTEsMTM2LjQ4LDQ4LDE2MS45LDIyLjU4YTIsMiwwLDAsMSwyLjgzLDBsNjguNjgsNjguNjlhMiwyLDAsMCwxLDAsMi44M1oiLz48L3N2Zz4=');}.icon-question{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTgwYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxODBaTTEyOCw3NGMtMjEsMC0zOCwxNS4yNS0zOCwzNHY0YTYsNiwwLDAsMCwxMiwwdi00YzAtMTIuMTMsMTEuNjYtMjIsMjYtMjJzMjYsOS44NywyNiwyMi0xMS42NiwyMi0yNiwyMmE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LTIuNDJjMTguMTEtMi41OCwzMi0xNi42NiwzMi0zMy41OEMxNjYsODkuMjUsMTQ5LDc0LDEyOCw3NFptMTAyLDU0QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+');}.icon-city{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsMjEwSDIzMFY4OGE2LDYsMCwwLDAtNi02SDE2MGE2LDYsMCwwLDAtNiw2djQySDEwMlY0MGE2LDYsMCwwLDAtNi02SDMyYTYsNiwwLDAsMC02LDZWMjEwSDE2YTYsNiwwLDAsMCwwLDEySDI0MGE2LDYsMCwwLDAsMC0xMlpNMTY2LDk0aDUyVjIxMEgxNjZabS0xMiw0OHY2OEgxMDJWMTQyWk0zOCw0Nkg5MFYyMTBIMzhaTTcwLDcyVjg4YTYsNiwwLDAsMS0xMiwwVjcyYTYsNiwwLDAsMSwxMiwwWm0wLDQ4djE2YTYsNiwwLDAsMS0xMiwwVjEyMGE2LDYsMCwwLDEsMTIsMFptMCw0OHYxNmE2LDYsMCwwLDEtMTIsMFYxNjhhNiw2LDAsMCwxLDEyLDBabTUyLDE2VjE2OGE2LDYsMCwwLDEsMTIsMHYxNmE2LDYsMCwwLDEtMTIsMFptNjQsMFYxNjhhNiw2LDAsMCwxLDEyLDB2MTZhNiw2LDAsMCwxLTEyLDBabTAtNDhWMTIwYTYsNiwwLDAsMSwxMiwwdjE2YTYsNiwwLDAsMS0xMiwwWiIvPjwvc3ZnPg==');}.icon-folder{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzRIMTMwLjQ5bC0yNy45LTI3LjlhMTMuOTQsMTMuOTQsMCwwLDAtOS45LTQuMUg0MEExNCwxNCwwLDAsMCwyNiw1NlYyMDAuNjJBMTMuMzksMTMuMzksMCwwLDAsMzkuMzgsMjE0SDIxNi44OUExMy4xMiwxMy4xMiwwLDAsMCwyMzAsMjAwLjg5Vjg4QTE0LDE0LDAsMCwwLDIxNiw3NFpNNDAsNTRIOTIuNjlhMiwyLDAsMCwxLDEuNDEuNTlMMTEzLjUxLDc0SDM4VjU2QTIsMiwwLDAsMSw0MCw1NFpNMjE4LDIwMC44OWExLjExLDEuMTEsMCwwLDEtMS4xMSwxLjExSDM5LjM4QTEuNCwxLjQsMCwwLDEsMzgsMjAwLjYyVjg2SDIxNmEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==');}.icon-hash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsOTBIMTczbDguODktNDguOTNhNiw2LDAsMSwwLTExLjgtMi4xNEwxNjAuODEsOTBIMTA5bDguODktNDguOTNhNiw2LDAsMCwwLTExLjgtMi4xNEw5Ni44MSw5MEg0OGE2LDYsMCwwLDAsMCwxMkg5NC42M2wtOS40Niw1MkgzMmE2LDYsMCwwLDAsMCwxMkg4M0w3NC4xLDIxNC45M2E2LDYsMCwwLDAsNC44Myw3QTUuNjQsNS42NCwwLDAsMCw4MCwyMjJhNiw2LDAsMCwwLDUuODktNC45M0w5NS4xOSwxNjZIMTQ3bC04Ljg5LDQ4LjkzYTYsNiwwLDAsMCw0LjgzLDcsNS42NCw1LjY0LDAsMCwwLDEuMDguMSw2LDYsMCwwLDAsNS44OS00LjkzTDE1OS4xOSwxNjZIMjA4YTYsNiwwLDAsMCwwLTEySDE2MS4zN2w5LjQ2LTUySDIyNGE2LDYsMCwwLDAsMC0xMlptLTc0LjgzLDY0SDk3LjM3bDkuNDYtNTJoNTEuOFoiLz48L3N2Zz4=');}.icon-shapes{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik02OS42OSw2Mi4xYTYsNiwwLDAsMC0xMS4zOCwwbC00MCwxMjBBNiw2LDAsMCwwLDI0LDE5MGg4MGE2LDYsMCwwLDAsNS42OS03LjlaTTMyLjMyLDE3OCw2NCw4M2wzMS42OCw5NVpNMjA2LDc2YTUwLDUwLDAsMSwwLTUwLDUwQTUwLjA2LDUwLjA2LDAsMCwwLDIwNiw3NlptLTg4LDBhMzgsMzgsMCwxLDEsMzgsMzhBMzgsMzgsMCwwLDEsMTE4LDc2Wm0xMDYsNzBIMTM2YTYsNiwwLDAsMC02LDZ2NTZhNiw2LDAsMCwwLDYsNmg4OGE2LDYsMCwwLDAsNi02VjE1MkE2LDYsMCwwLDAsMjI0LDE0NlptLTYsNTZIMTQyVjE1OGg3NloiLz48L3N2Zz4=');}.icon-diamonds-four{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjMuNzYsMTA4LjI0YTYsNiwwLDAsMCw4LjQ4LDBsNDAtNDBhNiw2LDAsMCwwLDAtOC40OGwtNDAtNDBhNiw2LDAsMCwwLTguNDgsMGwtNDAsNDBhNiw2LDAsMCwwLDAsOC40OFpNMTI4LDMyLjQ5LDE1OS41MSw2NCwxMjgsOTUuNTEsOTYuNDksNjRabTQuMjQsMTE1LjI3YTYsNiwwLDAsMC04LjQ4LDBsLTQwLDQwYTYsNiwwLDAsMCwwLDguNDhsNDAsNDBhNiw2LDAsMCwwLDguNDgsMGw0MC00MGE2LDYsMCwwLDAsMC04LjQ4Wk0xMjgsMjIzLjUxLDk2LjQ5LDE5MiwxMjgsMTYwLjQ5LDE1OS41MSwxOTJabTEwOC4yNC05OS43NS00MC00MGE2LDYsMCwwLDAtOC40OCwwbC00MCw0MGE2LDYsMCwwLDAsMCw4LjQ4bDQwLDQwYTYsNiwwLDAsMCw4LjQ4LDBsNDAtNDBBNiw2LDAsMCwwLDIzNi4yNCwxMjMuNzZaTTE5MiwxNTkuNTEsMTYwLjQ5LDEyOCwxOTIsOTYuNDksMjIzLjUxLDEyOFptLTgzLjc2LTM1Ljc1LTQwLTQwYTYsNiwwLDAsMC04LjQ4LDBsLTQwLDQwYTYsNiwwLDAsMCwwLDguNDhsNDAsNDBhNiw2LDAsMCwwLDguNDgsMGw0MC00MEE2LDYsMCwwLDAsMTA4LjI0LDEyMy43NlpNNjQsMTU5LjUxLDMyLjQ5LDEyOCw2NCw5Ni40OSw5NS41MSwxMjhaIi8+PC9zdmc+');}.icon-crosshair-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm02LDE5MS44VjE4NGE2LDYsMCwwLDAtMTIsMHYzMy44QTkwLjE1LDkwLjE1LDAsMCwxLDM4LjIsMTM0SDcyYTYsNiwwLDAsMCwwLTEySDM4LjJBOTAuMTUsOTAuMTUsMCwwLDEsMTIyLDM4LjJWNzJhNiw2LDAsMCwwLDEyLDBWMzguMkE5MC4xNSw5MC4xNSwwLDAsMSwyMTcuOCwxMjJIMTg0YTYsNiwwLDAsMCwwLDEyaDMzLjhBOTAuMTUsOTAuMTUsMCwwLDEsMTM0LDIxNy44WiIvPjwvc3ZnPg==');}.icon-circle-notch{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAsMTI4YTEwMiwxMDIsMCwwLDEtMjA0LDBjMC00MC4xOCwyMy4zNS03Ni44Niw1OS41LTkzLjQ1YTYsNiwwLDAsMSw1LDEwLjlDNTguNjEsNjAuMDksMzgsOTIuNDksMzgsMTI4YTkwLDkwLDAsMCwwLDE4MCwwYzAtMzUuNTEtMjAuNjEtNjcuOTEtNTIuNS04Mi41NWE2LDYsMCwwLDEsNS0xMC45QzIwNi42NSw1MS4xNCwyMzAsODcuODIsMjMwLDEyOFoiLz48L3N2Zz4=');}.icon-cards-three{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsOTBINDhhMTQsMTQsMCwwLDAtMTQsMTR2OTZhMTQsMTQsMCwwLDAsMTQsMTRIMjA4YTE0LDE0LDAsMCwwLDE0LTE0VjEwNEExNCwxNCwwLDAsMCwyMDgsOTBabTIsMTEwYTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlYxMDRhMiwyLDAsMCwxLDItMkgyMDhhMiwyLDAsMCwxLDIsMlpNNTAsNjRhNiw2LDAsMCwxLDYtNkgyMDBhNiw2LDAsMCwxLDAsMTJINTZBNiw2LDAsMCwxLDUwLDY0Wk02NiwzMmE2LDYsMCwwLDEsNi02SDE4NGE2LDYsMCwwLDEsMCwxMkg3MkE2LDYsMCwwLDEsNjYsMzJaIi8+PC9zdmc+');}.icon-sun-dim{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjIsNDBWMzJhNiw2LDAsMCwxLDEyLDB2OGE2LDYsMCwwLDEtMTIsMFptNjgsODhhNjIsNjIsMCwxLDEtNjItNjJBNjIuMDcsNjIuMDcsMCwwLDEsMTkwLDEyOFptLTEyLDBhNTAsNTAsMCwxLDAtNTAsNTBBNTAuMDYsNTAuMDYsMCwwLDAsMTc4LDEyOFpNNTkuNzYsNjguMjRhNiw2LDAsMSwwLDguNDgtOC40OGwtOC04YTYsNiwwLDAsMC04LjQ4LDguNDhabTAsMTE5LjUyLTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwxLDAtOC40OC04LjQ4Wm0xMzYtMTM2LTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwwLDAtOC40OC04LjQ4Wm0uNDgsMTM2YTYsNiwwLDAsMC04LjQ4LDguNDhsOCw4YTYsNiwwLDAsMCw4LjQ4LTguNDhaTTQwLDEyMkgzMmE2LDYsMCwwLDAsMCwxMmg4YTYsNiwwLDAsMCwwLTEyWm04OCw4OGE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LThBNiw2LDAsMCwwLDEyOCwyMTBabTk2LTg4aC04YTYsNiwwLDAsMCwwLDEyaDhhNiw2LDAsMCwwLDAtMTJaIi8+PC9zdmc+');}.icon-moon{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuMTMsMTQzLjY0YTYsNiwwLDAsMC02LTEuNDlBOTAuMDcsOTAuMDcsMCwwLDEsMTEzLjg2LDI5Ljg1YTYsNiwwLDAsMC03LjQ5LTcuNDhBMTAyLjg4LDEwMi44OCwwLDAsMCw1NC40OCw1OC42OCwxMDIsMTAyLDAsMCwwLDE5Ny4zMiwyMDEuNTJhMTAyLjg4LDEwMi44OCwwLDAsMCwzNi4zMS01MS44OUE2LDYsMCwwLDAsMjMyLjEzLDE0My42NFptLTQyLDQ4LjI5YTkwLDkwLDAsMCwxLTEyNi0xMjZBOTAuOSw5MC45LDAsMCwxLDk5LjY1LDM3LjY2LDEwMi4wNiwxMDIuMDYsMCwwLDAsMjE4LjM0LDE1Ni4zNSw5MC45LDkwLjksMCwwLDEsMTkwLjEsMTkxLjkzWiIvPjwvc3ZnPg==');}.icon-plus-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRabTIsMTc0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWm0tMzYtODBhNiw2LDAsMCwxLTYsNkgxMzR2MzRhNiw2LDAsMCwxLTEyLDBWMTM0SDg4YTYsNiwwLDAsMSwwLTEyaDM0Vjg4YTYsNiwwLDAsMSwxMiwwdjM0aDM0QTYsNiwwLDAsMSwxNzQsMTI4WiIvPjwvc3ZnPg==');}.icon-arrow-elbow-left-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTkyYTYsNiwwLDAsMS02LDZIODhhNiw2LDAsMCwxLTYtNlY2Mi40OUw0NC4yNCwxMDAuMjRhNiw2LDAsMCwxLTguNDgtOC40OGw0OC00OGE2LDYsMCwwLDEsOC40OCwwbDQ4LDQ4YTYsNiwwLDEsMS04LjQ4LDguNDhMOTQsNjIuNDlWMTg2SDIzMkE2LDYsMCwwLDEsMjM4LDE5MloiLz48L3N2Zz4=');}.icon-arrow-elbow-right-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjAuMjQsMTAwLjI0YTYsNiwwLDAsMS04LjQ4LDBMMTc0LDYyLjQ5VjE5MmE2LDYsMCwwLDEtNiw2SDI0YTYsNiwwLDAsMSwwLTEySDE2MlY2Mi40OWwtMzcuNzYsMzcuNzVhNiw2LDAsMCwxLTguNDgtOC40OGw0OC00OGE2LDYsMCwwLDEsOC40OCwwbDQ4LDQ4QTYsNiwwLDAsMSwyMjAuMjQsMTAwLjI0WiIvPjwvc3ZnPg==');}.icon-x{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjQsMTk1Ljc2YTYsNiwwLDEsMS04LjQ4LDguNDhMMTI4LDEzNi40OSw2MC4yNCwyMDQuMjRhNiw2LDAsMCwxLTguNDgtOC40OEwxMTkuNTEsMTI4LDUxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MWw2Ny43Ni02Ny43NWE2LDYsMCwwLDEsOC40OCw4LjQ4TDEzNi40OSwxMjhaIi8+PC9zdmc+');}.icon-floppy-disk{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSw3My40MiwxODIuNTgsMzguMWExMy45LDEzLjksMCwwLDAtOS44OS00LjFINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY4My4zMUExMy45LDEzLjksMCwwLDAsMjE3LjksNzMuNDJaTTE3MCwyMTBIODZWMTUyYTIsMiwwLDAsMSwyLTJoODBhMiwyLDAsMCwxLDIsMlptNDAtMmEyLDIsMCwwLDEtMiwySDE4MlYxNTJhMTQsMTQsMCwwLDAtMTQtMTRIODhhMTQsMTQsMCwwLDAtMTQsMTR2NThINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDE3Mi42OWEyLDIsMCwwLDEsMS40MS41OEwyMDkuNDIsODEuOWEyLDIsMCwwLDEsLjU4LDEuNDFaTTE1OCw3MmE2LDYsMCwwLDEtNiw2SDk2YTYsNiwwLDAsMSwwLTEyaDU2QTYsNiwwLDAsMSwxNTgsNzJaIi8+PC9zdmc+');}.icon-x-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMTAwLjI0LDEzNi40OCwxMjhsMjcuNzYsMjcuNzZhNiw2LDAsMSwxLTguNDgsOC40OEwxMjgsMTM2LjQ4bC0yNy43NiwyNy43NmE2LDYsMCwwLDEtOC40OC04LjQ4TDExOS41MiwxMjgsOTEuNzYsMTAwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MmwyNy43Ni0yNy43NmE2LDYsMCwwLDEsOC40OCw4LjQ4Wk0yMzAsMTI4QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+');}.icon-minus-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRabTIsMTc0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWm0tMzYtODBhNiw2LDAsMCwxLTYsNkg4OGE2LDYsMCwwLDEsMC0xMmg4MEE2LDYsMCwwLDEsMTc0LDEyOFoiLz48L3N2Zz4=');}.icon-pencil-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjUuOSw3NC43OCwxODEuMjEsMzAuMDlhMTQsMTQsMCwwLDAtMTkuOCwwTDM4LjEsMTUzLjQxYTEzLjk0LDEzLjk0LDAsMCwwLTQuMSw5LjlWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDkyLjY5YTEzLjk0LDEzLjk0LDAsMCwwLDkuOS00LjFMMjI1LjksOTQuNThhMTQsMTQsMCwwLDAsMC0xOS44Wk05NC4xLDIwOS40MWEyLDIsMCwwLDEtMS40MS41OUg0OGEyLDIsMCwwLDEtMi0yVjE2My4zMWEyLDIsMCwwLDEsLjU5LTEuNDFMMTM2LDcyLjQ4LDE4My41MSwxMjBaTTIxNy40MSw4Ni4xLDE5MiwxMTEuNTEsMTQ0LjQ5LDY0LDE2OS45LDM4LjU4YTIsMiwwLDAsMSwyLjgzLDBsNDQuNjgsNDQuNjlhMiwyLDAsMCwxLDAsMi44M1oiLz48L3N2Zz4=');}.icon-dots-six-vertical{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDIsNjBBMTAsMTAsMCwxLDEsOTIsNTAsMTAsMTAsMCwwLDEsMTAyLDYwWm02MiwxMGExMCwxMCwwLDEsMC0xMC0xMEExMCwxMCwwLDAsMCwxNjQsNzBaTTkyLDExOGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCw5MiwxMThabTcyLDBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTY0LDExOFpNOTIsMTg2YTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDkyLDE4NlptNzIsMGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCwxNjQsMTg2WiIvPjwvc3ZnPg==');}.icon-list{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4Wk00MCw3MEgyMTZhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==');}.icon-loading{--icon:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgICB3aWR0aD0iMzIiICAgIGhlaWdodD0iMzIiICAgIHZpZXdCb3g9IjAgMCAzMiAzMiIgICAgdmVyc2lvbj0iMS4xIiAgICB4bWw6c3BhY2U9InByZXNlcnZlIiAgICBzdHlsZT0iY2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNSIgICAgaWQ9InN2ZzEwIiAgICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxkZWZzICAgIGlkPSJkZWZzMTAiIC8+PHBhdGggICAgaWQ9InBhdGgxMSIgICAgc3R5bGU9ImJhc2VsaW5lLXNoaWZ0OmJhc2VsaW5lO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmVjdG9yLWVmZmVjdDpub25lO2ZpbGw6IzIyMjIyMjtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlO3N0b3AtY29sb3I6IzAwMDAwMDtzdG9wLW9wYWNpdHk6MSIgICAgZD0ibSAxNi42MjEwOTQsMS4xNDI1NzgxIGMgLTguMjY2MzIzMiwwIC0xNi4yMjA4NjczOCw2LjQ0MjgwOTUgLTE1LjU4NTkzNzgsMTQuNjg1NTQ2OSAwLjYwMTM0NTUsNy44MDczMDggNy40MzQxMjY0LDE0LjEyNjk4IDE0LjkzMzU5MzgsMTQuOTQzMzU5IDguODM5ODQ1LDAuOTYyMjgzIDE1LjUwNTQ2OSwtNi4zNzY5MTkgMTUuMDA1ODU5LC0xNC45ODYzMjggQyAzMC40OTU5LDcuNTM2MjY4NCAyNC44ODMzOTcsMS4xNDI1NzgxIDE2LjYyMTA5NCwxLjE0MjU3ODEgWiBtIDAsMC42NTAzOTA3IEMgMjYuNDg4Nzg2LDEuODAzODY0NSAyOS43MTQ1MTgsOS41OTM1ODMzIDMwLjMwMjczNCwxNS44MDQ2ODggMzEuMTQxOTgyLDI0LjY2NjM2NSAyMi4xNjA0NTksMzEuMTY4MDc3IDE2LjAzOTA2MiwzMC4xMjUgOC44OTUxMzI3LDI4LjkwNzY4MSAyLjI2MTMxNDIsMjMuMjc5Mzc2IDEuNjgzNTkzOCwxNS43NzkyOTcgMS4wNzY5MzM4LDcuOTAzMjc1NCA4LjcyMjU0NTEsMS43ODQyNjk5IDE2LjYyMTA5NCwxLjc5Mjk2ODggWiBtIC0wLjA2NDQ1LDEuMjE4NzUgYyAtMy42MTAwODMsMCAtNy4xNTQ3OTk1LDEuNDAxMDY4NyAtOS43MzA0NjkxLDMuNzAzMTI1IEMgNC4yNTA1MDIzLDkuMDE2OTAwMiAyLjY0MjAzNzIsMTIuMjI2Mjk1IDIuOTE5OTIxOSwxNS44MzM5ODQgMy40NDY5MzUsMjIuNjc1NzEyIDkuNDI4OTY0OSwyOC4xOTg5ODUgMTUuOTk4MDQ3LDI4LjkxNDA2MiAyMy43MTQyNTYsMjkuNzU0MDIzIDI5LjUzMTYwMywyMy4zMzE3IDI5LjA5NTcwMywxNS44MjAzMTIgMjguNjc3OTQ4LDguNjIxMzk1MyAyMy43NzY2ODYsMy4wMTE3MTg4IDE2LjU1NjY0MSwzLjAxMTcxODggWiBtIDAsMC4xOTUzMTI0IGMgNy4xMTkxMzQsMCAxMS45MzI3MSw1LjUwODEzNzMgMTIuMzQ1NzAzLDEyLjYyNDk5OTggQyAyOS4zMzIwNjIsMjMuMjM2ODk2IDIzLjYxODk1OCwyOS41NDU5OTggMTYuMDE5NTMxLDI4LjcxODc1IDkuNTQ1NDMyMSwyOC4wMTQwMTIgMy42MzQxNjM3LDIyLjU1NTE0MyAzLjExNTIzNDQsMTUuODE4MzU5IDIuODQyNDU2MywxMi4yNzY5NjcgNC40MTg0MTA5LDkuMTI4MzE2OSA2Ljk1NzAzMTIsNi44NTkzNzUgOS40OTU2NTE2LDQuNTkwNDMzMSAxMi45OTcwOTMsMy4yMDcwMzEyIDE2LjU1NjY0MSwzLjIwNzAzMTIgWiBtIC0wLjA3MDMxLDEuNDE2MDE1NyBjIC0zLjE2MTk3MywwIC02LjI2MzUwOSwxLjIyNTgxMzkgLTguNTE5NTMxMSwzLjI0MjE4NzUgQyA1LjcxMDc2OTEsOS44ODE2MDggNC4zMDE0NTQyLDEyLjY5NDU4OSA0LjU0NDkyMTksMTUuODU1NDY5IDUuMDA2NTYyNCwyMS44NDg1NTQgMTAuMjQ0MTc4LDI2LjY4NjE1OSAxNS45OTgwNDcsMjcuMzEyNSAyMi43NTcwMTMsMjguMDQ4MjYxIDI3Ljg1NDQ1MSwyMi40MjA5MzYgMjcuNDcyNjU2LDE1Ljg0MTc5NyAyNy4xMDY4MjQsOS41Mzc2MDI1IDIyLjgxMDE2LDQuNjIzMDQ2OSAxNi40ODYzMjgsNC42MjMwNDY5IFogbSAwLDAuMTk1MzEyNSBjIDYuMjIyOTIsMCAxMC40Mjk5NDYsNC44MTMwMTM4IDEwLjc5MTAxNiwxMS4wMzUxNTY2IDAuMzc1NjEzLDYuNDcyNjE1IC00LjYxNzU4NCwxMS45ODY3MiAtMTEuMjU5NzY2LDExLjI2MzY3MiBDIDEwLjM1ODY4NSwyNi41MDExODYgNS4xOTE4MzgxLDIxLjcyNzk4NSA0LjczODI4MTIsMTUuODM5ODQ0IDQuNDk5OTIwMSwxMi43NDUyNjIgNS44NzY3MzE1LDkuOTk0OTc3OCA4LjA5NTcwMzEsOC4wMTE3MTg4IDEwLjMxNDY3NSw2LjAyODQ1OTUgMTMuMzc0ODksNC44MTgzNTk0IDE2LjQ4NjMyOCw0LjgxODM1OTQgWiBtIC0wLjA2ODM2LDEuNDE2MDE1NiBjIC0yLjcxMzg3NywwIC01LjM3NjExOCwxLjA1MjUxNjQgLTcuMzEyNTAwMiwyLjc4MzIwMzEgLTEuOTM2MzgyOCwxLjczMDY4NjkgLTMuMTQ2NTUxNyw0LjE0NTMxMTkgLTIuOTM3NSw2Ljg1OTM3NDkgMC4zOTYyNjk5LDUuMTQ0NDMgNC44ODk0NDQyLDkuMjk0NDI5IDkuODI4MTI1Miw5LjgzMjAzMSA1LjgwMTc0OSwwLjYzMTU2MiAxMC4xNzkyNTcsLTQuMTk4ODI4IDkuODUxNTYyLC05Ljg0NTcwMyBDIDI1LjUzMzc1LDEwLjQ1MzgyMiAyMS44NDU2MTYsNi4yMzQzNzUgMTYuNDE3OTc0LDYuMjM0Mzc1IFogbSAwLDAuMTk1MzEyNSBjIDUuMzI2NzMsMCA4LjkyNTIyNiw0LjExNzkwNTUgOS4yMzQzNzUsOS40NDUzMTI1IDAuMzIxNTEzLDUuNTQwMzUxIC0zLjk0OTgwMSwxMC4yNTk0NzQgLTkuNjM0NzY2LDkuNjQwNjI1IEMgMTEuMTczODc1LDI0Ljk4ODM2MiA2Ljc0OTUxNDMsMjAuOTAwODE0IDYuMzYxMzI4MSwxNS44NjEzMjggNi4xNTczODMxLDEzLjIxMzU2MyA3LjMzNTA0MzEsMTAuODU5NjgyIDkuMjM0Mzc1LDkuMTYyMTA5NCAxMS4xMzM3MDcsNy40NjQ1MzcyIDEzLjc1NDYyOCw2LjQyOTY4NzUgMTYuNDE3OTY5LDYuNDI5Njg3NSBaIG0gLTAuMDY4MzYsMS40MTYwMTU2IGMgLTIuMjY1Nzc1LDAgLTQuNDg4NzI5LDAuODc5MjE5NiAtNi4xMDU0NjgsMi4zMjQyMTg5IC0xLjYxNjc0MDgsMS40NDQ5OTkgLTIuNjI3NzYwNywzLjQ2MTI2OSAtMi40NTMxMjU0LDUuNzI4NTE2IDAuMzMwODk4Niw0LjI5NTc2OCA0LjA4MTU5NjQsNy43NjAxMiA4LjIwNTA3ODQsOC4yMDg5ODQgNC44NDQ1MjUsMC41MjczNiA4LjUwMDE1NiwtMy41MDYwOTcgOC4yMjY1NjIsLTguMjIwNzAzIEMgMjMuOTYwNjcyLDExLjM3MTk5NiAyMC44ODEwNiw3Ljg0NTcwMzEgMTYuMzQ5NjE0LDcuODQ1NzAzMSBaIG0gMCwwLjE5NTMxMjUgYyA0LjQzMDUzNCwwIDcuNDIyNDYxLDMuNDIyNzk5NCA3LjY3OTY4OCw3Ljg1NTQ2ODQgMC4yNjc0MTIsNC42MDgwODIgLTMuMjgzOTc4LDguNTMyMjI2IC04LjAxMTcxOSw4LjAxNzU3OCBDIDExLjk4OTA3NSwyMy40NzU1MzggOC4zMDcxODk5LDIwLjA3NTU5MyA3Ljk4NDM3NSwxNS44ODQ3NjYgNy44MTQ4NDYzLDEzLjY4MzgxOSA4Ljc5NTMxMDUsMTEuNzI2MzM4IDEwLjM3NSwxMC4zMTQ0NTMgMTEuOTU0Njg5LDguOTAyNTY4OSAxNC4xMzQzNyw4LjA0MTAxNTYgMTYuMzQ5NjA5LDguMDQxMDE1NiBaIG0gLTAuMDY4MzYsMS40MTYwMTU2IGMgLTEuODE3NjcyLDAgLTMuNjAxMzQyLDAuNzAzOTY4OCAtNC44OTg0MzgsMS44NjMyODA4IC0xLjI5NzA5NSwxLjE1OTMxIC0yLjEwODk2ODMsMi43NzkxODUgLTEuOTY4NzQ5NSw0LjU5OTYxIDAuMjY1NTI2OSwzLjQ0NzExMSAzLjI3Mzc1MDUsNi4yMjU4MTMgNi41ODIwMzE1LDYuNTg1OTM3IDMuODg3Mjk1LDAuNDIzMTYgNi44MjMwMDgsLTIuODE1MzE4IDYuNjAzNTE1LC02LjU5NzY1NiBDIDIyLjM4OTU0MSwxMi4yODgyMjIgMTkuOTE2NDk1LDkuNDU3MDMxMSAxNi4yODEyNSw5LjQ1NzAzMTIgWiBtIDAsMC4xOTUzMTI2IGMgMy41MzQzMzMsMCA1LjkxNzc0MiwyLjcyNzY5NjIgNi4xMjMwNDcsNi4yNjU2MjUyIDAuMjEzMzExLDMuNjc1ODE0IC0yLjYxNjIwOCw2LjgwMzAyNSAtNi4zODY3MTksNi4zOTI1NzggLTMuMjEzMjk4LC0wLjM0OTc4NSAtNi4xNTA3NTk3LC0zLjA2MjEzIC02LjQwODIwMywtNi40MDQyOTcgLTAuMTM1MTEyMiwtMS43NTQxMjcgMC42NDQyNTIsLTMuMzEzMjU3IDEuOTA0Mjk3LC00LjQzOTQ1MyAxLjI2MDA0NSwtMS4xMjYxOTYgMy4wMDA0NDEsLTEuODE0NDUzMyA0Ljc2NzU3OCwtMS44MTQ0NTMyIHogbSAtMC4wNzAzMSwxLjQxNjAxNTIgYyAtMS4zNjk1NzIsMCAtMi43MTIsMC41MzA2NzUgLTMuNjg5NDU0LDEuNDA0Mjk3IC0wLjk3NzQ1MywwLjg3MzYyMiAtMS41OTAxNzcsMi4wOTUxNDUgLTEuNDg0Mzc1LDMuNDY4NzUgMC4yMDAxNTYsMi41OTg0NTIgMi40NjU5LDQuNjg5NTUxIDQuOTU4OTg1LDQuOTYwOTM4IDIuOTMwMDcsMC4zMTg5NTggNS4xNDM5MDgsLTIuMTIyNTg3IDQuOTc4NTE1LC00Ljk3MjY1NiAtMC4xNTgxNDUsLTIuNzI1MjQ0IC0yLjAyNDYyMiwtNC44NjEzMjkgLTQuNzYzNjcxLC00Ljg2MTMyOSB6IG0gMCwwLjE5NTMxMyBjIDIuNjM4MTM1LDAgNC40MTQ5NzUsMi4wMzQ1NDQgNC41NjgzNTksNC42Nzc3MzQgMC4xNTkyMTEsMi43NDM1NDYgLTEuOTUwMzg2LDUuMDczODI0IC00Ljc2MzY3Miw0Ljc2NzU3OCAtMi4zOTgxMDIsLTAuMjYxMDQ3IC00LjU5MTEzMSwtMi4yODc3NDEgLTQuNzgzMjAzLC00Ljc4MTI1IC0wLjEwMDY5NiwtMS4zMDczMDggMC40Nzk1MTksLTIuNDcwMDM5IDEuNDE5OTIyLC0zLjMxMDU0NiAwLjk0MDQwMywtMC44NDA1MDggMi4yMzk1NTcsLTEuMzUzNTE2IDMuNTU4NTk0LC0xLjM1MzUxNiB6IG0gLTAuMDY4MzYsMS40MTYwMTYgYyAtMC45MjE0NzIsMCAtMS44MjI2NTcsMC4zNTU0MjUgLTIuNDgwNDY5LDAuOTQzMzU5IC0wLjY1NzgxMSwwLjU4NzkzNCAtMS4wNzMzMzksMS40MTUwMSAtMS4wMDE5NTMsMi4zNDE3OTcgMC4xMzQ3ODUsMS43NDk3OTIgMS42NTYwOTUsMy4xNTMyOTEgMy4zMzM5ODUsMy4zMzU5MzcgMS45NzI4NDYsMC4yMTQ3NTkgMy40NjY3NiwtMS40MzE4MDkgMy4zNTU0NjgsLTMuMzQ5NjA5IC0wLjEwNjIyNCwtMS44MzA1MDMgLTEuMzY0MTc3LC0zLjI3MTQ4NyAtMy4yMDcwMzEsLTMuMjcxNDg0IHogbSAwLDAuMTk1MzEyIGMgMS43NDE5NDIsMCAyLjkxMjIwOSwxLjMzOTQ0IDMuMDEzNjcyLDMuMDg3ODkxIDAuMTA1MTEsMS44MTEyNzYgLTEuMjg0NTYyLDMuMzQ2NTc3IC0zLjE0MDYyNSwzLjE0NDUzMSAtMS41ODI5MDcsLTAuMTcyMzA3IC0zLjAzMzQ1NSwtMS41MTMzNTUgLTMuMTYwMTU2LC0zLjE1ODIwMyAtMC4wNjYyOCwtMC44NjA0OSAwLjMxNDc4NSwtMS42MjQ4NjggMC45MzU1NDcsLTIuMTc5Njg4IDAuNjIwNzQ5LC0wLjU1NDgxOSAxLjQ4MDYyLC0wLjg5NDUzMSAyLjM1MTU1NiwtMC44OTQ1MzEgeiBtIC0wLjA2ODM2LDEuNDE2MDE2IGMgLTAuNDczMzY5LDAgLTAuOTM1MjcxLDAuMTgyMTI5IC0xLjI3MzQzOCwwLjQ4NDM3NSAtMC4zMzgxNjcsMC4zMDIyNDYgLTAuNTU0NTQ2LDAuNzMwOTY5IC0wLjUxNzU3OCwxLjIxMDkzNyAwLjA2OTQxLDAuOTAxMTMzIDAuODQ4MjQ5LDEuNjE4OTgxIDEuNzEwOTM4LDEuNzEyODkxIDEuMDE1NjE2LDAuMTEwNTU3IDEuNzg5NjE0LC0wLjc0MTAzMSAxLjczMjQyMSwtMS43MjY1NjMgLTAuMDU0MywtMC45MzU3NjYgLTAuNzA1NjkxLC0xLjY4MTY0IC0xLjY1MjM0MywtMS42ODE2NCB6IG0gMCwwLjE5NTMxMiBjIDAuODQ1NzQsMCAxLjQwNzQ5LDAuNjQ0MzMzIDEuNDU3MDMxLDEuNDk4MDQ3IDAuMDUxMDEsMC44NzkwMDggLTAuNjE2NzkzLDEuNjE5MzI5IC0xLjUxNTYyNSwxLjUyMTQ4NCAtMC43Njc3MDYsLTAuMDgzNTcgLTEuNDc1NzgsLTAuNzM4OTY3IC0xLjUzNzEwOSwtMS41MzUxNTYgLTAuMDMxODYsLTAuNDEzNjcxIDAuMTUwMDU1LC0wLjc3OTY5NyAwLjQ1MTE3MiwtMS4wNDg4MjggMC4zMDExMTYsLTAuMjY5MTMxIDAuNzIxNjk4LC0wLjQzNTU0NyAxLjE0NDUzMSwtMC40MzU1NDcgeiIgLz48L3N2Zz4=');}.icon-magnifying-glass{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMjE5Ljc2bC01MS4zOC01MS4zOGE4Ni4xNSw4Ni4xNSwwLDEsMC04LjQ4LDguNDhsNTEuMzgsNTEuMzhhNiw2LDAsMCwwLDguNDgtOC40OFpNMzgsMTEyYTc0LDc0LDAsMSwxLDc0LDc0QTc0LjA5LDc0LjA5LDAsMCwxLDM4LDExMloiLz48L3N2Zz4=');}.icon-infinity{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI4YTU0LDU0LDAsMCwxLTkyLjE4LDM4LjE4LDMuMDcsMy4wNywwLDAsMS0uMjUtLjI2bC02MC02Ny43NGE0Miw0MiwwLDEsMCwwLDU5LjY0bDguNTctOS42N2E2LDYsMCwxLDEsOSw4bC04LjY5LDkuODFhMy4wNywzLjA3LDAsMCwxLS4yNS4yNiw1NCw1NCwwLDEsMSwwLTc2LjM2LDMuMDcsMy4wNywwLDAsMSwuMjUuMjZsNjAsNjcuNzRhNDIsNDIsMCwxLDAsMC01OS42NGwtOC41Nyw5LjY3YTYsNiwwLDEsMS05LThsOC42OS05LjgxYTMuMDcsMy4wNywwLDAsMSwuMjUtLjI2QTU0LDU0LDAsMCwxLDI0NiwxMjhaIi8+PC9zdmc+');}.icon-arrow-counter-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMCwxLTkyLjc0LDk0SDEyOGE5My40Myw5My40MywwLDAsMS02NC41LTI1LjY1LDYsNiwwLDEsMSw4LjI0LTguNzJBODIsODIsMCwxLDAsNzAsNzBsLS4xOS4xOUwzOS40NCw5OEg3MmE2LDYsMCwwLDEsMCwxMkgyNGE2LDYsMCwwLDEtNi02VjU2YTYsNiwwLDAsMSwxMiwwVjkwLjM0TDYxLjYzLDYxLjRBOTQsOTQsMCwwLDEsMjIyLDEyOFoiLz48L3N2Zz4=');}.icon-clock{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm0wLDE5MmE5MCw5MCwwLDEsMSw5MC05MEE5MC4xLDkwLjEsMCwwLDEsMTI4LDIxOFptNjItOTBhNiw2LDAsMCwxLTYsNkgxMjhhNiw2LDAsMCwxLTYtNlY3MmE2LDYsMCwwLDEsMTIsMHY1MGg1MEE2LDYsMCwwLDEsMTkwLDEyOFoiLz48L3N2Zz4=');}.icon-house{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSwxMTAuMWwtODAtODBhMTQsMTQsMCwwLDAtMTkuOCwwbC04MCw4MEExMy45MiwxMy45MiwwLDAsMCwzNCwxMjB2OTZhNiw2LDAsMCwwLDYsNmg2NGE2LDYsMCwwLDAsNi02VjE1OGgzNnY1OGE2LDYsMCwwLDAsNiw2aDY0YTYsNiwwLDAsMCw2LTZWMTIwQTEzLjkyLDEzLjkyLDAsMCwwLDIxNy45LDExMC4xWk0yMTAsMjEwSDE1OFYxNTJhNiw2LDAsMCwwLTYtNkgxMDRhNiw2LDAsMCwwLTYsNnY1OEg0NlYxMjBhMiwyLDAsMCwxLC41OC0xLjQybDgwLTgwYTIsMiwwLDAsMSwyLjg0LDBsODAsODBBMiwyLDAsMCwxLDIxMCwxMjBaIi8+PC9zdmc+');}.icon-logo{--icon:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgICB3aWR0aD0iMzIiICAgIGhlaWdodD0iMzIiICAgIHZpZXdCb3g9IjAgMCAzMiAzMiIgICAgdmVyc2lvbj0iMS4xIiAgICB4bWw6c3BhY2U9InByZXNlcnZlIiAgICBzdHlsZT0iY2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxLjUiICAgIGlkPSJzdmcxNCIgICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcyAgICBpZD0iZGVmczE0IiAvPjxwYXRoICAgIGQ9Ik0gMTYuNTgwMDc4LDIuMTMyODEyNSBDIDguODY0ODQ0OSwyLjEzMjgxMjUgMS40NDA2MDIxLDguMTQ2NjIxOCAyLjAzMzIwMzEsMTUuODM5ODQ0IDIuNTk0NDU4OCwyMy4xMjY2NjYgOC45NzEyMDEyLDI5LjAyNTE1NSAxNS45NzA3MDMsMjkuNzg3MTA5IDI0LjIyMTIyNCwzMC42ODUyNCAzMC40NDA5MTEsMjMuODM0Mjc3IDI5Ljk3NDYwOSwxNS43OTg4MjggMjkuNTI3ODEzLDguMDk5ODY1NSAyNC4yOTE1NiwyLjEzMjgxMjUgMTYuNTgwMDc4LDIuMTMyODEyNSBaIG0gMCwwLjYwNzQyMTkgYyAwLjAxMDQ2LDAgMC4wMjA4MywwIDAuMDMxMjUsMCBWIDI5LjIzMjQyMiBjIC0wLjE5MDMyMywtMC4wMTIxOCAtMC4zODE1MjEsLTAuMDI3ODMgLTAuNTc0MjE5LC0wLjA0ODgzIEMgOS4zMTMwNDUzLDI4LjQ1MTYxNSAzLjE3Nzg3NzUsMjIuNzkzMDQ0IDIuNjM4NjcxOSwxNS43OTI5NjkgMi4wNzI0NTYsOC40NDIwMTUzIDkuMjA4MTAwOCwyLjc0MDIzNDQgMTYuNTgwMDc4LDIuNzQwMjM0NCBaIE0gMTYuMDkxNzk3LDMuODg0NzY1NiAxNiwzLjg4ODY3MTkgQyAxMi43MjU0NTQsNC4wMTgzNDg5IDkuNTUyMzM3OSw1LjM2NDY4MzggNy4yNTU4NTk0LDcuNSA0Ljk1OTM4MDksOS42MzUzMTYyIDMuNTQwMjcwMywxMi41NjQ5NzIgMy43OTI5Njg4LDE1Ljg0NTcwMyA0LjI4NDc3MzksMjIuMjMwMDQ1IDkuODY0NDgxMiwyNy4zODM2MDYgMTUuOTk0MTQxLDI4LjA1MjczNCBsIDAuMDg5ODQsMC4wMDk4IDAuMDIxNDgsLTAuMTgxNjQxIC0wLjA5MTgsLTAuMDA5OCBDIDkuOTcyNjc0OSwyNy4yMTE2NDQgNC40NTg4NjkxLDIyLjExNjQ2OCAzLjk3NDYwOTQsMTUuODMwMDc4IDMuNzI2NTU1OSwxMi42MDk2NTEgNS4xMTU4MDg0LDkuNzM5MDQzNyA3LjM3ODkwNjIsNy42MzQ3NjU2IDkuNjQyMDA0MSw1LjUzMDQ4NzUgMTIuNzc4NTM5LDQuMTk4MTk2OCAxNi4wMDc4MTIsNC4wNzAzMTI1IGwgMC4wOTE4LC0wLjAwMzkxIHogbSAwLDEuNTAxOTUzMSBMIDE2LDUuMzkwNjI1IEMgMTMuMTMxOTQ5LDUuNTA0MjA0NyAxMC4zNTMyOTgsNi42ODQzNDE1IDguMzQxNzk2OSw4LjU1NDY4NzUgNi4zMzAyOTYyLDEwLjQyNTAzMyA1LjA4NzE5MjksMTIuOTkwODE5IDUuMzA4NTkzNywxNS44NjUyMzQgNS43MzkzOTQsMjEuNDU3NjY5IDEwLjYyNTE2MSwyNS45NzA1NDcgMTUuOTk0MTQxLDI2LjU1NjY0MSBsIDAuMDg5ODQsMC4wMDk4IDAuMDIxNDgsLTAuMTc5Njg3IC0wLjA5MTgsLTAuMDA5OCBDIDEwLjczMzM1NCwyNS44MDA1MzMgNS45MTM0ODkyLDIxLjM0NjA0NiA1LjQ5MDIzNDQsMTUuODUxNTYzIDUuMjczNDc4NCwxMy4wMzc0NTEgNi40ODY3MjM3LDEwLjUyNjgwOCA4LjQ2NDg0MzgsOC42ODc1IDEwLjQ0Mjk2NCw2Ljg0ODE5MjIgMTMuMTg1MDMyLDUuNjg0MDUyNiAxNi4wMDc4MTIsNS41NzIyNjU2IGwgMC4wOTE4LC0wLjAwMzkxIHogbSAwLDEuNTAzOTA2MyBMIDE2LDYuODkyNTc4MSBjIC0yLjQ2MTU3NywwLjA5NzQ4MyAtNC44NDU3MjgsMS4xMTE0MTc0IC02LjU3MjI2NTYsMi43MTY3OTY5IC0xLjcyNjUzOCwxLjYwNTM4IC0yLjc5NTU3NDEsMy44MDcyODIgLTIuNjA1NDY4OCw2LjI3NTM5MSAwLjM2OTc5ODYsNC44MDA1NCA0LjU2MzUzMzQsOC42NzQ2NzQgOS4xNzE4NzU0LDkuMTc3NzM0IGwgMC4wODk4NCwwLjAwOTggMC4wMjE0OCwtMC4xODE2NDEgLTAuMDkxOCwtMC4wMDk4IEMgMTEuNDk0MDM3LDI0LjM4NzQ4MSA3LjM2NjE1NTcsMjAuNTczNjM0IDcuMDAzOTA2MiwxNS44NzEwOTQgNi44MTg0NDgxLDEzLjQ2MzMyIDcuODU3NjQwNSwxMS4zMTY1MTMgOS41NTA3ODEzLDkuNzQyMTg3NSAxMS4yNDM5MjIsOC4xNjc4NjE4IDEzLjU5MTUyOSw3LjE3MTg2MDggMTYuMDA3ODEyLDcuMDc2MTcxOSBsIDAuMDkxOCwtMC4wMDM5MSB6IG0gMCwxLjUwMTk1MzEgTCAxNiw4LjM5NjQ4NDQgYyAtMi4wNTUwNzMsMC4wODEzODQgLTQuMDQ0Nzc1LDAuOTI1MjMzNCAtNS40ODYzMjgsMi4yNjU2MjQ2IC0xLjQ0MTU1MzUsMS4zNDAzOTMgLTIuMzM0NTg4MSwzLjE4MjM3OSAtMi4xNzU3ODE0LDUuMjQ0MTQxIDAuMzA4NzkyLDQuMDA4NTc5IDMuODA4NjA1NCw3LjI0MDEzNiA3LjY1NjI1MDQsNy42NjAxNTYgbCAwLjA4OTg0LDAuMDA5OCAwLjAwNzgsLTAuMDY4MzYgdiAtMC4wMDIgbCAwLjAwMiwtMC4wMDk4IGMgOS40OWUtNCwtMC4wMDM0IDAuMDAzNSwtMC4wMDYyIDAuMDAzOSwtMC4wMDk4IDYuNDJlLTQsLTAuMDA2NyA4LjAyZS00LC0wLjAxMzAxIDAsLTAuMDE5NTMgbCAwLjAwNzgsLTAuMDcyMjcgLTAuMDkxOCwtMC4wMDk4IEMgMTIuMjU0NjY3LDIyLjk3NDQyMiA4LjgyMDc3OTYsMTkuODAzMjMxIDguNTE5NTMxMywxNS44OTI1NzggOC4zNjUzNjgyLDEzLjg5MTEwNSA5LjIyODUzNzMsMTIuMTA2MjM3IDEwLjYzNjcxOSwxMC43OTY4NzUgMTIuMDQ0OSw5LjQ4NzUxMyAxMy45OTc5OTksOC42NTc3MTcgMTYuMDA3ODEyLDguNTc4MTI1IGwgMC4wOTE4LC0wLjAwMzkxIHogbSAwLDEuNTAxOTUzMiBMIDE2LDkuODk4NDM3NSBjIC0xLjY0ODU4OCwwLjA2NTI4NyAtMy4yNDU3NjEsMC43NDI5Mzg1IC00LjQwMjM0NCwxLjgxODM1OTUgLTEuMTU2NTgyLDEuMDc1NDIxIC0xLjg3MTY1MDYsMi41NTM1MzggLTEuNzQ0MTQwNCw0LjIwODk4NCAwLjI0Nzc4ODQsMy4yMTY2NjkgMy4wNTM2NDE0LDUuODA5NTAxIDYuMTQwNjI1NCw2LjE0NjQ4NSBsIDAuMDg5ODQsMC4wMDk4IDAuMDIxNDgsLTAuMTgxNjQgLTAuMDkxOCwtMC4wMDk4IGMgLTIuOTk4MzQ0LC0wLjMyNzMwOCAtNS43MzgyNzMsLTIuODU5Nzk4IC01Ljk3ODUxNiwtNS45Nzg1MTYgLTAuMTIyODY0NSwtMS41OTUxNDIgMC41NjQyOTgsLTMuMDE4MTE3IDEuNjg3NSwtNC4wNjI1IDEuMTIzMjAyLC0xLjA0NDM4MiAyLjY4MTgzOSwtMS43MDYwMzcgNC4yODUxNTYsLTEuNzY5NTMxIGwgMC4wOTE4LC0wLjAwMzkgeiBtIDAsMS41MDM5MDY3IC0wLjA5MTgsMC4wMDIgYyAtMS4yNDIwOTUsMC4wNDkxOSAtMi40NDQ4LDAuNTYwNjUyIC0zLjMxNjQwNiwxLjM3MTA5MyAtMC44NzE2MDYsMC44MTA0NDIgLTEuNDEyNjE5LDEuOTI0NzEzIC0xLjMxNjQwNiwzLjE3MzgyOSAwLjE4Njc4MywyLjQyNDczMiAyLjMwMDY0Myw0LjM3NjkxMyA0LjYyNjk1Myw0LjYzMDg1OSBsIDAuMDg5ODQsMC4wMDk4IDAuMDIxNDgsLTAuMTgxNjQgLTAuMDkxOCwtMC4wMDk4IGMgLTIuMjM3NjkyLC0wLjI0NDI3MiAtNC4yODU2MDQsLTIuMTM2MDgzIC00LjQ2NDg0NCwtNC40NjI4OSAtMC4wOTE1NywtMS4xODg4MjYgMC40MjE1MzIsLTIuMjQ3OTMzIDEuMjU5NzY2LC0zLjAyNzM0NCAwLjgzODIzNCwtMC43Nzk0MTEgMi4wMDIzODMsLTEuMjcyOTE2IDMuMTk5MjE4LC0xLjMyMDMxMyBsIDAuMDkxOCwtMC4wMDM5IHogbSAwLDEuNTAxOTUzIC0wLjA5MTgsMC4wMDM5IGMgLTAuODM1NjEzLDAuMDMzMDkgLTEuNjQzODMxLDAuMzc0NDUyIC0yLjIzMDQ2OSwwLjkxOTkyMiAtMC41ODY2MzcsMC41NDU0NyAtMC45NTE2MzYsMS4yOTk3NzggLTAuODg2NzE4LDIuMTQyNTc4IDAuMTI1NzgsMS42MzI4MjEgMS41NDU2NzMsMi45NDIzNyAzLjExMTMyOCwzLjExMzI4MSAtMC4wMDI2LC0yLjhlLTQgLTAuMDA1MywyLjg2ZS00IC0wLjAwNzgsMCAwLjAwMyw2LjAzZS00IDAuMDA2NiwwLjAwMTcgMC4wMDk4LDAuMDAyIDAuMDAzMSwzLjA4ZS00IDAuMDA2NywxLjJlLTUgMC4wMDk4LDAgbCAwLjA3ODEzLDAuMDA5OCAwLjAyMTQ4LC0wLjE4MTY0MSAtMC4wOTE4LC0wLjAwOTggYyAtMS40NzcwMTUsLTAuMTYxMjM1IC0yLjgzMDk4NCwtMS40MTIzOTYgLTIuOTQ5MjE5LC0yLjk0NzI2NiAtMC4wNjAyNywtMC43ODI0OTYgMC4yNzY4MjIsLTEuNDc5NzA5IDAuODMwMDc4LC0xLjk5NDE0MSAwLjU1MzI1NywtMC41MTQ0MzEgMS4zMjI5MzksLTAuODQxNzQ4IDIuMTEzMjgxLC0wLjg3MzA0NiBsIDAuMDkxOCwtMC4wMDM5IHogbSAwLDEuNTAxOTUzIEwgMTYsMTQuNDA2MjUgYyAtMC40MjkxMTcsMC4wMTY5OSAtMC44NDI4NzMsMC4xOTIxNjYgLTEuMTQ0NTMxLDAuNDcyNjU2IC0wLjMwMTY1OSwwLjI4MDQ5MSAtMC40OTA2NDgsMC42NzA5NTUgLTAuNDU3MDMxLDEuMTA3NDIyIDAuMDY0NzcsMC44NDA4ODYgMC43OTA3MjksMS41MTE3MzIgMS41OTU3MDMsMS41OTk2MDkgbCAwLjA4OTg0LDAuMDA5OCAwLjAyMTQ4LC0wLjE4MTY0MSAtMC4wOTE4LC0wLjAwOTggYyAtMC43MTYzNTcsLTAuMDc4MiAtMS4zNzYzNjIsLTAuNjg4NjgxIC0xLjQzMzU5NCwtMS40MzE2NDEgLTAuMDI4OTcsLTAuMzc2MTc5IDAuMTMyMTA1LC0wLjcxMTQ3NyAwLjQwMDM5MSwtMC45NjA5MzcgMC4yNjgyODYsLTAuMjQ5NDYxIDAuNjQzNDg1LC0wLjQwODYyNyAxLjAyNzM0MywtMC40MjM4MjggbCAwLjA5MTgsLTAuMDAzOSB6IiAgICBzdHlsZT0iYmFzZWxpbmUtc2hpZnQ6YmFzZWxpbmU7Y2xpcC1ydWxlOm5vbnplcm87ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2ZWN0b3ItZWZmZWN0Om5vbmU7ZmlsbDojMjIyMjIyO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGU7c3RvcC1jb2xvcjojMDAwMDAwO3N0b3Atb3BhY2l0eToxIiAgICBpZD0icGF0aDI3IiAvPjwvc3ZnPg==');}.icon-jakevan{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0eWxlPSJjbGlwLXJ1bGU6ZXZlbm9kZDtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MiIgdmlld0JveD0iMCAwIDMyIDMyIj48cGF0aCBkPSJNMTcuODggMTQuNjhIMTIuOWwtLjQzLTEuNjNIOS41OGwtLjQ1IDEuNjNINi41bDIuODktOC43NGgzLjJsMi44OSA4LjY0di04LjZoMi40djMuNzhjLjEtLjIuMjItLjM4LjM1LS41Ny4xMy0uMi4yNi0uMzcuMzktLjU0bDEuODYtMi42N2g3Ljh2MS44OUgyNS40djEuMzdoMi42NXYxLjg4SDI1LjR2MS42NWgyLjg2djEuOTFoLTcuOTNsLTEuNzUtMy4zMi0uNy40MXptNS4xMy04LjU5LTIuNyAzLjc5IDIuNyA0Ljc0em0tMTEuMDUgNS4wMy0uMzgtMS40M2ExMzYuODYgMTM2Ljg2IDAgMCAwLS40LTEuNTVMMTEgNy4zOGExNy43NiAxNy43NiAwIDAgMS0uMzYgMS42bC0uMTguNzEtLjM5IDEuNDN6bS04LjU4IDYuM2E1Ljc0IDUuNzQgMCAwIDEtMS4yNC0uMTN2LTEuODNsLjQxLjA4Yy4xNS4wMy4zLjA1LjQ3LjA1LjMgMCAuNTEtLjA2LjY3LS4xN2EuOTIuOTIgMCAwIDAgLjM0LS41MmMuMDYtLjIzLjEtLjUyLjEtLjg2VjUuOThoMi40djcuODVjMCAuODgtLjEzIDEuNTctLjQgMi4xLS4yNi41Mi0uNjMuOS0xLjEgMS4xNC0uNDguMjMtMS4wMy4zNS0xLjY1LjM1WiIgc3R5bGU9ImZpbGw6Y3VycmVudENvbG9yO3N0cm9rZS13aWR0aDouMDE4NDM5MiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS40IC42Nikgc2NhbGUoLjk2MDUwMTM0KSIvPjxwYXRoIGQ9Ik0yMi44MiAyMi4yN2gtNC4wNmwtLjM3LTEuNEgxNS45bC0uMzkgMS40aC0yLjI2bDIuNDktNy41M2gyLjc1bDIuNDkgNy40NHYtNy40MWgyLjdsMi43NyA1LjIxaC4wM2E0MS4xIDQxLjEgMCAwIDEtLjA3LTEuODJ2LTMuMzloMS44M3Y3LjVoLTIuN2wtMi43OS01LjI4aC0uMDRhMTIuODMgMTIuODMgMCAwIDEgLjA4IDEuMjZsLjAyLjY0em0tNC44Ni0zLjA3LS4zMy0xLjIzYTg5LjA3IDg5LjA3IDAgMCAwLS4zNS0xLjM0bC0uMTQtLjY1YTE1LjA0IDE1LjA0IDAgMCAxLS4zMSAxLjM3bC0uMTYuNjItLjMzIDEuMjN6bS0zLjg1LTQuNDMtMi41IDcuNUg5LjJsLTIuNS03LjVoMi4zMmwxLjA0IDMuOGExNS4wMyAxNS4wMyAwIDAgMSAuMzYgMS43NiA3LjYxIDcuNjEgMCAwIDEgLjItMS4ybC4xNC0uNTQgMS4wNi0zLjgyeiIgc3R5bGU9ImZpbGw6Y3VycmVudENvbG9yO3N0cm9rZS13aWR0aDouMDE1OTg4NCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS40IC42Nikgc2NhbGUoLjk2MDUwMTM0KSIvPjxwYXRoIGQ9Ik0xMS45IDI0LjIxYzAgLjQtLjA3LjcyLS4yLjk5LS4xNS4yNi0uMzYuNDYtLjYzLjYtLjI4LjEzLS42Mi4yLTEuMDMuMkg5LjJ2LTMuNWguOTdjLjM4IDAgLjcuMDYuOTYuMTkuMjUuMTMuNDUuMzIuNTguNTguMTQuMjUuMi41Ny4yLjk0em0tLjI2LjAxYzAtLjMzLS4wNS0uNjEtLjE2LS44M2ExLjEgMS4xIDAgMCAwLS41MS0uNTEgMS45NSAxLjk1IDAgMCAwLS44Ny0uMTdoLS42NnYzLjA3aC42Yy41MyAwIC45My0uMTMgMS4yLS4zOS4yNy0uMjYuNC0uNjUuNC0xLjE3ek0xNC4yNyAyNmgtMS45NXYtMy41aDEuOTV2LjIyaC0xLjd2MS4zMmgxLjZ2LjIzaC0xLjZ2MS41aDEuN3ptMS4yOC0zLjVjLjI4IDAgLjUyLjAyLjcuMDhhLjguOCAwIDAgMSAuNDQuM2MuMS4xNC4xNC4zMy4xNC41N2EuOS45IDAgMCAxLS4xLjQ1Ljg3Ljg3IDAgMCAxLS4yNy4zMmMtLjEyLjA4LS4yNS4xNC0uNC4xOGwuOTggMS42aC0uM2wtLjkyLTEuNTNoLS44OVYyNmgtLjI1di0zLjV6bS0uMDMuMjFoLS41OXYxLjU1aC43MWMuMyAwIC41Mi0uMDcuNjktLjIxLjE2LS4xNC4yNC0uMzQuMjQtLjYgMC0uMjgtLjA5LS40Ny0uMjYtLjU4LS4xNy0uMS0uNDMtLjE2LS43OS0uMTZ6bTUuNTctLjIyTDIwLjEyIDI2aC0uMjVsLS43Ni0yLjY1LS4wNS0uMTYtLjA0LS4xNGExOC44IDE4LjggMCAwIDEtLjA2LS4yNCAyMC42IDIwLjYgMCAwIDEtLjExLjQ4TDE4LjA5IDI2aC0uMjVsLS45Ni0zLjVoLjI2bC42NyAyLjQ3YTI3LjM2IDI3LjM2IDAgMCAxIC4wOS4zNWwuMDQuMTcuMDMuMTUuMDMtLjE2YTQuODMgNC44MyAwIDAgMSAuMTQtLjUzbC43LTIuNDZoLjI1bC43MyAyLjQ4YTExLjk4IDExLjk4IDAgMCAxIC4xMy41M2wuMDQuMTVhMTEuMDIgMTEuMDIgMCAwIDEgLjE1LS42OGwuNjktMi40OHpNMjMuMjYgMjZoLTEuOTV2LTMuNWgxLjk1di4yMmgtMS43djEuMzJoMS42di4yM2gtMS42djEuNWgxLjd6bTEuMjgtMy41Yy4yOCAwIC41Mi4wMi43MS4wOGEuOC44IDAgMCAxIC40My4zYy4xLjE0LjE0LjMzLjE0LjU3YS45LjkgMCAwIDEtLjEuNDUuODcuODcgMCAwIDEtLjI3LjMyYy0uMTEuMDgtLjI1LjE0LS40LjE4bC45OCAxLjZoLS4zbC0uOTItMS41M2gtLjg4VjI2aC0uMjZ2LTMuNXptLS4wMi4yMWgtLjZ2MS41NWguNzJjLjI5IDAgLjUxLS4wNy42OC0uMjEuMTYtLjE0LjI0LS4zNC4yNC0uNiAwLS4yOC0uMDgtLjQ3LS4yNi0uNTgtLjE3LS4xLS40My0uMTYtLjc4LS4xNnpNMjYuNSAyNmgtLjI1di0zLjVoMS45NXYuMjJoLTEuN3YxLjQ5aDEuNnYuMjJoLTEuNnoiIHN0eWxlPSJmaWxsOmN1cnJlbnRDb2xvcjtzdHJva2Utd2lkdGg6LjAxMDEwNjgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEuNCAuNjYpIHNjYWxlKC45NjA1MDEzNCkiLz48L3N2Zz4=');}.icon-user-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRaTTk0LDEyMGEzNCwzNCwwLDEsMSwzNCwzNEEzNCwzNCwwLDAsMSw5NCwxMjBaTTY1Ljc3LDIxMGE2Ni40Myw2Ni40MywwLDAsMSwyMC43Ny0yOS4zNiw2Niw2NiwwLDAsMSw4Mi45MiwwQTY2LjQzLDY2LjQzLDAsMCwxLDE5MC4yMywyMTBaTTIxMCwyMDhhMiwyLDAsMCwxLTIsMmgtNS4xN2E3Ny44NSw3Ny44NSwwLDAsMC00OS4zOC01MS43MSw0Niw0NiwwLDEsMC01MC45LDBBNzcuODUsNzcuODUsMCwwLDAsNTMuMTcsMjEwSDQ4YTIsMiwwLDAsMS0yLTJWNDhhMiwyLDAsMCwxLDItMkgyMDhhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-chat-teardrop{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzIsMjZhOTguMTEsOTguMTEsMCwwLDAtOTgsOTh2ODRhMTQsMTQsMCwwLDAsMTQsMTRoODRhOTgsOTgsMCwwLDAsMC0xOTZabTAsMTg0SDQ4YTIsMiwwLDAsMS0yLTJWMTI0YTg2LDg2LDAsMSwxLDg2LDg2WiIvPjwvc3ZnPg==');}.icon-house-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSwxMTAuMWwtODAtODBhMTQsMTQsMCwwLDAtMTkuOCwwbC04MCw4MEExMy45MiwxMy45MiwwLDAsMCwzNCwxMjB2OTZhNiw2LDAsMCwwLDYsNkgyMTZhNiw2LDAsMCwwLDYtNlYxMjBBMTMuOTIsMTMuOTIsMCwwLDAsMjE3LjksMTEwLjFaTTIxMCwyMTBINDZWMTIwYTIsMiwwLDAsMSwuNTgtMS40Mmw4MC04MGEyLDIsMCwwLDEsMi44NCwwbDgwLDgwQTIsMiwwLDAsMSwyMTAsMTIwWiIvPjwvc3ZnPg==');}.icon-caret-left{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMjAzLjc2YTYsNiwwLDEsMS04LjQ4LDguNDhsLTgwLTgwYTYsNiwwLDAsMSwwLTguNDhsODAtODBhNiw2LDAsMCwxLDguNDgsOC40OEw4OC40OSwxMjhaIi8+PC9zdmc+');}.icon-chat{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBINDBBMTQsMTQsMCwwLDAsMjYsNjRWMjI0YTEzLjg4LDEzLjg4LDAsMCwwLDguMDksMTIuNjlBMTQuMTEsMTQuMTEsMCwwLDAsNDAsMjM4YTEzLjg3LDEzLjg3LDAsMCwwLDktMy4zMWwuMDYtLjA1TDgyLjIzLDIwNkgyMTZhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjE2LDUwWm0yLDE0MmEyLDIsMCwwLDEtMiwySDgwYTYsNiwwLDAsMC0zLjkyLDEuNDZMNDEuMjYsMjI1LjUzQTIsMiwwLDAsMSwzOCwyMjRWNjRhMiwyLDAsMCwxLDItMkgyMTZhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-envelope{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBabS05Niw4NS44Nkw0Ny40Miw2MkgyMDguNThaTTEwMS42NywxMjgsMzgsMTg2LjM2VjY5LjY0Wm04Ljg4LDguMTRMMTI0LDE0OC40MmE2LDYsMCwwLDAsOC4xLDBsMTMuNC0xMi4yOEwyMDguNTgsMTk0SDQ3LjQzWk0xNTQuMzMsMTI4LDIxOCw2OS42NFYxODYuMzZaIi8+PC9zdmc+');}.icon-caret-right{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xODAuMjQsMTMyLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OC04LjQ4TDE2Ny41MSwxMjgsOTEuNzYsNTIuMjRhNiw2LDAsMCwxLDguNDgtOC40OGw4MCw4MEE2LDYsMCwwLDEsMTgwLjI0LDEzMi4yNFoiLz48L3N2Zz4=');}.icon-calendar{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRIMTgyVjI0YTYsNiwwLDAsMC0xMiwwVjM0SDg2VjI0YTYsNiwwLDAsMC0xMiwwVjM0SDQ4QTE0LDE0LDAsMCwwLDM0LDQ4VjIwOGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMjA4LDM0Wk00OCw0Nkg3NFY1NmE2LDYsMCwwLDAsMTIsMFY0Nmg4NFY1NmE2LDYsMCwwLDAsMTIsMFY0NmgyNmEyLDIsMCwwLDEsMiwyVjgySDQ2VjQ4QTIsMiwwLDAsMSw0OCw0NlpNMjA4LDIxMEg0OGEyLDIsMCwwLDEtMi0yVjk0SDIxMFYyMDhBMiwyLDAsMCwxLDIwOCwyMTBabS05OC05MHY2NGE2LDYsMCwwLDEtMTIsMFYxMjkuNzFsLTcuMzIsMy42NmE2LDYsMCwxLDEtNS4zNi0xMC43NGwxNi04QTYsNiwwLDAsMSwxMTAsMTIwWm01OS41NywyOS4yNUwxNDgsMTc4aDIwYTYsNiwwLDAsMSwwLDEySDEzNmE2LDYsMCwwLDEtNC44LTkuNkwxNjAsMTQyYTEwLDEwLDAsMSwwLTE2LjY1LTExQTYsNiwwLDEsMSwxMzMsMTI1YTIyLDIyLDAsMSwxLDM2LjYyLDI0LjI2WiIvPjwvc3ZnPg==');}.icon-shuffle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTc5Ljc2YTYsNiwwLDAsMSwwLDguNDhsLTI0LDI0YTYsNiwwLDAsMS04LjQ4LTguNDhMMjE3LjUyLDE5MEgyMDAuOTRhNzAuMTYsNzAuMTYsMCwwLDEtNTctMjkuMzFsLTQxLjcxLTU4LjRBNTguMTEsNTguMTEsMCwwLDAsNTUuMDYsNzhIMzJhNiw2LDAsMCwxLDAtMTJINTUuMDZhNzAuMTYsNzAuMTYsMCwwLDEsNTcsMjkuMzFsNDEuNzEsNTguNEE1OC4xMSw1OC4xMSwwLDAsMCwyMDAuOTQsMTc4aDE2LjU4bC0xMy43Ni0xMy43NmE2LDYsMCwwLDEsOC40OC04LjQ4Wm0tOTIuMDYtNzQuNDFhNS45MSw1LjkxLDAsMCwwLDMuNDgsMS4xMiw2LDYsMCwwLDAsNC44OS0yLjUxbDEuMTktMS42N0E1OC4xMSw1OC4xMSwwLDAsMSwyMDAuOTQsNzhoMTYuNThMMjAzLjc2LDkxLjc2YTYsNiwwLDEsMCw4LjQ4LDguNDhsMjQtMjRhNiw2LDAsMCwwLDAtOC40OGwtMjQtMjRhNiw2LDAsMCwwLTguNDgsOC40OEwyMTcuNTIsNjZIMjAwLjk0YTcwLjE2LDcwLjE2LDAsMCwwLTU3LDI5LjMxTDE0Mi43OCw5N0E2LDYsMCwwLDAsMTQ0LjE4LDEwNS4zNVptLTMyLjM2LDQ1LjNhNiw2LDAsMCwwLTguMzcsMS4zOWwtMS4xOSwxLjY3QTU4LjExLDU4LjExLDAsMCwxLDU1LjA2LDE3OEgzMmE2LDYsMCwwLDAsMCwxMkg1NS4wNmE3MC4xNiw3MC4xNiwwLDAsMCw1Ny0yOS4zMWwxLjE5LTEuNjdBNiw2LDAsMCwwLDExMS44MiwxNTAuNjVaIi8+PC9zdmc+');}.icon-sort-descending{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00MiwxMjhhNiw2LDAsMCwxLDYtNmg3MmE2LDYsMCwwLDEsMCwxMkg0OEE2LDYsMCwwLDEsNDIsMTI4Wm02LTU4aDU2YTYsNiwwLDAsMCwwLTEySDQ4YTYsNiwwLDAsMCwwLDEyWk0xODQsMTg2SDQ4YTYsNiwwLDAsMCwwLDEySDE4NGE2LDYsMCwwLDAsMC0xMlpNMjI4LjI0LDgzLjc2bC00MC00MGE2LDYsMCwwLDAtOC40OCwwbC00MCw0MGE2LDYsMCwwLDAsOC40OCw4LjQ4TDE3OCw2Mi40OVYxNDRhNiw2LDAsMCwwLDEyLDBWNjIuNDlsMjkuNzYsMjkuNzVhNiw2LDAsMCwwLDguNDgtOC40OFoiLz48L3N2Zz4=');}.icon-sort-ascending{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjYsMTI4YTYsNiwwLDAsMS02LDZINDhhNiw2LDAsMCwxLDAtMTJoNzJBNiw2LDAsMCwxLDEyNiwxMjhaTTQ4LDcwSDE4NGE2LDYsMCwwLDAsMC0xMkg0OGE2LDYsMCwwLDAsMCwxMlptNTYsMTE2SDQ4YTYsNiwwLDAsMCwwLDEyaDU2YTYsNiwwLDAsMCwwLTEyWm0xMjQuMjQtMjIuMjRhNiw2LDAsMCwwLTguNDgsMEwxOTAsMTkzLjUxVjExMmE2LDYsMCwwLDAtMTIsMHY4MS41MWwtMjkuNzYtMjkuNzVhNiw2LDAsMCwwLTguNDgsOC40OGw0MCw0MGE2LDYsMCwwLDAsOC40OCwwbDQwLTQwQTYsNiwwLDAsMCwyMjguMjQsMTYzLjc2WiIvPjwvc3ZnPg==');}.icon-arrow-elbow-left-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsNzJhNiw2LDAsMCwxLTYsNkg5NFYyMDEuNTFsMzcuNzYtMzcuNzVhNiw2LDAsMCwxLDguNDgsOC40OGwtNDgsNDhhNiw2LDAsMCwxLTguNDgsMGwtNDgtNDhhNiw2LDAsMCwxLDguNDgtOC40OEw4MiwyMDEuNTFWNzJhNiw2LDAsMCwxLDYtNkgyMzJBNiw2LDAsMCwxLDIzOCw3MloiLz48L3N2Zz4=');}.icon-arrow-elbow-right-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMTY0LjI0bC00OCw0OGE2LDYsMCwwLDEtOC40OCwwbC00OC00OGE2LDYsMCwxLDEsOC40OC04LjQ4TDE3MCwxOTMuNTFWNzBIMzJhNiw2LDAsMCwxLDAtMTJIMTc2YTYsNiwwLDAsMSw2LDZWMTkzLjUxbDM3Ljc2LTM3Ljc1YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+');}.icon-heart{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzgsNDJjLTIxLDAtMzkuMjYsOS40Ny01MCwyNS4zNEMxMTcuMjYsNTEuNDcsOTksNDIsNzgsNDJhNjAuMDcsNjAuMDcsMCwwLDAtNjAsNjBjMCwyOS4yLDE4LjIsNTkuNTksNTQuMSw5MC4zMWEzMzQuNjgsMzM0LjY4LDAsMCwwLDUzLjA2LDM3LDYsNiwwLDAsMCw1LjY4LDAsMzM0LjY4LDMzNC42OCwwLDAsMCw1My4wNi0zN0MyMTkuOCwxNjEuNTksMjM4LDEzMS4yLDIzOCwxMDJBNjAuMDcsNjAuMDcsMCwwLDAsMTc4LDQyWk0xMjgsMjE3LjExQzExMS41OSwyMDcuNjQsMzAsMTU3LjcyLDMwLDEwMkE0OC4wNSw0OC4wNSwwLDAsMSw3OCw1NGMyMC4yOCwwLDM3LjMxLDEwLjgzLDQ0LjQ1LDI4LjI3YTYsNiwwLDAsMCwxMS4xLDBDMTQwLjY5LDY0LjgzLDE1Ny43Miw1NCwxNzgsNTRhNDguMDUsNDguMDUsMCwwLDEsNDgsNDhDMjI2LDE1Ny43MiwxNDQuNDEsMjA3LjY0LDEyOCwyMTcuMTFaIi8+PC9zdmc+');}.icon-dots-three{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTI4YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxMjhaTTYwLDExOGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCw2MCwxMThabTEzNiwwYTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDE5NiwxMThaIi8+PC9zdmc+');}.icon-logo-jakevan{--icon:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgICB3aWR0aD0iMzIiICAgIGhlaWdodD0iMzIiICAgIHZpZXdCb3g9IjAgMCAzMiAzMiIgICAgdmVyc2lvbj0iMS4xIiAgICB4bWw6c3BhY2U9InByZXNlcnZlIiAgICBzdHlsZT0iY2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxLjUiICAgIGlkPSJzdmcxNCIgICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcyAgICBpZD0iZGVmczE0IiAvPjxwYXRoICAgIGQ9Ik0gMTYuNTgwMDc4LDIuMTMyODEyNSBDIDguODY0ODQ1LDIuMTMyODEyNSAxLjQ0MDYwMjEsOC4xNDY2MjE2IDIuMDMzMjAzMSwxNS44Mzk4NDQgMi41OTQ0NTg4LDIzLjEyNjY2NiA4Ljk3MTIwMTMsMjkuMDI1MTU1IDE1Ljk3MDcwMywyOS43ODcxMDkgMjQuMjIxMjI0LDMwLjY4NTI0IDMwLjQ0MDkxMSwyMy44MzQyNzcgMjkuOTc0NjA5LDE1Ljc5ODgyOCAyOS41Mjc4MTMsOC4wOTk4NjU2IDI0LjI5MTU2LDIuMTMyODEyNSAxNi41ODAwNzgsMi4xMzI4MTI1IFogbSAwLDAuNjA3NDIxOSBoIDAuMDMxMjUgViAyOS4yMzI0MjIgYyAtMC4xOTAzMjMsLTAuMDEyMTggLTAuMzgxNTIxLC0wLjAyNzgzIC0wLjU3NDIxOSwtMC4wNDg4MyBDIDkuMzEzMDQ1NywyOC40NTE2MTcgMy4xNzc4Nzc1LDIyLjc5MzA0NCAyLjYzODY3MTksMTUuNzkyOTY5IDIuMDcyNDU2LDguNDQyMDE1IDkuMjA4MTAwOSwyLjc0MDIzNDQgMTYuNTgwMDc4LDIuNzQwMjM0NCBaIG0gLTAuNDkyMTg3LDEuMTQ0NTMxMiAtMC4wOTE4LDAuMDAzOTEgQyAxMi43MjE1Niw0LjAxODM0MTMgOS41NDY0NzQzLDUuMzY0Njg3NyA3LjI1LDcuNSA0Ljk1MzUyNTcsOS42MzUzMTIzIDMuNTM2MzY0NywxMi41NjQ5OCAzLjc4OTA2MjUsMTUuODQ1NzAzIDQuMjgwODY2OCwyMi4yMzAwMzIgOS44NjA1ODc5LDI3LjM4MzYwOCAxNS45OTAyMzQsMjguMDUyNzM0IGwgMC4wODk4NCwwLjAwOTggMC4wMTk1MywtMC4xODE2NDEgLTAuMDg5ODQsLTAuMDA5OCBDIDkuOTY4NzM4OSwyNy4yMTE2NDMgNC40NTQ5NjM4LDIyLjExNjQ4MSAzLjk3MDcwMzEsMTUuODMwMDc4IDMuNzIyNjQ5MSwxMi42MDk2NDQgNS4xMTE4OTc5LDkuNzM5MDQ3NyA3LjM3NSw3LjYzNDc2NTYgOS42MzgxMDIxLDUuNTMwNDgzNiAxMi43NzQ2MjUsNC4xOTgxOTcgMTYuMDAzOTA2LDQuMDcwMzEyNSBsIDAuMDg5ODQsLTAuMDAzOTEgeiBtIDAsMS41MDE5NTMxIC0wLjA5MTgsMC4wMDM5MSBDIDEzLjEyODA1NCw1LjUwNDE5NzEgMTAuMzQ3NDM0LDYuNjg0MzQ1NCA4LjMzNTkzNzUsOC41NTQ2ODc1IDYuMzI0NDQxMSwxMC40MjUwMyA1LjA4MTMzNDEsMTIuOTkwODI3IDUuMzAyNzM0NCwxNS44NjUyMzQgNS43MzM1MzM2LDIxLjQ1NzY1NiAxMC42MjEyNjUsMjUuOTcwNTQ4IDE1Ljk5MDIzNCwyNi41NTY2NDEgbCAwLjA4OTg0LDAuMDA5OCAwLjAxOTUzLC0wLjE3OTY4NyAtMC4wODk4NCwtMC4wMDk4IEMgMTAuNzI5NDE5LDI1LjgwMDUzMiA1LjkwNzYzMDcsMjEuMzQ2MDU2IDUuNDg0Mzc1LDE1Ljg1MTU2MyA1LjI2NzYxODQsMTMuMDM3NDQyIDYuNDgyODEzMywxMC41MjY4MTIgOC40NjA5Mzc1LDguNjg3NSAxMC40MzkwNjIsNi44NDgxODgzIDEzLjE4MTEyLDUuNjg0MDUyOCAxNi4wMDM5MDYsNS41NzIyNjU2IGwgMC4wODk4NCwtMC4wMDM5MSB6IG0gMCwxLjUwMzkwNjMgLTAuMDkxOCwwLjAwMTk1IEMgMTMuNTM0NTQsNi45OTAwNjYzIDExLjE0ODQsOC4wMDQwMDcyIDkuNDIxODc1LDkuNjA5Mzc1IDcuNjk1MzQ5NywxMS4yMTQ3NDMgNi42MjgyNTU4LDEzLjQxNjY4IDYuODE4MzU5NCwxNS44ODQ3NjYgNy4xODgxNTUxLDIwLjY4NTI2OSAxMS4zODE5NiwyNC41NTk0NDQgMTUuOTkwMjM0LDI1LjA2MjUgbCAwLjA4OTg0LDAuMDA5OCAwLjAxOTUzLC0wLjE4MTY0MSAtMC4wODk4NCwtMC4wMDk4IEMgMTEuNDkwMTE0LDI0LjM4NzQ3NyA3LjM2MjI1MjIsMjAuNTczNjcxIDcsMTUuODcxMDk0IDYuODE0NTQwMSwxMy40NjMyOTcgNy44NTM3MjE5LDExLjMxNjUyNSA5LjU0Njg3NSw5Ljc0MjE4NzUgYyAxLjY5MzE1MywtMS41NzQzMzc0IDQuMDQwNzMsLTIuNTcwMzI2IDYuNDU3MDMxLC0yLjY2NjAxNTYgbCAwLjA4OTg0LC0wLjAwMzkxIHogbSAwLDEuNTAxOTUzMSAtMC4wOTE4LDAuMDAzOTEgYyAtMi4wNTUwNzMsMC4wODEzODQgLTQuMDQ2NzI4LDAuOTI1MjMzMSAtNS40ODgyODEsMi4yNjU2MjQ5IC0xLjQ0MTU1NDcsMS4zNDAzOTMgLTIuMzMyNjM1NCwzLjE4MjM3OSAtMi4xNzM4Mjg2LDUuMjQ0MTQxIDAuMzA2Mzk5NCwzLjk3NzUyIDMuNzU3OTY0Niw3LjE3NDg3NyA3LjU3MDMxMjYsNy42MzQ3NjYgbCAtMC4wMDIsMC4wMTc1OCAwLjA4Nzg5LDAuMDA3OCBoIDAuMDAyIGwgMC4wMDk4LDAuMDAyIDAuMDgwMDgsMC4wMDk4IDAuMDIxNDgsLTAuMTgxNjQxIGggLTAuMDAzOSB2IC0wLjAwMiBsIC0wLjA4OTg0LC0wLjAwOTggaCAtMC4wMDIgQyAxMi4yNDk0NzYsMjIuOTczNTY1IDguODE2ODE1NSwxOS44MDI1NzYgOC41MTU2MTcsMTUuODkyNTcxIDguMzYxNDU0LDEzLjg5MTA5OCA5LjIyNDYyMjUsMTIuMTA2MjMgMTAuNjMyODA1LDEwLjc5Njg2OCAxMi4wNDA5ODUsOS40ODc1MDU0IDEzLjk5NDA4NCw4LjY1NzcwOTcgMTYuMDAzODk4LDguNTc4MTE3NyBsIDAuMDg5ODQsLTAuMDAzOTEgeiBtIDAsMS41MDE5NTMyIC0wLjA5MTgsMC4wMDM5MSBjIC0xLjY0ODU4MiwwLjA2NTI4NyAtMy4yNDU3NjYsMC43NDI5NDI3IC00LjQwMjM0NCwxLjgxODM1OTcgLTEuMTU2NTc4LDEuMDc1NDE3IC0xLjg3MTY1MDMsMi41NTM1NDUgLTEuNzQ0MTQwNiw0LjIwODk4NCAwLjI0NTM5NDYsMy4xODU1OSAzLjAwMzEwMzYsNS43NDQzODEgNi4wNTQ2ODc2LDYuMTIxMDk0IGwgLTAuMDAyLDAuMDE3NTggMC4wODc4OSwwLjAwNzggaCAwLjAwMiBsIDAuMDg3ODksMC4wMDk4IGggMC4wMDIgbCAwLjAyMTQ4LC0wLjE4MTY0IGggLTAuMDAzOSBsIC0wLjA4Nzg5LC0wLjAwOTggaCAtMC4wMDIgLTAuMDAyIGMgLTIuOTk3NjIzLC0wLjMyODE1NiAtNS43MzYzNjgsLTIuODYwNDMzIC01Ljk3NjU2MiwtNS45Nzg1MTYgLTAuMTIyODY1OSwtMS41OTUxNSAwLjU2NDI5NCwtMy4wMTgxMTMgMS42ODc1LC00LjA2MjUgMS4xMjMyMDYsLTEuMDQ0Mzg2IDIuNjgxODMzLC0xLjcwNjAzNyA0LjI4NTE1NiwtMS43Njk1MzEgbCAwLjA4OTg0LC0wLjAwMzkgeiBtIDQuMTIxMDkzLDEuMzIwMzEyNyBoIDEuNDY2Nzk3IGwgMS4zMjgxMjUsMy45NjI4OSB2IC0zLjk0NzI2NSBoIDEuMTAzNTE2IHYgMS43MzYzMjggYyAwLjA0NjM5LC0wLjA4NzQyIDAuMDk5ODksLTAuMTc2MjEyIDAuMTYwMTU2LC0wLjI2MzY3MiAwLjA2MDMxLC0wLjA4NzQ2IDAuMTIwMjk0LC0wLjE2OTY4NyAwLjE3NzczNCwtMC4yNDgwNDcgbCAwLjg1MzUxNiwtMS4yMjQ2MDkgaCAzLjU3ODEyNSB2IDAuODY3MTg3IGggLTEuMzE2NDA2IHYgMC42MjY5NTMgaCAxLjIxODc1IHYgMC44NjcxODggaCAtMS4yMTg3NSB2IDAuNzUzOTA2IGggMS4zMTY0MDYgdiAwLjg4MDg2IGggLTMuNjM4NjcyIGwgLTAuODA0Njg3LC0xLjUyNzM0NCAtMC4zMjYxNzIsMC4xOTE0MDYgdiAxLjMzNTkzOCBoIC0yLjI4OTA2MyBsIC0wLjIwMTE3MiwtMC43NDgwNDcgaCAtMS4zMjAzMTIgbCAtMC4yMDcwMzEsMC43NDgwNDcgaCAtMS4yMDcwMzIgeiBtIC0yLjQxNDA2MiwwLjAxNTYzIGggMS4xMDM1MTUgdiAzLjYwNTQ2OSBjIDEwZS03LDAuNDAwOTA1IC0wLjA2MTE3LDAuNzIyMzU1IC0wLjE4MzU5MywwLjk2Mjg5IC0wLjEyMjQyMiwwLjI0MDUzNSAtMC4yOTA4MTMsMC40MTQwMTEgLTAuNTA3ODEzLDAuNTIxNDg0IC0wLjIxNjk5OSwwLjEwNzUxMyAtMC40NjgyNTUsMC4xNjAxNTcgLTAuNzUzOTA2LDAuMTYwMTU3IC0wLjEyNDI1NiwwIC0wLjIzNDQ2NSwtMC4wMDU3IC0wLjMyODEyNSwtMC4wMTc1OCAtMC4wOTM2NiwtMC4wMTE3NyAtMC4xNzM1NzgsLTAuMDI0NDYgLTAuMjQyMTg4LC0wLjAzOTA2IFYgMTUuNTgzOTkgYyAwLjA1OTM2LDAuMDEwODYgMC4xMjI2NzQsMC4wMjM0MSAwLjE4OTQ1NCwwLjAzNzExIDAuMDY2NzcsMC4wMTM3IDAuMTM4OTM2LDAuMDIxNDggMC4yMTY3OTYsMC4wMjE0OCAwLjEzMTY3NiwwIDAuMjMzMzYxLC0wLjAyNzIzIDAuMzA2NjQxLC0wLjA4MDA4IDAuMDczMjMsLTAuMDUyODYgMC4xMjQ2MTcsLTAuMTMyNjExIDAuMTU0Mjk3LC0wLjIzODI4MSAwLjAyOTY4LC0wLjEwNTY3IDAuMDQ0OTIsLTAuMjM3OTE5IDAuMDQ0OTIsLTAuMzk2NDg1IHogbSA4LjY2Nzk2OSwwLjA1NDY5IC0xLjI0NDE0MSwxLjczNjMyOCAxLjI0NDE0MSwyLjE3NTc4MiB6IG0gLTEwLjM3NSwwLjExMzI4MiAtMC4wOTE4LDAuMDAyIGMgLTEuMjQyMDk1LDAuMDQ5MTkgLTIuNDQ0OCwwLjU2MDY1MiAtMy4zMTY0MDYsMS4zNzEwOTMgLTAuODcxNjA4LDAuODEwNDQyIC0xLjQxMjYyLDEuOTI0NzEyIC0xLjMxNjQwNywzLjE3MzgyOSAwLjE4NDM5LDIuMzkzNjU0IDIuMjUwMTk1LDQuMzEyMDIyIDQuNTQxMDE2LDQuNjA1NDY4IGwgLTAuMDAyLDAuMDE3NTggMC4wODc4OSwwLjAwNzggaCAwLjAwMiBsIDAuMDg3ODksMC4wMDk4IGggMC4wMDIgbCAwLjAxOTUzLC0wLjE3OTY4NyBoIC0wLjAwMiB2IC0wLjAwMiBsIC0wLjA3ODEzLC0wLjAwNzggLTAuMDA5OCwtMC4wMDIgaCAtMC4wMDIgLTAuMDAyIGMgLTIuMjM2OTYxLC0wLjI0NTEyMSAtNC4yODM3LC0yLjEzNjczMSAtNC40NjI4OSwtNC40NjI4OSAtMC4wOTE1NywtMS4xODg4MjYgMC40MjE1MzEsLTIuMjQ3OTMzIDEuMjU5NzY2LC0zLjAyNzM0NCAwLjgzODIzNCwtMC43Nzk0MTEgMi4wMDIzODIsLTEuMjcyOTE2IDMuMTk5MjE4LC0xLjMyMDMxMyBsIDAuMDg5ODQsLTAuMDAzOSB6IG0gNC44NjEzMjgsMC40NzQ2MDkgYyAtMC4wMTY3MSwwLjA5MTExIC0wLjAzOTcyLDAuMjAzOTI4IC0wLjA3MDMxLDAuMzM3ODkxIC0wLjAzMDYsMC4xMzM5MjIgLTAuMDYxMjgsMC4yNjUyNjkgLTAuMDkzNzUsMC4zOTY0ODQgLTAuMDMyNDIsMC4xMzExNzUgLTAuMDYxODUsMC4yNDA2NjUgLTAuMDg1OTQsMC4zMjgxMjUgbCAtMC4xNzU3ODIsMC42NTYyNSBoIDAuODY1MjM1IGwgLTAuMTczODI4LC0wLjY1NjI1IGMgLTAuMDE4NTEsLTAuMDcxMDYgLTAuMDQ2ODEsLTAuMTcyNTI5IC0wLjA4MjAzLC0wLjMwNDY4OCAtMC4wMzUyNiwtMC4xMzIxMTggLTAuMDY5MjEsLTAuMjY4OTM1IC0wLjEwMzUxNSwtMC40MTAxNTYgLTAuMDM0MywtMC4xNDEyMjEgLTAuMDYxNTMsLTAuMjU2NTQ2IC0wLjA4MDA4LC0wLjM0NzY1NiB6IG0gLTQuODYxMzI4LDEuMDI3MzQ0IC0wLjA5MTgsMC4wMDM5IGMgLTAuODM1NjA4LDAuMDMzMDkgLTEuNjQzODM2LDAuMzc0NDU2IC0yLjIzMDQ2OSwwLjkxOTkyMiAtMC41ODY2MzMsMC41NDU0NjYgLTAuOTUxNjM1LDEuMjk5Nzg2IC0wLjg4NjcxOSwyLjE0MjU3OCAwLjEyMzIwNSwxLjU5OTM3OCAxLjQ5ODEyOCwyLjg1OTQ2NyAzLjAyNTM5MSwzLjA3MjI2NSBsIC0wLjAwMzksMC4wMzMyIDAuMDg5ODQsMC4wMDk4IDAuMDgyMDMsMC4wMDc4IDAuMDA3OCwwLjAwMiBoIDAuMDAyIGwgMC4wMTk1MywtMC4xODE2NDEgaCAtMC4wMDIgbCAtMC4wODc4OSwtMC4wMDk4IGggLTAuMDAyIC0wLjAwMiBjIC0xLjQ3NjMzNywtMC4xNjIwNDEgLTIuODI5MDc2LC0xLjQxMjk5NiAtMi45NDcyNjUsLTIuOTQ3MjY2IC0wLjA2MDI3LC0wLjc4MjUwMyAwLjI3NjgxNywtMS40Nzk3MDUgMC44MzAwNzgsLTEuOTk0MTQxIDAuNTUzMjYxLC0wLjUxNDQzNSAxLjMyMjkzMywtMC44NDE3NDggMi4xMTMyODEsLTAuODczMDQ2IGwgMC4wODk4NCwtMC4wMDM5IHogbSAwLDEuNTAxOTUzIC0wLjA5MTgsMC4wMDM5IGMgLTAuNDI5MTE4LDAuMDE2OTkgLTAuODQyODczLDAuMTkyMTY2IC0xLjE0NDUzMSwwLjQ3MjY1NiAtMC4zMDE2NiwwLjI4MDQ5MSAtMC40OTA2NDgsMC42NzA5NTUgLTAuNDU3MDMyLDEuMTA3NDIyIDAuMDYyMTksMC44MDczNiAwLjc0MzgwMSwxLjQzMDI1NyAxLjUwOTc2NiwxLjU1ODU5NCBsIC0wLjAwMzksMC4wMzMyIDAuMDg5ODQsMC4wMDc4IGggMC4wMDIgbCAwLjA4Nzg5LDAuMDA5OCAwLjAyMTQ4LC0wLjE3OTY4NyBoIC0wLjAwMiB2IC0wLjAwMiBsIC0wLjA3ODEzLC0wLjAwNzggLTAuMDA5OCwtMC4wMDIgaCAtMC4wMDIgLTAuMDAyIGMgLTAuNzE1Njc1LC0wLjA3OTAxIC0xLjM3NDQ1NSwtMC42ODkyOSAtMS40MzE2NCwtMS40MzE2NDEgLTAuMDI4OTcsLTAuMzc2MTc5IDAuMTMyMTA0LC0wLjcxMTQ3NyAwLjQwMDM5MSwtMC45NjA5MzcgMC4yNjgyODYsLTAuMjQ5NDYxIDAuNjQzNDg1LC0wLjQwODYyNyAxLjAyNzM0MywtMC40MjM4MjggbCAwLjA4OTg0LC0wLjAwMzkgeiBtIDcuMDQxMDE1LDAuODQ5NjA5IGggMS4yNjE3MTkgbCAxLjE0MjU3OCwzLjQxNDA2MyB2IC0zLjQwMDM5MSBoIDEuMjM2MzI4IGwgMS4yNzUzOTEsMi4zOTI1NzggaCAwLjAxMzY3IGMgLTAuMDA0NywtMC4wNzUzNyAtMC4wMDg5LC0wLjE2NDAzMiAtMC4wMTM2NywtMC4yNjM2NzIgLTAuMDA0OCwtMC4wOTk2NCAtMC4wMDk3LC0wLjIwMDcxMyAtMC4wMTM2NywtMC4zMDI3MzQgLTAuMDA0LC0wLjEwMjAyIC0wLjAwNTksLTAuMTkxMDUxIC0wLjAwNTksLTAuMjY5NTMxIHYgLTEuNTU2NjQxIGggMC44NDM3NSB2IDMuNDQxNDA2IGggLTEuMjQyMTg4IGwgLTEuMjc5Mjk3LC0yLjQyMzgyOCBoIC0wLjAyMTQ4IGMgMC4wMDgsMC4wNzM3NyAwLjAxNTA4LDAuMTYyMDg4IDAuMDIxNDgsMC4yNjU2MjUgMC4wMDY0LDAuMTAzNTc5IDAuMDEyODgsMC4yMDg4OTEgMC4wMTc1OCwwLjMxNjQwNiAwLjAwNDgsMC4xMDc0NzQgMC4wMDc4LDAuMjA0NzA2IDAuMDA3OCwwLjI5MTAxNiB2IDEuNTUwNzgxIEggMjQuNTEzNjcyIEwgMjQuMzM5ODQ0LDE4LjA2MjUgaCAtMS4xMzY3MTkgbCAtMC4xNzc3MzQsMC42NDQ1MzEgaCAtMS4wMzkwNjMgeiBtIC00LjE1NDI5NywwLjAxMzY3IGggMS4wNjI1IGwgMC40NzY1NjMsMS43NDQxNDEgYyAwLjAxNzU0LDAuMDY1ODkgMC4wMzkzNywwLjE1MTEwNyAwLjA2MjUsMC4yNTM5MDYgMC4wMjMxNywwLjEwMjc1OCAwLjA0NDQ4LDAuMjA0NjYxIDAuMDY0NDUsMC4zMDY2NCAwLjAxOTk3LDAuMTAyMDIgMC4wMzIzMSwwLjE4NTYyIDAuMDM3MTEsMC4yNSAwLjAwNjQsLTAuMDY0MzggMC4wMTc2MSwtMC4xNDc2MjUgMC4wMzUxNiwtMC4yNDgwNDYgMC4wMTc1OSwtMC4xMDA0MjEgMC4wMzcwNCwtMC4yMDE1MzUgMC4wNTg1OSwtMC4zMDI3MzUgMC4wMjE2LC0wLjEwMTI0MSAwLjA0MTM4LC0wLjE4NDA2IDAuMDYwNTUsLTAuMjUgbCAwLjQ4NjMyOCwtMS43NTM5MDYgaCAxLjA2MDU0NyBsIC0xLjE0ODQzNywzLjQ0MTQwNiBoIC0xLjExMzI4MiB6IG0gNC43OTEwMTYsMC41NTI3MzQgYyAtMC4wMTQyOSwwLjA3ODQ0IC0wLjAzNDIxLDAuMTc1NjY5IC0wLjA2MDU1LDAuMjkxMDE2IC0wLjAyNjM0LDAuMTE1MzQ3IC0wLjA1NDA2LDAuMjMwNzgyIC0wLjA4MjAzLDAuMzQzNzUgLTAuMDI3OTMsMC4xMTI5NjkgLTAuMDUxNDcsMC4yMDU5MiAtMC4wNzIyNywwLjI4MTI1IGwgLTAuMTUyMzQ0LDAuNTY0NDUzIGggMC43NDYwOTQgbCAtMC4xNTAzOSwtMC41NjQ0NTMgYyAtMC4wMTU5MiwtMC4wNjEyMiAtMC4wMzk4OCwtMC4xNDc5NzEgLTAuMDcwMzEsLTAuMjYxNzE5IC0wLjAzMDI3LC0wLjExMzc4OSAtMC4wNjAyOSwtMC4yMzE5MzUgLTAuMDg5ODQsLTAuMzUzNTE1IC0wLjAyOTU2LC0wLjEyMTYyIC0wLjA1MjM1LC0wLjIyMjM0MiAtMC4wNjgzNiwtMC4zMDA3ODIgeiBtIC0zLjY0NjQ4NCwyLjk5MjE4OCBIIDIwLjU2MjUgYyAwLjE3NDc3NSwwIDAuMzIwNjU4LDAuMDI5NjQgMC40Mzk0NTMsMC4wODk4NCAwLjExODc1NCwwLjA2MDIgMC4yMTAyNTUsMC4xNDg1NTYgMC4yNzE0ODQsMC4yNjU2MjUgMC4wNjEyNywwLjExNzA2OSAwLjA5MTgsMC4yNjE4NjQgMC4wOTE4LDAuNDMzNTkzIDAsMC4xNzk4MDcgLTAuMDMzMDEsMC4zMzEzIC0wLjA5NzY2LDAuNDUzMTI1IC0wLjA2NDYxLDAuMTIxODI2IC0wLjE2MDQ3NywwLjIxMzc2MSAtMC4yODcxMDksMC4yNzUzOTEgLTAuMTI2NTksMC4wNjE2NyAtMC4yODM5NjUsMC4wOTE4IC0wLjQ3MDcwMywwLjA5MTggSCAyMC4xMTkxNCBaIG0gMS40MzU1NDYsMCBoIDAuODk2NDg1IHYgMC4xMDE1NjIgaCAtMC43ODEyNSB2IDAuNjExMzI4IEggMjIuNDA2MjUgViAxOS42MjUgaCAtMC43MzYzMjggdiAwLjY5MzM1OSBoIDAuNzgxMjUgdiAwLjEwMTU2MyBoIC0wLjg5NjQ4NSB6IG0gMS4wODM5ODUsMCBoIDAuMzk4NDM3IGMgMC4xMzAwMDgsMCAwLjIzNzIyOSwwLjAxMzA5IDAuMzI0MjE5LDAuMDQxMDIgMC4wODcwMywwLjAyNzg4IDAuMTUzMTY2LDAuMDc0NzggMC4xOTcyNjYsMC4xMzg2NzIgMC4wNDQwMiwwLjA2Mzg0IDAuMDY2NDEsMC4xNDkxOTEgMC4wNjY0MSwwLjI1NzgxMyAwLDAuMDgxNDggLTAuMDE0NjIsMC4xNTI1ODcgLTAuMDQ0OTIsMC4yMTA5MzcgLTAuMDMwMywwLjA1ODM1IC0wLjA3MjAyLDAuMTA1MTc4IC0wLjEyNSwwLjE0MjU3OCAtMC4wNTMxLDAuMDM3NDggLTAuMTE0MTU0LDAuMDY2MTMgLTAuMTgzNTk0LDAuMDg1OTQgbCAwLjQ0OTIxOSwwLjczMjQyMiBIIDIzLjU4Mzk5IGwgLTAuNDIzODI4LC0wLjY5OTIxOSBoIC0wLjQwNjI1IHYgMC42OTkyMTkgaCAtMC4xMTUyMzQgeiBtIDEuMDA3ODEyLDAgaCAwLjExOTE0MSBsIDAuMzA4NTk0LDEuMTM0NzY1IGMgMC4wMDgxLDAuMDMwMDkgMC4wMTQzOCwwLjA1ODc5IDAuMDIxNDgsMC4wODU5NCAwLjAwNywwLjAyNzE1IDAuMDEzNTMsMC4wNTI4MSAwLjAxOTUzLDAuMDc4MTMgMC4wMDYsMC4wMjUzNCAwLjAxMTk4LDAuMDUwMzUgMC4wMTc1OCwwLjA3NDIyIDAuMDA1NiwwLjAyMzgzIDAuMDExMTIsMC4wNDgwNCAwLjAxNTYzLDAuMDcyMjcgMC4wMDUyLC0wLjAyNDI0IDAuMDEwNDMsLTAuMDQ5MjUgMC4wMTU2MywtMC4wNzQyMiAwLjAwNTIsLTAuMDI0OTMgMC4wMTEyOCwtMC4wNDg1NSAwLjAxNzU4LC0wLjA3NDIyIDAuMDA2NCwtMC4wMjU3MSAwLjAxMjMzLC0wLjA1MjU2IDAuMDE5NTMsLTAuMDgwMDggMC4wMDcsLTAuMDI3NTIgMC4wMTYzOSwtMC4wNTc3OSAwLjAyNTM5LC0wLjA4Nzg5IGwgMC4zMjAzMTMsLTEuMTI4OTA2IGggMC4xMTUyMzQgbCAwLjMzMzk4NSwxLjEzNjcxOSBjIDAuMDA5LDAuMDMxNTMgMC4wMTc5OSwwLjA1OTk3IDAuMDI1MzksMC4wODc4OSAwLjAwNzUsMC4wMjc4OSAwLjAxMzUzLDAuMDU0NzQgMC4wMTk1MywwLjA4MDA4IDAuMDA1OSwwLjAyNTMgMC4wMTIyOCwwLjA1MDM1IDAuMDE3NTgsMC4wNzQyMiAwLjAwNTIsMC4wMjM4MyAwLjAxMDQzLDAuMDQ4MDggMC4wMTU2MywwLjA3MjI3IDAuMDA2LC0wLjAzMzAxIDAuMDExMTgsLTAuMDY1NzIgMC4wMTc1OCwtMC4wOTc2NiAwLjAwNjMsLTAuMDMxOSAwLjAxNDQ0LC0wLjA2NDM5IDAuMDIzNDQsLTAuMDk5NjEgMC4wMDksLTAuMDM1MjYgMC4wMiwtMC4wNzUzNyAwLjAzMTI1LC0wLjExNzE4NyBsIDAuMzE0NDUzLC0xLjEzNjcxOSBoIDAuMTE5MTQxIGwgLTAuNDQ3MjY2LDEuNjA5Mzc1IGggLTAuMTExMzI4IGwgLTAuMzUxNTYyLC0xLjIxNDg0NCBjIC0wLjAwODMsLTAuMDI1NjcgLTAuMDE1MDgsLTAuMDUxMDkgLTAuMDIxNDgsLTAuMDc0MjIgLTAuMDA2MywtMC4wMjMxMyAtMC4wMTE4OCwtMC4wNDM1NCAtMC4wMTc1OCwtMC4wNjQ0NSAtMC4wMDU2LC0wLjAyMDkyIC0wLjAxMDczLC0wLjA0MTg1IC0wLjAxNTYzLC0wLjA2MDU1IC0wLjAwNDgsLTAuMDE4NjkgLTAuMDA4NywtMC4wMzU0IC0wLjAxMTcyLC0wLjA1MDc4IC0wLjAwMjksMC4wMTUzOCAtMC4wMDY0LDAuMDMxNjEgLTAuMDA5OCwwLjA0ODgzIC0wLjAwMzQsMC4wMTcyNiAtMC4wMDcyLDAuMDM0MzggLTAuMDExNzIsMC4wNTI3MyAtMC4wMDQ1LDAuMDE4MzMgLTAuMDA4NCwwLjAzODc0IC0wLjAxMzY3LDAuMDU4NTkgLTAuMDA1MiwwLjAxOTgxIC0wLjAxMTU4LDAuMDM5MjcgLTAuMDE3NTgsMC4wNjA1NSBsIC0wLjM0OTYwOSwxLjI0NDE0MSBoIC0wLjExMzI4MSB6IG0gMi4wMzUxNTcsMCBoIDAuODk2NDg0IHYgMC4xMDE1NjIgaCAtMC43NzkyOTcgdiAwLjYxMTMyOCBoIDAuNzM2MzI4IFYgMTkuNjI1IGggLTAuNzM2MzI4IHYgMC42OTMzNTkgaCAwLjc3OTI5NyB2IDAuMTAxNTYzIGggLTAuODk2NDg0IHogbSAxLjA4Mzk4NCwwIGggMC4zOTg0MzcgYyAwLjEyOTkyNiwwIDAuMjM5MTQyLDAuMDEzMDkgMC4zMjYxNzIsMC4wNDEwMiAwLjA4Njk5LDAuMDI3ODggMC4xNTEyMTMsMC4wNzQ3OCAwLjE5NTMxMywwLjEzODY3MiAwLjA0NDAyLDAuMDYzODQgMC4wNjY0MSwwLjE0OTE5MSAwLjA2NjQxLDAuMjU3ODEzIDAsMC4wODE0OCAtMC4wMTQ2MiwwLjE1MjU4NyAtMC4wNDQ5MiwwLjIxMDkzNyAtMC4wMzAyNiwwLjA1ODM1IC0wLjA3MjAyLDAuMTA1MTc4IC0wLjEyNSwwLjE0MjU3OCAtMC4wNTMwNiwwLjAzNzQ4IC0wLjExNDE1MywwLjA2NjEzIC0wLjE4MzU5NCwwLjA4NTk0IGwgMC40NDkyMTksMC43MzI0MjIgaCAtMC4xMzY3MTkgbCAtMC40MjE4NzUsLTAuNjk5MjE5IGggLTAuNDA4MjAzIHYgMC42OTkyMTkgaCAtMC4xMTUyMzQgeiBtIDEuMTgxNjQxLDAgaCAwLjg5NjQ4NCB2IDAuMTAxNTYyIEggMjguMDYyNSB2IDAuNjg3NSBoIDAuNzM4MjgxIHYgMC4wOTk2MSBIIDI4LjA2MjUgdiAwLjcyMDcwMyBoIC0wLjExNTIzNCB6IG0gLTcuNzEyODkxLDAuMDk5NjEgdiAxLjQxMDE1NiBoIDAuMjcxNDg0IGMgMC4yNDcyNjEsMCAwLjQzMjE4MywtMC4wNjA0IDAuNTU0Njg4LC0wLjE3OTY4NyAwLjEyMjU0OSwtMC4xMTkyNDIgMC4xODM1OTQsLTAuMjk4NTQyIDAuMTgzNTk0LC0wLjUzNzEwOSAwLC0wLjE1MzM1OCAtMC4wMjUzNiwtMC4yODAwNTQgLTAuMDc2MTcsLTAuMzgyODEzIC0wLjA1MDc3LC0wLjEwMjc1OCAtMC4xMjk3OTMsLTAuMTgwNjcyIC0wLjIzNDM3NSwtMC4yMzI0MjIgLTAuMTA0NTgyLC0wLjA1MTcxIC0wLjIzNTg4MiwtMC4wNzgxMyAtMC4zOTY0ODUsLTAuMDc4MTMgeiBtIDIuNTE5NTMxLDAgdiAwLjcwODk4NSBoIDAuMzI2MTcyIGMgMC4xMzM3NiwwIDAuMjM3NDcsLTAuMDMwNjQgMC4zMTI1LC0wLjA5Mzc1IDAuMDc1MTYsLTAuMDYzMTUgMC4xMTMyODEsLTAuMTUzMzUgMC4xMTMyODEsLTAuMjcxNDg1IDAsLTAuMTI5OTQ0IC0wLjAzOTk4LC0wLjIyMDMyMSAtMC4xMTkxNCwtMC4yNjk1MzEgLTAuMDc5MjQsLTAuMDQ5MTcgLTAuMTk5MjI0LC0wLjA3NDIyIC0wLjM2MTMyOCwtMC4wNzQyMiB6IG0gNC4xMjY5NTMsMCB2IDAuNzA4OTg1IGggMC4zMjYxNzIgYyAwLjEzMzc1OSwwIDAuMjM5NDIzLC0wLjAzMDY0IDAuMzE0NDUzLC0wLjA5Mzc1IDAuMDc1MTIsLTAuMDYzMTUgMC4xMTEzMjgsLTAuMTUzMzUgMC4xMTEzMjgsLTAuMjcxNDg1IDAsLTAuMTI5OTQ0IC0wLjAzODA4LC0wLjIyMDMyMSAtMC4xMTcxODcsLTAuMjY5NTMxIC0wLjA3OTI0LC0wLjA0OTE3IC0wLjIwMTE3OCwtMC4wNzQyMiAtMC4zNjMyODEsLTAuMDc0MjIgeiIgICAgc3R5bGU9ImJhc2VsaW5lLXNoaWZ0OmJhc2VsaW5lO2NsaXAtcnVsZTpub256ZXJvO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmVjdG9yLWVmZmVjdDpub25lO2ZpbGw6IzIyMjIyMjtmaWxsLXJ1bGU6bm9uemVybztzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlO3N0b3AtY29sb3I6IzAwMDAwMDtzdG9wLW9wYWNpdHk6MSIgICAgaWQ9InBhdGgxNyIgLz48L3N2Zz4=');}.icon-sign-out{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMTgsMjE2YTYsNiwwLDAsMS02LDZINDhhNiw2LDAsMCwxLTYtNlY0MGE2LDYsMCwwLDEsNi02aDY0YTYsNiwwLDAsMSwwLDEySDU0VjIxMGg1OEE2LDYsMCwwLDEsMTE4LDIxNlptMTEwLjI0LTkyLjI0LTQwLTQwYTYsNiwwLDAsMC04LjQ4LDguNDhMMjA5LjUxLDEyMkgxMTJhNiw2LDAsMCwwLDAsMTJoOTcuNTFsLTI5Ljc1LDI5Ljc2YTYsNiwwLDEsMCw4LjQ4LDguNDhsNDAtNDBBNiw2LDAsMCwwLDIyOC4yNCwxMjMuNzZaIi8+PC9zdmc+');}.icon-arrow-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsNTZ2NDhhNiw2LDAsMCwxLTYsNkgxODRhNiw2LDAsMCwxLDAtMTJoMzIuNTVsLTMwLjM4LTI3LjhjLS4wNi0uMDYtLjEyLS4xMy0uMTktLjE5YTgyLDgyLDAsMSwwLTEuNywxMTcuNjUsNiw2LDAsMCwxLDguMjQsOC43M0E5My40Niw5My40NiwwLDAsMSwxMjgsMjIyaC0xLjI4QTk0LDk0LDAsMSwxLDE5NC4zNyw2MS40TDIyNiw5MC4zNVY1NmE2LDYsMCwxLDEsMTIsMFoiLz48L3N2Zz4=');}.icon-x-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRabTIsMTc0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWk0xNjQuMjQsMTAwLjI0LDEzNi40OCwxMjhsMjcuNzYsMjcuNzZhNiw2LDAsMSwxLTguNDgsOC40OEwxMjgsMTM2LjQ4bC0yNy43NiwyNy43NmE2LDYsMCwwLDEtOC40OC04LjQ4TDExOS41MiwxMjgsOTEuNzYsMTAwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MmwyNy43Ni0yNy43NmE2LDYsMCwwLDEsOC40OCw4LjQ4WiIvPjwvc3ZnPg==');}.icon-eye-closed{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuMjEsMTY1YTYsNiwwLDAsMS0xMC40Miw2bC0yMC0zNS4wOGExMjIsMTIyLDAsMCwxLTM5LDE4LjA5bDYuMTcsMzdhNiw2LDAsMCwxLTQuOTMsNi45MSw2Ljg1LDYuODUsMCwwLDEtMSwuMDgsNiw2LDAsMCwxLTUuOTEtNUwxNDgsMTU2LjQ0YTEyOC44NiwxMjguODYsMCwwLDEtNDAsMEwxMDEuOTIsMTkzQTYsNiwwLDAsMSw5NiwxOThhNi44NSw2Ljg1LDAsMCwxLTEtLjA4QTYsNiwwLDAsMSw5MC4wOCwxOTFsNi4xNy0zN2ExMjIsMTIyLDAsMCwxLTM5LTE4LjA5TDM3LjIxLDE3MWE2LDYsMCwxLDEtMTAuNDItNmwyMC44NS0zNi40OGExNTIsMTUyLDAsMCwxLTIwLjMxLTIwLjc3LDYsNiwwLDAsMSw5LjM0LTcuNTRDNTMuNTQsMTIxLjExLDgzLjA3LDE0NiwxMjgsMTQ2czc0LjQ2LTI0Ljg5LDkxLjMzLTQ1Ljc3YTYsNiwwLDAsMSw5LjM0LDcuNTQsMTUyLDE1MiwwLDAsMS0yMC4zMSwyMC43N1oiLz48L3N2Zz4=');}.icon-hourglass{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTgsNzUuNjRWNDBhMTQsMTQsMCwwLDAtMTQtMTRINzJBMTQsMTQsMCwwLDAsNTgsNDBWNzZhMTQuMDYsMTQuMDYsMCwwLDAsNS42LDExLjJMMTE4LDEyOCw2My42LDE2OC44QTE0LjA2LDE0LjA2LDAsMCwwLDU4LDE4MHYzNmExNCwxNCwwLDAsMCwxNCwxNEgxODRhMTQsMTQsMCwwLDAsMTQtMTRWMTgwLjM2YTE0LjA4LDE0LjA4LDAsMCwwLTUuNTYtMTEuMTdMMTM4LDEyOGw1NC40OS00MS4xOUExNC4wOCwxNC4wOCwwLDAsMCwxOTgsNzUuNjRaTTE4NiwxODAuMzZWMjE2YTIsMiwwLDAsMS0yLDJINzJhMiwyLDAsMCwxLTItMlYxODBhMiwyLDAsMCwxLC44LTEuNkwxMjgsMTM1LjUxbDU3LjIyLDQzLjI1QTIsMiwwLDAsMSwxODYsMTgwLjM2Wm0wLTEwNC43MmEyLDIsMCwwLDEtLjc5LDEuNkwxMjgsMTIwLjQ5LDcwLjgsNzcuNkEyLDIsMCwwLDEsNzAsNzZWNDBhMiwyLDAsMCwxLDItMkgxODRhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-star-half-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzkuMTgsOTcuMjZBMTYuMzgsMTYuMzgsMCwwLDAsMjI0LjkyLDg2bC01OS00Ljc2TDE0My4xNCwyNi4xNWExNi4zNiwxNi4zNiwwLDAsMC0zMC4yNywwTDkwLjExLDgxLjIzLDMxLjA4LDg2YTE2LjQ2LDE2LjQ2LDAsMCwwLTkuMzcsMjguODZsNDUsMzguODNMNTMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMCwyNC41LDE3LjgyTDEyOCwxOTguNDlsNTAuNTMsMzEuMDhBMTYuNCwxNi40LDAsMCwwLDIwMywyMTEuNzVsLTEzLjc2LTU4LjA3LDQ1LTM4LjgzQTE2LjQzLDE2LjQzLDAsMCwwLDIzOS4xOCw5Ny4yNlptLTE1LjM0LDUuNDctNDguNyw0MmE4LDgsMCwwLDAtMi41Niw3LjkxbDE0Ljg4LDYyLjhhLjM3LjM3LDAsMCwxLS4xNy40OGMtLjE4LjE0LS4yMy4xMS0uMzgsMGwtNTQuNzItMzMuNjVBOCw4LDAsMCwwLDEyOCwxODEuMVYzMmMuMjQsMCwuMjcuMDguMzUuMjZMMTUzLDkxLjg2YTgsOCwwLDAsMCw2Ljc1LDQuOTJsNjMuOTEsNS4xNmMuMTYsMCwuMjUsMCwuMzQuMjlTMjI0LDEwMi42MywyMjMuODQsMTAyLjczWiIvPjwvc3ZnPg==');}.icon-star-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzQuMjksMTE0Ljg1bC00NSwzOC44M0wyMDMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMS0yNC41LDE3LjgyTDEyOCwxOTguNDksNzcuNDcsMjI5LjU3QTE2LjQsMTYuNCwwLDAsMSw1MywyMTEuNzVsMTMuNzYtNTguMDctNDUtMzguODNBMTYuNDYsMTYuNDYsMCwwLDEsMzEuMDgsODZsNTktNC43NiwyMi43Ni01NS4wOGExNi4zNiwxNi4zNiwwLDAsMSwzMC4yNywwbDIyLjc1LDU1LjA4LDU5LDQuNzZhMTYuNDYsMTYuNDYsMCwwLDEsOS4zNywyOC44NloiLz48L3N2Zz4=');}.icon-heart-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsMTAyYzAsNzAtMTAzLjc5LDEyNi42Ni0xMDguMjEsMTI5YTgsOCwwLDAsMS03LjU4LDBDMTE5Ljc5LDIyOC42NiwxNiwxNzIsMTYsMTAyQTYyLjA3LDYyLjA3LDAsMCwxLDc4LDQwYzIwLjY1LDAsMzguNzMsOC44OCw1MCwyMy44OUMxMzkuMjcsNDguODgsMTU3LjM1LDQwLDE3OCw0MEE2Mi4wNyw2Mi4wNywwLDAsMSwyNDAsMTAyWiIvPjwvc3ZnPg==');} |
| | |
| | | } |
| | | |
| | | let itemsToRemove = []; |
| | | console.log(data); |
| | | switch (true) { |
| | | case data.config.element === this.ui.forms.edit: |
| | | theChanges[this.currentItemID] = changes; |
| | |
| | | data: { |
| | | posts: changes, |
| | | }, |
| | | delay: true, |
| | | popup: `Saving changes`, |
| | | title: title |
| | | }; |
| | |
| | | console.log(data); |
| | | if (data === this.ui.uploader.querySelector('[data-uploader]')?.dataset.uploader) { |
| | | window.debouncer.schedule('crud-complete', ()=> { |
| | | this.store.clearHttpHeaders(); |
| | | this.store.clearCache(); |
| | | }); |
| | | } |
| | | } |
| | |
| | | |
| | | setupFilters() { |
| | | // Search |
| | | const searchInput = document.querySelector('input[type="search"]'); |
| | | const searchInput = document.querySelector('.all-filters input[type="search"]'); |
| | | if (searchInput) { |
| | | let searchTimeout; |
| | | searchInput.addEventListener('input', () => { |
| | |
| | | endpoint: store.config.endpoint, |
| | | filters: { ...store.filters }, |
| | | etag: response.headers.get('ETag'), |
| | | lastModified: response.headers.get('Last-Modified') |
| | | lastModified: response.headers.get('Last-Modified'), |
| | | has_more: data.has_more || false |
| | | }; |
| | | |
| | | store.cache.set(cacheKey, cacheEntry); |
| | |
| | | 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) { |
| | |
| | | }); |
| | | } |
| | | |
| | | 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 |
| | | ***********************************************************************/ |
| | |
| | | |
| | | // FormData - convert and continue |
| | | if (obj instanceof FormData) { |
| | | if (validate) return { valid: false, error: `FormData at ${path}` }; |
| | | console.debug(`[DataStore] Converted FormData at ${path}`); |
| | | console.debug(`[DataStore] Converting FormData at ${path}`); |
| | | return { valid: true, data: this.formDataToObject(obj) }; |
| | | } |
| | | |
| | |
| | | const cacheKey = this.generateCacheKey(store.filters); |
| | | const cacheEntry = store.cache.get(cacheKey); |
| | | |
| | | // First check if we have cached results for exact filters |
| | | if (cacheEntry && cacheEntry.items) { |
| | | return cacheEntry.items.reduce((acc, id) => { |
| | | const item = store.data.get(id); |
| | |
| | | }, []); |
| | | } |
| | | |
| | | // If we have a search filter and complete base data, filter locally |
| | | if (store.filters.search && store.filters.search.trim()) { |
| | | const searchQuery = store.filters.search.toLowerCase().trim(); |
| | | |
| | | // Get all items and filter them locally |
| | | const allItems = Array.from(store.data.values()); |
| | | |
| | | // Filter by current filters (excluding search and page) |
| | | let filtered = allItems.filter(item => { |
| | | // Apply all filters except search and page |
| | | for (const [key, value] of Object.entries(store.filters)) { |
| | | if (key === 'search' || key === 'page') continue; |
| | | |
| | | if (value !== null && value !== undefined && value !== '') { |
| | | if (item[key] !== value) return false; |
| | | } |
| | | } |
| | | return true; |
| | | }); |
| | | |
| | | // Apply search filter to common searchable fields |
| | | filtered = filtered.filter(item => { |
| | | // Search in common fields: name, title, path, description |
| | | const searchableFields = ['name', 'title', 'path', 'description', 'slug']; |
| | | |
| | | return searchableFields.some(field => { |
| | | const value = item[field]; |
| | | if (!value) return false; |
| | | return value.toLowerCase().includes(searchQuery); |
| | | }); |
| | | }); |
| | | |
| | | return filtered; |
| | | } |
| | | |
| | | // Fallback to all data |
| | | return this.getAll(name); |
| | | } |
| | | |
| | |
| | | |
| | | if (clearAll) { |
| | | store.filters = { ...store.config.filters }; |
| | | } else { |
| | | // Apply updates (null/undefined/'' = delete) |
| | | Object.entries(updates).forEach(([key, value]) => { |
| | | if (value === null || value === undefined || value === '') { |
| | | delete store.filters[key]; |
| | | } else { |
| | | store.filters[key] = value; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | Object.entries(updates).forEach(([key, value]) => { |
| | | if (value === null || value === undefined || value === '') { |
| | | delete store.filters[key]; |
| | | } else { |
| | | store.filters[key] = value; |
| | | } |
| | | }); |
| | | |
| | | const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters); |
| | | |
| | | this.notify(name, 'filters-changed', { |
| | | oldFilters, |
| | | filters: store.filters, |
| | | updates |
| | | }); |
| | | |
| | | if (store.config.endpoint) { |
| | | if (store.config.endpoint && shouldFetch) { |
| | | await this.fetch(name); |
| | | } else if (store.config.endpoint) { |
| | | this.notify(name, 'data-loaded'); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Determine if we need to fetch or can use local data |
| | | * @param {string} name - Store name |
| | | * @param {object} updates - Filter updates being applied |
| | | * @param {object} oldFilters - Previous filter state |
| | | * @returns {Promise<boolean>} - True if fetch is needed, false if local filtering suffices |
| | | */ |
| | | async shouldFetchWithFilters(name, updates, oldFilters) { |
| | | const store = this.stores.get(name); |
| | | |
| | | // If no endpoint or no lastResponse, always fetch |
| | | if (!store.config.endpoint || !store.lastResponse) { |
| | | return true; |
| | | } |
| | | |
| | | // PAGE OPTIMIZATION: Don't fetch if trying to go beyond available pages |
| | | if ('page' in updates) { |
| | | const newPage = updates.page; |
| | | const oldPage = oldFilters.page || 1; |
| | | |
| | | // If trying to go to a higher page but no more data available |
| | | if (newPage > oldPage && !store.lastResponse.has_more) { |
| | | // Reset page to last valid page |
| | | store.filters.page = oldPage; |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // SEARCH OPTIMIZATION: Check if we need to fetch for search |
| | | if ('search' in updates) { |
| | | const searchQuery = updates.search?.trim() || ''; |
| | | const oldSearch = oldFilters.search?.trim() || ''; |
| | | |
| | | // If search is being cleared, we might already have the data |
| | | if (!searchQuery && oldSearch) { |
| | | // Check if we have all base data (without search) |
| | | const baseFilters = { ...store.filters }; |
| | | delete baseFilters.search; |
| | | baseFilters.page = 1; |
| | | |
| | | // If we have complete base data, no need to fetch |
| | | if (this.hasCompleteData(store, baseFilters)) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // If search is new or changed, check if we have all data to filter locally |
| | | if (searchQuery && searchQuery !== oldSearch) { |
| | | // Check: do we have all data for base filters (no search, page 1)? |
| | | const baseFilters = { ...store.filters }; |
| | | delete baseFilters.search; |
| | | baseFilters.page = 1; |
| | | |
| | | // If we have complete base data, we can filter locally |
| | | if (this.hasCompleteData(store, baseFilters)) { |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Default: fetch is needed |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Check if we have complete data for given filters |
| | | * @param {object} store - Store instance |
| | | * @param {object} filters - Filters to check |
| | | * @returns {boolean} - True if we have all data |
| | | */ |
| | | hasCompleteData(store, filters) { |
| | | const cacheKey = this.generateCacheKey(filters); |
| | | const cached = store.cache.get(cacheKey); |
| | | |
| | | if (!cached) return false; |
| | | |
| | | // Check if cache indicates no more data |
| | | return cached.has_more === false || store.lastResponse?.has_more === false; |
| | | } |
| | | |
| | | setFilter(name, key, value) { |
| | | return this.updateFilters(name, { [key]: value }); |
| | | } |
| | |
| | | |
| | | 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; |
| | |
| | | /** |
| | | * QueueManager |
| | | * Uses DataStore for persistent storage |
| | | */ |
| | | class QueueManager { |
| | | constructor(config = {}) { |
| | | this.canUpdateUI = true; |
| | | this.config = { |
| | | apiBase: jvbSettings.api, |
| | | maxRetries: 3, |
| | | pollInterval: 5000, |
| | | activityDelay: 2000, //2 seconds |
| | | autosync: true, |
| | | endpoint: 'queue', |
| | | ...config |
| | | }; |
| | | |
| | | // Queue state |
| | | this.isProcessing = false; |
| | | this.isPolling = false; |
| | | this.subscribers = new Set(); |
| | | |
| | | // Status definitions |
| | | this.statuses = [ |
| | | 'queued', |
| | | 'localProcessing', |
| | | 'uploading', |
| | | 'pending', |
| | | 'processing', |
| | | 'completed', |
| | | 'failed', |
| | | 'failed_permanent' |
| | | ]; |
| | | constructor() { |
| | | this.a11y = window.jvbA11y; |
| | | this.error = window.jvbError; |
| | | |
| | | this.user = window.auth.getUser(); |
| | | |
| | | if (!this.user) { |
| | | console.log('Queue: User not logged in, queue disabled'); |
| | | this.store = null; |
| | | this.canUpdateUI = false; |
| | | return; |
| | | } |
| | | this.canUpdateUI = true; |
| | | this.isProcessing = false; |
| | | this.isPolling = false; |
| | | this.queue = new Map(); |
| | | this.items = new Map(); |
| | | this.subscribers = new Set(); |
| | | |
| | | this.api = jvbSettings.api; |
| | | this.endpoint = 'queue'; |
| | | |
| | | this.init(); |
| | | } |
| | | init() { |
| | | this.headers = { |
| | | 'X-WP-Nonce': window.auth.getNonce(), |
| | | ...config.headers |
| | | }; |
| | | |
| | | this.a11y = window.jvbA11y; |
| | | this.errors = window.jvbError; |
| | | |
| | | // Initialize DataStore for queue persistence |
| | | const store = window.jvbStore.register('queue', { |
| | | storeName: 'queue', |
| | | keyPath: 'id', |
| | | endpoint: this.config.endpoint, |
| | | TTL: Infinity, |
| | | indexes: [ |
| | | {name: 'status', keyPath: 'status'}, |
| | | {name: 'type', keyPath: 'type'}, |
| | | ], |
| | | showLoading: false, |
| | | delayFetch: false, // Queue should fetch immediately |
| | | }); |
| | | this.store = store.queue; |
| | | |
| | | this.classes = [ |
| | | 'offline', |
| | | 'synced', |
| | | 'pending' |
| | | ]; |
| | | |
| | | |
| | | |
| | | // Initialize |
| | | this.initUI(); |
| | | this.initElements(); |
| | | this.initListeners(); |
| | | if (this.ui.panel) { |
| | | this.initStore(); |
| | | if (this.canUpdateUI) { |
| | | this.popup = new window.jvbPopup({ |
| | | popup: this.ui.panel, |
| | | toggle: this.ui.toggle, |
| | | toggle: this.ui.toggle.button, |
| | | name: 'Queue Panel', |
| | | }); |
| | | } |
| | | this.updateUI = () => window.debouncer.schedule('queue-ui-update', this._updateUI.bind(this), 100); |
| | | this.initQueue(); |
| | | } |
| | | |
| | | async initQueue() { |
| | | let polling = this.maybeStartPolling(); |
| | | if (!polling) { |
| | | this.updateStatusPanel('synced'); |
| | | initElements() { |
| | | this.panelStatuses = ['syncing', 'synced', 'pending', 'offline']; |
| | | this.statuses = ['queued', 'localProcessing', 'uploading', 'pending', 'processing', 'completed', 'failed', 'failed_permanent']; |
| | | this.pendingStatuses = ['queued', 'localProcessing', 'uploading']; |
| | | this.workingStatuses = ['pending','processing']; |
| | | this.completedStatuses = ['completed', 'failed', 'failed_permanent']; |
| | | |
| | | this.icons = { |
| | | queued: 'arrows-clockwise', localProcessing: 'arrows-clockwise', uploading: 'syncing', |
| | | pending: 'cloud', processing: 'syncing', completed: 'cloud-check', |
| | | failed: 'cloud-warning', failed_permanent: 'cloud-warning' |
| | | }; |
| | | this.selectors = { |
| | | panel: 'aside#queue', |
| | | toggle: { |
| | | button: 'button.qtoggle', |
| | | indicator: '.qtoggle .indicator', |
| | | count: '.qtoggle .count' |
| | | }, |
| | | refresh: { |
| | | button: '#queue .refresh .refreshNow', |
| | | countdown: '#queue .refresh .countdown' |
| | | }, |
| | | popup: { |
| | | popup: '#queue .popup', |
| | | message: '#queue .popup span' |
| | | }, |
| | | items: { |
| | | container: '#queue .qitems', |
| | | }, |
| | | actions: { |
| | | retry: '#queue .retry-all', |
| | | clear: '#queue .dismiss-all' |
| | | }, |
| | | filters: { |
| | | filter: '#queue [data-filter]', |
| | | all: { |
| | | label: '#queue [for="qfilter-all"]', |
| | | radio: '#queue [data-filter="all"]', |
| | | count: '#queue [data-filter="all"] .count' |
| | | }, |
| | | queued: { |
| | | label: '#queue [for="qfilter-queued"]', |
| | | input: '#queue [data-filter="queued"]', |
| | | count: '#queue [for="qfilter-queued"] .count' |
| | | }, |
| | | localProcessing: { |
| | | label: '#queue [for="qfilter-localProcessing"]', |
| | | input: '#queue [data-filter="localProcessing"]', |
| | | count: '#queue [for="qfilter-localProcessing"] .count', |
| | | }, |
| | | uploading: { |
| | | label: '#queue [for="qfilter-uploading"]', |
| | | input: '#queue [data-filter="uploading"]', |
| | | count: '#queue [for="qfilter-uploading"] .count', |
| | | }, |
| | | pending: { |
| | | label: '#queue [for="qfilter-pending"]', |
| | | input: '#queue [data-filter="pending"]', |
| | | count: '#queue [for="qfilter-pending"] .count', |
| | | }, |
| | | processing: { |
| | | label: '#queue [for="qfilter-processing"]', |
| | | input: '#queue [data-filter="processing"]', |
| | | count: '#queue [for="qfilter-processing"] .count', |
| | | }, |
| | | completed: { |
| | | label: '#queue [for="qfilter-completed"]', |
| | | input: '#queue [data-filter="completed"]', |
| | | count: '#queue [for="qfilter-completed"] .count', |
| | | }, |
| | | failed: { |
| | | label: '#queue [for="qfilter-failed"]', |
| | | input: '#queue [data-filter="failed"]', |
| | | count: '#queue [for="qfilter-failed"] .count', |
| | | }, |
| | | }, |
| | | item: { |
| | | type: '.type', |
| | | status: '.status', |
| | | details: '.info .details', |
| | | icon: '.status .icon', |
| | | startedAt: '.started time', |
| | | completed: { |
| | | wrap: '.completed', |
| | | label: '.completed span', |
| | | time: '.completed time', |
| | | }, |
| | | progress: { |
| | | progress: '.progress', |
| | | fill: '.progress .fill', |
| | | details: '.progress .details', |
| | | icon: '.progress .icon' |
| | | }, |
| | | actions: { |
| | | cancel: 'button.cancel', |
| | | retry: 'button.retry', |
| | | dismiss: 'button.dismiss', |
| | | } |
| | | }, |
| | | }; |
| | | this.ui = window.uiFromSelectors(this.selectors); |
| | | if (!this.ui.panel) this.canUpdateUI = false; |
| | | } |
| | | |
| | | |
| | | initListeners() { |
| | | this.activityListeners = null; |
| | | this.clickHandler = this.handleClick.bind(this); |
| | | this.onlineHandler = this.handleOnline.bind(this); |
| | | this.offlineHandler = this.handleOffline.bind(this); |
| | | this.unloadHandler = this.handleBeforeUnload.bind(this); |
| | | |
| | | document.addEventListener('click', this.clickHandler); |
| | | window.addEventListener('online', this.onlineHandler); |
| | | window.addEventListener('offline', this.offlineHandler); |
| | | window.addEventListener('beforeunload', this.unloadHandler); |
| | | } |
| | | handleOnline() { |
| | | this.updatePanel(); |
| | | if (this.getQueueByStatus(this.pendingStatuses).length > 0) { |
| | | this.processQueue(); |
| | | } |
| | | } |
| | | handleOffline() { |
| | | this.updatePanel('offline'); |
| | | } |
| | | handleBeforeUnload(e) { |
| | | const total = this.getQueueByStatus(this.pendingStatuses).length; |
| | | if (total > 0) { |
| | | // Modern browsers ignore custom messages, but this triggers the native dialog |
| | | e.preventDefault(); |
| | | e.returnValue = ''; // Required for Chrome |
| | | return ''; // Required for some older browsers |
| | | } |
| | | } |
| | | handleClick(e) { |
| | | if (!window.targetCheck(e, this.selectors.panel+', '+this.selectors.toggle.button)) return; |
| | | const refresh = window.targetCheck(e, this.selectors.refresh.button); |
| | | if (refresh) { |
| | | this.store.clearCache(); |
| | | this.store.fetch(); |
| | | return; |
| | | } |
| | | |
| | | const clear = window.targetCheck(e, this.selectors.actions.clear); |
| | | if (clear) { |
| | | this.opActions('completed', 'dismiss').then(()=>{}); |
| | | return; |
| | | } |
| | | |
| | | const retry = window.targetCheck(e, this.selectors.actions.retry); |
| | | if (retry) { |
| | | this.opActions('failed', 'retry').then(()=>{}); |
| | | return; |
| | | } |
| | | |
| | | const action = window.targetCheck(e, '[data-action]'); |
| | | if (action) { |
| | | const opId = action.closest('[data-id]')?.dataset.id; |
| | | if (opId) { |
| | | this.opActions(opId, action.dataset.action); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | const filter = window.targetCheck(e, this.selectors.filters.filter); |
| | | if (filter) { |
| | | this.setFilter(filter.dataset.filter); |
| | | } |
| | | } |
| | | |
| | | setFilter(filter) { |
| | | // Update active button |
| | | Object.values(this.ui.filters).forEach(filterObj => { |
| | | if (filterObj.input?.dataset.filter === filter) { |
| | | filterObj.input.checked = true; |
| | | } |
| | | }); |
| | | |
| | | if (filter === 'all') { |
| | | this.store.clearFilters(); |
| | | } else { |
| | | this.store.setFilter('status', filter); |
| | | } |
| | | } |
| | | |
| | | trackActivity() { |
| | | if (!this.activityListeners) { |
| | | const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart']; |
| | | this.activityListeners = events.map(event => { |
| | | const handler = () => this.resetActivityTimer(); |
| | | document.addEventListener(event, handler, {passive: true}); |
| | | return {event, handler}; |
| | | }); |
| | | } |
| | | this.resetActivityTimer(); |
| | | } |
| | | resetActivityTimer() { |
| | | if (this.activityTimer) { |
| | | clearTimeout(this.activityTimer); |
| | | } |
| | | this.activityTimer = setTimeout(() => { |
| | | this.processQueue(); |
| | | }, 1750); |
| | | } |
| | | stopActivityTracking() { |
| | | if (this.activityTimer) { |
| | | clearTimeout(this.activityTimer); |
| | | this.activityTimer = null; |
| | | } |
| | | if (this.activityListeners) { |
| | | this.activityListeners.forEach(({event, handler}) => { |
| | | document.removeEventListener(event, handler); |
| | | }); |
| | | this.activityListeners = null; |
| | | } |
| | | } |
| | | |
| | | initStore() { |
| | | if (!this.user) return; |
| | | const store = window.jvbStore.register( |
| | | 'queue', |
| | | { |
| | | storeName: 'queue', |
| | | keyPath: 'id', |
| | | endpoint: this.endpoint, |
| | | TTL: Infinity, |
| | | indexes: [ |
| | | {name: 'status', keyPath: 'status'}, |
| | | {name: 'type', keyPath: 'type'}, |
| | | ], |
| | | showLoading: false, |
| | | } |
| | | ) |
| | | this.store = store.queue; |
| | | this.store.subscribe((event, data) => { |
| | | switch (event) { |
| | | case 'data-loaded': |
| | | case 'items-saved': |
| | | case 'items-save': |
| | | this.maybeStartPolling(); |
| | | this.updateUI(); |
| | | break; |
| | | case 'item-saved': |
| | | console.log(data,'Item saved data'); |
| | | if (data.previousItem && data.previousItem.status !== data.item.status) { |
| | | this.handleOperationStatusChange(data.item, data.previousItem.status); |
| | | this.updateOperationStatus(data.item.id, data.item.status); |
| | | } |
| | | this.maybeStartPolling(); |
| | | break; |
| | | default: |
| | | this.updateUI(); |
| | | |
| | | break; |
| | | } |
| | | |
| | | }); |
| | | } |
| | | |
| | | maybeStartPolling() |
| | | { |
| | | const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false); |
| | | if (incomplete.length > 0) { |
| | | this.startPolling(); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Handle operation status changes and notify subscribers |
| | | */ |
| | | handleOperationStatusChange(operation) { |
| | | |
| | | // Notify based on new status |
| | | switch(operation.status) { |
| | | case 'completed': |
| | | console.log(operation); |
| | | this.notify('operation-completed', operation); |
| | | break; |
| | | case 'failed': |
| | | this.notify('operation-failed', operation); |
| | | break; |
| | | case 'failed_permanent': |
| | | this.notify('operation-failed-permanent', operation); |
| | | break; |
| | | } |
| | | } |
| | | /** |
| | | * |
| | | * @param {object} operation |
| | | * @param {string} operation.endpoint The endpoint, excluding the apiBase |
| | | * @param {object} operation.data The data to save |
| | | * @param {boolean} operation.canMerge Whether data can merge |
| | | * @param {string} operation.title The title of the operation for the Queue Panel |
| | | * @param {string} operation.popup The string to show in the popup |
| | | * @param {object} operation.headers Optional additional headers. Defaults to the API nonce |
| | | * |
| | | * @returns {string|null} Returns the operation id, for reference |
| | | */ |
| | | /**************************************************************************** |
| | | OPERATIONS |
| | | ****************************************************************************/ |
| | | addToQueue(operation) { |
| | | const item = { |
| | | id: `u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, |
| | |
| | | method: 'POST', |
| | | headers: {}, |
| | | data: {}, |
| | | sendNow: false, // true = process immediately |
| | | delay: false, |
| | | canMerge: true, |
| | | popup: 'Saving changes...', |
| | | title: 'Operation', |
| | |
| | | }; |
| | | |
| | | item.headers = { |
| | | ...this.headers, |
| | | ...item.headers |
| | | }; |
| | | ... this.headers, |
| | | ... item.headers |
| | | } |
| | | if (!item.endpoint || !item.data) return null; |
| | | |
| | | if (!item.endpoint || !item.data) { |
| | | console.error('Invalid operation queued: missing endpoint or data'); |
| | | return null; |
| | | if (item.popup && this.ui.popup?.message) { // Add popup support |
| | | this.ui.popup.message.textContent = item.popup; |
| | | this.ui.popup.popup.hidden = false; |
| | | setTimeout(() => this.ui.popup.popup.hidden = true, 2000); |
| | | } |
| | | |
| | | if (item.sendNow) { |
| | | if (!item.delay) { |
| | | this.queue.set(item.id, item); |
| | | this.processOperation(item).then(()=> {}); |
| | | this.store.clearCache(); |
| | | window.debouncer.schedule('fastQueue', this.startPolling.bind(this), 200); |
| | | this.showQueue(); |
| | | this.maybeStartPolling(); |
| | | this.toggleQueue(); |
| | | return item.id; |
| | | } |
| | | |
| | | const existingOps = Array.from(this.store.data.values()).filter(op=> |
| | | op.status === 'queued' && |
| | | const existingOps = Array.from(this.getAllQueue()).filter(op=> { |
| | | return op.status === 'queued' && |
| | | op.endpoint === item.endpoint && |
| | | op.canMerge |
| | | ); |
| | | |
| | | }); |
| | | if (existingOps.length > 0) { |
| | | const existing = existingOps[0]; |
| | | existing.data = window.deepMerge(existing.data, item.data); |
| | |
| | | |
| | | this.updateOperationStatus(existing.id, existing.status); |
| | | this.updateUI(); |
| | | |
| | | this.startActivityTracking(); |
| | | this.trackActivity(); |
| | | return existing.id; |
| | | } |
| | | |
| | | this.store.clearCache(); |
| | | |
| | | //Add new operation to DataStore |
| | | this.setQueue(item); |
| | | |
| | | this.updateOperationStatus(item.id, item.status); |
| | | this.updateUI(); |
| | | |
| | | this.startActivityTracking(); |
| | | this.trackActivity(); |
| | | return item.id; |
| | | |
| | | |
| | | } |
| | | |
| | | async opActions(statusOrId, action) { |
| | | //Extract ids based on status, if it exists |
| | | if (this.statuses.includes(statusOrId)) { |
| | | statusOrId = this.getQueueByStatus(statusOrId).map(op => op.id); |
| | | } else if (typeof statusOrId === 'string') { |
| | | //If it's still a string, wrap the id inside an array |
| | | statusOrId = [statusOrId]; |
| | | } |
| | | if (statusOrId.length ===0) return; |
| | | if (!['cancel', 'dismiss', 'retry'].includes(action)) return; |
| | | const shouldRemove = ['cancel', 'dismiss'].includes(action); |
| | | if (shouldRemove) { |
| | | statusOrId.forEach(id => this.removeOperationUI(id)); |
| | | } |
| | | |
| | | setQueue(item) { |
| | | this.store.save(item); |
| | | } |
| | | |
| | | updateOperationStatus(itemID, status) { |
| | | let item = this.store.get(itemID); |
| | | if (!item) return; |
| | | |
| | | // Update status |
| | | item.status = status; |
| | | |
| | | this.notify('operation-status', item); |
| | | this.updateOperationUI(item); |
| | | } |
| | | |
| | | getQueue(itemID) { |
| | | return this.store.get(itemID); |
| | | } |
| | | |
| | | clearQueue(itemID) { |
| | | this.store.delete(itemID); |
| | | } |
| | | |
| | | startActivityTracking() { |
| | | if (!this.activityListeners) { |
| | | const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart']; |
| | | this.activityListeners = activityEvents.map(event => { |
| | | const handler = () => this.resetActivityTimer(); |
| | | document.addEventListener(event, handler, {passive: true}); |
| | | return {event, handler}; |
| | | try { |
| | | const response = await fetch( |
| | | `${this.api}${this.endpoint}`, |
| | | { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | ... this.headers |
| | | }, |
| | | body: JSON.stringify({ |
| | | action, |
| | | ids: statusOrId, |
| | | user: this.user |
| | | }) |
| | | } |
| | | ); |
| | | if (!response.ok) { |
| | | throw new Error(`${action} failed: ${response.status}`); |
| | | } |
| | | const result = await response.json(); |
| | | if (!result.success) { |
| | | throw new Error(result.message || `${action} operation failed`); |
| | | } |
| | | statusOrId.forEach(id => { |
| | | let item = this.getQueue(id); |
| | | this.notify(`${action}-operation`, item); |
| | | if (shouldRemove) { |
| | | this.clearQueue(id); |
| | | } else { |
| | | let item = this.getQueue(id); |
| | | item.status = 'queued'; |
| | | this.setQueue(item); |
| | | this.updateOperationStatus(item.id, item.status); |
| | | } |
| | | }); |
| | | } |
| | | this.resetActivityTimer(); |
| | | } |
| | | |
| | | resetActivityTimer() { |
| | | if (this.activityTimer) { |
| | | clearTimeout(this.activityTimer); |
| | | } |
| | | |
| | | this.activityTimer = setTimeout(() => { |
| | | this.processQueue(); |
| | | }, this.config.activityDelay); |
| | | } |
| | | |
| | | stopActivityTracking() { |
| | | if (this.activityTimer) { |
| | | clearTimeout(this.activityTimer); |
| | | this.activityTimer = null; |
| | | } |
| | | if (this.activityListeners) { |
| | | this.activityListeners.forEach(({event, handler}) => { |
| | | document.removeEventListener(event, handler); |
| | | }); |
| | | this.activityListeners = null; |
| | | if (action === 'retry') { |
| | | this.trackActivity(); |
| | | } |
| | | this.updateUI(); |
| | | return result; |
| | | } catch (error) { |
| | | await window.jvbError.log(error, { |
| | | component: 'Queue', |
| | | operation: 'performQueueAction', |
| | | action: action, |
| | | operationIds: statusOrId, |
| | | itemCount: statusOrId.length |
| | | }, () => this.opActions(statusOrId, action)); |
| | | return {success: false, error: error.message}; |
| | | } |
| | | } |
| | | |
| | | hideQueue(){ |
| | | this.ui.panel.hidden = true; |
| | | this.ui.toggle.hidden = true; |
| | | } |
| | | showQueue() { |
| | | this.ui.panel.hidden = false; |
| | | this.ui.toggle.hidden = false; |
| | | } |
| | | |
| | | setProcessing(on) { |
| | | this.isProcessing = on; |
| | | this.ui.toggle.classList.toggle('saving', on); |
| | | } |
| | | /** |
| | | * Send any queued operations to the server |
| | | * @returns {Promise<void>} |
| | | */ |
| | | async processQueue() { |
| | | if (this.isProcessing) return; |
| | | |
| | | const queue = this.getOperationsByStatus('queued'); |
| | | const queue = this.getQueueByStatus('queued'); |
| | | |
| | | if (queue.length === 0) { |
| | | this.stopActivityTracking(); |
| | | return; |
| | | } |
| | | this.setProcessing(true); |
| | | this.setProcessing(); |
| | | |
| | | for (const operation of queue) { |
| | | await this.processOperation(operation); |
| | |
| | | this.setProcessing(false); |
| | | this.stopActivityTracking(); |
| | | |
| | | this.maybeStartPolling() ? this.showQueue() : this.hideQueue(); |
| | | // this.toggleQueue(this.maybeStartPolling()); |
| | | |
| | | } |
| | | |
| | | async processOperation(operation, skip = false) { |
| | | async processOperation(operation) { |
| | | try { |
| | | if (!skip) { |
| | | this.updateOperationStatus(operation.id, 'uploading'); |
| | | |
| | | if (operation.data?._isFormData) { |
| | | operation.data = await this.store.objectToFormData(operation.data); |
| | | } |
| | | //Add it to memory if it isn't already there |
| | | if (!this.queue.has(operation.id)) { |
| | | this.queue.set(operation.id, operation); |
| | | } |
| | | let skip = false; |
| | | if (operation.data?._isFormData && !operation.data instanceof FormData) { |
| | | skip = true; |
| | | operation.data = await this.store.objectToFormData(operation.data); |
| | | } |
| | | |
| | | const url = `${this.config.apiBase}${operation.endpoint}`; |
| | | let requestBody; |
| | | this.updateOperationStatus(operation.id, 'uploading'); |
| | | |
| | | let requestBody; |
| | | if (operation.data instanceof FormData) { |
| | | operation.data.append('id', operation.id); |
| | | operation.data.append('user', this.user); |
| | |
| | | }); |
| | | operation.headers['Content-Type'] = 'application/json'; |
| | | } |
| | | const response = await fetch(url, { |
| | | method: operation.method, |
| | | headers: operation.headers, |
| | | body: requestBody |
| | | }); |
| | | if (requestBody === undefined || requestBody === null) return; |
| | | |
| | | const response = await fetch( |
| | | `${this.api}${operation.endpoint}`, |
| | | { |
| | | method: operation.method, |
| | | headers: operation.headers, |
| | | body: requestBody |
| | | } |
| | | ); |
| | | const result = await response.json(); |
| | | if (skip) { |
| | | operation.data = {}; |
| | | } |
| | | if (response.ok && result.success !== false) { |
| | | // Handle server-side merge |
| | | if (response.ok && result.success) { |
| | | if (result.id && operation.id !== result.id) { |
| | | operation = await this.handleServerMerge(operation, result); |
| | | } else { |
| | | operation.status = result.status || 'pending'; |
| | | operation.status = result.status??'pending'; |
| | | operation.serverData = result; |
| | | this.updateOperationStatus(operation.id, operation.status); |
| | | } |
| | | |
| | | this.a11y.announce(`${operation.title} sent to server for processing.`); |
| | | |
| | | this.a11y.announce(`${operation.title} sent to server for processing`); |
| | | } else { |
| | | throw new Error(result.message || `HTTP ${response.status}`); |
| | | } |
| | | this.setQueue(operation); |
| | | } catch (error) { |
| | | console.error('Operation failed:', error); |
| | | |
| | | console.error('Operation failed: ', error); |
| | | operation.retries++; |
| | | operation.lastError = error.message; |
| | | |
| | | if (operation.retries >= this.config.maxRetries) { |
| | | if (operation.retries >= 3) { |
| | | operation.status = 'failed_permanent'; |
| | | } else { |
| | | operation.status = 'failed'; |
| | | operation.nextRetry = Date.now() + (Math.pow(2, operation.retries) * 1000); |
| | | } |
| | | this.updateOperationStatus(operation.id, operation.status); |
| | | |
| | | this.setQueue(operation); |
| | | } |
| | | } |
| | | |
| | | async handleServerMerge(operation, result) { |
| | | const existingOp = this.getQueue(result.id); |
| | | |
| | | if (existingOp) { |
| | | // Merge with existing local operation |
| | | existingOp.data = window.deepMerge(existingOp.data, operation.data); |
| | | existingOp.status = result.status || 'pending'; |
| | | existingOp.serverData = result; |
| | | this.updateOperationStatus(existingOp.id, existingOp.status); |
| | | this.removeOperationFromUI(operation.id); |
| | | this.clearQueue(operation.id); |
| | | return existingOp; |
| | | } else { |
| | | // Server merged with unknown operation |
| | | this.clearQueue(operation.id); |
| | | operation.id = result.id; |
| | | operation.status = result.status || 'pending'; |
| | | operation.status = result.status||'pending'; |
| | | operation.serverData = result; |
| | | this.updateOperationStatus(operation.id, operation.status); |
| | | return operation; |
| | | return this.mergeOp(existingOp, operation); |
| | | } else { |
| | | this.clearQueue(operation.id); |
| | | this.setQueue(result); |
| | | return result; |
| | | } |
| | | } |
| | | |
| | | mergeOp(oldOp, newOp) { |
| | | oldOp.data = window.deepMerge(oldOp.data, newOp.data); |
| | | oldOp.status = newOp.status; |
| | | if (Object.hasOwn(newOp, 'serverData')) { |
| | | oldOp.serverData = newOp.serverData; |
| | | } |
| | | this.updateOperationStatus(oldOp.id, oldOp.status); |
| | | this.removeOperationUI(newOp.id); |
| | | this.clearQueue(newOp.id); |
| | | return oldOp; |
| | | } |
| | | sortByDate(ops) { |
| | | return ops.sort((a, b) => { |
| | | const aTime = a.updated_at ?? a.timestamp ?? 0; |
| | | const bTime = b.updated_at ?? b.timestamp ?? 0; |
| | | return aTime - bTime; |
| | | }); |
| | | } |
| | | |
| | | getAllQueue() { |
| | | let ops = [... new Set([ |
| | | ...Array.from(this.store.data.values()), |
| | | ... Array.from(this.queue.values()) |
| | | ])]; |
| | | //Sort operations by operation updated_at |
| | | return this.sortByDate(ops); |
| | | } |
| | | |
| | | getQueueByStatus(status) { |
| | | if (typeof status === 'string') { |
| | | status = [status]; |
| | | } |
| | | |
| | | let ops = [...new Set([ |
| | | ...Array.from(this.store.filterByIndex({status: status})), |
| | | ...Array.from(this.queue.values()).filter(op => status.includes(op.status)) |
| | | ])]; |
| | | return this.sortByDate(ops); |
| | | } |
| | | |
| | | |
| | | updateOperationStatus(itemID, status) { |
| | | let item = this.getQueue(itemID); |
| | | if (!item || !this.statuses.includes(status)) return; |
| | | item.status = status; |
| | | this.notify('operation-status', item); |
| | | this.setQueue(item); |
| | | } |
| | | setQueue(item) { |
| | | this.store.save(item); |
| | | this.queue.set(item.id, item); |
| | | } |
| | | getQueue(itemID) { |
| | | return this.queue.has(itemID) ? this.queue.get(itemID) : this.store.get(itemID); |
| | | } |
| | | clearQueue(itemID) { |
| | | this.queue.delete(itemID); |
| | | this.store.delete(itemID); |
| | | } |
| | | /**************************************************************************** |
| | | POLLING |
| | | ****************************************************************************/ |
| | | maybeStartPolling() { |
| | | const incomplete = this.getQueueByStatus(this.pendingStatuses); |
| | | if (incomplete.length > 0) { |
| | | this.startPolling(); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | startPolling() { |
| | | if (this.isPolling) return; |
| | | |
| | | this.isPolling = true; |
| | | this.updateStatusPanel('pending'); |
| | | this.updatePanel('pending'); |
| | | |
| | | this.pollTimer = setInterval(async () => { |
| | | try { |
| | | this.store.clearCache(); |
| | | await this.store.fetch(); // Fetches from server, updates store.data |
| | | |
| | | await this.store.fetch(); |
| | | if (!this.maybeStartPolling()) { |
| | | this.stopPolling(); |
| | | this.updateStatusPanel('synced'); |
| | | this.updatePanel('synced'); |
| | | } |
| | | } catch (error) { |
| | | console.error('Polling error:', error); |
| | | } |
| | | }, this.config.pollInterval); |
| | | }, |
| | | 5000); |
| | | this.startCountdown(); |
| | | } |
| | | |
| | | stopPolling() { |
| | |
| | | clearInterval(this.pollTimer); |
| | | this.pollTimer = null; |
| | | } |
| | | this.stopCountdown(); |
| | | } |
| | | startCountdown(count = 5) { |
| | | if (!this.isPolling) return; |
| | | this.ui.refresh.countdown.textContent = count; |
| | | this.countdownTimer = setInterval(async() => { |
| | | count--; |
| | | if (count >= 0) { |
| | | this.ui.refresh.countdown.textContent = count; |
| | | }else { |
| | | this.ui.refresh.countdown.textContent = ''; |
| | | this.stopCountdown(); |
| | | } |
| | | },1000); |
| | | } |
| | | stopCountdown() { |
| | | if (this.countdownTimer) { |
| | | clearInterval(this.countdownTimer); |
| | | this.countdownTimer = null; |
| | | } |
| | | } |
| | | getOperationIds(operations) { |
| | | return operations.map(op => op.id); |
| | | /**************************************************************************** |
| | | UI |
| | | ****************************************************************************/ |
| | | updateUI() { |
| | | if (!this.canUpdateUI) return; |
| | | |
| | | window.debouncer.schedule( |
| | | 'queue-ui', |
| | | this.handleUpdateUI.bind(this) |
| | | ) |
| | | } |
| | | /*********************************************************** |
| | | USER ACTIONS |
| | | ***********************************************************/ |
| | | handleUpdateUI() { |
| | | const operations = this.getAllQueue(); |
| | | this.ui.actions.retry.disabled = operations.filter(op => op.status === 'failed').length === 0; |
| | | this.ui.actions.clear.disabled = operations.filter(op => op.status === 'completed').length ===0; |
| | | |
| | | /** |
| | | * |
| | | * @param {array} ids |
| | | * @param {string }action |
| | | * @returns {Promise<void>} |
| | | */ |
| | | async updateServerOperations(ids, action) { |
| | | ids = Array.isArray(ids) ? ids : (ids.includes(',') ? ids.split(',') : [ids]); |
| | | ids = ids.filter(id => { |
| | | let item = this.getQueue(id); |
| | | return this.getAllowedActions(item.status).includes(action); |
| | | }); |
| | | |
| | | if (ids.length === 0) return; |
| | | |
| | | // SINGLE place to handle UI removal |
| | | const shouldRemove = ['cancel', 'dismiss'].includes(action); |
| | | if (shouldRemove) { |
| | | ids.forEach(id => this.removeOperationFromUI(id)); |
| | | } |
| | | |
| | | try { |
| | | const response = await fetch(`${this.config.apiBase}${this.config.endpoint}`, { |
| | | method: 'POST', |
| | | headers: { 'Content-Type': 'application/json', ...this.headers }, |
| | | body: JSON.stringify({ ids, action, user: window.auth.getUser() }) |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | throw new Error(`${action} failed: ${response.status}`); |
| | | } |
| | | |
| | | const result = await response.json(); |
| | | if (!result.success) { |
| | | throw new Error(result.message || `${action} operation failed`); |
| | | } |
| | | |
| | | // SINGLE place to handle store updates |
| | | ids.forEach(id => { |
| | | let item = this.getQueue(id); |
| | | this.notify(`${action}-operation`, item); |
| | | |
| | | if (shouldRemove) { |
| | | this.clearQueue(id); |
| | | for (let status of this.statuses) { |
| | | if (status === 'failed_permanent') continue; |
| | | let total = operations.filter(op => op.status === status).length; |
| | | this.ui.filters[status].label.hidden = total === 0; |
| | | this.ui.filters[status].input.dataset.count = `${total}`; |
| | | if (total > 0) { |
| | | this.ui.filters[status].count.textContent = total; |
| | | } else { |
| | | item.status = 'queued'; |
| | | item.retries = 0; |
| | | this.setQueue(item); |
| | | this.updateOperationStatus(item.id, item.status); |
| | | this.ui.filters[status].count.textContent = ''; |
| | | } |
| | | }); |
| | | |
| | | if (action === 'retry') { |
| | | this.startActivityTracking(); |
| | | } |
| | | |
| | | this.updateUI(); |
| | | return result; |
| | | |
| | | } catch (error) { |
| | | // Log and let jvbError handle retry |
| | | await window.jvbError.log(error, { |
| | | component: 'QueueManager', |
| | | operation: 'performQueueAction', |
| | | action: action, |
| | | operationIds: ids, |
| | | itemCount: ids.length |
| | | }, () => this.updateServerOperations(ids, action)); |
| | | |
| | | // Don't re-throw - error is logged and handled |
| | | return { success: false, error: error.message }; |
| | | } |
| | | } |
| | | |
| | | getAllowedActions(status) { |
| | | const actionMap = { |
| | | 'queued': ['cancel'], |
| | | 'localProcessing': ['cancel'], |
| | | 'pending': ['cancel'], |
| | | 'processing': [], |
| | | 'completed': ['dismiss'], |
| | | 'failed': ['retry', 'dismiss'], |
| | | 'failed_permanent': ['dismiss'] |
| | | }; |
| | | return actionMap[status] || []; |
| | | } |
| | | |
| | | |
| | | /********************************************* |
| | | LISTENERS |
| | | *********************************************/ |
| | | initListeners() { |
| | | this.clickHandler = this.handleClick.bind(this); |
| | | |
| | | document.addEventListener('click', this.clickHandler); |
| | | |
| | | this.handleOnline = () => { |
| | | this.updateStatusPanel(); |
| | | if (this.hasQueuedOperations()) { |
| | | this.processQueue(); |
| | | } |
| | | }; |
| | | this.handleOffline = () => this.updateStatusPanel('offline'); |
| | | this.handleBeforeUnload = (e) => { |
| | | if (this.isPolling || this.isProcessing) { |
| | | e.preventDefault(); |
| | | return 'You have unsaved changes in the queue. Proceed?'; |
| | | } |
| | | }; |
| | | |
| | | window.addEventListener('online', this.handleOnline); |
| | | window.addEventListener('offline', this.handleOffline); |
| | | window.addEventListener('beforeunload', this.handleBeforeUnload); |
| | | } |
| | | handleClick(e) { |
| | | if (!e.target.closest(this.selectors.panel, this.selectors.toggle)) { |
| | | return; |
| | | } |
| | | if (e.target.closest(this.selectors.refreshButton)) { |
| | | this.store.clearCache(); |
| | | this.store.clearHttpHeaders(); // Clear cached headers first |
| | | this.store.fetch(); |
| | | } else if (e.target.closest(this.selectors.clearButton)) { |
| | | const completedOps = this.getOperationIds(this.getOperationsByStatus('completed')); |
| | | if (completedOps.length > 0) { |
| | | this.updateServerOperations(completedOps, 'dismiss'); |
| | | } |
| | | } else if (e.target.closest(this.selectors.retryButton)) { |
| | | const failedOps = this.getOperationIds(this.getOperationsByStatus('failed')); |
| | | if (failedOps.length > 0) { |
| | | this.updateServerOperations(failedOps, 'retry'); |
| | | } |
| | | } else if (e.target.closest('[data-action]')) { |
| | | const button = e.target.closest('[data-action]'); |
| | | const operationId = button.closest('[data-id]')?.dataset.id; |
| | | if (operationId) { |
| | | this.updateServerOperations(operationId, button.dataset.action); |
| | | } |
| | | } else if (e.target.closest('.filters [data-filter]')) { |
| | | const filter = e.target.closest('[data-filter]').dataset.filter; |
| | | this.setFilter(filter); |
| | | this.renderOperations(); |
| | | } |
| | | |
| | | } |
| | | renderOperations() { |
| | | if (!this.ui.items.container) return; |
| | | |
| | | /********************************************* |
| | | UI |
| | | *********************************************/ |
| | | initUI() { |
| | | this.icons = { |
| | | queued: 'arrows-clockwise', localProcessing: 'arrows-clockwise', uploading: 'syncing', |
| | | pending: 'cloud', processing: 'syncing', completed: 'cloud-check', |
| | | failed: 'cloud-warning', failed_permanent: 'cloud-warning' |
| | | }; |
| | | const status = this.store.filters?.status ?? 'all'; |
| | | const operations = (status === 'all') ? this.getAllQueue() : this.getQueueByStatus(status); |
| | | |
| | | this.selectors = { |
| | | panel: 'aside#queue', |
| | | toggle: 'button.qtoggle', |
| | | refreshButton: 'button.refreshNow', |
| | | countdown: '.countdown', |
| | | indicator: '.qtoggle .indicator', |
| | | count: '.qtoggle .count', |
| | | popup: '.popup', |
| | | itemsContainer: '.qitems', |
| | | clearButton: '.dismiss-all', |
| | | retryButton: '.retry-all', |
| | | filters: { |
| | | all: '.filters [data-filter="all"]', |
| | | received: '.filters [data-filter="queued"]', |
| | | localProcessing: '.filters [data-filter="localProcessing"]', |
| | | uploading: '.filters [data-filter="uploading"]', |
| | | pending: '.filters [data-filter="pending"]', |
| | | processing: '.filters [data-filter="processing"]', |
| | | completed: '.filters [data-filter="completed"]', |
| | | failed: '.filters [data-filter="failed"]', |
| | | } |
| | | }; |
| | | window.removeChildren(this.ui.items.container); |
| | | |
| | | this.ui = window.uiFromSelectors(this.selectors); |
| | | if (!this.ui.panel) { |
| | | this.canUpdateUI = false; |
| | | } |
| | | } |
| | | |
| | | _updateUI() { |
| | | if (!this.canUpdateUI) { |
| | | if (operations.length === 0) { |
| | | const empty = window.getTemplate('emptyQueue'); |
| | | this.ui.items.container.append(empty); |
| | | this.a11y.announce('No items in queue'); |
| | | return; |
| | | } |
| | | |
| | | // Get current operations from store |
| | | const operations = Array.from(this.store.data.values()); |
| | | operations.forEach(op => { |
| | | let item = this.items.get(op.id); |
| | | |
| | | // Get stats from last fetch response (server-provided) |
| | | const stats = this.store.lastResponse?.queue_stats || { |
| | | queued: 0, |
| | | localProcessing: 0, |
| | | uploading: 0, |
| | | pending: 0, |
| | | processing: 0, |
| | | completed: 0, |
| | | failed: 0, |
| | | failed_permanent: 0 |
| | | }; |
| | | |
| | | // Update count badge |
| | | if (this.ui.count) { |
| | | const activeCount = operations.length - stats.completed; |
| | | this.ui.count.textContent = activeCount > 0 ? activeCount : ''; |
| | | this.ui.count.style.display = activeCount > 0 ? '' : 'none'; |
| | | } |
| | | |
| | | // Update indicator |
| | | if (this.ui.indicator) { |
| | | const hasActive = stats.queued > 0 || stats.uploading > 0 || |
| | | stats.pending > 0 || stats.processing > 0; |
| | | this.ui.indicator.classList.toggle('active', hasActive); |
| | | } |
| | | |
| | | // Update button states |
| | | this.ui.clearButton.disabled = this.getOperationsByStatus('completed').length === 0; |
| | | this.ui.retryButton.disabled = this.getOperationsByStatus('failed').length === 0 && this.getOperationsByStatus('failed_permanent').length === 0; |
| | | |
| | | // Update filter counts (from server stats) |
| | | Object.entries(this.ui.filters).forEach(([status, button]) => { |
| | | const count = status === 'all' |
| | | ? operations.length |
| | | : stats[status] || 0; |
| | | const countEl = button.querySelector('.count'); |
| | | if (countEl) { |
| | | countEl.textContent = count > 0 ? count : ''; |
| | | if (!item) { |
| | | // Create new element and reference |
| | | item = this.createOperationElement(op); |
| | | } |
| | | button.setAttribute('data-count', count); |
| | | |
| | | if (item?.element) { |
| | | this.updateOperationUI(op.id); |
| | | this.ui.items.container.append(item.element); |
| | | } |
| | | }); |
| | | |
| | | // Render current operations |
| | | this.renderOperations(); |
| | | } |
| | | |
| | | getStatusLabel(status) { |
| | | createOperationElement(op) { |
| | | const el = window.getTemplate('queueItem'); |
| | | el.dataset.id = op.id; |
| | | |
| | | const item = { |
| | | element: el, |
| | | ui: window.uiFromSelectors(this.selectors.item, el) |
| | | }; |
| | | |
| | | this.items.set(op.id, item); |
| | | return item; |
| | | } |
| | | |
| | | updateOperationUI(opId) { |
| | | let item = (this.items.has(opId)) ? this.items.get(opId) : this.createOperationElement(opId); |
| | | if (!item) return; |
| | | let op = this.getQueue(opId); |
| | | |
| | | let element = item.element; |
| | | element.classList.remove(this.statuses); |
| | | element.classList.add(op.status); |
| | | |
| | | let progress = this.getProgress(op); |
| | | if (item.ui.type && item.ui.type.textContent !== op.title) item.ui.type.textContent = op.title; |
| | | if (item.ui.status) { |
| | | item.ui.status.title = this.statusLabel(op.status); |
| | | } |
| | | if (item.ui.icon) { |
| | | item.ui.icon.className = `icon icon-${this.icons[op.status]}`; |
| | | } |
| | | if (item.ui.details) item.ui.details.textContent = this.itemMessage(op); |
| | | if (item.ui.startedAt) { |
| | | item.ui.startedAt.setAttribute('datetime', op.created_at); |
| | | item.ui.startedAt.textContent = window.formatTimeAgo(op.created_at); |
| | | } |
| | | let text = op.status === 'completed' ? 'Completed: ' : 'Last updated: '; |
| | | let shouldShow =Object.hasOwn(op, 'updated_at') || Object.hasOwn(op, 'completed_at'); |
| | | item.ui.completed.wrap.hidden = !shouldShow; |
| | | if (shouldShow && item.ui.completed.label && item.ui.completed.time) { |
| | | let time; |
| | | if (Object.hasOwn(op, 'completed_at')) { |
| | | time = op.completed_at; |
| | | } else { |
| | | time = op.updated_at; |
| | | } |
| | | |
| | | item.ui.completed.label.textContent = text; |
| | | item.ui.completed.time.setAttribute('datetime', time); |
| | | item.ui.completed.time.textContent = window.formatTimeAgo(time); |
| | | } |
| | | window.showProgress(item.ui.progress, progress, 100, this.statusLabel(op.status)); |
| | | if (item.ui.actions.cancel) item.ui.actions.cancel.hidden = this.completedStatuses.includes(op.status); |
| | | if (item.ui.actions['retry']) { |
| | | if (op.retries >= 3) item.ui.actions['retry'].disabled = true; |
| | | item.ui.actions['retry'].hidden = op.status !=='failed'; |
| | | } |
| | | if (item.ui.actions.dismiss) item.ui.actions.dismiss.hidden = this.pendingStatuses.includes(op.status); |
| | | } |
| | | getProgress(op) { |
| | | if (op.progress) return op.progress; |
| | | if (!this.statuses.includes(op.status)) return 0; |
| | | let statusProgress = { |
| | | 'queued': 10, |
| | | 'uploading': 25, |
| | | 'pending': 40, |
| | | 'processing':70, |
| | | 'completed':100, |
| | | 'failed':0, |
| | | 'failed_permanent':0 |
| | | }; |
| | | return statusProgress[op.status]??0; |
| | | } |
| | | removeOperationUI(opId) { |
| | | let op = this.items.get(opId); |
| | | if (!op) return; |
| | | window.fade(op.element, false); |
| | | } |
| | | |
| | | updatePanel(status = 'syncing') { |
| | | if (!this.panelStatuses.includes(status)) return; |
| | | this.ui.panel.classList.remove(this.panelStatuses); |
| | | this.ui.panel.classList.add(status); |
| | | } |
| | | /**************************************************************************** |
| | | UTILITY |
| | | ****************************************************************************/ |
| | | statusLabel(status) { |
| | | if (!this.statuses.includes(status)) return''; |
| | | const labels = { |
| | | 'queued': 'Queued', |
| | | 'localProcessing': 'Processing locally', |
| | |
| | | 'pending': 'Waiting on server', |
| | | 'processing': 'Processing', |
| | | 'completed': 'Completed', |
| | | 'failed': 'Failed (will retry)', |
| | | 'failed': 'Failed', |
| | | 'failed_permanent': 'Failed permanently' |
| | | }; |
| | | return labels[status] || status; |
| | | return labels[status]; |
| | | } |
| | | |
| | | getItemMessage(item) { |
| | | if (item.message) return item.message; |
| | | if (item.error_message) return item.error_message; |
| | | itemMessage(item) { |
| | | if (Object.hasOwn(item, 'message') && item.message !== '') return item.message; |
| | | if (Object.hasOwn(item, 'error_message') && item.error_message) return item.error_message; |
| | | |
| | | switch(item.status) { |
| | | case 'queued': |
| | |
| | | return ''; |
| | | } |
| | | } |
| | | |
| | | calculateProgress(item) { |
| | | if (item.progress) return item.progress; |
| | | |
| | | // Estimate progress based on status |
| | | const statusProgress = { |
| | | 'queued': 10, |
| | | 'uploading': 25, |
| | | 'pending': 40, |
| | | 'processing': 70, |
| | | 'completed': 100, |
| | | 'failed': 0, |
| | | 'failed_permanent': 0 |
| | | }; |
| | | |
| | | return statusProgress[item.status] || 0; |
| | | toggleQueue(on = true) { |
| | | this.ui.panel.hidden = !on; |
| | | this.ui.toggle.button.hidden = !on; |
| | | } |
| | | |
| | | |
| | | renderOperations() { |
| | | if (!this.ui.itemsContainer) return; |
| | | |
| | | const operations = this.store.getFiltered(); |
| | | |
| | | // Clear container |
| | | window.removeChildren(this.ui.itemsContainer); |
| | | |
| | | // Render operations or empty state |
| | | if (operations.length === 0) { |
| | | let empty = window.getTemplate('emptyQueue'); |
| | | this.ui.itemsContainer.append(empty); |
| | | this.a11y.announce('Nothing queued.'); |
| | | } else { |
| | | operations.forEach(op => { |
| | | const element = this.createOperationUI(op); |
| | | this.ui.itemsContainer.append(element); |
| | | }); |
| | | } |
| | | setProcessing(on = true) { |
| | | this.isProcessing = on; |
| | | this.ui.toggle.button.classList.toggle('saving', on); |
| | | } |
| | | |
| | | createOperationUI(operation) { |
| | | const listItem = window.getTemplate('queueItem'); |
| | | listItem.dataset.id = operation.id; |
| | | |
| | | this.updateOperationUI(operation, listItem); |
| | | return listItem; |
| | | } |
| | | |
| | | updateOperationUI(item, element = null) { |
| | | if (!element) { |
| | | element = this.ui.itemsContainer?.querySelector(`[data-id="${item.id}"]`); |
| | | } |
| | | if (!element) { |
| | | element = this.createOperationUI(item); |
| | | } |
| | | |
| | | // Remove old status classes |
| | | this.statuses.forEach(status => element.classList.remove(status)); |
| | | element.classList.add(item.status); |
| | | |
| | | // Update content |
| | | let timeDisplay = ''; |
| | | |
| | | if (item.updated_at) { |
| | | // Server now sends ISO format timestamps - much more reliable! |
| | | timeDisplay = window.formatTimeAgo(new Date(item.updated_at)); |
| | | } else if (item.created_at) { |
| | | timeDisplay = window.formatTimeAgo(new Date(item.created_at)); |
| | | } |
| | | const progressPercent = this.calculateProgress(item); |
| | | |
| | | // Update text content safely |
| | | const typeEl = element.querySelector('.type'); |
| | | const statusEl = element.querySelector('.status'); |
| | | const detailsEl = element.querySelector('.info .details'); |
| | | const timeEl = element.querySelector('.info .time'); |
| | | const progressFill = element.querySelector('.progress .fill'); |
| | | |
| | | if (typeEl) typeEl.textContent = item.title; |
| | | if (statusEl) { |
| | | statusEl.querySelector('.icon')?.remove(); |
| | | let status = this.getStatusLabel(item.status); |
| | | statusEl.title = status; |
| | | statusEl.prepend(window.getIcon(this.icons[item.status])); |
| | | statusEl.querySelector('span').textContent = status; |
| | | } |
| | | if (detailsEl) detailsEl.textContent = this.getItemMessage(item); |
| | | if (timeEl) timeEl.textContent = timeDisplay; |
| | | if (progressFill) progressFill.style.width = `${progressPercent}%`; |
| | | |
| | | // Update action buttons |
| | | const actionsContainer = element.querySelector('.actions'); |
| | | if (actionsContainer) { |
| | | this.updateActionButtons(item, actionsContainer); |
| | | } |
| | | } |
| | | |
| | | updateActionButtons(item, container) { |
| | | window.removeChildren(container); |
| | | |
| | | switch (item.status) { |
| | | case 'queued': |
| | | case 'localProcessing': |
| | | case 'pending': |
| | | // Show cancel button for in-progress items |
| | | const cancelBtn = window.getTemplate('button'); |
| | | cancelBtn.classList.add('cancel'); |
| | | cancelBtn.dataset.action = 'cancel'; |
| | | cancelBtn.textContent = 'Cancel'; |
| | | container.appendChild(cancelBtn); |
| | | break; |
| | | |
| | | case 'failed': |
| | | case 'failed_permanent': |
| | | // Show retry and dismiss buttons |
| | | const retryBtn = window.getTemplate('button'); |
| | | const dismissBtn = window.getTemplate('button'); |
| | | |
| | | retryBtn.classList.add('retry'); |
| | | retryBtn.textContent = 'Retry'; |
| | | retryBtn.disabled = item.retries >= this.maxRetries; |
| | | retryBtn.dataset.action = 'retry'; |
| | | |
| | | dismissBtn.classList.add('dismiss'); |
| | | dismissBtn.textContent = 'Dismiss'; |
| | | dismissBtn.dataset.action = 'dismiss'; |
| | | |
| | | container.appendChild(retryBtn); |
| | | container.appendChild(dismissBtn); |
| | | break; |
| | | |
| | | case 'completed': |
| | | // Show dismiss button only |
| | | const dismissCompletedBtn = window.getTemplate('button'); |
| | | dismissCompletedBtn.dataset.action = 'dismiss'; |
| | | dismissCompletedBtn.classList.add('dismiss'); |
| | | dismissCompletedBtn.textContent = 'Dismiss'; |
| | | container.appendChild(dismissCompletedBtn); |
| | | break; |
| | | } |
| | | } |
| | | |
| | | removeOperationFromUI(operationId) { |
| | | const element = this.ui.itemsContainer?.querySelector(`[data-id="${operationId}"]`); |
| | | if (element) { |
| | | element.style.opacity = '0'; |
| | | element.style.transform = 'scale(0.9)'; |
| | | setTimeout(() => element.remove(), 300); |
| | | } |
| | | } |
| | | |
| | | updateStatusPanel(status) { |
| | | this.ui.panel?.classList.remove(...this.classes); |
| | | if (!this.classes.includes(status)) { |
| | | return; |
| | | } |
| | | this.ui.panel?.classList.add(status); |
| | | } |
| | | |
| | | /*************************************************** |
| | | FILTERS |
| | | **************************************************/ |
| | | setFilter(filter) { |
| | | // Update active button |
| | | Object.values(this.ui.filters).forEach(button => { |
| | | if (button) { |
| | | button.classList.toggle('active', button.dataset.filter === filter); |
| | | } |
| | | }); |
| | | |
| | | if (filter === 'all') { |
| | | this.store.clearFilters(); |
| | | } else { |
| | | this.store.setFilter('status', filter); |
| | | } |
| | | } |
| | | |
| | | /************************************************************************** |
| | | HELPERS |
| | | **************************************************************************/ |
| | | getOperationsByStatus(status, include = true) { |
| | | |
| | | if (!Array.isArray(status) && typeof status === 'string') { |
| | | status = [status]; |
| | | } |
| | | return (include) |
| | | ? Array.from(this.store.data.values()).filter((item) => status.includes(item.status)) |
| | | : Array.from(this.store.data.values()).filter((item) => !status.includes(item.status)); |
| | | } |
| | | hasQueuedOperations() { |
| | | return this.getOperationsByStatus('queued').length > 0; |
| | | } |
| | | /**************************************************************************** |
| | | SUBSCRIPTION |
| | | ****************************************************************************/ |
| | | subscribe(callback) { |
| | | if (!this.subscribers) { |
| | | return; |
| | |
| | | notify(event, data) { |
| | | this.subscribers.forEach(cb => cb(event, data)); |
| | | } |
| | | |
| | | /************************************************************************** |
| | | /**************************************************************************** |
| | | CLEANUP |
| | | **************************************************************************/ |
| | | ****************************************************************************/ |
| | | destroy() { |
| | | this.stopPolling(); |
| | | if (this.isPolling) { |
| | | this.stopPolling(); |
| | | } |
| | | this.stopActivityTracking(); |
| | | |
| | | if (this.clickHandler) { |
| | | document.removeEventListener('click', this.clickHandler); |
| | | } |
| | | |
| | | if (this.keyHandler) { |
| | | document.removeEventListener('keydown', this.keyHandler); |
| | | } |
| | | |
| | | document.removeEventListener('click', this.clickHandler); |
| | | this.subscribers.clear(); |
| | | } |
| | | } |
| | | |
| | | document.addEventListener('DOMContentLoaded', async function() { |
| | | window.auth.subscribe((event) => { |
| | | if (event === 'auth-loaded') { |
| New file |
| | |
| | | /** |
| | | * QueueManager |
| | | * Uses DataStore for persistent storage |
| | | */ |
| | | class QueueManager { |
| | | constructor(config = {}) { |
| | | this.canUpdateUI = true; |
| | | this.config = { |
| | | apiBase: jvbSettings.api, |
| | | maxRetries: 3, |
| | | pollInterval: 5000, |
| | | activityDelay: 2000, //2 seconds |
| | | autosync: true, |
| | | endpoint: 'queue', |
| | | ...config |
| | | }; |
| | | |
| | | // Queue state |
| | | this.isProcessing = false; |
| | | this.isPolling = false; |
| | | this.queue = new Map(); |
| | | this.subscribers = new Set(); |
| | | |
| | | // Status definitions |
| | | this.statuses = [ |
| | | 'queued', |
| | | 'localProcessing', |
| | | 'uploading', |
| | | 'pending', |
| | | 'processing', |
| | | 'completed', |
| | | 'failed', |
| | | 'failed_permanent' |
| | | ]; |
| | | |
| | | this.user = window.auth.getUser(); |
| | | |
| | | if (!this.user) { |
| | | console.log('Queue: User not logged in, queue disabled'); |
| | | this.store = null; |
| | | this.canUpdateUI = false; |
| | | return; |
| | | } |
| | | |
| | | this.headers = { |
| | | 'X-WP-Nonce': window.auth.getNonce(), |
| | | ...config.headers |
| | | }; |
| | | |
| | | this.a11y = window.jvbA11y; |
| | | this.errors = window.jvbError; |
| | | |
| | | // Initialize DataStore for queue persistence |
| | | const store = window.jvbStore.register('queue', { |
| | | storeName: 'queue', |
| | | keyPath: 'id', |
| | | endpoint: this.config.endpoint, |
| | | TTL: Infinity, |
| | | indexes: [ |
| | | {name: 'status', keyPath: 'status'}, |
| | | {name: 'type', keyPath: 'type'}, |
| | | ], |
| | | showLoading: false, |
| | | delayFetch: false, // Queue should fetch immediately |
| | | }); |
| | | this.store = store.queue; |
| | | |
| | | this.classes = [ |
| | | 'offline', |
| | | 'synced', |
| | | 'pending' |
| | | ]; |
| | | |
| | | |
| | | |
| | | // Initialize |
| | | this.initUI(); |
| | | this.initListeners(); |
| | | if (this.ui.panel) { |
| | | this.popup = new window.jvbPopup({ |
| | | popup: this.ui.panel, |
| | | toggle: this.ui.toggle, |
| | | name: 'Queue Panel', |
| | | }); |
| | | } |
| | | this.updateUI = () => window.debouncer.schedule('queue-ui-update', this._updateUI.bind(this), 100); |
| | | this.initQueue(); |
| | | } |
| | | |
| | | async initQueue() { |
| | | let polling = this.maybeStartPolling(); |
| | | if (!polling) { |
| | | this.updateStatusPanel('synced'); |
| | | } |
| | | |
| | | |
| | | this.store.subscribe((event, data) => { |
| | | switch (event) { |
| | | case 'data-loaded': |
| | | case 'items-saved': |
| | | this.maybeStartPolling(); |
| | | this.updateUI(); |
| | | break; |
| | | case 'item-saved': |
| | | console.log(data,'Item saved data'); |
| | | if (data.previousItem && data.previousItem.status !== data.item.status) { |
| | | this.handleOperationStatusChange(data.item, data.previousItem.status); |
| | | } |
| | | this.maybeStartPolling(); |
| | | break; |
| | | default: |
| | | this.updateUI(); |
| | | break; |
| | | } |
| | | |
| | | }); |
| | | } |
| | | |
| | | maybeStartPolling() |
| | | { |
| | | const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false); |
| | | if (incomplete.length > 0) { |
| | | this.startPolling(); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Handle operation status changes and notify subscribers |
| | | */ |
| | | handleOperationStatusChange(operation) { |
| | | |
| | | // Notify based on new status |
| | | switch(operation.status) { |
| | | case 'completed': |
| | | console.log(operation); |
| | | this.notify('operation-completed', operation); |
| | | break; |
| | | case 'failed': |
| | | this.notify('operation-failed', operation); |
| | | break; |
| | | case 'failed_permanent': |
| | | this.notify('operation-failed-permanent', operation); |
| | | break; |
| | | } |
| | | } |
| | | /** |
| | | * |
| | | * @param {object} operation |
| | | * @param {string} operation.endpoint The endpoint, excluding the apiBase |
| | | * @param {object} operation.data The data to save |
| | | * @param {boolean} operation.canMerge Whether data can merge |
| | | * @param {string} operation.title The title of the operation for the Queue Panel |
| | | * @param {string} operation.popup The string to show in the popup |
| | | * @param {object} operation.headers Optional additional headers. Defaults to the API nonce |
| | | * |
| | | * @returns {string|null} Returns the operation id, for reference |
| | | */ |
| | | addToQueue(operation) { |
| | | const item = { |
| | | id: `u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, |
| | | endpoint: null, |
| | | method: 'POST', |
| | | headers: {}, |
| | | data: {}, |
| | | sendNow: false, // true = process immediately |
| | | canMerge: true, |
| | | popup: 'Saving changes...', |
| | | title: 'Operation', |
| | | status: 'queued', |
| | | timestamp: Date.now(), |
| | | retries: 0, |
| | | user: this.user, |
| | | ... operation |
| | | }; |
| | | |
| | | item.headers = { |
| | | ...this.headers, |
| | | ...item.headers |
| | | }; |
| | | |
| | | if (!item.endpoint || !item.data) { |
| | | console.error('Invalid operation queued: missing endpoint or data'); |
| | | return null; |
| | | } |
| | | |
| | | if (item.sendNow) { |
| | | this.processOperation(item).then(()=> {}); |
| | | this.store.clearCache(); |
| | | window.debouncer.schedule('fastQueue', this.startPolling.bind(this), 200); |
| | | this.showQueue(); |
| | | return item.id; |
| | | } |
| | | |
| | | const existingOps = Array.from(this.store.data.values()).filter(op=> |
| | | op.status === 'queued' && |
| | | op.endpoint === item.endpoint && |
| | | op.canMerge |
| | | ); |
| | | |
| | | if (existingOps.length > 0) { |
| | | const existing = existingOps[0]; |
| | | existing.data = window.deepMerge(existing.data, item.data); |
| | | existing.timestamp = Date.now(); |
| | | |
| | | this.updateOperationStatus(existing.id, existing.status); |
| | | this.updateUI(); |
| | | |
| | | this.startActivityTracking(); |
| | | return existing.id; |
| | | } |
| | | |
| | | this.store.clearCache(); |
| | | |
| | | //Add new operation to DataStore |
| | | this.setQueue(item); |
| | | |
| | | this.updateOperationStatus(item.id, item.status); |
| | | this.updateUI(); |
| | | |
| | | this.startActivityTracking(); |
| | | return item.id; |
| | | |
| | | |
| | | } |
| | | |
| | | |
| | | setQueue(item) { |
| | | this.store.save(item); |
| | | } |
| | | |
| | | updateOperationStatus(itemID, status) { |
| | | let item = this.store.get(itemID); |
| | | if (!item) return; |
| | | |
| | | // Update status |
| | | item.status = status; |
| | | |
| | | this.notify('operation-status', item); |
| | | this.updateOperationUI(item); |
| | | } |
| | | |
| | | getQueue(itemID) { |
| | | return this.store.get(itemID); |
| | | } |
| | | |
| | | clearQueue(itemID) { |
| | | this.store.delete(itemID); |
| | | } |
| | | |
| | | startActivityTracking() { |
| | | if (!this.activityListeners) { |
| | | const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart']; |
| | | this.activityListeners = activityEvents.map(event => { |
| | | const handler = () => this.resetActivityTimer(); |
| | | document.addEventListener(event, handler, {passive: true}); |
| | | return {event, handler}; |
| | | }); |
| | | } |
| | | this.resetActivityTimer(); |
| | | } |
| | | |
| | | resetActivityTimer() { |
| | | if (this.activityTimer) { |
| | | clearTimeout(this.activityTimer); |
| | | } |
| | | |
| | | this.activityTimer = setTimeout(() => { |
| | | this.processQueue(); |
| | | }, this.config.activityDelay); |
| | | } |
| | | |
| | | stopActivityTracking() { |
| | | if (this.activityTimer) { |
| | | clearTimeout(this.activityTimer); |
| | | this.activityTimer = null; |
| | | } |
| | | if (this.activityListeners) { |
| | | this.activityListeners.forEach(({event, handler}) => { |
| | | document.removeEventListener(event, handler); |
| | | }); |
| | | this.activityListeners = null; |
| | | } |
| | | } |
| | | |
| | | hideQueue(){ |
| | | this.ui.panel.hidden = true; |
| | | this.ui.toggle.hidden = true; |
| | | } |
| | | showQueue() { |
| | | this.ui.panel.hidden = false; |
| | | this.ui.toggle.hidden = false; |
| | | } |
| | | |
| | | setProcessing(on) { |
| | | this.isProcessing = on; |
| | | this.ui.toggle.classList.toggle('saving', on); |
| | | } |
| | | /** |
| | | * Send any queued operations to the server |
| | | * @returns {Promise<void>} |
| | | */ |
| | | async processQueue() { |
| | | if (this.isProcessing) return; |
| | | |
| | | const queue = this.getOperationsByStatus('queued'); |
| | | |
| | | if (queue.length === 0) { |
| | | this.stopActivityTracking(); |
| | | return; |
| | | } |
| | | this.setProcessing(true); |
| | | |
| | | for (const operation of queue) { |
| | | await this.processOperation(operation); |
| | | } |
| | | |
| | | this.setProcessing(false); |
| | | this.stopActivityTracking(); |
| | | |
| | | this.maybeStartPolling() ? this.showQueue() : this.hideQueue(); |
| | | } |
| | | |
| | | async processOperation(operation, skip = false) { |
| | | try { |
| | | if (!skip) { |
| | | this.updateOperationStatus(operation.id, 'uploading'); |
| | | |
| | | if (operation.data?._isFormData) { |
| | | operation.data = await this.store.objectToFormData(operation.data); |
| | | } |
| | | } |
| | | |
| | | const url = `${this.config.apiBase}${operation.endpoint}`; |
| | | let requestBody; |
| | | |
| | | if (operation.data instanceof FormData) { |
| | | operation.data.append('id', operation.id); |
| | | operation.data.append('user', this.user); |
| | | requestBody = operation.data; |
| | | } else { |
| | | requestBody = JSON.stringify({ |
| | | ...operation.data, |
| | | id: operation.id, |
| | | user: this.user |
| | | }); |
| | | operation.headers['Content-Type'] = 'application/json'; |
| | | } |
| | | const response = await fetch(url, { |
| | | method: operation.method, |
| | | headers: operation.headers, |
| | | body: requestBody |
| | | }); |
| | | |
| | | const result = await response.json(); |
| | | if (skip) { |
| | | operation.data = {}; |
| | | } |
| | | if (response.ok && result.success !== false) { |
| | | // Handle server-side merge |
| | | if (result.id && operation.id !== result.id) { |
| | | operation = await this.handleServerMerge(operation, result); |
| | | } else { |
| | | operation.status = result.status || 'pending'; |
| | | operation.serverData = result; |
| | | this.updateOperationStatus(operation.id, operation.status); |
| | | } |
| | | |
| | | this.a11y.announce(`${operation.title} sent to server for processing.`); |
| | | |
| | | } else { |
| | | throw new Error(result.message || `HTTP ${response.status}`); |
| | | } |
| | | } catch (error) { |
| | | console.error('Operation failed:', error); |
| | | |
| | | operation.retries++; |
| | | operation.lastError = error.message; |
| | | |
| | | if (operation.retries >= this.config.maxRetries) { |
| | | operation.status = 'failed_permanent'; |
| | | } else { |
| | | operation.status = 'failed'; |
| | | operation.nextRetry = Date.now() + (Math.pow(2, operation.retries) * 1000); |
| | | } |
| | | this.updateOperationStatus(operation.id, operation.status); |
| | | |
| | | this.setQueue(operation); |
| | | } |
| | | } |
| | | |
| | | async handleServerMerge(operation, result) { |
| | | const existingOp = this.getQueue(result.id); |
| | | |
| | | if (existingOp) { |
| | | // Merge with existing local operation |
| | | existingOp.data = window.deepMerge(existingOp.data, operation.data); |
| | | existingOp.status = result.status || 'pending'; |
| | | existingOp.serverData = result; |
| | | this.updateOperationStatus(existingOp.id, existingOp.status); |
| | | this.removeOperationFromUI(operation.id); |
| | | this.clearQueue(operation.id); |
| | | return existingOp; |
| | | } else { |
| | | // Server merged with unknown operation |
| | | this.clearQueue(operation.id); |
| | | operation.id = result.id; |
| | | operation.status = result.status || 'pending'; |
| | | operation.serverData = result; |
| | | this.updateOperationStatus(operation.id, operation.status); |
| | | return operation; |
| | | } |
| | | } |
| | | |
| | | startPolling() { |
| | | if (this.isPolling) return; |
| | | |
| | | this.isPolling = true; |
| | | this.updateStatusPanel('pending'); |
| | | |
| | | this.pollTimer = setInterval(async () => { |
| | | try { |
| | | this.store.clearCache(); |
| | | await this.store.fetch(); // Fetches from server, updates store.data |
| | | |
| | | if (!this.maybeStartPolling()) { |
| | | this.stopPolling(); |
| | | this.updateStatusPanel('synced'); |
| | | } |
| | | } catch (error) { |
| | | console.error('Polling error:', error); |
| | | } |
| | | }, this.config.pollInterval); |
| | | } |
| | | |
| | | stopPolling() { |
| | | if (!this.isPolling) return; |
| | | this.isPolling = false; |
| | | if (this.pollTimer) { |
| | | clearInterval(this.pollTimer); |
| | | this.pollTimer = null; |
| | | } |
| | | if (this.countdownTimer) { |
| | | clearInterval(this.countdownTimer); |
| | | this.countdownTimer = null; |
| | | } |
| | | } |
| | | getOperationIds(operations) { |
| | | return operations.map(op => op.id); |
| | | } |
| | | /*********************************************************** |
| | | USER ACTIONS |
| | | ***********************************************************/ |
| | | |
| | | /** |
| | | * |
| | | * @param {array} ids |
| | | * @param {string }action |
| | | * @returns {Promise<void>} |
| | | */ |
| | | async updateServerOperations(ids, action) { |
| | | ids = Array.isArray(ids) ? ids : (ids.includes(',') ? ids.split(',') : [ids]); |
| | | ids = ids.filter(id => { |
| | | let item = this.getQueue(id); |
| | | return this.getAllowedActions(item.status).includes(action); |
| | | }); |
| | | |
| | | if (ids.length === 0) return; |
| | | |
| | | // SINGLE place to handle UI removal |
| | | const shouldRemove = ['cancel', 'dismiss'].includes(action); |
| | | if (shouldRemove) { |
| | | ids.forEach(id => this.removeOperationFromUI(id)); |
| | | } |
| | | |
| | | try { |
| | | const response = await fetch(`${this.config.apiBase}${this.config.endpoint}`, { |
| | | method: 'POST', |
| | | headers: { 'Content-Type': 'application/json', ...this.headers }, |
| | | body: JSON.stringify({ ids, action, user: window.auth.getUser() }) |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | throw new Error(`${action} failed: ${response.status}`); |
| | | } |
| | | |
| | | const result = await response.json(); |
| | | if (!result.success) { |
| | | throw new Error(result.message || `${action} operation failed`); |
| | | } |
| | | |
| | | // SINGLE place to handle store updates |
| | | ids.forEach(id => { |
| | | let item = this.getQueue(id); |
| | | this.notify(`${action}-operation`, item); |
| | | |
| | | if (shouldRemove) { |
| | | this.clearQueue(id); |
| | | } else { |
| | | item.status = 'queued'; |
| | | item.retries = 0; |
| | | this.setQueue(item); |
| | | this.updateOperationStatus(item.id, item.status); |
| | | } |
| | | }); |
| | | |
| | | if (action === 'retry') { |
| | | this.startActivityTracking(); |
| | | } |
| | | |
| | | this.updateUI(); |
| | | return result; |
| | | |
| | | } catch (error) { |
| | | // Log and let jvbError handle retry |
| | | await window.jvbError.log(error, { |
| | | component: 'QueueManager', |
| | | operation: 'performQueueAction', |
| | | action: action, |
| | | operationIds: ids, |
| | | itemCount: ids.length |
| | | }, () => this.updateServerOperations(ids, action)); |
| | | |
| | | // Don't re-throw - error is logged and handled |
| | | return { success: false, error: error.message }; |
| | | } |
| | | } |
| | | |
| | | getAllowedActions(status) { |
| | | const actionMap = { |
| | | 'queued': ['cancel'], |
| | | 'localProcessing': ['cancel'], |
| | | 'pending': ['cancel'], |
| | | 'processing': [], |
| | | 'completed': ['dismiss'], |
| | | 'failed': ['retry', 'dismiss'], |
| | | 'failed_permanent': ['dismiss'] |
| | | }; |
| | | return actionMap[status] || []; |
| | | } |
| | | |
| | | |
| | | /********************************************* |
| | | LISTENERS |
| | | *********************************************/ |
| | | initListeners() { |
| | | this.clickHandler = this.handleClick.bind(this); |
| | | |
| | | document.addEventListener('click', this.clickHandler); |
| | | |
| | | this.handleOnline = () => { |
| | | this.updateStatusPanel(); |
| | | if (this.hasQueuedOperations()) { |
| | | this.processQueue(); |
| | | } |
| | | }; |
| | | this.handleOffline = () => this.updateStatusPanel('offline'); |
| | | this.handleBeforeUnload = (e) => { |
| | | if (this.isPolling || this.isProcessing) { |
| | | e.preventDefault(); |
| | | return 'You have unsaved changes in the queue. Proceed?'; |
| | | } |
| | | }; |
| | | |
| | | window.addEventListener('online', this.handleOnline); |
| | | window.addEventListener('offline', this.handleOffline); |
| | | window.addEventListener('beforeunload', this.handleBeforeUnload); |
| | | } |
| | | handleClick(e) { |
| | | if (!e.target.closest(this.selectors.panel, this.selectors.toggle)) { |
| | | return; |
| | | } |
| | | if (e.target.closest(this.selectors.refreshButton)) { |
| | | this.store.clearCache(); |
| | | this.store.clearCache(); // Clear cached headers first |
| | | this.store.fetch(); |
| | | } else if (e.target.closest(this.selectors.clearButton)) { |
| | | const completedOps = this.getOperationIds(this.getOperationsByStatus('completed')); |
| | | if (completedOps.length > 0) { |
| | | this.updateServerOperations(completedOps, 'dismiss'); |
| | | } |
| | | } else if (e.target.closest(this.selectors.retryButton)) { |
| | | const failedOps = this.getOperationIds(this.getOperationsByStatus('failed')); |
| | | if (failedOps.length > 0) { |
| | | this.updateServerOperations(failedOps, 'retry'); |
| | | } |
| | | } else if (e.target.closest('[data-action]')) { |
| | | const button = e.target.closest('[data-action]'); |
| | | const operationId = button.closest('[data-id]')?.dataset.id; |
| | | if (operationId) { |
| | | this.updateServerOperations(operationId, button.dataset.action); |
| | | } |
| | | } else if (e.target.closest('.filters [data-filter]')) { |
| | | const filter = e.target.closest('[data-filter]').dataset.filter; |
| | | this.setFilter(filter); |
| | | } |
| | | |
| | | } |
| | | |
| | | /********************************************* |
| | | UI |
| | | *********************************************/ |
| | | initUI() { |
| | | this.icons = { |
| | | queued: 'arrows-clockwise', localProcessing: 'arrows-clockwise', uploading: 'syncing', |
| | | pending: 'cloud', processing: 'syncing', completed: 'cloud-check', |
| | | failed: 'cloud-warning', failed_permanent: 'cloud-warning' |
| | | }; |
| | | |
| | | this.selectors = { |
| | | panel: 'aside#queue', |
| | | toggle: 'button.qtoggle', |
| | | refreshButton: 'button.refreshNow', |
| | | countdown: '.countdown', |
| | | indicator: '.qtoggle .indicator', |
| | | count: '.qtoggle .count', |
| | | popup: '.popup', |
| | | itemsContainer: '.qitems', |
| | | clearButton: '.dismiss-all', |
| | | retryButton: '.retry-all', |
| | | filters: { |
| | | all: '.filters [data-filter="all"]', |
| | | received: '.filters [data-filter="queued"]', |
| | | localProcessing: '.filters [data-filter="localProcessing"]', |
| | | uploading: '.filters [data-filter="uploading"]', |
| | | pending: '.filters [data-filter="pending"]', |
| | | processing: '.filters [data-filter="processing"]', |
| | | completed: '.filters [data-filter="completed"]', |
| | | failed: '.filters [data-filter="failed"]', |
| | | } |
| | | }; |
| | | |
| | | this.ui = window.uiFromSelectors(this.selectors); |
| | | if (!this.ui.panel) { |
| | | this.canUpdateUI = false; |
| | | } |
| | | } |
| | | |
| | | _updateUI() { |
| | | if (!this.canUpdateUI) { |
| | | return; |
| | | } |
| | | |
| | | // Get current operations from store |
| | | const operations = Array.from(this.store.data.values()); |
| | | |
| | | // Get stats from last fetch response (server-provided) |
| | | const stats = this.store.lastResponse?.queue_stats || { |
| | | queued: 0, |
| | | localProcessing: 0, |
| | | uploading: 0, |
| | | pending: 0, |
| | | processing: 0, |
| | | completed: 0, |
| | | failed: 0, |
| | | failed_permanent: 0 |
| | | }; |
| | | |
| | | // Update count badge |
| | | if (this.ui.count) { |
| | | const activeCount = operations.length - stats.completed; |
| | | this.ui.count.textContent = activeCount > 0 ? activeCount : ''; |
| | | this.ui.count.style.display = activeCount > 0 ? '' : 'none'; |
| | | } |
| | | |
| | | // Update indicator |
| | | if (this.ui.indicator) { |
| | | const hasActive = stats.queued > 0 || stats.uploading > 0 || |
| | | stats.pending > 0 || stats.processing > 0; |
| | | this.ui.indicator.classList.toggle('active', hasActive); |
| | | } |
| | | |
| | | // Update button states |
| | | this.ui.clearButton.disabled = this.getOperationsByStatus('completed').length === 0; |
| | | this.ui.retryButton.disabled = this.getOperationsByStatus('failed').length === 0 && this.getOperationsByStatus('failed_permanent').length === 0; |
| | | |
| | | // Update filter counts (from server stats) |
| | | Object.entries(this.ui.filters).forEach(([status, button]) => { |
| | | const count = status === 'all' |
| | | ? operations.length |
| | | : stats[status] || 0; |
| | | const countEl = button.querySelector('.count'); |
| | | if (countEl) { |
| | | countEl.textContent = count > 0 ? count : ''; |
| | | } |
| | | button.setAttribute('data-count', count); |
| | | }); |
| | | |
| | | // Render current operations |
| | | this.renderOperations(); |
| | | } |
| | | |
| | | getStatusLabel(status) { |
| | | const labels = { |
| | | 'queued': 'Queued', |
| | | 'localProcessing': 'Processing locally', |
| | | 'uploading': 'Uploading', |
| | | 'pending': 'Waiting on server', |
| | | 'processing': 'Processing', |
| | | 'completed': 'Completed', |
| | | 'failed': 'Failed (will retry)', |
| | | 'failed_permanent': 'Failed permanently' |
| | | }; |
| | | return labels[status] || status; |
| | | } |
| | | |
| | | getItemMessage(item) { |
| | | if (item.message) return item.message; |
| | | if (item.error_message) return item.error_message; |
| | | |
| | | switch(item.status) { |
| | | case 'queued': |
| | | return 'Waiting to send...'; |
| | | case 'uploading': |
| | | return 'Sending to server...'; |
| | | case 'pending': |
| | | return item.position ? `Position ${item.position} in queue` : 'In server queue'; |
| | | case 'processing': |
| | | return item.progress ? `${item.progress}% complete` : 'Processing...'; |
| | | case 'completed': |
| | | return 'Successfully completed'; |
| | | case 'failed': |
| | | return `Failed: ${item.lastError || 'Unknown error'} (Retry ${item.retries}/${this.config.maxRetries})`; |
| | | case 'failed_permanent': |
| | | return `Failed: ${item.lastError || 'Unknown error'}`; |
| | | default: |
| | | return ''; |
| | | } |
| | | } |
| | | |
| | | calculateProgress(item) { |
| | | if (item.progress) return item.progress; |
| | | |
| | | // Estimate progress based on status |
| | | const statusProgress = { |
| | | 'queued': 10, |
| | | 'uploading': 25, |
| | | 'pending': 40, |
| | | 'processing': 70, |
| | | 'completed': 100, |
| | | 'failed': 0, |
| | | 'failed_permanent': 0 |
| | | }; |
| | | |
| | | return statusProgress[item.status] || 0; |
| | | } |
| | | |
| | | |
| | | renderOperations() { |
| | | if (!this.ui.itemsContainer) return; |
| | | |
| | | const operations = this.store.getFiltered(); |
| | | |
| | | // Clear container |
| | | window.removeChildren(this.ui.itemsContainer); |
| | | |
| | | // Render operations or empty state |
| | | if (operations.length === 0) { |
| | | let empty = window.getTemplate('emptyQueue'); |
| | | this.ui.itemsContainer.append(empty); |
| | | this.a11y.announce('Nothing queued.'); |
| | | } else { |
| | | operations.forEach(op => { |
| | | const element = this.createOperationUI(op); |
| | | this.ui.itemsContainer.prepend(element); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | createOperationUI(operation) { |
| | | const listItem = window.getTemplate('queueItem'); |
| | | listItem.dataset.id = operation.id; |
| | | |
| | | this.updateOperationUI(operation, listItem); |
| | | return listItem; |
| | | } |
| | | |
| | | updateOperationUI(item, element = null) { |
| | | if (!element) { |
| | | element = this.ui.itemsContainer?.querySelector(`[data-id="${item.id}"]`); |
| | | } |
| | | if (!element) { |
| | | element = this.createOperationUI(item); |
| | | } |
| | | |
| | | // Remove old status classes |
| | | this.statuses.forEach(status => element.classList.remove(status)); |
| | | element.classList.add(item.status); |
| | | |
| | | // Update content |
| | | let timeDisplay = ''; |
| | | |
| | | if (item.updated_at) { |
| | | // Server now sends ISO format timestamps - much more reliable! |
| | | timeDisplay = window.formatTimeAgo(new Date(item.updated_at)); |
| | | } else if (item.created_at) { |
| | | timeDisplay = window.formatTimeAgo(new Date(item.created_at)); |
| | | } |
| | | const progressPercent = this.calculateProgress(item); |
| | | |
| | | // Update text content safely |
| | | const typeEl = element.querySelector('.type'); |
| | | const statusEl = element.querySelector('.status'); |
| | | const detailsEl = element.querySelector('.info .details'); |
| | | const timeEl = element.querySelector('.info .time'); |
| | | const progressFill = element.querySelector('.progress .fill'); |
| | | |
| | | if (typeEl) typeEl.textContent = item.title; |
| | | if (statusEl) { |
| | | statusEl.querySelector('.icon')?.remove(); |
| | | let status = this.getStatusLabel(item.status); |
| | | statusEl.title = status; |
| | | statusEl.prepend(window.getIcon(this.icons[item.status])); |
| | | statusEl.querySelector('span').textContent = status; |
| | | } |
| | | if (detailsEl) detailsEl.textContent = this.getItemMessage(item); |
| | | if (timeEl) timeEl.textContent = timeDisplay; |
| | | if (progressFill) progressFill.style.width = `${progressPercent}%`; |
| | | |
| | | // Update action buttons |
| | | const actionsContainer = element.querySelector('.actions'); |
| | | if (actionsContainer) { |
| | | this.updateActionButtons(item, actionsContainer); |
| | | } |
| | | } |
| | | |
| | | updateActionButtons(item, container) { |
| | | window.removeChildren(container); |
| | | |
| | | switch (item.status) { |
| | | case 'queued': |
| | | case 'localProcessing': |
| | | case 'pending': |
| | | // Show cancel button for in-progress items |
| | | const cancelBtn = window.getTemplate('button'); |
| | | cancelBtn.classList.add('cancel'); |
| | | cancelBtn.dataset.action = 'cancel'; |
| | | cancelBtn.textContent = 'Cancel'; |
| | | container.appendChild(cancelBtn); |
| | | break; |
| | | |
| | | case 'failed': |
| | | case 'failed_permanent': |
| | | // Show retry and dismiss buttons |
| | | const retryBtn = window.getTemplate('button'); |
| | | const dismissBtn = window.getTemplate('button'); |
| | | |
| | | retryBtn.classList.add('retry'); |
| | | retryBtn.textContent = 'Retry'; |
| | | retryBtn.disabled = item.retries >= this.maxRetries; |
| | | retryBtn.dataset.action = 'retry'; |
| | | |
| | | dismissBtn.classList.add('dismiss'); |
| | | dismissBtn.textContent = 'Dismiss'; |
| | | dismissBtn.dataset.action = 'dismiss'; |
| | | |
| | | container.appendChild(retryBtn); |
| | | container.appendChild(dismissBtn); |
| | | break; |
| | | |
| | | case 'completed': |
| | | // Show dismiss button only |
| | | const dismissCompletedBtn = window.getTemplate('button'); |
| | | dismissCompletedBtn.dataset.action = 'dismiss'; |
| | | dismissCompletedBtn.classList.add('dismiss'); |
| | | dismissCompletedBtn.textContent = 'Dismiss'; |
| | | container.appendChild(dismissCompletedBtn); |
| | | break; |
| | | } |
| | | } |
| | | |
| | | removeOperationFromUI(operationId) { |
| | | const element = this.ui.itemsContainer?.querySelector(`[data-id="${operationId}"]`); |
| | | if (element) { |
| | | element.style.opacity = '0'; |
| | | element.style.transform = 'scale(0.9)'; |
| | | setTimeout(() => element.remove(), 300); |
| | | } |
| | | } |
| | | |
| | | updateStatusPanel(status) { |
| | | this.ui.panel?.classList.remove(...this.classes); |
| | | if (!this.classes.includes(status)) { |
| | | return; |
| | | } |
| | | this.ui.panel?.classList.add(status); |
| | | } |
| | | |
| | | /*************************************************** |
| | | FILTERS |
| | | **************************************************/ |
| | | setFilter(filter) { |
| | | // Update active button |
| | | Object.values(this.ui.filters).forEach(button => { |
| | | if (button) { |
| | | button.classList.toggle('active', button.dataset.filter === filter); |
| | | } |
| | | }); |
| | | |
| | | if (filter === 'all') { |
| | | this.store.clearFilters(); |
| | | } else { |
| | | this.store.setFilter('status', filter); |
| | | } |
| | | } |
| | | |
| | | /************************************************************************** |
| | | HELPERS |
| | | **************************************************************************/ |
| | | getOperationsByStatus(status, include = true) { |
| | | |
| | | if (!Array.isArray(status) && typeof status === 'string') { |
| | | status = [status]; |
| | | } |
| | | return (include) |
| | | ? Array.from(this.store.data.values()).filter((item) => status.includes(item.status)) |
| | | : Array.from(this.store.data.values()).filter((item) => !status.includes(item.status)); |
| | | } |
| | | hasQueuedOperations() { |
| | | return this.getOperationsByStatus('queued').length > 0; |
| | | } |
| | | subscribe(callback) { |
| | | if (!this.subscribers) { |
| | | return; |
| | | } |
| | | this.subscribers.add(callback); |
| | | return () => this.subscribers.delete(callback); |
| | | } |
| | | |
| | | notify(event, data) { |
| | | this.subscribers.forEach(cb => cb(event, data)); |
| | | } |
| | | |
| | | /************************************************************************** |
| | | CLEANUP |
| | | **************************************************************************/ |
| | | destroy() { |
| | | this.stopPolling(); |
| | | this.stopActivityTracking(); |
| | | |
| | | if (this.clickHandler) { |
| | | document.removeEventListener('click', this.clickHandler); |
| | | } |
| | | |
| | | if (this.keyHandler) { |
| | | document.removeEventListener('keydown', this.keyHandler); |
| | | } |
| | | |
| | | this.subscribers.clear(); |
| | | } |
| | | } |
| | | |
| | | document.addEventListener('DOMContentLoaded', async function() { |
| | | window.auth.subscribe((event) => { |
| | | if (event === 'auth-loaded') { |
| | | window.jvbQueue = new QueueManager(); |
| | | } |
| | | }); |
| | | }); |
| | |
| | | |
| | | initElements() { |
| | | this.selectors = { |
| | | details: 'details.create-new', |
| | | details: 'details.create-term', |
| | | parent: '#select_parent', |
| | | summary: '.create-new summary', |
| | | summary: '.create-term summary', |
| | | suggestion: '.term-suggestions', |
| | | name: '#term_name', |
| | | button: '.submit-term', |
| | |
| | | this.ui = window.uiFromSelectors(this.selectors, this.selector.container); |
| | | } |
| | | |
| | | handleOpen(field) { |
| | | this.field = field; |
| | | if (this.ui.details) { |
| | | this.ui.details.hidden = !field.canCreate; |
| | | |
| | | if (this.ui.summary) { |
| | | this.ui.summary.textContent = `Add new ${field.singular}`; |
| | | } |
| | | if (this.ui.label.name) { |
| | | this.ui.label.name.textContent = `Name this ${field.singular}`; |
| | | } |
| | | if (this.ui.label.parent) { |
| | | this.ui.label.parent.textContent = `Nest it under`; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Initialize event listeners |
| | | */ |
| | |
| | | |
| | | if (this.ui.form) { |
| | | this.ui.form.addEventListener('change', (e) => { |
| | | console.log('Creator form change, prevents default'); |
| | | e.preventDefault(); |
| | | e.stopPropagation(); |
| | | }) |
| | | }); |
| | | } |
| | | } |
| | | |
| | |
| | | this.ui.name?.focus(); |
| | | } |
| | | this.resetParentOptions(); |
| | | } |
| | | |
| | | // Handle term creation submission |
| | | if (window.targetCheck(e, this.selectors.button)) { |
| | | this.handleTermCreation(e).then(() => {}); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Handle term creation from modal form |
| | | */ |
| | | async handleTermCreation(e) { |
| | | let field = this.selector.currentField(); |
| | | const taxonomy = field.taxonomy; |
| | | if (!taxonomy) return; |
| | | |
| | | let data = { |
| | | parent: 0, |
| | | taxonomy: taxonomy |
| | | }; |
| | | // If it's open, we can get the parent element and everything |
| | | if (this.selector.container.open) { |
| | | data.name = this.ui.name.value.trim(); |
| | | data.parent = parseInt(this.ui.parent?.value??0); |
| | | } else if (this.selector.activeField) { |
| | | //It's autocomplete, so just the term name |
| | | data.name = this.selector.store.query; |
| | | } |
| | | if (!data.name || data.name.length < 2) return; |
| | | |
| | | async handleTermCreation(data) { |
| | | if (!data.name || data.name.length < 2) return false; |
| | | try { |
| | | if (this.ui.button) { |
| | | this.ui.button.disabled = true; |
| | | } |
| | | |
| | | const response = await this.createTerm(data); |
| | | |
| | | if (!response.success) { |
| | | // Term already exists - still add it |
| | | if (response.term && response.term.id) { |
| | | this.selector.setMessage(true, `Using existing "${response.term.name}"`); |
| | | return response.term; |
| | | } |
| | | |
| | | // Other failure |
| | | this.selector.setMessage(true, response.message || 'Creation failed', false); |
| | | return false; |
| | | } |
| | | if (response.term?.pending) { |
| | | // Term requires approval |
| | | this.selector.setMessage( |
| | | true, |
| | | `"${data.name}" submitted for approval`, |
| | | false |
| | | ); |
| | | // Don't add to selection since it's pending |
| | | return false; |
| | | } |
| | | if (response.success && response.term) { |
| | | await this.handleSuccessfulCreation(response.term, data); |
| | | this.clearForm(); |
| | | } |
| | | return response.term; |
| | | } catch (error) { |
| | | console.error('Error creating term:', error); |
| | | this.selector.handleError(error, 'handleTermCreation'); |
| | | } finally { |
| | | if (this.ui.button) { |
| | | this.ui.button.disabled = false; |
| | | } |
| | | return false; |
| | | } |
| | | } |
| | | |
| | |
| | | * Handle successful term creation |
| | | */ |
| | | async handleSuccessfulCreation(term, data) { |
| | | const termPath = term.path || term.name; |
| | | // Close create form |
| | | this.ui.details.open = false; |
| | | |
| | | // Clear cache to ensure fresh data |
| | | await this.selector.store.clearCache(); |
| | | |
| | | // Add to DataStore |
| | | this.selector.store.data.set(term.id, { |
| | | id: term.id, |
| | | name: term.name, |
| | | path: termPath, |
| | | taxonomy: data.taxonomy, |
| | | parent: data.parent, |
| | | count: 0, |
| | | hasChildren: false, |
| | | slug: term.slug || term.name.toLowerCase().replace(/\s+/g, '-') |
| | | }); |
| | | |
| | | // Add to modal selection |
| | | this.selector.addSelected(term.id,this.selector.activeField); |
| | | |
| | | // Refresh current view if we're viewing the same parent |
| | | if (this.selector.container.open) { |
| | | const currentParent = this.selector.store.filters.parent || 0; |
| | | if (currentParent === data.parent) { |
| | | await this.selector.store.setFilters({ |
| | | taxonomy: data.taxonomy, |
| | | parent: data.parent, |
| | | page: 1, |
| | | search: '' |
| | | }); |
| | | } |
| | | } |
| | | this.selector.store.clearCache(); |
| | | await this.selector.store.fetch(); |
| | | } |
| | | |
| | | /** |
| | |
| | | if (this.ui.name) { |
| | | this.ui.name.value = ''; |
| | | } |
| | | if (this.selector.ui.search.input){ |
| | | this.selector.ui.search.input.value = ''; |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | class TaxonomySelector { |
| | | constructor() { |
| | | this.container = document.querySelector('dialog#jvb-selector'); |
| | |
| | | this.subscribers = new Set(); |
| | | this.fields = new Map(); |
| | | this.selectedTerms = new Map(); // a map of fieldId => Set of selected term Ids |
| | | this.loadedTaxonomies = new Set(); // a set of taxonomies, to know whether we should preload a newly registered field |
| | | this.batchFetch = new Set(); |
| | | |
| | | this.activeField = null; |
| | | this.isInitializing = true; |
| | | this.messageText = {} |
| | | this.init(); |
| | | } |
| | | |
| | |
| | | initElements() { |
| | | this.selectors = { |
| | | search: { |
| | | input: '[type=search]', |
| | | input: '[type="search"]', |
| | | clear: '.clear-search', |
| | | container: '.search-wrapper', |
| | | results: '.search-results' |
| | | results: '.search-results', |
| | | }, |
| | | create: { |
| | | button: 'button.submit-term', |
| | | span: '.submit-term span', |
| | | }, |
| | | terms: { |
| | | list: '.items-container', |
| | |
| | | child: '.toggle-children', |
| | | pathLevel: '.path-level', |
| | | }, |
| | | loading: { |
| | | loading: '.loading', |
| | | text: '.loading span', |
| | | message: { |
| | | message: 'p.message', |
| | | text: 'p.message span', |
| | | }, |
| | | selected: '.selected-items', |
| | | modal: { |
| | |
| | | }, |
| | | favourites: '.favourite-terms', |
| | | field: { |
| | | toggle: 'button.taxonomy-toggle', |
| | | toggle: 'button.taxonomy-toggle, [data-filter="taxonomy"]', |
| | | value: 'input[type="hidden"]', |
| | | selected: '.selected-items', |
| | | dropdown: '.search-results', |
| | | search: '[data-autocomplete]', |
| | | 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.ui = window.uiFromSelectors(this.selectors, this.container); |
| | | } |
| | | |
| | | initListeners() { |
| | |
| | | } |
| | | |
| | | handleClick(e) { |
| | | const fieldId = 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) { |
| | | field.ui.dropdown.hidden = true; |
| | | } |
| | | |
| | | this.scheduleHideDropdown(fieldId); |
| | | if (field.ui.search) { |
| | | field.ui.search.value = ''; |
| | | } |
| | | } |
| | | |
| | | const toggleButton = window.targetCheck(e, field.ui.toggle); |
| | | const toggleButton = window.targetCheck(e, this.selectors.field.toggle); |
| | | |
| | | if (toggleButton) { |
| | | e.preventDefault(); |
| | | this.openModal(fieldId); |
| | | return; |
| | | } |
| | | |
| | | const removeButton = window.targetCheck(e, 'button.remove-item'); |
| | | const removeButton = window.targetCheck(e, '.remove-term'); |
| | | if (removeButton) { |
| | | const fieldId = this.getFieldId(removeButton); |
| | | const termId = removeButton.closest('.selected-item').dataset.id??false; |
| | | const termId = removeButton.closest('[data-id]').dataset.id??false; |
| | | if (fieldId && termId) { |
| | | this.removeSelected(termId, fieldId); |
| | | this.removeSelected(parseInt(termId), fieldId); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | if (e.target.matches('.modal-close')) { |
| | | this.updateFieldValue(fieldId); |
| | | this.modal?.handleClose(); |
| | | return; |
| | | } |
| | |
| | | this.navigateTo(termId); |
| | | } |
| | | |
| | | const dropdown = window.targetCheck(e, field.selectors.dropdown); |
| | | const dropdown = window.targetCheck(e, this.selectors.field.dropdown); |
| | | if (dropdown) { |
| | | // reset the timer for hiding the dropdown |
| | | this.scheduleHideDropdown(fieldId); |
| | | return; |
| | | } |
| | | |
| | | const clearSearch = window.targetCheck(e, this.selectors.search.clear); |
| | |
| | | } |
| | | } |
| | | |
| | | if (this.creator) { |
| | | let button = window.targetCheck(e, this.selectors.create.button); |
| | | if (button) { |
| | | this.maybeCreateTerm(e).then(()=>{}); |
| | | } |
| | | } |
| | | |
| | | } |
| | | handleChange(e) { |
| | | 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(); |
| | | |
| | |
| | | if (!fieldId) return; |
| | | const field = this.fields.get(fieldId); |
| | | if (!field) return; |
| | | if (['checkbox', 'button'].includes(e.target.type)) return; |
| | | |
| | | e.preventDefault(); |
| | | e.stopPropagation(); |
| | | |
| | | //If it's the autocomplete field, we need to set the active field |
| | | if (!this.container.open) { |
| | | this.activeField = fieldId; |
| | | this.setField(fieldId); |
| | | } |
| | | |
| | | const query = e.target.value.trim(); |
| | | let query = e.target.value.trim(); |
| | | this.setMessage(true, `Searching for "${query}" in ${field.plural??'items'}`); |
| | | window.debouncer.schedule( |
| | | `${fieldId}-search`, |
| | | async () => { |
| | | if (this.container.open) { |
| | | window.removeChildren(this.ui.terms.list); |
| | | } |
| | | await this.store.setFilters({ |
| | | taxonomy: field.taxonomy, |
| | | search: query, |
| | | page: 1, |
| | | parent: query ? 0 : (this.store.filters.parent || 0) |
| | | }); |
| | | if (this.container.open) { |
| | | window.removeChildren(this.ui.terms.list); |
| | | } |
| | | }, |
| | | 100 |
| | | ); |
| | | } |
| | | |
| | | setField(fieldId) { |
| | | const field = this.fields.get(fieldId); |
| | | if (!field) { |
| | | console.error('No field found...'); |
| | | return; |
| | | } |
| | | this.activeField = fieldId; |
| | | this.setMessage(true, `Loading ${field.plural}...`); |
| | | this.resetFilters({taxonomy: field.taxonomy}); |
| | | } |
| | | |
| | | resetFilters(filters) { |
| | | if (!Object.hasOwn(filters, 'taxonomy')) { |
| | | return; |
| | | } |
| | | filters = { |
| | | page: 1, |
| | | search: '', |
| | | parent: 0, |
| | | ... filters |
| | | }; |
| | | this.store.setFilters(filters); |
| | | } |
| | | |
| | | handleFocus(e) { |
| | | const fieldId = this.getFieldId(e.target); |
| | | const field = this.fields.get(fieldId); |
| | |
| | | window.debouncer.cancel(`${fieldId}-search-results`); |
| | | |
| | | if (!this.container.open){ |
| | | this.activeField = fieldId; |
| | | this.preloadTaxonomy(field.taxonomy); |
| | | this.setField(fieldId); |
| | | } |
| | | } |
| | | |
| | |
| | | const fieldId = this.getFieldId(e.target); |
| | | const field = this.fields.get(fieldId); |
| | | if (!fieldId || ! field) return; |
| | | if (!field.hasAutocomplete) return; |
| | | if (!field.hasAutocomplete || this.container.open) return; |
| | | if (e.relatedTarget && field.ui.dropdown.wrapper?.contains(e.relatedTarget)) return; |
| | | |
| | | this.scheduleHideDropdown(fieldId); |
| | | } |
| | |
| | | window.debouncer.schedule( |
| | | `${fieldId}-search-results`, |
| | | () => { |
| | | this.activeField = null; |
| | | field.ui.dropdown.hidden = true; |
| | | if (!this.container.open) { |
| | | this.activeField = null; |
| | | } |
| | | if (field.ui.dropdown.wrapper) { |
| | | field.ui.dropdown.wrapper.hidden = true; |
| | | } |
| | | }, |
| | | 1500 |
| | | ); |
| | |
| | | this.container, |
| | | { |
| | | handleForm: false, |
| | | save: null, |
| | | open: null |
| | | } |
| | | ); |
| | | this.modal.subscribe((event, data) => { |
| | | switch (event) { |
| | | |
| | | case 'modal-close': |
| | | this.closeModal() |
| | | break; |
| | | } |
| | | }); |
| | | } |
| | |
| | | const field = this.fields.get(fieldId); |
| | | if (!field) return; |
| | | |
| | | this.activeField = fieldId; |
| | | this.ui.modal.title.textContent = `Select ${field.plural}`; |
| | | this.setField(fieldId); |
| | | 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; |
| | | } |
| | | if (this.ui.create.details) { |
| | | this.ui.create.details.hidden = !field.canCreate; |
| | | |
| | | if (this.ui.create.summary) { |
| | | this.ui.create.summary.textContent = `Add new ${field.singular}`; |
| | | } |
| | | if (this.ui.create.label.name) { |
| | | this.ui.create.label.name.textContent = `Name this ${field.singular}`; |
| | | } |
| | | if (this.ui.create.label.parent) { |
| | | this.ui.create.label.parent.textContent = `Nest it under`; |
| | | } |
| | | if (this.creator) { |
| | | this.creator.handleOpen(field); |
| | | } |
| | | 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(); |
| | | this.setLoading(); |
| | | |
| | | this.store.setFilters({ |
| | | taxonomy: field.taxonomy, |
| | | page: 1, |
| | | search: '', |
| | | parent: 0, |
| | | }); |
| | | |
| | | this.a11y.announce(message); |
| | | } |
| | | |
| | | closeModal() { |
| | | this.modal.handleClose(); |
| | | const field = this.fields.get(this.activeField); |
| | | if (!field) return; |
| | | this.observer.unobserve(this.ui.terms.sentinel); |
| | |
| | | const current = this.store.filters.parent; |
| | | if (current === 0) return; |
| | | let term = this.store.get(parseInt(current)); |
| | | if (!term) return; |
| | | if (!term) { |
| | | this.navigateTo(0); |
| | | return; |
| | | } |
| | | let parent = term.parent; |
| | | this.navigateTo(parseInt(parent)); |
| | | } |
| | |
| | | addTermToModal(termId) { |
| | | const term = this.store.get(termId); |
| | | if (!term) return; |
| | | const field = this.currentField(); |
| | | if (!field) return; |
| | | if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return; |
| | | |
| | | const item = window.getTemplate('selectedTerm'); |
| | | if (!item) return; |
| | | |
| | | item.dataset.id = termId; |
| | | item.querySelector('span').textContent = term.path; |
| | | item.querySelector('button').title = `Remove ${name}`; |
| | | item.dataset.taxonomy = field.taxonomy; |
| | | item.querySelector('.item-name').textContent = term.path; |
| | | item.querySelector('button').title = `Remove ${term.name}`; |
| | | |
| | | this.ui.selected.append(item); |
| | | } |
| | |
| | | |
| | | 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; |
| | | } |
| | |
| | | |
| | | |
| | | let selectors = this.selectors.field; |
| | | let button = element.querySelector('button.taxonomy-toggle'); |
| | | if (options.size === 0){ |
| | | 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; |
| | | options = button.dataset; |
| | | if (options.size === 0) return; |
| | | options = { |
| | | taxonomy: button.dataset.taxonomy, |
| | | single: button.dataset.single, |
| | | plural: button.dataset.plural, |
| | | search: Object.hasOwn(button.dataset, 'search'), |
| | | autocomplete: Object.hasOwn(button.dataset, 'autocomplete'), |
| | | creatable: Object.hasOwn(button.dataset, 'creatable') |
| | | }; |
| | | if (Object.keys(options).length === 0) return; |
| | | } else if (Object.hasOwn(options, 'toggle')) { |
| | | button = document.querySelector(options.toggle); |
| | | selectors.toggle = options.toggle; |
| | |
| | | singular: options.single??'', |
| | | plural: options.plural??'', |
| | | name: element.dataset.field, |
| | | canSearch: Object.hasOwn(options, 'search'), |
| | | canSearch: options.search??false, |
| | | limit: options.limit??0, |
| | | hasAutocomplete: Object.hasOwn(options, 'autocomplete'), |
| | | canCreate: Object.hasOwn(options, 'creatable'), |
| | | isRequired: Object.hasOwn(options, 'required'), |
| | | hasAutocomplete: options.autocomplete??false, |
| | | canCreate: options.creatable??false, |
| | | isRequired: options.required??false, |
| | | isFilter: isFilter, |
| | | toggle: button, |
| | | create: { |
| | | button: null, |
| | | span: null |
| | | }, |
| | | selectors: selectors, |
| | | ui: window.uiFromSelectors(selectors, element), |
| | | checked: false, |
| | | }; |
| | | if (!config.taxonomy) return; |
| | | |
| | | if (isFilter && !config.ui.toggle) { |
| | | config.ui.toggle = element; |
| | | } |
| | | if (!config.taxonomy) { |
| | | console.error('TaxonomySelector: Field missing taxonomy', element); |
| | | return; |
| | | } |
| | | if (!config.singular || !config.plural) { |
| | | console.warn('TaxonomySelector: Field missing singular/plural labels', element); |
| | | config.singular = config.taxonomy.replace('jvb_', ''); |
| | | config.plural = config.singular + 's'; |
| | | } |
| | | this.fields.set(fieldId, config); |
| | | |
| | | //Check for stored selected terms in hidden input |
| | | this.setSelectedFromValue(input); |
| | | this.setSelectedFromValue(fieldId, input); |
| | | |
| | | |
| | | if (this.isInitializing) { |
| | |
| | | } |
| | | |
| | | setSelectedFromValue(fieldId, input) { |
| | | if (!fieldId) return; |
| | | let field = this.fields.get(fieldId); |
| | | if (!field) return; |
| | | if (!input && !field.isFilter) return; |
| | | |
| | | let selected = new Set(); |
| | | input.value.value.trim() |
| | | .split(',') |
| | | .map(id => parseInt(id.trim())) |
| | | .filter(id => !isNaN(id)) |
| | | .forEach(id => selected.add(id)); |
| | | if (input) { |
| | | input.value.trim() |
| | | .split(',') |
| | | .map(id => parseInt(id.trim())) |
| | | .filter(id => !isNaN(id)) |
| | | .forEach(id => selected.add(id)); |
| | | } |
| | | this.selectedTerms.set(fieldId, selected); |
| | | } |
| | | |
| | |
| | | if (field.limit !== 0 && selected.size >= field.limit) return; |
| | | |
| | | selected.add(parseInt(termId)); |
| | | if (!this.container.open && !field.isFilter) { |
| | | this.updateFieldValue(fieldId); |
| | | } |
| | | this.addTermToDisplay(termId, fieldId); |
| | | this.updateFieldValue(fieldId); |
| | | this.checkLimits(fieldId); |
| | | } |
| | | removeSelected(termId, fieldId = null) { |
| | |
| | | if (!field || !term) return; |
| | | this.selectedTerms.get(fieldId).delete(parseInt(termId)); |
| | | |
| | | const selectedItem = field.ui.selected.querySelector(`[data-i"${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; |
| | | } |
| | | } |
| | | this.updateFieldValue(fieldId); |
| | | if (!this.container.open && !field.isFilter) { |
| | | this.updateFieldValue(fieldId); |
| | | } |
| | | |
| | | this.checkLimits(fieldId); |
| | | } |
| | | updateFieldValue(fieldId) { |
| | | const field = this.fields.get(fieldId); |
| | | if (!field) return; |
| | | let selected = Array.from(this.selectedTerms.get(fieldId)); |
| | | field.ui.value = selected.join(','); |
| | | field.ui.value.value = selected.join(','); |
| | | } |
| | | |
| | | 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); |
| | | } |
| | | |
| | | updateFieldFromInput(input) { |
| | | const fieldId = this.getFieldId(input); |
| | | if (!fieldId) return; |
| | | const field = this.fields.get(fieldId); |
| | | if (!fieldId || !field) return; |
| | | if(!field) return; |
| | | |
| | | this.setSelectedFromValue(fieldId, input); |
| | | this.updateFieldUI(fieldId); |
| | |
| | | |
| | | updateFieldUI(fieldId) { |
| | | const field = this.fields.get(fieldId); |
| | | let selected = this.selectedTerms.get(fieldId); |
| | | if (!field || selected.size === 0) return; |
| | | let selected = this.selectedTerms.get(fieldId)??new Set(); |
| | | if (!field || field.isFilter || selected.size === 0) return; |
| | | |
| | | Array.from(selected).forEach(termId => { |
| | | this.addTermToDisplay(termId, fieldId); |
| | |
| | | .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}`; |
| | | |
| | |
| | | }); |
| | | } |
| | | |
| | | showModalTerms(append = true, showPath = false) { |
| | | showModalTerms(showPath = false) { |
| | | const field = this.currentField(); |
| | | const terms = this.store.getFiltered(); |
| | | if (terms.size === 0) return; |
| | | if (!append) { |
| | | window.removeChildren(this.ui.terms.list); |
| | | if (terms.length === 0) { |
| | | if (this.store.filters.page??1 === 1) { |
| | | window.removeChildren(this.ui.terms.list); |
| | | } |
| | | this.setMessage(true, this.store.filters.search === '' |
| | | ? `No matching ${field.plural}.` |
| | | : `No ${field.plural} found.`, |
| | | false); |
| | | if (this.ui.terms.sentinel) { |
| | | this.observer.unobserve(this.ui.terms.sentinel); |
| | | } |
| | | } |
| | | |
| | | this.setCreateButton(true); |
| | | |
| | | if (this.ui.terms.sentinel) { |
| | | if (this.store.lastResponse?.has_more) { |
| | | this.observer.observe(this.ui.terms.sentinel); |
| | | } else { |
| | | this.observer.unobserve(this.ui.terms.sentinel); |
| | | } |
| | | } |
| | | |
| | | const currentParent = this.store.filters.parent??0; |
| | |
| | | ... term |
| | | }); |
| | | if (element) { |
| | | fragment.appendChild(element); |
| | | fragment.append(element); |
| | | } |
| | | }); |
| | | |
| | | if (terms.length > 0) { |
| | | this.setMessage(false); |
| | | } |
| | | |
| | | this.ui.terms.list.append(fragment); |
| | | } |
| | | createTermElement(term) { |
| | |
| | | let limitReached = field.limit > 0 && this.selectedTerms.get(this.activeField).size >= field.limit; |
| | | if (checkbox && label && nameSpan) { |
| | | [ |
| | | checkbox.dataset.id, |
| | | checkbox.id, |
| | | checkbox.name, |
| | | checkbox.value, |
| | |
| | | label.dataset.path, |
| | | nameSpan.textContent |
| | | ] = [ |
| | | term.id, |
| | | `${field.element.id}-${term.id}`, |
| | | `${field.container.id}-${field.taxonomy}-select`, |
| | | `${field.element.id}-${field.taxonomy}-select`, |
| | | term.id, |
| | | !isSelected && limitReached, |
| | | isSelected, |
| | |
| | | showAutocompleteTerms() { |
| | | const field = this.currentField(); |
| | | const terms = this.currentTerms(); |
| | | if (!field || terms.size ===0) return; |
| | | if (!field) return; |
| | | |
| | | const dropdown = field.ui.dropdown; |
| | | const dropdown = field.ui.dropdown.list; |
| | | if (!dropdown) return; |
| | | |
| | | window.removeChildren(dropdown); |
| | | if (terms.length === 0) { |
| | | this.showEmptyState(`No ${field.plural} found.`, dropdown); |
| | | this.setMessage(true, `No ${field.plural} found.`, false); |
| | | } else { |
| | | terms.forEach(term => { |
| | | const item = this.createAutocompleteTerm(term); |
| | | if (item) { |
| | | dropdown.append(item); |
| | | } |
| | | }) |
| | | }); |
| | | this.setMessage(false); |
| | | } |
| | | this.setCreateButton(true); |
| | | |
| | | const query = field.ui.search?.value; |
| | | if (field.canCreate && query.length >= 2 && this.creator) { |
| | | const createButton = this.createTermButton(query); |
| | | if (createButton) { |
| | | dropdown.append(createButton); |
| | | } |
| | | if (field.ui.dropdown.wrapper) { |
| | | field.ui.dropdown.wrapper.hidden = false; |
| | | } |
| | | |
| | | dropdown.hidden = false; |
| | | } |
| | | createAutocompleteTerm(term) { |
| | | const item = window.getTemplate('autocompleteItem'); |
| | |
| | | |
| | | item.dataset.id = term.id; |
| | | item.textContent = term.path || term.name; |
| | | |
| | | return item; |
| | | } |
| | | /****************************************************************** |
| | |
| | | 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; |
| | |
| | | item.querySelector('.item-name').textContent = term.path; |
| | | item.querySelector('button').title = `Remove ${term.name}`; |
| | | |
| | | field.ui.selected.append(item); |
| | | if (field.ui.selected) { |
| | | field.ui.selected.append(item); |
| | | } |
| | | |
| | | if (this.container.open) { |
| | | this.addTermToModal(termId); |
| | |
| | | if (checkbox) checkbox.checked = true; |
| | | } |
| | | } |
| | | createTermButton(query) { |
| | | const button = window.getTemplate('autocompleteButton'); |
| | | if(!button) return; |
| | | |
| | | let queryEl = button.querySelector('span'); |
| | | queryEl.textContent = `"${query}"`; |
| | | |
| | | return button; |
| | | } |
| | | |
| | | updateBreadcrumbs(termId) { |
| | | const nav = this.ui.nav.nav; |
| | |
| | | handleStoreEvent(event, data) { |
| | | const handlers = { |
| | | 'data-loaded': () => this.handleDataLoaded(), |
| | | 'filters-changed': () => this.handleFiltersChanged(), |
| | | 'filters-changed': () => this.handleFiltersChanged(data), |
| | | 'fetch-error': () => this.handleFetchError() |
| | | }; |
| | | |
| | | handlers[event]?.(); |
| | | try { |
| | | handlers[event]?.(data); |
| | | } catch (error) { |
| | | console.error(`Error handling store event "${event}":`, error); |
| | | this.setMessage(true, 'An error occurred loading data', false); |
| | | } |
| | | } |
| | | 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)); |
| | | } |
| | |
| | | } |
| | | if (this.activeField) { |
| | | this.showResults(true); |
| | | return; |
| | | } |
| | | this.setMessage(false); |
| | | } |
| | | |
| | | showResults(isAutoComplete = false) { |
| | | this.setLoading(false); |
| | | this.setMessage(false); |
| | | const terms = this.store.getFiltered(); |
| | | const filters = this.store.filters; |
| | | const response = this.store.lastResponse?.page || {}; |
| | | const isSearch = filters.search && filters.search.length > 0; |
| | | const append = filters.page > 1; |
| | | const field = this.currentField(); |
| | | |
| | | this.notify('terms-loaded', { |
| | | terms, |
| | | filters |
| | | }); |
| | | |
| | | if (terms.length === 0) { |
| | | if (!append) { |
| | | this.showEmptyState(isSearch ? `No matching ${field.plural}.` : `No ${field.plural} available.`); |
| | | } |
| | | this.observer.unobserve(this.ui.terms.sentinel); |
| | | } else { |
| | | if (!isAutoComplete) { |
| | | this.showModalTerms(append, isSearch); |
| | | |
| | | if (response.has_more) { |
| | | this.observer.observe(this.ui.terms.sentinel); |
| | | } else { |
| | | this.observer.unobserve(this.ui.terms.sentinel); |
| | | } |
| | | } else { |
| | | this.showAutocompleteTerms() |
| | | } |
| | | if (isAutoComplete) { |
| | | this.showAutocompleteTerms(); |
| | | } else { |
| | | this.showModalTerms(isSearch); |
| | | } |
| | | |
| | | this.a11y.announce(terms.length, append); |
| | | |
| | | this.a11y.announce(terms.length); |
| | | } |
| | | handleFiltersChanged() { |
| | | // if (this.modal?.open) { |
| | | // this.setLoading(); |
| | | // } |
| | | handleFiltersChanged(data) { |
| | | //maybe do something? |
| | | } |
| | | |
| | | handleFetchError(error) { |
| | | this.setLoading(false); |
| | | const field = this.currentField(); |
| | | const message = field |
| | | ? `Failed to load ${field.plural}` |
| | | : 'Failed to load data'; |
| | | |
| | | this.setMessage(true, message, false); |
| | | console.error('Store fetch error:', error); |
| | | } |
| | | async batchFetchTaxonomies() { |
| | | if (this.batchFetch.size === 0) return; |
| | | |
| | | const taxonomies = Array.from(this.batchFetch); |
| | | taxonomies.forEach(tax => this.loadedTaxonomies.add(tax)); |
| | | this.batchFetch.clear(); |
| | | |
| | | try { |
| | | taxonomies.forEach(tax => this.loadedTaxonomies.add(tax)); |
| | | |
| | | await this.store.setFilters({ |
| | | taxonomy: taxonomies.join(','), |
| | | page: 1, |
| | |
| | | } |
| | | |
| | | preloadTaxonomy(taxonomy) { |
| | | if (this.loadedTaxonomies.has(taxonomy)) return; |
| | | |
| | | this.store.setFilters( { |
| | | taxonomy: taxonomy, |
| | | page: 1, |
| | | search: '', |
| | | parent: 0 |
| | | }); |
| | | |
| | | this.loadedTaxonomies.add(taxonomy); |
| | | } |
| | | |
| | | /************************************************** |
| | | LOADING |
| | | **************************************************/ |
| | | setLoading(on = true) { |
| | | this.ui.loading.loading.hidden = on; |
| | | this.modal.classList.toggle('loading', on); |
| | | setCreateButton(show = true) { |
| | | const field = this.currentField(); |
| | | if (!field || !field.canCreate || !this.creator) return; |
| | | |
| | | if (on) { |
| | | let searchQuery = this.store.filters.search || ''; |
| | | searchQuery = searchQuery === '' ? false : searchQuery; |
| | | const currentParent = this.store.filters.parent || 0; |
| | | const message = searchQuery |
| | | ? `Searching for "${searchQuery} items` : |
| | | currentParent === 0 |
| | | ? 'loading items' |
| | | : 'loading child items'; |
| | | const conf = (this.container.open) ? this.ui : field.ui; |
| | | if (!conf.create?.button || !conf.create?.span) return; |
| | | |
| | | if (window.typeLoop && this.ui.loading.text) { |
| | | this.stopTyping = window.typeLoop(this.ui.loading.text, message); |
| | | } else { |
| | | this.ui.loading.text.textContenet = message; |
| | | } |
| | | } else { |
| | | if (this.stopTyping) { |
| | | this.stopTyping(); |
| | | this.stopTyping = null; |
| | | } |
| | | const createButton = conf.create.button; |
| | | const buttonSpan = conf.create.span; |
| | | const input = (this.container.open) ? conf.search.input : conf.search; |
| | | if (!input) return; |
| | | |
| | | let results = this.currentTerms()??[]; |
| | | let matches = results.map(t => t.name); |
| | | |
| | | let query = input.value; |
| | | const willShow = show && query.length >= 2 && !matches.includes(query); |
| | | createButton.hidden = !willShow; |
| | | if (willShow) { |
| | | buttonSpan.textContent = input.value??''; |
| | | } |
| | | } |
| | | showEmptyState(message = 'No items found.', container = null) { |
| | | if (!container) container = this.ui.terms.list; |
| | | const emptyElement = window.getTemplate('noTermResults'); |
| | | const span = emptyElement.querySelector('span'); |
| | | if (message && span) { |
| | | span.textContent = message; |
| | | async maybeCreateTerm(e) { |
| | | const field = this.currentField(); |
| | | if (!field) return; |
| | | |
| | | window.debouncer.cancel(`${field.id}-search-results`); |
| | | let data = { |
| | | taxonomy: field.taxonomy, |
| | | parent: this.store.filters.parent??0 |
| | | } |
| | | container.append(emptyElement); |
| | | //If it's autocomplete or the selector's search input, we just need the name |
| | | if (!this.container.open || this.ui.search.input.value !== '') { |
| | | data.name = (this.container.open) ? this.ui.search.input.value : field.ui.search.value; |
| | | } else { |
| | | //Otherwise, we've created it from the details element |
| | | data.parent = this.creator.ui.parent.value??data.parent; |
| | | data.name = this.creator.ui.name.value??false; |
| | | } |
| | | if (data.parent !== undefined && data.name) { |
| | | this.setMessage(true, `Creating "${data.name}"...`); |
| | | this.setCreateButton(false); |
| | | if (this.container.open) { |
| | | window.removeChildren(this.ui.terms.list); |
| | | } else { |
| | | field.ui.search.disabled = true; |
| | | window.removeChildren(field.ui.dropdown.list); |
| | | if (field.ui.dropdown.wrapper) { |
| | | field.ui.dropdown.wrapper.hidden = false; |
| | | } |
| | | } |
| | | let term = await this.creator.handleTermCreation(data); |
| | | if (term) { |
| | | this.addSelected(term.id, field.id); |
| | | } |
| | | if (!this.container.open) { |
| | | field.ui.search.disabled = false; |
| | | field.ui.search.value = ''; |
| | | } |
| | | this.scheduleHideDropdown(field.id); |
| | | this.setMessage(false); |
| | | } |
| | | } |
| | | 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 p = conf.message.message; |
| | | const pText = conf.message.text; |
| | | |
| | | p.hidden = !show; |
| | | if (show) { |
| | | if (message && pText) { |
| | | if (type && window.typeLoop && pText) { |
| | | if (this.messageText[field.id]) { |
| | | this.messageText[field.id](); |
| | | delete this.messageText[field.id]; |
| | | } |
| | | this.messageText[field.id] = window.typeLoop(pText, message); |
| | | } else { |
| | | pText.textContent = message; |
| | | } |
| | | |
| | | } |
| | | } else { |
| | | if (this.messageText[field.id]) { |
| | | this.messageText[field.id](); |
| | | delete this.messageText[field.id]; |
| | | } |
| | | } |
| | | } |
| | | /************************************************** |
| | | SUBSCRIBERS |
| | |
| | | CLEANUP |
| | | ******************************************************/ |
| | | destroy() { |
| | | // Cancel all debounced operations for this instance |
| | | this.fields.forEach((field, fieldId) => { |
| | | window.debouncer.cancel(`${fieldId}-search`); |
| | | window.debouncer.cancel(`${fieldId}-search-results`); |
| | | }); |
| | | |
| | | // Stop any typeLoop animations |
| | | Object.keys(this.messageText).forEach(key => { |
| | | if (this.messageText[key]) { |
| | | this.messageText[key](); |
| | | } |
| | | }); |
| | | this.messageText = {}; |
| | | |
| | | // Disconnect observer |
| | | if (this.ui.terms?.sentinel) { |
| | | this.observer?.unobserve(this.ui.terms.sentinel); |
| | | } |
| | | this.observer?.disconnect(); |
| | | |
| | | // Remove event listeners |
| | | document.removeEventListener('click', this.clickHandler); |
| | | document.removeEventListener('change', this.changeHandler); |
| | | document.removeEventListener('input', this.inputHandler); |
| | | document.removeEventListener('focus', this.focusHandler); |
| | | document.removeEventListener('blur', this.blurHandler); |
| | | document.removeEventListener('focus', this.focusHandler, true); |
| | | document.removeEventListener('blur', this.blurHandler, true); |
| | | |
| | | this.observer?.disconnect(); |
| | | // Clear data structures |
| | | this.subscribers.clear(); |
| | | this.fields.clear(); |
| | | this.selectedTerms.clear(); |
| | | this.batchFetch.clear(); |
| | | |
| | | // Cleanup creator if exists |
| | | if (this.creator) { |
| | | this.creator.destroy(); |
| | | this.creator = null; |
| | | } |
| | | |
| | | // Unsubscribe from store |
| | | if (this.store) { |
| | | this.store = null; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | this.stores.uploads.subscribe(this.handleStores.bind(this, 'uploads')); |
| | | this.stores.groups.subscribe(this.handleStores.bind(this, 'groups')); |
| | | this.queue.subscribe((event, operation) => { |
| | | if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) { |
| | | console.log(event, operation); |
| | | if (!Object.hasOwn(operation, 'endpoint') || !['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) { |
| | | return; |
| | | } |
| | | |
| | |
| | | * @returns {Promise<unknown>} |
| | | */ |
| | | window.typeText = function(container, text, speed = 50) { |
| | | container.classList.add('typeText'); |
| | | return new Promise((resolve) => { |
| | | // Cancel any existing animation on this container |
| | | if (container._typeInterval) { |
| | | clearInterval(container._typeInterval); |
| | | delete container._typeInterval; |
| | | } |
| | | |
| | | let index = 0; |
| | | container.textContent = ''; |
| | | |
| | | const interval = setInterval(() => { |
| | | container._typeInterval = setInterval(() => { |
| | | if (index < text.length) { |
| | | container.textContent += text.charAt(index); |
| | | index++; |
| | | } else { |
| | | clearInterval(interval); |
| | | clearInterval(container._typeInterval); |
| | | delete container._typeInterval; |
| | | resolve(); |
| | | } |
| | | }, speed); |
| | |
| | | } |
| | | |
| | | /** |
| | | * Erases text like a keyboard would. TODO: erase a set word from existing text |
| | | * Erases text like a keyboard would. |
| | | * @param container |
| | | * @param speed |
| | | * @returns {Promise<unknown>} |
| | | */ |
| | | window.eraseText = function(container, speed = 10) { |
| | | return new Promise((resolve) => { |
| | | // Cancel any existing animation on this container |
| | | if (container._eraseInterval) { |
| | | clearInterval(container._eraseInterval); |
| | | delete container._eraseInterval; |
| | | } |
| | | |
| | | let text = container.textContent; |
| | | let index = text.length; |
| | | |
| | | const interval = setInterval(() => { |
| | | container._eraseInterval = setInterval(() => { |
| | | if (index > 0) { |
| | | index--; |
| | | container.textContent = text.substring(0, index); |
| | | } else { |
| | | clearInterval(interval); |
| | | clearInterval(container._eraseInterval); |
| | | delete container._eraseInterval; |
| | | resolve(); |
| | | } |
| | | }, speed); |
| | |
| | | * @returns {Function} - Call this function to stop the loop |
| | | */ |
| | | window.typeLoop = function(container, text, typeSpeed = 50, eraseSpeed = 10, pauseAfterType = 1000, pauseAfterErase = 250) { |
| | | // Generate unique key for this container |
| | | const containerId = container.id || container.dataset.typeKey || `type-${Date.now()}`; |
| | | if (!container.dataset.typeKey) { |
| | | container.dataset.typeKey = containerId; |
| | | } |
| | | |
| | | // Stop any existing loop immediately |
| | | if (container._stopTyping) { |
| | | container._stopTyping(); |
| | | } |
| | | |
| | | let isRunning = true; |
| | | |
| | | async function loop() { |
| | | while (isRunning) { |
| | | // Type the text |
| | | await window.typeText(container, text, typeSpeed); |
| | | |
| | | // Wait 1 second |
| | | if (!isRunning) break; |
| | | await new Promise(resolve => setTimeout(resolve, pauseAfterType)); |
| | | |
| | | // Erase the text |
| | | if (!isRunning) break; |
| | | await window.eraseText(container, eraseSpeed); |
| | | |
| | | // Wait 0.25 seconds before next iteration |
| | | if (!isRunning) break; |
| | | await new Promise(resolve => setTimeout(resolve, pauseAfterErase)); |
| | | } |
| | | } |
| | | |
| | | // Start the loop |
| | | loop(); |
| | | |
| | | // Return a function to stop the loop |
| | | return function stopLoop() { |
| | | const stopLoop = function() { |
| | | isRunning = false; |
| | | if (container._typeInterval) { |
| | | clearInterval(container._typeInterval); |
| | | delete container._typeInterval; |
| | | } |
| | | if (container._eraseInterval) { |
| | | clearInterval(container._eraseInterval); |
| | | delete container._eraseInterval; |
| | | } |
| | | }; |
| | | |
| | | container._stopTyping = stopLoop; |
| | | loop(); // Start immediately |
| | | |
| | | return stopLoop; |
| | | }; |
| | | |
| | | window.toCamelCase = function (string) { |
| | |
| | | window.jvbTaxCreator=class{constructor(e){this.selector=e,this.queue=window.jvbQueue,this.initElements(),this.initListeners()}initElements(){this.selectors={details:"details.create-new",parent:"#select_parent",summary:".create-new 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)}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler),this.ui.form&&this.ui.form.addEventListener("change",(e=>{e.preventDefault(),e.stopPropagation()}))}handleClick(e){window.targetCheck(e,this.selectors.summary)&&(this.ui.details.open&&this.ui.name?.focus(),this.resetParentOptions()),window.targetCheck(e,this.selectors.button)&&this.handleTermCreation(e).then((()=>{}))}async handleTermCreation(e){const t=this.selector.currentField().taxonomy;if(!t)return;let r={parent:0,taxonomy:t};if(this.selector.container.open?(r.name=this.ui.name.value.trim(),r.parent=parseInt(this.ui.parent?.value??0)):this.selector.activeField&&(r.name=this.selector.store.query),r.name&&!(r.name.length<2))try{this.ui.button&&(this.ui.button.disabled=!0);const e=await this.createTerm(r);e.success&&e.term&&(await this.handleSuccessfulCreation(e.term,r),this.clearForm())}catch(e){console.error("Error creating term:",e),this.selector.handleError(e,"handleTermCreation")}finally{this.ui.button&&(this.ui.button.disabled=!1)}}async handleSuccessfulCreation(e,t){const r=e.path||e.name;this.ui.details.open=!1,await this.selector.store.clearCache(),this.selector.store.data.set(e.id,{id:e.id,name:e.name,path:r,taxonomy:t.taxonomy,parent:t.parent,count:0,hasChildren:!1,slug:e.slug||e.name.toLowerCase().replace(/\s+/g,"-")}),this.selector.addSelected(e.id,this.selector.activeField),this.selector.container.open&&(this.selector.store.filters.parent||0)===t.parent&&await this.selector.store.setFilters({taxonomy:t.taxonomy,parent:t.parent,page:1,search:""})}resetParentOptions(){const e=this.selector.currentField();if(!e)return;const t=e.taxonomy;if(!t)return;if(!this.ui.parent)return;let r=this.ui.parent.querySelector("option");if(!r)return;window.removeChildren(this.ui.parent),this.ui.parent.append(r.cloneNode(!0));const i=this.selector.store.filters.parent||0;if(0!==i){const e=this.selector.store.get(i);if(e){let t=r.cloneNode(!0);t.value=e.id,t.textContent=e.name,this.ui.parent.append(t)}}const n=[];this.selector.store.getFiltered().forEach((e=>{e.taxonomy===t&&e.parent===i&&n.push(e)})),n.sort(((e,t)=>e.name.localeCompare(t.name))),n.forEach((e=>{let t=r.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="")}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)}}; |
| | |
| | | (()=>{class e{constructor(e){if(this.queue=window.jvbQueue,this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.a11y=window.jvbA11y,!this.content)return;this.isTimeline=!1,this.currentItemID=null,this.initElements(),this.updateBulkOptions();const t=window.jvbStore.register(this.content,{storeName:this.content,keyPath:"id",endpoint:"content",headers:{action_nonce:window.auth.getNonce("dash")},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:{content:this.content,user:window.auth.getUser(),page:1,status:"all",orderby:"modified",order:"desc"},TTL:18e5,showLoading:!0});this.store=t[this.content],this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.tableForm=null,this.tableChanges=new Map,this.formController=this.isTimeline?new window.jvbForm({collectFormData:()=>this.collectTimelineData.bind(this)}):new window.jvbForm,this.viewController.subscribe(((e,t)=>{if("table-view"!==e||this.tableForm){if("not-table-view"===e)this.tableForm;else if("order-changed"===e){let e=this.store.get(t);if(!e)return;let s={};s[t]=e,this.savePosts(s,"Updating progression order")}}else this.tableForm||(this.tableForm=this.formController.registerForm(t,{autosave:!1,formStatus:!1,isTable:!0}))})),this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&["content","uploads/groups"].includes(t.endpoint)&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initialized=!1,this.init()}handleFormChange(e,t){let s=t.fullData.post_title,i=Object.hasOwn(t,"changes")?t.changes:t.fullData,l={};if(this.isTimeline)return l[this.currentItemID]=i,void this.savePosts(l,s);let o=[];switch(!0){case t.config.element===this.ui.forms.edit:l[this.currentItemID]=i,s=`Saving ${s} Changes`,i.post_status&&this.shouldRemoveItem(i.post_status)&&o.push(this.currentItemID);break;case t.config.element===this.ui.forms.bulkEdit:let a=t.config.element.querySelectorAll(".selected input:checked");a.forEach((e=>{l[e.value]=i,i.post_status&&this.shouldRemoveItem(i.post_status)&&o.push(e.value)})),s=`Updating ${a.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(l[t.config.data["form-id"]]=i,s=`Saving ${s} Changes`)}if(o.length>0){let e=0;o.forEach((t=>{setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50})),t.config.element===this.ui.forms.bulkEdit&&setTimeout((()=>{this.viewController.clearSelection()}),e+100)}0!==Object.keys(l).length&&this.savePosts(l,s)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(0===Object.keys(e).length)return;for(let t in e)e[t].content||(e[t].content=this.content);let s={endpoint:"content",headers:{action_nonce:window.auth.getNonce("dash")},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(s)}async handleQueueSuccess(e,t){this.store.clearCache(),this.store.fetch()}handleQueueFailure(e,t){console.error("Operation failed permanently:",t),this.a11y?.announce(`Operation failed: ${t.error_message||"Unknown error"}`)}initElements(){this.elements={modals:{create:"dialog.create",edit:"dialog.edit",bulkEdit:"dialog.bulkEdit"},container:".crud[data-content]",grid:".item-grid",bulkSelectActions:".bulk-action-select",forms:{create:"dialog.create form",edit:"dialog.edit form",bulkEdit:"dialog.bulkEdit form"},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.elements),this.ui.uploader&&(window.jvbUploads.scanFields(document.querySelector(this.elements.uploader)),window.jvbUploads.subscribe(((e,t)=>{"sent-to-queue"===e&&(console.log(t),t===this.ui.uploader.querySelector("[data-uploader]")?.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearHttpHeaders()})))}))),this.isTimeline=!!document.querySelector("[data-timeline]")}init(){this.ui.uploader&&(this.settings.addSetting(this.ui.uploader,"open"),this.ui.uploader.addEventListener("toggle",(e=>{this.settings.saveSetting("open",this.ui.uploader.open?"on":"off")}))),this.filterHandler=this.handleFilterChange.bind(this),this.changeHandler=this.handleChange.bind(this),this.modals={};for(let[e,t]of Object.entries(this.ui.modals))this.modals[e]=new window.jvbModal(t),this.modals[e].subscribe(((t,s)=>{if("modal-close"===t)this.currentItemID=null,this.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId)}));this.setupEventDelegation(),this.setupFilters(),this.initialized=!0}setupEventDelegation(){document.addEventListener("change",this.changeHandler),document.addEventListener("click",(e=>{const t=e.target.closest("[data-action]");if(t){e.preventDefault();const s=t.dataset.action,i=t.dataset.id;switch(s){case"edit":this.populateEditForm(i),this.modals.edit.handleOpen();break;case"delete":if(confirm("Delete this item?")){let e={};e[t.dataset.id]={post_status:"delete",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`),this.store.delete(i)}break;case"trash":let e={};e[t.dataset.id]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"create":this.modals.create.dataset.itemId="new",this.modals.create.dataset.content=this.content,this.modals.create.handleOpen();break;case"bulk-edit":Array.from(this.viewController.selectedItems).length>0&&this.modals.bulkEdit.handleOpen();break;case"bulk-delete":const s=Array.from(this.viewController.selectedItems);s.length>0&&confirm(`Delete ${s.length} items?`)&&(s.forEach((e=>this.store.delete(e))),this.viewController.clearSelection());break;case"sync":break;case"refresh":this.store.fetch()}}e.target.closest(".create-item")&&(this.formController.registerForm(this.ui.forms.create),this.modals.create.handleOpen()),e.target.closest(".cancel-bulk")&&this.viewController.selectAll(!1)})),document.addEventListener("keydown",(e=>{(e.ctrlKey||e.metaKey)&&"a"===e.key&&this.ui.container&&this.ui.container.contains(document.activeElement)&&(e.preventDefault(),this.viewController.selectAll()),"Escape"===e.key&&this.viewController?.selectedItems.size>0&&0===window.jvbModal.getAllModals().length&&this.viewController.clearSelection()}))}handleChange(e){if(e.target.closest("[data-id]"))this.isTimeline?this.handleTimelineTableChange(e):this.handleTableChange(e);else{if(e.target.classList.contains("bulk-action-select")){if(e.target.value.startsWith("tax-")){const t=e.target.value.replace("tax-","");return this.openTaxonomyModal(t),void(e.target.value="")}switch(e.target.value){case"edit":this.populateBulkEdit(),this.modals.bulkEdit.handleOpen();break;case"publish":this.setBulkStatus("publish");break;case"draft":case"restore":this.setBulkStatus("draft");break;case"trash":this.setBulkStatus("trash");break;case"delete":this.setBulkStatus("delete")}}window.targetCheck(e,"select[data-filter]")&&this.handleFilterChange(e)}}handleTableChange(e){const t=e.target.closest("tr[data-id]");if(!t)return;const s=e.target,i=parseInt(t.dataset.id),l=s.closest(["data-field"])?.dataset.field;if(!l)return;const o=this.store.get(i);if(!o)return;o.fields[l]=this.getInputValue(s),this.store.save(o);let a={};a[i]=o.fields,this.savePosts(a,`Saving changes to ${this.content}`)}handleTimelineTableChange(e){const t=e.target.closest("tbody[data-id]");if(!t)return;const s=e.target,i=s.closest("[data-field]")?.dataset.field;if(!i)return;const l=parseInt(t.dataset.id),o=s.closest("tr.timeline-point"),a=this.store.get(l);if(!a)return;const n=this.getInputValue(s);if(o){const e=o.dataset.imageId;a.fields.timeline||(a.fields.timeline={}),a.fields.timeline[e]||(a.fields.timeline[e]={}),a.fields.timeline[e][i]=n}else a.fields[i]=n;this.store.save(a);let r={};r[l]=a.fields,this.savePosts(r,"Updating progress post")}getInputValue(e){return"checkbox"===e.type?e.checked?e.value||"1":"":"radio"===e.type?e.checked?e.value:null:e.value}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(e.length>0){e=e.join(",");let s={},i=Array.from(this.viewController.selectedItems);i.forEach((i=>{s[i]={content:this.content},s[i][t]=e}));let l=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,l)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,s={};for(let t of this.viewController.selectedItems)s[t]={post_status:e,content:this.content};if("delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";if("all"===this.status&&!["publish","draft"].includes(e)||e!==this.status){let e=0;for(let t of this.viewController.selectedItems)setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50}this.viewController.clearSelection(),0!==Object.keys(s).length&&this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target;if("taxonomies"===t.dataset.filter){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,t.value)}else this[t.dataset.filter]=t.value,this.store.setFilter(t.dataset.filter,t.value),"status"===t.dataset.filter&&this.updateBulkOptions(t.value)}updateBulkOptions(e="all"){if("trash"===e){if(this.ui.bulkSelectActions?.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("trashOptions").querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulkSelectActions.append(e)}))}}else if(this.ui.bulkSelectActions&&!this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}this.ui.bulkSelectActions&&(this.ui.bulkSelectActions.value="")}populateBulkEdit(){const e=this.modals.bulkEdit.modal.querySelector("form .selected");if(!e)return;window.removeChildren(e);for(let t of this.viewController.selectedItems){let s=this.store.get(t);const i=window.getTemplate("bulkItem");if(!i)return;const l=i.querySelector("input[type=checkbox]"),o=i.querySelector("img");l&&(l.id=`bulk_${s.id}`,l.value=s.id,l.checked=!0),o&&s.thumbnail&&(o.src=s.thumbnail,o.alt=s.alt||""),e.append(i)}let t=this.modals.bulkEdit.modal;[t.querySelector("h2 span").textContent]=[this.viewController.selectedItems.size],this.formController.registerForm(this.ui.forms.bulkEdit)}populateEditForm(e){this.currentItemID=e;let t=this.store.get(parseInt(e));if(t){this.ui.modals.edit.dataset.itemId=e,this.ui.modals.edit.dataset.content=this.content;let s=this.ui.modals.edit.querySelector("form");this.ui.modals.edit.querySelector("h2").textContent=`Editing ${t.fields.post_title}`,s.dataset.formId=`edit-${e}`,new window.jvbPopulate(s,t),this.formController.registerForm(this.ui.forms.edit)}}setupFilters(){const e=document.querySelector('input[type="search"]');if(e){let t;e.addEventListener("input",(()=>{e.value.length>3?(clearTimeout(t),t=setTimeout((()=>{this.store.setFilter("search",e.value)}),300)):0===e.value.length&&this.store.removeFilter("search")}))}}destroy(){document.querySelectorAll("[data-filter]").forEach((e=>{e.removeEventListener("change",this.filterHandler)}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{if("auth-loaded"===t){let t=document.querySelector("[data-content]");t&&!Object.hasOwn(t.dataset,"ignore")&&(window.crudManager=new e({content:t.dataset.content}))}}))}))})(); |
| | | (()=>{class e{constructor(e){if(this.queue=window.jvbQueue,this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.a11y=window.jvbA11y,!this.content)return;this.isTimeline=!1,this.currentItemID=null,this.initElements(),this.updateBulkOptions();const t=window.jvbStore.register(this.content,{storeName:this.content,keyPath:"id",endpoint:"content",headers:{action_nonce:window.auth.getNonce("dash")},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:{content:this.content,user:window.auth.getUser(),page:1,status:"all",orderby:"modified",order:"desc"},TTL:18e5,showLoading:!0});this.store=t[this.content],this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.tableForm=null,this.tableChanges=new Map,this.formController=this.isTimeline?new window.jvbForm({collectFormData:()=>this.collectTimelineData.bind(this)}):new window.jvbForm,this.viewController.subscribe(((e,t)=>{if("table-view"!==e||this.tableForm){if("not-table-view"===e)this.tableForm;else if("order-changed"===e){let e=this.store.get(t);if(!e)return;let s={};s[t]=e,this.savePosts(s,"Updating progression order")}}else this.tableForm||(this.tableForm=this.formController.registerForm(t,{autosave:!1,formStatus:!1,isTable:!0}))})),this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&["content","uploads/groups"].includes(t.endpoint)&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initialized=!1,this.init()}handleFormChange(e,t){let s=t.fullData.post_title,i=Object.hasOwn(t,"changes")?t.changes:t.fullData,l={};if(this.isTimeline)return l[this.currentItemID]=i,void this.savePosts(l,s);let o=[];switch(console.log(t),!0){case t.config.element===this.ui.forms.edit:l[this.currentItemID]=i,s=`Saving ${s} Changes`,i.post_status&&this.shouldRemoveItem(i.post_status)&&o.push(this.currentItemID);break;case t.config.element===this.ui.forms.bulkEdit:let a=t.config.element.querySelectorAll(".selected input:checked");a.forEach((e=>{l[e.value]=i,i.post_status&&this.shouldRemoveItem(i.post_status)&&o.push(e.value)})),s=`Updating ${a.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(l[t.config.data["form-id"]]=i,s=`Saving ${s} Changes`)}if(o.length>0){let e=0;o.forEach((t=>{setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50})),t.config.element===this.ui.forms.bulkEdit&&setTimeout((()=>{this.viewController.clearSelection()}),e+100)}0!==Object.keys(l).length&&this.savePosts(l,s)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(0===Object.keys(e).length)return;for(let t in e)e[t].content||(e[t].content=this.content);let s={endpoint:"content",headers:{action_nonce:window.auth.getNonce("dash")},data:{posts:e},delay:!0,popup:"Saving changes",title:t};this.queue.addToQueue(s)}async handleQueueSuccess(e,t){this.store.clearCache(),this.store.fetch()}handleQueueFailure(e,t){console.error("Operation failed permanently:",t),this.a11y?.announce(`Operation failed: ${t.error_message||"Unknown error"}`)}initElements(){this.elements={modals:{create:"dialog.create",edit:"dialog.edit",bulkEdit:"dialog.bulkEdit"},container:".crud[data-content]",grid:".item-grid",bulkSelectActions:".bulk-action-select",forms:{create:"dialog.create form",edit:"dialog.edit form",bulkEdit:"dialog.bulkEdit form"},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.elements),this.ui.uploader&&(window.jvbUploads.scanFields(document.querySelector(this.elements.uploader)),window.jvbUploads.subscribe(((e,t)=>{"sent-to-queue"===e&&(console.log(t),t===this.ui.uploader.querySelector("[data-uploader]")?.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearCache()})))}))),this.isTimeline=!!document.querySelector("[data-timeline]")}init(){this.ui.uploader&&(this.settings.addSetting(this.ui.uploader,"open"),this.ui.uploader.addEventListener("toggle",(e=>{this.settings.saveSetting("open",this.ui.uploader.open?"on":"off")}))),this.filterHandler=this.handleFilterChange.bind(this),this.changeHandler=this.handleChange.bind(this),this.modals={};for(let[e,t]of Object.entries(this.ui.modals))this.modals[e]=new window.jvbModal(t),this.modals[e].subscribe(((t,s)=>{if("modal-close"===t)this.currentItemID=null,this.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId)}));this.setupEventDelegation(),this.setupFilters(),this.initialized=!0}setupEventDelegation(){document.addEventListener("change",this.changeHandler),document.addEventListener("click",(e=>{const t=e.target.closest("[data-action]");if(t){e.preventDefault();const s=t.dataset.action,i=t.dataset.id;switch(s){case"edit":this.populateEditForm(i),this.modals.edit.handleOpen();break;case"delete":if(confirm("Delete this item?")){let e={};e[t.dataset.id]={post_status:"delete",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`),this.store.delete(i)}break;case"trash":let e={};e[t.dataset.id]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"create":this.modals.create.dataset.itemId="new",this.modals.create.dataset.content=this.content,this.modals.create.handleOpen();break;case"bulk-edit":Array.from(this.viewController.selectedItems).length>0&&this.modals.bulkEdit.handleOpen();break;case"bulk-delete":const s=Array.from(this.viewController.selectedItems);s.length>0&&confirm(`Delete ${s.length} items?`)&&(s.forEach((e=>this.store.delete(e))),this.viewController.clearSelection());break;case"sync":break;case"refresh":this.store.fetch()}}e.target.closest(".create-item")&&(this.formController.registerForm(this.ui.forms.create),this.modals.create.handleOpen()),e.target.closest(".cancel-bulk")&&this.viewController.selectAll(!1)})),document.addEventListener("keydown",(e=>{(e.ctrlKey||e.metaKey)&&"a"===e.key&&this.ui.container&&this.ui.container.contains(document.activeElement)&&(e.preventDefault(),this.viewController.selectAll()),"Escape"===e.key&&this.viewController?.selectedItems.size>0&&0===window.jvbModal.getAllModals().length&&this.viewController.clearSelection()}))}handleChange(e){if(e.target.closest("[data-id]"))this.isTimeline?this.handleTimelineTableChange(e):this.handleTableChange(e);else{if(e.target.classList.contains("bulk-action-select")){if(e.target.value.startsWith("tax-")){const t=e.target.value.replace("tax-","");return this.openTaxonomyModal(t),void(e.target.value="")}switch(e.target.value){case"edit":this.populateBulkEdit(),this.modals.bulkEdit.handleOpen();break;case"publish":this.setBulkStatus("publish");break;case"draft":case"restore":this.setBulkStatus("draft");break;case"trash":this.setBulkStatus("trash");break;case"delete":this.setBulkStatus("delete")}}window.targetCheck(e,"select[data-filter]")&&this.handleFilterChange(e)}}handleTableChange(e){const t=e.target.closest("tr[data-id]");if(!t)return;const s=e.target,i=parseInt(t.dataset.id),l=s.closest(["data-field"])?.dataset.field;if(!l)return;const o=this.store.get(i);if(!o)return;o.fields[l]=this.getInputValue(s),this.store.save(o);let a={};a[i]=o.fields,this.savePosts(a,`Saving changes to ${this.content}`)}handleTimelineTableChange(e){const t=e.target.closest("tbody[data-id]");if(!t)return;const s=e.target,i=s.closest("[data-field]")?.dataset.field;if(!i)return;const l=parseInt(t.dataset.id),o=s.closest("tr.timeline-point"),a=this.store.get(l);if(!a)return;const n=this.getInputValue(s);if(o){const e=o.dataset.imageId;a.fields.timeline||(a.fields.timeline={}),a.fields.timeline[e]||(a.fields.timeline[e]={}),a.fields.timeline[e][i]=n}else a.fields[i]=n;this.store.save(a);let r={};r[l]=a.fields,this.savePosts(r,"Updating progress post")}getInputValue(e){return"checkbox"===e.type?e.checked?e.value||"1":"":"radio"===e.type?e.checked?e.value:null:e.value}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(e.length>0){e=e.join(",");let s={},i=Array.from(this.viewController.selectedItems);i.forEach((i=>{s[i]={content:this.content},s[i][t]=e}));let l=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,l)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,s={};for(let t of this.viewController.selectedItems)s[t]={post_status:e,content:this.content};if("delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";if("all"===this.status&&!["publish","draft"].includes(e)||e!==this.status){let e=0;for(let t of this.viewController.selectedItems)setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50}this.viewController.clearSelection(),0!==Object.keys(s).length&&this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target;if("taxonomies"===t.dataset.filter){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,t.value)}else this[t.dataset.filter]=t.value,this.store.setFilter(t.dataset.filter,t.value),"status"===t.dataset.filter&&this.updateBulkOptions(t.value)}updateBulkOptions(e="all"){if("trash"===e){if(this.ui.bulkSelectActions?.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("trashOptions").querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulkSelectActions.append(e)}))}}else if(this.ui.bulkSelectActions&&!this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}this.ui.bulkSelectActions&&(this.ui.bulkSelectActions.value="")}populateBulkEdit(){const e=this.modals.bulkEdit.modal.querySelector("form .selected");if(!e)return;window.removeChildren(e);for(let t of this.viewController.selectedItems){let s=this.store.get(t);const i=window.getTemplate("bulkItem");if(!i)return;const l=i.querySelector("input[type=checkbox]"),o=i.querySelector("img");l&&(l.id=`bulk_${s.id}`,l.value=s.id,l.checked=!0),o&&s.thumbnail&&(o.src=s.thumbnail,o.alt=s.alt||""),e.append(i)}let t=this.modals.bulkEdit.modal;[t.querySelector("h2 span").textContent]=[this.viewController.selectedItems.size],this.formController.registerForm(this.ui.forms.bulkEdit)}populateEditForm(e){this.currentItemID=e;let t=this.store.get(parseInt(e));if(t){this.ui.modals.edit.dataset.itemId=e,this.ui.modals.edit.dataset.content=this.content;let s=this.ui.modals.edit.querySelector("form");this.ui.modals.edit.querySelector("h2").textContent=`Editing ${t.fields.post_title}`,s.dataset.formId=`edit-${e}`,new window.jvbPopulate(s,t),this.formController.registerForm(this.ui.forms.edit)}}setupFilters(){const e=document.querySelector('.all-filters input[type="search"]');if(e){let t;e.addEventListener("input",(()=>{e.value.length>3?(clearTimeout(t),t=setTimeout((()=>{this.store.setFilter("search",e.value)}),300)):0===e.value.length&&this.store.removeFilter("search")}))}}destroy(){document.querySelectorAll("[data-filter]").forEach((e=>{e.removeEventListener("change",this.filterHandler)}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{if("auth-loaded"===t){let t=document.querySelector("[data-content]");t&&!Object.hasOwn(t.dataset,"ignore")&&(window.crudManager=new e({content:t.dataset.content}))}}))}))})(); |
| | |
| | | (()=>{class e{constructor(){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 d;o.oncomplete=()=>e(d),o.onerror=()=>a(o.error);try{d=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")};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);return r&&r.items?r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]):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})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t}),r.config.endpoint&&await this.fetch(e)}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 console.debug(`[DataStore] Converting FormData at ${s}`),{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0!==e.data&&(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:(console.debug(`[DataStore] Stripped unknown type at ${s}`),{valid:!0,data:void 0})}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}get(e,t){return this.stores.get(e).data.get(t)}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter((e=>Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&r.items)return r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]);if(t.filters.search&&t.filters.search.trim()){const e=t.filters.search.toLowerCase().trim();let s=Array.from(t.data.values()).filter((e=>{for(const[s,r]of Object.entries(t.filters))if("search"!==s&&"page"!==s&&null!=r&&""!==r&&e[s]!==r)return!1;return!0}));return s=s.filter((t=>["name","title","path","description","slug"].some((s=>{const r=t[s];return!!r&&r.toLowerCase().includes(e)})))),s}return this.getAll(e)}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t}));const a=await this.shouldFetchWithFilters(e,t,i);this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t}),r.config.endpoint&&a?await this.fetch(e):r.config.endpoint&&this.notify(e,"data-loaded")}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e]))||Object.keys(s.filters).some((e=>!(e in t)&&t!==s.config.filters)))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})(); |
| | |
| | | (()=>{class e{constructor(e={}){if(this.canUpdateUI=!0,this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...e},this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.user=window.auth.getUser(),!this.user)return console.log("Queue: User not logged in, queue disabled"),this.store=null,void(this.canUpdateUI=!1);this.headers={"X-WP-Nonce":window.auth.getNonce(),...e.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError;const t=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,delayFetch:!1});this.store=t.queue,this.classes=["offline","synced","pending"],this.initUI(),this.initListeners(),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.updateUI=()=>window.debouncer.schedule("queue-ui-update",this._updateUI.bind(this),100),this.initQueue()}async initQueue(){this.maybeStartPolling()||this.updateStatusPanel("synced"),this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-saved":this.maybeStartPolling(),this.updateUI();break;case"item-saved":console.log(t,"Item saved data"),t.previousItem&&t.previousItem.status!==t.item.status&&this.handleOperationStatusChange(t.item,t.previousItem.status),this.maybeStartPolling();break;default:this.updateUI()}}))}maybeStartPolling(){return this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&(this.startPolling(),!0)}handleOperationStatusChange(e){switch(e.status){case"completed":console.log(e),this.notify("operation-completed",e);break;case"failed":this.notify("operation-failed",e);break;case"failed_permanent":this.notify("operation-failed-permanent",e)}}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},sendNow:!1,canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return console.error("Invalid operation queued: missing endpoint or data"),null;if(t.sendNow)return this.processOperation(t).then((()=>{})),this.store.clearCache(),window.debouncer.schedule("fastQueue",this.startPolling.bind(this),200),this.showQueue(),t.id;const s=Array.from(this.store.data.values()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}return this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}setQueue(e){this.store.save(e)}updateOperationStatus(e,t){let s=this.store.get(e);s&&(s.status=t,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(e){return this.store.get(e)}clearQueue(e){this.store.delete(e)}startActivityTracking(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}this.resetActivityTimer()}resetActivityTimer(){this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}hideQueue(){this.ui.panel.hidden=!0,this.ui.toggle.hidden=!0}showQueue(){this.ui.panel.hidden=!1,this.ui.toggle.hidden=!1}setProcessing(e){this.isProcessing=e,this.ui.toggle.classList.toggle("saving",e)}async processQueue(){if(this.isProcessing)return;const e=this.getOperationsByStatus("queued");if(0!==e.length){this.setProcessing(!0);for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking(),this.maybeStartPolling()?this.showQueue():this.hideQueue()}else this.stopActivityTracking()}async processOperation(e,t=!1){try{t||(this.updateOperationStatus(e.id,"uploading"),e.data?._isFormData&&(e.data=await this.store.objectToFormData(e.data)));const s=`${this.config.apiBase}${e.endpoint}`;let i;e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),i=e.data):(i=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json");const a=await fetch(s,{method:e.method,headers:e.headers,body:i}),n=await a.json();if(t&&(e.data={}),!a.ok||!1===n.success)throw new Error(n.message||`HTTP ${a.status}`);n.id&&e.id!==n.id?e=await this.handleServerMerge(e,n):(e.status=n.status||"pending",e.serverData=n,this.updateOperationStatus(e.id,e.status)),this.a11y.announce(`${e.title} sent to server for processing.`)}catch(t){console.error("Operation failed:",t),e.retries++,e.lastError=t.message,e.retries>=this.config.maxRetries?e.status="failed_permanent":(e.status="failed",e.nextRetry=Date.now()+1e3*Math.pow(2,e.retries)),this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(s.data=window.deepMerge(s.data,e.data),s.status=t.status||"pending",s.serverData=t,this.updateOperationStatus(s.id,s.status),this.removeOperationFromUI(e.id),this.clearQueue(e.id),s):(this.clearQueue(e.id),e.id=t.id,e.status=t.status||"pending",e.serverData=t,this.updateOperationStatus(e.id,e.status),e)}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch(),this.maybeStartPolling()||(this.stopPolling(),this.updateStatusPanel("synced"))}catch(e){console.error("Polling error:",e)}}),this.config.pollInterval))}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}getOperationIds(e){return e.map((e=>e.id))}async updateServerOperations(e,t){if(0===(e=(e=Array.isArray(e)?e:e.includes(",")?e.split(","):[e]).filter((e=>{let s=this.getQueue(e);return this.getAllowedActions(s.status).includes(t)}))).length)return;const s=["cancel","dismiss"].includes(t);s&&e.forEach((e=>this.removeOperationFromUI(e)));try{const i=await fetch(`${this.config.apiBase}${this.config.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:e,action:t,user:window.auth.getUser()})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const a=await i.json();if(!a.success)throw new Error(a.message||`${t} operation failed`);return e.forEach((e=>{let i=this.getQueue(e);this.notify(`${t}-operation`,i),s?this.clearQueue(e):(i.status="queued",i.retries=0,this.setQueue(i),this.updateOperationStatus(i.id,i.status))})),"retry"===t&&this.startActivityTracking(),this.updateUI(),a}catch(s){return await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.updateServerOperations(e,t))),{success:!1,error:s.message}}}getAllowedActions(e){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[e]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=e=>{if(this.isPolling||this.isProcessing)return e.preventDefault(),"You have unsaved changes in the queue. Proceed?"},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(e){if(e.target.closest(this.selectors.panel,this.selectors.toggle))if(e.target.closest(this.selectors.refreshButton))this.store.clearCache(),this.store.clearHttpHeaders(),this.store.fetch();else if(e.target.closest(this.selectors.clearButton)){const e=this.getOperationIds(this.getOperationsByStatus("completed"));e.length>0&&this.updateServerOperations(e,"dismiss")}else if(e.target.closest(this.selectors.retryButton)){const e=this.getOperationIds(this.getOperationsByStatus("failed"));e.length>0&&this.updateServerOperations(e,"retry")}else if(e.target.closest("[data-action]")){const t=e.target.closest("[data-action]"),s=t.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,t.dataset.action)}else if(e.target.closest(".filters [data-filter]")){const t=e.target.closest("[data-filter]").dataset.filter;this.setFilter(t)}}initUI(){this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui=window.uiFromSelectors(this.selectors),this.ui.panel||(this.canUpdateUI=!1)}_updateUI(){if(!this.canUpdateUI)return;const e=Array.from(this.store.data.values()),t=this.store.lastResponse?.queue_stats||{queued:0,localProcessing:0,uploading:0,pending:0,processing:0,completed:0,failed:0,failed_permanent:0};if(this.ui.count){const s=e.length-t.completed;this.ui.count.textContent=s>0?s:"",this.ui.count.style.display=s>0?"":"none"}if(this.ui.indicator){const e=t.queued>0||t.uploading>0||t.pending>0||t.processing>0;this.ui.indicator.classList.toggle("active",e)}this.ui.clearButton.disabled=0===this.getOperationsByStatus("completed").length,this.ui.retryButton.disabled=0===this.getOperationsByStatus("failed").length&&0===this.getOperationsByStatus("failed_permanent").length,Object.entries(this.ui.filters).forEach((([s,i])=>{const a="all"===s?e.length:t[s]||0,n=i.querySelector(".count");n&&(n.textContent=a>0?a:""),i.setAttribute("data-count",a)})),this.renderOperations()}getStatusLabel(e){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[e]||e}getItemMessage(e){if(e.message)return e.message;if(e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":return e.progress?`${e.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}calculateProgress(e){if(e.progress)return e.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]||0}renderOperations(){if(!this.ui.itemsContainer)return;const e=this.store.getFiltered();if(window.removeChildren(this.ui.itemsContainer),0===e.length){let e=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(e),this.a11y.announce("Nothing queued.")}else e.forEach((e=>{const t=this.createOperationUI(e);this.ui.itemsContainer.append(t)}))}createOperationUI(e){const t=window.getTemplate("queueItem");return t.dataset.id=e.id,this.updateOperationUI(e,t),t}updateOperationUI(e,t=null){t||(t=this.ui.itemsContainer?.querySelector(`[data-id="${e.id}"]`)),t||(t=this.createOperationUI(e)),this.statuses.forEach((e=>t.classList.remove(e))),t.classList.add(e.status);let s="";e.updated_at?s=window.formatTimeAgo(new Date(e.updated_at)):e.created_at&&(s=window.formatTimeAgo(new Date(e.created_at)));const i=this.calculateProgress(e),a=t.querySelector(".type"),n=t.querySelector(".status"),r=t.querySelector(".info .details"),o=t.querySelector(".info .time"),d=t.querySelector(".progress .fill");if(a&&(a.textContent=e.title),n){n.querySelector(".icon")?.remove();let t=this.getStatusLabel(e.status);n.title=t,n.prepend(window.getIcon(this.icons[e.status])),n.querySelector("span").textContent=t}r&&(r.textContent=this.getItemMessage(e)),o&&(o.textContent=s),d&&(d.style.width=`${i}%`);const l=t.querySelector(".actions");l&&this.updateActionButtons(e,l)}updateActionButtons(e,t){switch(window.removeChildren(t),e.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",t.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),a=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=e.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",t.appendChild(i),t.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",t.appendChild(n)}}removeOperationFromUI(e){const t=this.ui.itemsContainer?.querySelector(`[data-id="${e}"]`);t&&(t.style.opacity="0",t.style.transform="scale(0.9)",setTimeout((()=>t.remove()),300))}updateStatusPanel(e){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(e)&&this.ui.panel?.classList.add(e)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t&&t.classList.toggle("active",t.dataset.filter===e)})),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}getOperationsByStatus(e,t=!0){return Array.isArray(e)||"string"!=typeof e||(e=[e]),t?Array.from(this.store.data.values()).filter((t=>e.includes(t.status))):Array.from(this.store.data.values()).filter((t=>!e.includes(t.status)))}hasQueuedOperations(){return this.getOperationsByStatus("queued").length>0}subscribe(e){if(this.subscribers)return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbQueue=new e)}))}))})(); |
| | | (()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.user=window.auth.getUser(),this.canUpdateUI=!0,this.isProcessing=!1,this.isPolling=!1,this.queue=new Map,this.items=new Map,this.subscribers=new Set,this.api=jvbSettings.api,this.endpoint="queue",this.init()}init(){this.headers={"X-WP-Nonce":window.auth.getNonce()},this.initElements(),this.initListeners(),this.initStore(),this.canUpdateUI&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle.button,name:"Queue Panel"}))}initElements(){this.panelStatuses=["syncing","synced","pending","offline"],this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.pendingStatuses=["queued","localProcessing","uploading"],this.workingStatuses=["pending","processing"],this.completedStatuses=["completed","failed","failed_permanent"],this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:{button:"button.qtoggle",indicator:".qtoggle .indicator",count:".qtoggle .count"},refresh:{button:"#queue .refresh .refreshNow",countdown:"#queue .refresh .countdown"},popup:{popup:"#queue .popup",message:"#queue .popup span"},items:{container:"#queue .qitems"},actions:{retry:"#queue .retry-all",clear:"#queue .dismiss-all"},filters:{filter:"#queue [data-filter]",all:{label:'#queue [for="qfilter-all"]',radio:'#queue [data-filter="all"]',count:'#queue [data-filter="all"] .count'},queued:{label:'#queue [for="qfilter-queued"]',input:'#queue [data-filter="queued"]',count:'#queue [for="qfilter-queued"] .count'},localProcessing:{label:'#queue [for="qfilter-localProcessing"]',input:'#queue [data-filter="localProcessing"]',count:'#queue [for="qfilter-localProcessing"] .count'},uploading:{label:'#queue [for="qfilter-uploading"]',input:'#queue [data-filter="uploading"]',count:'#queue [for="qfilter-uploading"] .count'},pending:{label:'#queue [for="qfilter-pending"]',input:'#queue [data-filter="pending"]',count:'#queue [for="qfilter-pending"] .count'},processing:{label:'#queue [for="qfilter-processing"]',input:'#queue [data-filter="processing"]',count:'#queue [for="qfilter-processing"] .count'},completed:{label:'#queue [for="qfilter-completed"]',input:'#queue [data-filter="completed"]',count:'#queue [for="qfilter-completed"] .count'},failed:{label:'#queue [for="qfilter-failed"]',input:'#queue [data-filter="failed"]',count:'#queue [for="qfilter-failed"] .count'}},item:{type:".type",status:".status",details:".info .details",icon:".status .icon",startedAt:".started time",completed:{wrap:".completed",label:".completed span",time:".completed time"},progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},actions:{cancel:"button.cancel",retry:"button.retry",dismiss:"button.dismiss"}}},this.ui=window.uiFromSelectors(this.selectors),this.ui.panel||(this.canUpdateUI=!1)}initListeners(){this.activityListeners=null,this.clickHandler=this.handleClick.bind(this),this.onlineHandler=this.handleOnline.bind(this),this.offlineHandler=this.handleOffline.bind(this),this.unloadHandler=this.handleBeforeUnload.bind(this),document.addEventListener("click",this.clickHandler),window.addEventListener("online",this.onlineHandler),window.addEventListener("offline",this.offlineHandler),window.addEventListener("beforeunload",this.unloadHandler)}handleOnline(){this.updatePanel(),this.getQueueByStatus(this.pendingStatuses).length>0&&this.processQueue()}handleOffline(){this.updatePanel("offline")}handleBeforeUnload(e){if(this.getQueueByStatus(this.pendingStatuses).length>0)return e.preventDefault(),e.returnValue="",""}handleClick(e){if(!window.targetCheck(e,this.selectors.panel+", "+this.selectors.toggle.button))return;if(window.targetCheck(e,this.selectors.refresh.button))return this.store.clearCache(),void this.store.fetch();if(window.targetCheck(e,this.selectors.actions.clear))return void this.opActions("completed","dismiss").then((()=>{}));if(window.targetCheck(e,this.selectors.actions.retry))return void this.opActions("failed","retry").then((()=>{}));const t=window.targetCheck(e,"[data-action]");if(t){const e=t.closest("[data-id]")?.dataset.id;return void(e&&this.opActions(e,t.dataset.action))}const s=window.targetCheck(e,this.selectors.filters.filter);s&&this.setFilter(s.dataset.filter)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t.input?.dataset.filter===e&&(t.input.checked=!0)})),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}trackActivity(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}this.resetActivityTimer()}resetActivityTimer(){this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),1750)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}initStore(){if(!this.user)return;const e=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1});this.store=e.queue,this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-save":this.maybeStartPolling(),this.updateUI();break;case"item-saved":t.previousItem&&t.previousItem.status!==t.item.status&&this.updateOperationStatus(t.item.id,t.item.status),this.maybeStartPolling()}}))}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},delay:!1,canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return null;if(t.popup&&this.ui.popup?.message&&(this.ui.popup.message.textContent=t.popup,this.ui.popup.popup.hidden=!1,setTimeout((()=>this.ui.popup.popup.hidden=!0),2e3)),!t.delay)return this.queue.set(t.id,t),this.processOperation(t).then((()=>{})),this.store.clearCache(),this.maybeStartPolling(),this.toggleQueue(),t.id;const s=Array.from(this.getAllQueue()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.trackActivity(),e.id}return this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.trackActivity(),t.id}async opActions(e,t){if(this.statuses.includes(e)?e=this.getQueueByStatus(e).map((e=>e.id)):"string"==typeof e&&(e=[e]),0===e.length)return;if(!["cancel","dismiss","retry"].includes(t))return;const s=["cancel","dismiss"].includes(t);s&&e.forEach((e=>this.removeOperationUI(e)));try{const i=await fetch(`${this.api}${this.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({action:t,ids:e,user:this.user})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const n=await i.json();if(!n.success)throw new Error(n.message||`${t} operation failed`);return e.forEach((e=>{let i=this.getQueue(e);if(this.notify(`${t}-operation`,i),s)this.clearQueue(e);else{let t=this.getQueue(e);t.status="queued",this.setQueue(t),this.updateOperationStatus(t.id,t.status)}})),"retry"===t&&this.trackActivity(),this.updateUI(),n}catch(s){return await window.jvbError.log(s,{component:"Queue",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.opActions(e,t))),{success:!1,error:s.message}}}async processQueue(){if(this.isProcessing)return;const e=this.getQueueByStatus("queued");if(0!==e.length){this.setProcessing();for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking()}else this.stopActivityTracking()}async processOperation(e){try{this.queue.has(e.id)||this.queue.set(e.id,e);let t,s=!1;if(e.data?._isFormData&&!e.data instanceof FormData&&(s=!0,e.data=await this.store.objectToFormData(e.data)),this.updateOperationStatus(e.id,"uploading"),e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),t=e.data):(t=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json"),null==t)return;const i=await fetch(`${this.api}${e.endpoint}`,{method:e.method,headers:e.headers,body:t}),n=await i.json();if(s&&(e.data={}),!i.ok||!n.success)throw new Error(n.message||`HTTP ${i.status}`);n.id&&e.id!==n.id?e=await this.handleServerMerge(e,n):(e.status=n.status??"pending",e.serverData=n,this.updateOperationStatus(e.id,e.status)),this.a11y.announce(`${e.title} sent to server for processing`),this.setQueue(e)}catch(t){console.error("Operation failed: ",t),e.retries++,e.lastError=t.message,e.retries>=3?e.status="failed_permanent":e.status="failed",this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(e.status=t.status||"pending",e.serverData=t,this.mergeOp(s,e)):(this.clearQueue(e.id),this.setQueue(t),t)}mergeOp(e,t){return e.data=window.deepMerge(e.data,t.data),e.status=t.status,Object.hasOwn(t,"serverData")&&(e.serverData=t.serverData),this.updateOperationStatus(e.id,e.status),this.removeOperationUI(t.id),this.clearQueue(t.id),e}sortByDate(e){return e.sort(((e,t)=>(e.updated_at??e.timestamp??0)-(t.updated_at??t.timestamp??0)))}getAllQueue(){let e=[...new Set([...Array.from(this.store.data.values()),...Array.from(this.queue.values())])];return this.sortByDate(e)}getQueueByStatus(e){"string"==typeof e&&(e=[e]);let t=[...new Set([...Array.from(this.store.filterByIndex({status:e})),...Array.from(this.queue.values()).filter((t=>e.includes(t.status)))])];return this.sortByDate(t)}updateOperationStatus(e,t){let s=this.getQueue(e);s&&this.statuses.includes(t)&&(s.status=t,this.notify("operation-status",s),this.setQueue(s))}setQueue(e){this.store.save(e),this.queue.set(e.id,e)}getQueue(e){return this.queue.has(e)?this.queue.get(e):this.store.get(e)}clearQueue(e){this.queue.delete(e),this.store.delete(e)}maybeStartPolling(){return this.getQueueByStatus(this.pendingStatuses).length>0&&(this.startPolling(),!0)}startPolling(){this.isPolling||(this.isPolling=!0,this.updatePanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch(),this.maybeStartPolling()||(this.stopPolling(),this.updatePanel("synced"))}catch(e){console.error("Polling error:",e)}}),5e3),this.startCountdown())}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.stopCountdown())}startCountdown(e=5){this.isPolling&&(this.ui.refresh.countdown.textContent=e,this.countdownTimer=setInterval((async()=>{--e>=0?this.ui.refresh.countdown.textContent=e:(this.ui.refresh.countdown.textContent="",this.stopCountdown())}),1e3))}stopCountdown(){this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null)}updateUI(){this.canUpdateUI&&window.debouncer.schedule("queue-ui",this.handleUpdateUI.bind(this))}handleUpdateUI(){const e=this.getAllQueue();this.ui.actions.retry.disabled=0===e.filter((e=>"failed"===e.status)).length,this.ui.actions.clear.disabled=0===e.filter((e=>"completed"===e.status)).length;for(let t of this.statuses){if("failed_permanent"===t)continue;let s=e.filter((e=>e.status===t)).length;this.ui.filters[t].label.hidden=0===s,this.ui.filters[t].input.dataset.count=`${s}`,this.ui.filters[t].count.textContent=s>0?s:""}this.renderOperations()}renderOperations(){if(!this.ui.items.container)return;const e=this.store.filters?.status??"all",t="all"===e?this.getAllQueue():this.getQueueByStatus(e);if(window.removeChildren(this.ui.items.container),0===t.length){const e=window.getTemplate("emptyQueue");return this.ui.items.container.append(e),void this.a11y.announce("No items in queue")}t.forEach((e=>{let t=this.items.get(e.id);t||(t=this.createOperationElement(e)),t?.element&&(this.updateOperationUI(e.id),this.ui.items.container.append(t.element))}))}createOperationElement(e){const t=window.getTemplate("queueItem");t.dataset.id=e.id;const s={element:t,ui:window.uiFromSelectors(this.selectors.item,t)};return this.items.set(e.id,s),s}updateOperationUI(e){let t=this.items.has(e)?this.items.get(e):this.createOperationElement(e);if(!t)return;let s=this.getQueue(e),i=t.element;i.classList.remove(this.statuses),i.classList.add(s.status);let n=this.getProgress(s);t.ui.type&&t.ui.type.textContent!==s.title&&(t.ui.type.textContent=s.title),t.ui.status&&(t.ui.status.title=this.statusLabel(s.status)),t.ui.icon&&(t.ui.icon.className=`icon icon-${this.icons[s.status]}`),t.ui.details&&(t.ui.details.textContent=this.itemMessage(s)),t.ui.startedAt&&(t.ui.startedAt.setAttribute("datetime",s.created_at),t.ui.startedAt.textContent=window.formatTimeAgo(s.created_at));let a="completed"===s.status?"Completed: ":"Last updated: ",r=Object.hasOwn(s,"updated_at")||Object.hasOwn(s,"completed_at");if(t.ui.completed.wrap.hidden=!r,r&&t.ui.completed.label&&t.ui.completed.time){let e;e=Object.hasOwn(s,"completed_at")?s.completed_at:s.updated_at,t.ui.completed.label.textContent=a,t.ui.completed.time.setAttribute("datetime",e),t.ui.completed.time.textContent=window.formatTimeAgo(e)}window.showProgress(t.ui.progress,n,100,this.statusLabel(s.status)),t.ui.actions.cancel&&(t.ui.actions.cancel.hidden=this.completedStatuses.includes(s.status)),t.ui.actions.retry&&(s.retries>=3&&(t.ui.actions.retry.disabled=!0),t.ui.actions.retry.hidden="failed"!==s.status),t.ui.actions.dismiss&&(t.ui.actions.dismiss.hidden=this.pendingStatuses.includes(s.status))}getProgress(e){if(e.progress)return e.progress;if(!this.statuses.includes(e.status))return 0;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]??0}removeOperationUI(e){let t=this.items.get(e);t&&window.fade(t.element,!1)}updatePanel(e="syncing"){this.panelStatuses.includes(e)&&(this.ui.panel.classList.remove(this.panelStatuses),this.ui.panel.classList.add(e))}statusLabel(e){if(!this.statuses.includes(e))return"";return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed",failed_permanent:"Failed permanently"}[e]}itemMessage(e){if(Object.hasOwn(e,"message")&&""!==e.message)return e.message;if(Object.hasOwn(e,"error_message")&&e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":return e.progress?`${e.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}toggleQueue(e=!0){this.ui.panel.hidden=!e,this.ui.toggle.button.hidden=!e}setProcessing(e=!0){this.isProcessing=e,this.ui.toggle.button.classList.toggle("saving",e)}subscribe(e){if(this.subscribers)return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.isPolling&&this.stopPolling(),this.stopActivityTracking(),document.removeEventListener("click",this.clickHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbQueue=new e)}))}))})(); |
| | |
| | | (()=>{class e{constructor(){this.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.loadedTaxonomies=new Set,this.batchFetch=new Set,this.activeField=null,this.isInitializing=!0,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"},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"},loading:{loading:".loading",text:".loading 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:".search-results",search:"[data-autocomplete]"}},this.ui=window.uiFromSelectors(this.selectors)}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),i=this.fields.get(t);if(!t||!i)return;const s=window.targetCheck(e,"[data-autocomplete-select]");if(s){let e=parseInt(s.dataset.id);this.addSelected(e,t),i.ui.dropdown&&(i.ui.dropdown.hidden=!0),i.ui.search&&(i.ui.search.value="")}if(window.targetCheck(e,i.ui.toggle))return e.preventDefault(),void this.openModal(t);const n=window.targetCheck(e,"button.remove-item");if(n){const e=this.getFieldId(n),t=n.closest(".selected-item").dataset.id??!1;return void(e&&t&&this.removeSelected(t,e))}if(e.target.matches(".modal-close"))return 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"),i=parseInt(t.dataset.id);return void(i&&this.navigateTo(i))}const r=window.targetCheck(e,this.selectors.nav.pathLevel);if(r){const e=parseInt(r.dataset.id)??0;this.navigateTo(e)}if(window.targetCheck(e,i.selectors.dropdown))return void 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="")}}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 i=this.getFieldId(e.target);e.target.checked?this.addSelected(t,i):this.removeSelected(t,i)}handleInput(e){let t=this.getFieldId(e.target)??this.activeField;if(!t)return;const i=this.fields.get(t);if(!i)return;this.container.open||(this.activeField=t);const s=e.target.value.trim();window.debouncer.schedule(`${t}-search`,(async()=>{await this.store.setFilters({taxonomy:i.taxonomy,search:s,page:1,parent:s?0:this.store.filters.parent||0}),this.container.open&&window.removeChildren(this.ui.terms.list)}),100)}handleFocus(e){const t=this.getFieldId(e.target),i=this.fields.get(t);t&&i&&(i.hasAutocomplete||i.hasSearch)&&(window.debouncer.cancel(`${t}-search-results`),this.container.open||(this.activeField=t,this.preloadTaxonomy(i.taxonomy)))}handleBlur(e){const t=this.getFieldId(e.target),i=this.fields.get(t);t&&i&&i.hasAutocomplete&&this.scheduleHideDropdown(t)}scheduleHideDropdown(e){const t=this.fields.get(e);t&&window.debouncer.schedule(`${e}-search-results`,(()=>{this.activeField=null,t.ui.dropdown.hidden=!0}),1500)}initModal(){this.modalID="dialog#jvb-selector",this.container=document.querySelector(this.modalID),this.modal=new window.jvbModal(this.container,{handleForm:!1,save:null,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.activeField=e,this.ui.modal.title.textContent=`Select ${t.plural}`,this.ui.search.container&&(this.ui.search.container.hidden=!t.canSearch),this.ui.create.details&&(this.ui.create.details.hidden=!t.canCreate,this.ui.create.summary&&(this.ui.create.summary.textContent=`Add new ${t.singular}`),this.ui.create.label.name&&(this.ui.create.label.name.textContent=`Name this ${t.singular}`),this.ui.create.label.parent&&(this.ui.create.label.parent.textContent="Nest it under"));let i=`Opened ${t.singular} selection. Choose from checkboxes, or search to filter results.`;window.removeChildren(this.ui.terms.list),this.modal.handleOpen(),this.setLoading(),this.store.setFilters({taxonomy:t.taxonomy,page:1,search:"",parent:0}),this.a11y.announce(i)}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;let i=t.parent;this.navigateTo(parseInt(i))}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 i=window.getTemplate("selectedTerm");i.dataset.id=e,i.querySelector("span").textContent=t.path,i.querySelector("button").title=`Remove ${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 i=e.querySelector('input[type="hidden"]');if(!i)return void console.warn("TaxonomySelector: No hidden input found for field",e);"fieldId"in e.dataset||(e.dataset.fieldId=window.generateID("selector"));const s=e.dataset.fieldId;let n=this.selectors.field,r=e.querySelector("button.taxonomy-toggle");if(0===t.size){if(!r)return;if(0===(t=r.dataset).size)return}else Object.hasOwn(t,"toggle")&&(r=document.querySelector(t.toggle),n.toggle=t.toggle);const a={id:s,value:i,element:e,taxonomy:t.taxonomy??!1,singular:t.single??"",plural:t.plural??"",name:e.dataset.field,canSearch:Object.hasOwn(t,"search"),limit:t.limit??0,hasAutocomplete:Object.hasOwn(t,"autocomplete"),canCreate:Object.hasOwn(t,"creatable"),isRequired:Object.hasOwn(t,"required"),toggle:r,selectors:n,ui:window.uiFromSelectors(n,e),checked:!1};return a.taxonomy?(this.fields.set(s,a),this.setSelectedFromValue(i),this.isInitializing&&this.batchFetch.add(a.taxonomy),this.updateFieldUI(s),s):void 0}setSelectedFromValue(e,t){let i=new Set;t.value.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 i=this.fields.get(t),s=this.store.get(e);if(!i||!s)return;const n=this.selectedTerms.get(t);0!==i.limit&&n.size>=i.limit||(n.add(parseInt(e)),this.addTermToDisplay(e,t),this.updateFieldValue(t),this.checkLimits(t))}removeSelected(e,t=null){t||(t=this.activeField);const i=this.fields.get(t),s=this.store.get(e);if(!i||!s)return;this.selectedTerms.get(t).delete(parseInt(e));const n=i.ui.selected.querySelector(`[data-i"${e}"]`);if(n&&n.remove(),this.container.open){let t=this.ui.selected.querySelector(`[data-id="${e}"]`);t&&t.remove()}this.updateFieldValue(t),this.checkLimits(t)}updateFieldValue(e){const t=this.fields.get(e);if(!t)return;let i=Array.from(this.selectedTerms.get(e));t.ui.value=i.join(",")}checkLimits(e){if(!this.container.open)return;const t=this.fields.get(e);if(!t||0===t.limit)return;const i=this.selectedTerms.get(e).size>=t.limit;this.setCheckboxes(i)}updateFieldFromInput(e){const t=this.getFieldId(e),i=this.fields.get(t);t&&i&&(this.setSelectedFromValue(t,e),this.updateFieldUI(t))}updateFieldUI(e){const t=this.fields.get(e);let i=this.selectedTerms.get(e);t&&0!==i.size&&Array.from(i).forEach((t=>{this.addTermToDisplay(t,e)}))}updateFieldsForTaxonomy(e){let t=Array.from(this.fields.values()).filter((t=>!t.checked&&t.taxonomy===e));const i=Array.from(this.store.data.values()).some((t=>t.taxonomy===e));t.forEach((e=>{e.ui.toggle.disabled=!i&&!e.canCreate,e.ui.toggle.title=i?`Select ${e.plural}`:`No ${e.singular} available`,e.checked=!0}))}showModalTerms(e=!0,t=!1){const i=this.store.getFiltered();if(0===i.size)return;e||window.removeChildren(this.ui.terms.list);const s=this.store.filters.parent??0;this.ui.nav.back.hidden=0===s;const n=document.createDocumentFragment();i.forEach((e=>{const i=this.createTermElement({show:t,...e});i&&n.appendChild(i)})),this.ui.terms.list.append(n)}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem");t.dataset.id=e.id;const i=this.selectedTerms.get(this.activeField).has(e.id);let[s,n,r]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("span, .term-name")],a=this.currentField(),o=a.limit>0&&this.selectedTerms.get(this.activeField).size>=a.limit;if(s&&n&&r&&([s.id,s.name,s.value,s.disabled,s.checked,n.htmlFor,n.title,n.dataset.path,r.textContent]=[`${a.element.id}-${e.id}`,`${a.container.id}-${a.taxonomy}-select`,e.id,!i&&o,i,`${a.element.id}-${e.id}`,e.path??e.name,e.path,e.show?e.path:e.name],e.hasChildren)){const i=window.getTemplate("termChildrenToggle");i&&(i.ariaLabel=`View ${a.plural} nested under ${e.name}`,t.append(i))}return t}showAutocompleteTerms(){const e=this.currentField(),t=this.currentTerms();if(!e||0===t.size)return;const i=e.ui.dropdown;window.removeChildren(i),0===t.length?this.showEmptyState(`No ${e.plural} found.`,i):t.forEach((e=>{const t=this.createAutocompleteTerm(e);t&&i.append(t)}));const s=e.ui.search?.value;if(e.canCreate&&s.length>=2&&this.creator){const e=this.createTermButton(s);e&&i.append(e)}i.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 i=this.store.get(e),s=this.fields.get(t);if(!i||!s)return;if(s.ui.selected.querySelector(`[data-id="${e}"]`))return;const n=window.getTemplate("selectedTerm");if(n&&(n.dataset.id=e,n.dataset.taxonomy=s.taxonomy,n.querySelector(".item-name").textContent=i.path,n.querySelector("button").title=`Remove ${i.name}`,s.ui.selected.append(n),this.container.open)){this.addTermToModal(e);const t=this.ui.terms.list.querySelector(`input[value="${e}"]`);t&&(t.checked=!0)}}createTermButton(e){const t=window.getTemplate("autocompleteButton");if(!t)return;return t.querySelector("span").textContent=`"${e}"`,t}updateBreadcrumbs(e){const t=this.ui.nav.nav;if(!t)return;const i=Array.from(t.children).find((t=>parseInt(t.dataset.id)===e));if(i){let e=i.nextElementSibling;for(;e;){const t=e;e=e.nextElementSibling,t.remove()}}else{const i=this.store.get(e);if(!i)return;const s=window.getTemplate("termBreadcrumb");if(!s)return;s.dataset.id=e,s.textContent=i.name,s.title=i.name,t.append(s)}}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 i={"data-loaded":()=>this.handleDataLoaded(),"filters-changed":()=>this.handleFiltersChanged(),"fetch-error":()=>this.handleFetchError()};i[e]?.()}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)}showResults(e=!1){this.setLoading(!1);const t=this.store.getFiltered(),i=this.store.filters,s=this.store.lastResponse?.page||{},n=i.search&&i.search.length>0,r=i.page>1,a=this.currentField();this.notify("terms-loaded",{terms:t,filters:i}),0===t.length?(r||this.showEmptyState(n?`No matching ${a.plural}.`:`No ${a.plural} available.`),this.observer.unobserve(this.ui.terms.sentinel)):e?this.showAutocompleteTerms():(this.showModalTerms(r,n),s.has_more?this.observer.observe(this.ui.terms.sentinel):this.observer.unobserve(this.ui.terms.sentinel)),this.a11y.announce(t.length,r)}handleFiltersChanged(){}handleFetchError(e){this.setLoading(!1)}async batchFetchTaxonomies(){if(0===this.batchFetch.size)return;const e=Array.from(this.batchFetch);e.forEach((e=>this.loadedTaxonomies.add(e))),this.batchFetch.clear();try{e.forEach((e=>this.loadedTaxonomies.add(e))),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.loadedTaxonomies.has(e)||(this.store.setFilters({taxonomy:e,page:1,search:"",parent:0}),this.loadedTaxonomies.add(e))}setLoading(e=!0){if(this.ui.loading.loading.hidden=e,this.modal.classList.toggle("loading",e),e){let e=this.store.filters.search||"";e=""!==e&&e;const t=this.store.filters.parent||0,i=e?`Searching for "${e} items`:0===t?"loading items":"loading child items";window.typeLoop&&this.ui.loading.text?this.stopTyping=window.typeLoop(this.ui.loading.text,i):this.ui.loading.text.textContenet=i}else this.stopTyping&&(this.stopTyping(),this.stopTyping=null)}showEmptyState(e="No items found.",t=null){t||(t=this.ui.terms.list);const i=window.getTemplate("noTermResults"),s=i.querySelector("span");e&&s&&(s.textContent=e),t.append(i)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((i=>{try{i(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("input",this.inputHandler),document.removeEventListener("focus",this.focusHandler),document.removeEventListener("blur",this.blurHandler),this.observer?.disconnect(),this.subscribers.clear(),this.fields.clear(),this.selectedTerms.clear()}}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)}))}))})(); |
| | |
| | | (()=>{class e{constructor(){this.a11y=window.jvbA11y,this.queue=window.jvbQueue,this.error=window.jvbError,this.subscribers=new Set,this.initStores(),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.sortables=new Map,this.previewUrls=new Set,this.initElements(),this.initListeners()}initStores(){const{uploads:e,groups:t}=window.jvbStore.register("uploads",[{storeName:"uploads",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"status",keyPath:"status"},{name:"group",keyPath:"group"},{name:"src",keyPath:"src"}]},{storeName:"groups",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"src",keyPath:"src"}]}]);this.stores={uploads:e,groups:t,ready:[]},this.stores.uploads.subscribe(this.handleStores.bind(this,"uploads")),this.stores.groups.subscribe(this.handleStores.bind(this,"groups")),this.queue.subscribe(((e,t)=>{if(!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;if(s)switch(e){case"cancel-operation":this.handleOperationCancelled(s).then((()=>{}));break;case"operation-status":this.handleFieldStatus(s,t).then((()=>{}));break;case"operation-completed":this.handleOperationComplete(t,s).then((()=>{}));break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,s).then((()=>{}))}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery())}initWorker(){this.worker=null,this.workerState={worker:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:3e3,maxConcurrent:3,restartAfterTimeout:!0}}}initElements(){this.selectors={fields:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-container",preview:".preview-wrap",grid:".item-grid.preview",progress:{progress:".file-upload-container .progress",fill:".file-upload-container .progress .fill",details:".file-upload-container .progress .details",icon:".file-upload-container .progress .icon"},selectAll:'[name="select-all-uploads"]',actions:".selection-actions",count:".selection-count",hidden:'input[type="hidden"]'},groups:{container:".group-display",grid:".item-grid.groups",empty:".empty-group",header:".sidebar .header"},group:{item:".upload-group",actions:".selection-actions",selectAll:'[name="select-all-group"]',count:".group-header .info",fields:"details .fields",grid:".item-grid.group",total:".group-content .group-count"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',image:"img",details:"details",progress:{progress:".progress",fill:".fill",details:".details",icon:".icon"}}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dropHandler=this.handleDrop.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}async setUpload(e,t){const s={...{id:e,attachment:null,group:null,field:null,src:window.location.href,blob:null,status:"local_processing",operationId:null,fields:{}},...t};return Object.preventExtensions(s),await this.stores.uploads.save(s),s}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls.delete(e))}formatFile(e){return e.blob?new File([e.blob],e.fields.originalName||"file",{type:e.fields.type||e.blob.type,lastModified:e.fields.lastModified||Date.now()}):null}handleClick(e){let t=window.targetCheck(e,this.selectors.fields.dropZone);t&&!e.target.matches("input, button, a")&&t.querySelector(this.selectors.fields.input)?.click();const s=window.targetCheck(e,"[data-action]");s&&this.handleAction(s)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(s).then((()=>{}));break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e).then((()=>{}));break;case"upload":this.queueUploads("uploads/groups",s).then((()=>{}));break;case"restore":this.handleRestoreSelected().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":this.handleClearCache().then((()=>{}))}}handleChange(e){let t=this.getFieldIdFromElement(e.target);if(!t)return;if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);return void(s.length>0&&this.processFiles(t,s).then((()=>{})))}if(e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]'))return;let s=this.fields.get(t);s&&s.config.autoUpload&&("post_group"===s.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e).then((()=>{})))}handleGroupMetaChange(e){const t=e.closest(this.selectors.group.fields);if(!t)return;const s=t.dataset.groupId,r=this.stores.groups.get(s);r&&window.debouncer.schedule(`group-meta-${s}`,(async(e,t)=>{let s=e.name.replace(`${t}_`,"").replace(`${t}[`,"").replace("]","");r.fields[s]=e.value,await this.setGroup(t,r)}),300)}handleDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.fields.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleDragLeave(e){const t=e.target.closest(this.selectors.fields.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.fields.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleDrop(e){const t=e.target.closest(this.selectors.fields.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover"),t.classList.add("uploading");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const r=this.getFieldIdFromElement(t);r&&(this.processFiles(r,s).then((()=>{})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t){let s=new FormData;const r=this.fields.get(t);if(!r)return;let i=this.stores.uploads.filterByIndex({field:t});if(0===i.length)return;const[o,a]=["uploads"===e,"uploads/groups"===e];let l,n,d,u,c;s.append("fieldId",r.id),s.append("content",r.config.content),o&&(s.append("mode",r.config.mode),s.append("field_name",r.config.name),s.append("fieldId",r.id),s.append("field_type",r.config.type),s.append("subtype",r.config.subtype),s.append("item_id",r.config.itemID),s.append("destination",r.config.destination)),a?({posts:l,uploadMap:n,files:d}=this.collectGroups(t)):o&&({uploadMap:n,files:d}=this.collectUploads(t)),a&&s.append("posts",JSON.stringify(l)),d.forEach((e=>{s.append("files[]",e)})),s.append("upload_ids",JSON.stringify(n)),o?(u=`Uploading ${i.length} file${i.length>1?"s":""} to server...`,c=`Uploading ${i.length} file${i.length>1?"s":""}...`):a&&(u=`Creating ${l.length} ${r.config.content}${l.length>1?"s":""} from uploads...`,c=`Creating ${l.length} post${l.length>1?"s":""}...`),await this.setBulkUpload(i,"status","queued");let p=this.sendToQueue(e,s,u,c);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1)}return p?(r.operationId=p,await this.setBulkUpload(i,"operationId",p),await this.setBulkUpload(i,"status","uploading"),await this.setBulkGroup(t,"operationId",p),this.fields.set(r.id,r)):await this.setBulkUpload(i,"status","failed"),this.notify("sent-to-queue",t),p}async sendToQueue(e,t,s="",r="",i=!1){""===r&&(r=s);const o={endpoint:e,method:"POST",data:t,title:s,popup:r,canMerge:i,sendNow:"uploads/groups"===e,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(o)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e}),r=[],i=[],o=[];for(const e of s){const s={images:[],fields:e.fields??{}},a=t.filter((t=>t.group===e.id));for(const e of a){const t=this.formatFile(e);if(t){o.push(t);const r={upload_id:e.id,index:i.length};let a=this.uploads.get(e.id);a.ui?.featured?.checked&&(s.fields.featured=e.id),s.images.push(r),i.push(e.id)}}r.push(s)}const a=t.filter((e=>!e.group));for(const e of a){const t={images:[],fields:{}},s=this.formatFile(e);if(s){o.push(s);const r={upload_id:e.id,index:i.length};t.images.push(r),i.push(e.id)}r.push(t)}return{posts:r,uploadMap:i,files:o}}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],r=[];for(const e of t){const t=this.formatFile(e);t&&(r.push(t),s.push(e.id))}return{uploadMap:s,files:r}}async queueUploadMeta(e){const t=e.target.closest(this.selectors.items.item)?.dataset.uploadId,s=this.stores.uploads.get(t);if(!t||!s)return;if(!this.fields.get(s.field))return;let r={};r[e.target.name]=e.target.value,s.fields={...s.fields,...r},await this.setUpload(s.id,s);let i={};return i[s.attachmentId??s.id]=s.fields,await this.sendToQueue("uploads/meta",i,"Uploading Meta","",!0)}async handleOperationComplete(e,t){const s=e.response;if(s?.data){const e=Array.isArray(s.data)?s.data:Object.values(s.data);for(const t of e)if(t.upload_id&&t.attachment_id){const e=this.stores.uploads.get(t.upload_id);e&&(e.attachmentId=t.attachment_id,e.status="completed",await this.stores.uploads.save(e))}}const r=this.stores.uploads.filterByIndex({field:t}),i=this.stores.groups.filterByIndex({field:t});await Promise.all([...r.filter((e=>"completed"===e.status)).map((e=>this.clearUpload(e.id))),...i.map((e=>this.stores.groups.delete(e.id)))]),this.notify("uploads-complete",{fieldId:t,response:s})}scanFields(e,t=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t)))}registerField(e,t=!0,s=null){const r={element:e,id:s||this.determineFieldId(e),config:this.extractFieldConfig(e,t),uploads:new Set,operationId:null,groups:[],ui:window.uiFromSelectors(this.selectors.fields,e),groupUI:window.uiFromSelectors(this.selectors.groups,e)};return this.fields.set(r.id,r),e.dataset.uploader=r.id,this.getSelectionHandler(r.id),"single"!==r.config.type&&this.initSortable(r.id),r.id}extractFieldConfig(e,t){return{autoUpload:t,destination:e.dataset.destination||"meta",content:this.extractFieldContent(e),mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:this.extractFieldItemId(e)??0,maxFiles:parseInt(e.dataset.maxFiles)??25,subType:e.dataset.subtype??"image"}}extractFieldContent(e){return e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||null}extractFieldItemId(e){return e.dataset.itemId||e.closest("dialog")?.dataset.itemId||null}determineFieldId(e){let t=this.extractFieldContent(e);t=null===t?"":t+"_";let s=this.extractFieldItemId(e);s=null===s?"":s+"_";return`${t}${s}${e.dataset.field||""}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,r){const i=this.fields.get(e);i&&window.showProgress(i.ui.progress,t,s,r)}getWorker(){return this.workerState.worker||"undefined"==typeof OffscreenCanvas||(this.workerState.worker=new Worker("worker.js"),this.workerState.worker.onmessage=e=>this.handleWorkerMessage(e),this.workerState.worker.onerror=e=>this.handleWorkerError(e)),this.workerState.worker}handleWorkerMessage(e){const{id:t,blob:s}=e.data,r=this.workerState.tasks.get(t);r&&(clearTimeout(r.timeoutId),r.resolve(s),this.workerState.tasks.delete(t))}handleWorkerError(e){this.workerState.tasks.forEach((t=>{clearTimeout(t.timeoutId),t.reject(e)})),this.workerState.tasks.clear(),this.restartWorker()}restartWorker(){this.workerState.worker&&(this.workerState.worker.terminate(),this.workerState.worker=null),this.workerState.restart.count++}async processImages(e,t=2200,s=2200){const r=[],i=[...e],o=this.workerState.settings.maxConcurrent,a=async()=>{for(;i.length>0;){const e=i.shift();r.push(await this.processImage(e,t,s))}};return await Promise.all(Array.from({length:Math.min(o,e.length)},(()=>a()))),r}async processImage(e,t=2200,s=2200,r=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),r)}catch(r){return this.resizeImage(e,t,s)}}withTimeout(e,t){return Promise.race([e,new Promise(((e,s)=>setTimeout((()=>s(new Error("Timeout"))),t)))])}async workerImage(e,t=2200,s=2200){const{settings:r,restart:i}=this.workerState;if(i.count>=i.max)throw new Error("Worker max restarts exceeded");const o=await createImageBitmap(e);let{width:a,height:l}=o;if(a>t||l>s){const e=Math.min(t/a,s/l);a=Math.round(a*e),l=Math.round(l*e)}const n=this.getWorker(),d=crypto.randomUUID();return new Promise(((t,s)=>{const i=setTimeout((()=>{this.workerState.tasks.delete(d),r.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),r.timeout);this.workerState.tasks.set(d,{resolve:t,reject:s,timeoutId:i}),n.postMessage({id:d,imageBitmap:o,width:a,height:l,type:e.type,quality:.9},[o])}))}resizeImage(e,t,s){return new Promise((r=>{const i=new Image;i.onload=()=>{URL.revokeObjectURL(i.src);let{width:o,height:a}=i;if(o>t||a>s){const e=Math.min(t/o,s/a);o=Math.round(o*e),a=Math.round(a*e)}const l=document.createElement("canvas");l.width=o,l.height=a,l.getContext("2d").drawImage(i,0,0,o,a),l.toBlob(r,e.type,.9)},i.src=URL.createObjectURL(e)}))}async processFiles(e,t){let s=this.fields.get(e);if(!s)return;s.groupUI.container&&(s.groupUI.container.hidden=!1);const r=t.length;let i=0;this.updateFieldProgress(e,0,r,"Processing files...");const o=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),r=await this.setUpload(s,{id:s,field:e,status:"local_processing",blob:null,fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),i=await this.createUpload(s,t,e);return this.uploads.set(s,{element:i,ui:window.uiFromSelectors(this.selectors.items,i)}),await this.addToGroup(s,null),{uploadId:s,upload:r,file:t}}))),a=o.filter((e=>e.file.type.startsWith("image/"))),l=o.filter((e=>!e.file.type.startsWith("image/"))),n=await this.processImages(a.map((e=>e.file)));for(let t=0;t<a.length;t++){const{uploadId:s,upload:o}=a[t];o.blob=n[t],o.fields.size=n[t].size,o.status="queued",await this.setUpload(s,o),i++,this.updateFieldProgress(e,i,r,"Processing files...")}for(const{uploadId:t,upload:s,file:o}of l)s.blob=o,s.status="queued",await this.setUpload(t,s),i++,this.updateFieldProgress(e,i,r,"Processing files...");this.maybeLockUploads(e),s.config.autoUpload&&"post_group"!==s.config.destination&&await this.queueUploads("uploads",e)}async checkRecovery(){const e=this.stores.uploads.filterByIndex({status:["local_processing","queued","uploading"]});if(0===e.length)return;let t=window.getTemplate("restoreNotification");if(!t)return void this.error.log("No restore notification",{component:"UploadManager",src:window.location.href});const s=new Map;e.forEach((e=>{const t=e.src||"unknown";s.has(t)||s.set(t,[]),s.get(t).push(e)}));const r=window.location.href;let i=s.size>1?` across ${s.size} pages`:"",o=e.length>1?"uploads":"upload",a=`${e.length} ${o} can be recovered${i}`,l=t.querySelector(".details");l&&(l.textContent=a);let n=1;for(const[e,i]of s){let s=window.getTemplate("restoreField");if(!s)continue;let o=this.registerField(s,!1,"recovery_"+n),a=this.fields.get(o);n++;let l=e===r,[d,u,c]=[s.querySelector("h3"),s.querySelector("h3 a"),s.querySelector(".item-grid")];s.open=l,l?(u.remove(),d.textContent="From this page:"):[u.href,u.title,u.textContent]=[e,"Navigate to Page and Restore",e];let p=[...new Set(i.map((e=>e.group??"preview")))];for(let e of p){let t="preview"===e||this.stores.groups.get(e);if(!t)continue;let s=await this.createGroupElement(e,a.id),r=s.querySelector(".item-grid"),o=i.filter((t=>t.group===("preview"===e)?null:e));for(const[e,r]of Object.entries(t.fields??{})){let t=s.querySelector(`input[name*="${e}"]`);t&&(t.value=r)}for(let e of o){let t=await this.createUpload(e.id,this.formatFile(e),a.id);r.append(t)}c.append(s)}t.querySelector(".wrap").append(s)}document.body.append(t),t=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(t),this.restoreSelection=new window.jvbHandleSelection({container:t,wrapper:".restore-uploads .wrap",bulkControls:".selection-actions",selectAll:"#select-all-restore",count:".selection-count"}),this.restoreModal.handleOpen()}async handleRestoreSelected(){if(!this.restoreSelection)return;let e=Array.from(this.restoreSelection.selectedItems);0!==e.length&&await this.restoreSelectedUploads(e)}async handleRestoreAll(){if(!this.restoreModal)return;const e=Array.from(this.restoreModal.modal.querySelectorAll(".item.upload")).map((e=>e.dataset.uploadId));await this.restoreSelectedUploads(e)}async restoreSelectedUploads(e){let t=window.location.href,s=Array.from(this.stores.uploads.data.values()).filter((s=>e.includes(s.id)&&s.src===t)),r=[...new Set(s.map((e=>e.group)))].filter(Boolean),i=s[0].field;if(!document.querySelector(`[data-uploader="${i}"]`))return void console.log("No field found for "+i);let o=this.fields.get(i);o.groupUI.container&&(o.groupUI.container.hidden=!1);let a=[];for(let e of r){let t=this.stores.groups.get(e);await this.createGroup(i,e);let r=this.groups.get(e),o=s.filter((t=>t.group===e));if(t&&this.groups.has(e)){let e=t.fields;for(const[t,s]of Object.entries(e)){let e=r.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of o){let s=await this.createUpload(t.id,this.formatFile(t),i);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),a.push(t.id)}}let l=s.filter((e=>!a.includes(e.id)));for(let e of l){let t=await this.createUpload(e.id,this.formatFile(e),i);this.uploads.set(e.id,{element:t,ui:window.uiFromSelectors(this.selectors.items,t)}),await this.addToGroup(e.id,null)}this.cleanupRestore()}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}getStatusText(e){return{received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"}[e]||e}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]??0}async createUpload(e,t,s){let r=window.getTemplate("uploadItem");if(!r)return null;let i=this.fields.get(s);if(!i)return null;r.dataset.uploadId=e;let o=this.getSubtypeFromMime(t.type)||"image";r.dataset.subtype=o;let[a,l,n,d,u]=[r.querySelector('[name="featured"]'),r.querySelector("img"),r.querySelector("video"),r.querySelector("label > span"),r.querySelector("details")];switch(a&&(a.value=e),o){case"image":if(l){const e=this.createPreviewUrl(t);l.src=e,l.alt=t.name||"",l.dataset.previewUrl=e}n?.remove(),d?.remove();break;case"video":if(n){const e=this.createPreviewUrl(t);n.src=e,n.dataset.previewUrl=e}l?.remove(),d?.remove();break;case"document":let e=t.name.split(".").pop()?.toLowerCase()??"",s={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},r=window.getIcon(s[e]??"file");d&&(d.innerText=t.name,d.prepend(r)),l?.remove(),n?.remove()}if(u){let e=window.getTemplate("uploadMeta");e&&u.append(e)}return r.draggable="single"!==i.config.type??!1,r.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e,i=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,i&&(i.htmlFor=r)}})),r}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}async handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId;confirm("Remove this item?")&&(await this.removeUpload(s),this.a11y.announce("Item removed"))}async setBulkUpload(e,t,s){const r=Array.from(e).map((async e=>("status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e))));await Promise.all(r)}async setUploadStatus(e,t){e.progress&&window.showProgress(e.progress,this.getStatusProgress(t),100,this.getStatusText(t),this.queue.icons[t]??"")}async removeUpload(e){let t=this.stores.uploads.get(e);if(!t)return;if(t.group){let s=this.stores.groups.get(t.group);s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length&&await this.removeGroup(s.id,!1)}await this.clearUpload(e),this.maybeLockUploads(t.field);let s=this.selectionHandlers.get(t.field);s&&s.deselect(e),this.a11y.announce("Upload removed")}async clearUpload(e){const t=this.uploads.get(e);if(t&&(this.revokePreviewUrl(t.preview),t.element)){const e=t.element.dataset.previewUrl;this.revokePreviewUrl(e),t.element.remove()}this.uploads.delete(e),await this.stores.uploads.delete(e)}async handleAddToGroup(e){const t=this.selected.get(e);if(!t||0===t.size)return;let s=await this.createGroup(e);s&&(await Promise.all(Array.from(t).map((e=>this.addToGroup(e,s)))),this.selectionHandlers.get(e)?.clearSelection(),this.a11y.announce(`Created group with ${t.size} items`))}async createGroup(e,t=null){let s=this.fields.get(e);if(!s)return;t||(t=window.generateID("group"));const r=this.createGroupElement(t,e);if(!r)return null;s.groupUI.grid.append(r);const i=r.querySelector(".item-grid");i&&(i.dataset.groupId=t,this.createSortable(e,i,t));let o=this.stores.groups.data.has(t)?this.stores.groups.data.get(t):{};return await this.setGroup(t,{...o,id:t,field:e}),t}createGroupElement(e,t=null){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,t&&(s.dataset.fieldId=t);const r=s.querySelector("[data-select-all]");if(r){const t=`select-all-${e}`,i=s.querySelector(`label[for="${r.id}"]`);r.id=t,r.name=t,i&&(i.htmlFor=t)}let i=window.getTemplate("groupMetadata"),o=s.querySelector(".fields");if(i&&o){o.append(i);let t=o.querySelector('[name="post_title"]'),s=o.querySelector('[name="post_excerpt"]');t&&(t.id=`${e}_title`,t.name=`${e}[post_title]`),s&&(s.id=`${e}_excerpt`,s.name=`${e}[post_excerpt]`)}else s.querySelector("details")?.remove();const a=s.querySelector(".item-grid");return a&&(a.dataset.groupId=e),this.groups.set(e,{element:s,ui:window.uiFromSelectors(this.selectors.group,s)}),s}async setGroup(e,t){const s={...{id:e,src:window.location.href,uploads:[],operationId:null,field:null,fields:{}},...t};Object.preventExtensions(s),await this.stores.groups.save(s)}async setBulkGroup(e,t,s){let r=this.stores.groups.filterByIndex({field:e});if(0===r.length)return;let i=r.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(i)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),r=this.uploads.get(e);if(!s||!r)return;const i=this.fields.get(s.field);if(!i)return;if(null!==r.element?.parentElement&&(!t&&null===s.group||t===s.group))return void this.handleReorder(s.field,t);if(s.group){const t=this.stores.groups.get(s.group);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length&&await this.removeGroup(t.id,!1))}if(r.ui.checkbox&&(r.ui.checkbox.checked=!1),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),r.ui.featured&&(r.ui.featured.hidden=!t),t){r.ui.featured&&(r.ui.featured.name=`${t}_featured`);let i=this.stores.groups.get(t);i&&(i.uploads.push(e),s.group=t,this.stores.groups.save(i))}else s.group=null;let o=t?this.groups.get(t)?.ui.grid:i.ui.grid;o&&o.append(r.element),this.stores.uploads.save(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.group.item);if(!t)return;let s=t.dataset.groupId;if(!confirm("Delete this group? Items will be moved back to the upload area."))return;let r=this.stores.uploads.filterByIndex({group:s});Promise.all(r.map((e=>this.addToGroup(e.id,null)))).then((()=>{this.removeGroup(s,!1).then((()=>{})),this.a11y.announce("Group deleted. Items returned to upload area")}))}async removeGroup(e,t=!0){let s=this.groups.get(e),r=this.stores.groups.get(e);if(!r)return;let i=!0;t&&r.uploads.length>0&&(i=window.confirm("Keep uploads in this group?")),await Promise.all(r.uploads.map((e=>i?this.addToGroup(e,null):this.removeUpload(e))));const o=this.getGroupKey(r.field,e),a=this.sortables.get(o);a?.destroy&&a.destroy(),this.sortables.delete(o),s?.element&&s.element.remove(),this.groups.delete(e),await this.stores.groups.delete(e),this.a11y.announce("Group removed")}maybeLockUploads(e){let t=this.fields.get(e);if(!t||!t.ui.dropZone)return;let s=this.stores.uploads.filterByIndex({field:e}).length,r=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=r}async handleOperationCancelled(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all([...t.map((e=>this.removeUpload(e.id))),...s.map((e=>this.removeGroup(e.id,!1)))]),this.a11y.announce("Upload Cancelled")}async handleOperationFailed(e,t){await this.setBulkUpload(this.stores.uploads.filterByIndex({field:t}),"status","failed")}async handleFieldStatus(e,t){let s=t.status,r=this.stores.uploads.filterByIndex({field:e});await this.setBulkUpload(r,"status",s)}getGroupKey(e,t=null){return t?`${e}_${t}`:`${e}`}getSelectionHandler(e){let t=this.getGroupKey(e);if(!this.selectionHandlers.has(t)){let s=this.fields.get(e);if(!s)return;let r=new window.jvbHandleSelection({container:s.element,item:this.selectors.items.item,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions,checkbox:this.selectors.items.checkbox,selectAll:this.selectors.fields.selectAll,wrapper:`${this.selectors.fields.preview}, ${this.selectors.group.item}`});r.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems),console.log(Array.from(this.selected)),this.syncSortableSelection(e,s.selectedItems)})),this.selectionHandlers.set(t,r)}return this.selectionHandlers.get(t)}initSortable(e){if(!window.Sortable)return;const t=this.fields.get(e);t&&(!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0),this.createSortable(e,t.ui.grid,null),this.initEmptyGroupDropZone(e))}createSortable(e,t,s){if(!t)return null;const r=this.getGroupKey(e,s);if(this.sortables.has(r))return this.sortables.get(r);const i=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},ghostClass:"ghost",chosenClass:"chosen",dragClass:"dragging",onStart:t=>{const s=t.item,r=s?.dataset.uploadId,i=this.selected.get(e);if(r&&(!i||!i.has(r))){const t=this.selectionHandlers.get(e);t&&t.select(r)}this.syncSortableSelection(e)},onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(r,i),i}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),e.dataTransfer.dropEffect="move",s.classList.add("drag-over")})),s.addEventListener("dragleave",(e=>{s.contains(e.relatedTarget)||s.classList.remove("drag-over")})),s.addEventListener("drop",(async t=>{t.preventDefault(),s.classList.remove("drag-over");const r=this.selected.get(e);if(!r||0===r.size)return;const i=await this.createGroup(e);i&&(await Promise.all(Array.from(r).map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,r=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===r.length)return;const i=s.dataset.groupId||null;await Promise.all(r.map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(t)?.clearSelection()}syncSortableSelection(e){const t=this.selected.get(e)||new Set;for(const[s,r]of this.uploads){const i=this.stores.uploads.get(s);if(!i||i.field!==e)continue;const o=r.element;if(!o)continue;const a=t.has(s);a&&!o.classList.contains("selected")?Sortable.utils.select(o):!a&&o.classList.contains("selected")&&Sortable.utils.deselect(o)}}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(!s)return void console.log("Couldn't Reorder items...");let r=Array.from(s.querySelectorAll(this.selectors.items.item+":not(.ghost)")).map((e=>e.dataset.uploadId)).filter((e=>e));if(t){let e=this.groups.get(t);e&&(e.uploads=r)}else{let t=this.fields.get(e)?.ui.hidden;t&&(t.value=r.join(","))}this.a11y.announce("Items reordered")}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.previewUrls.forEach((e=>{this.revokePreviewUrl(e)})),this.previewUrls.clear()}cleanupAllPreviewUrls(){this.previewUrls.forEach((e=>this.revokePreviewUrl(e))),this.previewUrls.clear()}async handleClearCache(){const e=window.location.href,t=this.stores.uploads.filterByIndex({src:e}),s=this.stores.groups.filterByIndex({src:e});await Promise.all([...t.map((e=>this.clearUpload(e.id))),...s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id))))]),this.restoreModal&&this.cleanupRestore(),this.a11y.announce("Cache cleared for this page")}async getFilesForForm(e){const t=e.querySelectorAll(this.selectors.fields.field),s=[];for(const e of t){const t=this.determineFieldId(e),r=e.dataset.field,i=this.stores.uploads.filterByIndex({field:t});for(const e of i){const t=this.formatFile(e);t&&s.push({file:t,fieldName:r,uploadId:e.id,meta:e.fields||{}})}}return s}async clearFieldFromStores(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all(t.map((e=>this.clearUpload(e.id)))),await Promise.all(s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id)))))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUploads=new e)}))}))})(); |
| | | (()=>{class e{constructor(){this.a11y=window.jvbA11y,this.queue=window.jvbQueue,this.error=window.jvbError,this.subscribers=new Set,this.initStores(),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.sortables=new Map,this.previewUrls=new Set,this.initElements(),this.initListeners()}initStores(){const{uploads:e,groups:t}=window.jvbStore.register("uploads",[{storeName:"uploads",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"status",keyPath:"status"},{name:"group",keyPath:"group"},{name:"src",keyPath:"src"}]},{storeName:"groups",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"src",keyPath:"src"}]}]);this.stores={uploads:e,groups:t,ready:[]},this.stores.uploads.subscribe(this.handleStores.bind(this,"uploads")),this.stores.groups.subscribe(this.handleStores.bind(this,"groups")),this.queue.subscribe(((e,t)=>{if(console.log(e,t),!Object.hasOwn(t,"endpoint")||!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;if(s)switch(e){case"cancel-operation":this.handleOperationCancelled(s).then((()=>{}));break;case"operation-status":this.handleFieldStatus(s,t).then((()=>{}));break;case"operation-completed":this.handleOperationComplete(t,s).then((()=>{}));break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,s).then((()=>{}))}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery())}initWorker(){this.worker=null,this.workerState={worker:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:3e3,maxConcurrent:3,restartAfterTimeout:!0}}}initElements(){this.selectors={fields:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-container",preview:".preview-wrap",grid:".item-grid.preview",progress:{progress:".file-upload-container .progress",fill:".file-upload-container .progress .fill",details:".file-upload-container .progress .details",icon:".file-upload-container .progress .icon"},selectAll:'[name="select-all-uploads"]',actions:".selection-actions",count:".selection-count",hidden:'input[type="hidden"]'},groups:{container:".group-display",grid:".item-grid.groups",empty:".empty-group",header:".sidebar .header"},group:{item:".upload-group",actions:".selection-actions",selectAll:'[name="select-all-group"]',count:".group-header .info",fields:"details .fields",grid:".item-grid.group",total:".group-content .group-count"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',image:"img",details:"details",progress:{progress:".progress",fill:".fill",details:".details",icon:".icon"}}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dropHandler=this.handleDrop.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}async setUpload(e,t){const s={...{id:e,attachment:null,group:null,field:null,src:window.location.href,blob:null,status:"local_processing",operationId:null,fields:{}},...t};return Object.preventExtensions(s),await this.stores.uploads.save(s),s}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls.delete(e))}formatFile(e){return e.blob?new File([e.blob],e.fields.originalName||"file",{type:e.fields.type||e.blob.type,lastModified:e.fields.lastModified||Date.now()}):null}handleClick(e){let t=window.targetCheck(e,this.selectors.fields.dropZone);t&&!e.target.matches("input, button, a")&&t.querySelector(this.selectors.fields.input)?.click();const s=window.targetCheck(e,"[data-action]");s&&this.handleAction(s)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(s).then((()=>{}));break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e).then((()=>{}));break;case"upload":this.queueUploads("uploads/groups",s).then((()=>{}));break;case"restore":this.handleRestoreSelected().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":this.handleClearCache().then((()=>{}))}}handleChange(e){let t=this.getFieldIdFromElement(e.target);if(!t)return;if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);return void(s.length>0&&this.processFiles(t,s).then((()=>{})))}if(e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]'))return;let s=this.fields.get(t);s&&s.config.autoUpload&&("post_group"===s.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e).then((()=>{})))}handleGroupMetaChange(e){const t=e.closest(this.selectors.group.fields);if(!t)return;const s=t.dataset.groupId,r=this.stores.groups.get(s);r&&window.debouncer.schedule(`group-meta-${s}`,(async(e,t)=>{let s=e.name.replace(`${t}_`,"").replace(`${t}[`,"").replace("]","");r.fields[s]=e.value,await this.setGroup(t,r)}),300)}handleDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.fields.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleDragLeave(e){const t=e.target.closest(this.selectors.fields.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.fields.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleDrop(e){const t=e.target.closest(this.selectors.fields.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover"),t.classList.add("uploading");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const r=this.getFieldIdFromElement(t);r&&(this.processFiles(r,s).then((()=>{})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t){let s=new FormData;const r=this.fields.get(t);if(!r)return;let i=this.stores.uploads.filterByIndex({field:t});if(0===i.length)return;const[o,a]=["uploads"===e,"uploads/groups"===e];let l,n,d,u,c;s.append("fieldId",r.id),s.append("content",r.config.content),o&&(s.append("mode",r.config.mode),s.append("field_name",r.config.name),s.append("fieldId",r.id),s.append("field_type",r.config.type),s.append("subtype",r.config.subtype),s.append("item_id",r.config.itemID),s.append("destination",r.config.destination)),a?({posts:l,uploadMap:n,files:d}=this.collectGroups(t)):o&&({uploadMap:n,files:d}=this.collectUploads(t)),a&&s.append("posts",JSON.stringify(l)),d.forEach((e=>{s.append("files[]",e)})),s.append("upload_ids",JSON.stringify(n)),o?(u=`Uploading ${i.length} file${i.length>1?"s":""} to server...`,c=`Uploading ${i.length} file${i.length>1?"s":""}...`):a&&(u=`Creating ${l.length} ${r.config.content}${l.length>1?"s":""} from uploads...`,c=`Creating ${l.length} post${l.length>1?"s":""}...`),await this.setBulkUpload(i,"status","queued");let p=this.sendToQueue(e,s,u,c);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1)}return p?(r.operationId=p,await this.setBulkUpload(i,"operationId",p),await this.setBulkUpload(i,"status","uploading"),await this.setBulkGroup(t,"operationId",p),this.fields.set(r.id,r)):await this.setBulkUpload(i,"status","failed"),this.notify("sent-to-queue",t),p}async sendToQueue(e,t,s="",r="",i=!1){""===r&&(r=s);const o={endpoint:e,method:"POST",data:t,title:s,popup:r,canMerge:i,sendNow:"uploads/groups"===e,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(o)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e}),r=[],i=[],o=[];for(const e of s){const s={images:[],fields:e.fields??{}},a=t.filter((t=>t.group===e.id));for(const e of a){const t=this.formatFile(e);if(t){o.push(t);const r={upload_id:e.id,index:i.length};let a=this.uploads.get(e.id);a.ui?.featured?.checked&&(s.fields.featured=e.id),s.images.push(r),i.push(e.id)}}r.push(s)}const a=t.filter((e=>!e.group));for(const e of a){const t={images:[],fields:{}},s=this.formatFile(e);if(s){o.push(s);const r={upload_id:e.id,index:i.length};t.images.push(r),i.push(e.id)}r.push(t)}return{posts:r,uploadMap:i,files:o}}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],r=[];for(const e of t){const t=this.formatFile(e);t&&(r.push(t),s.push(e.id))}return{uploadMap:s,files:r}}async queueUploadMeta(e){const t=e.target.closest(this.selectors.items.item)?.dataset.uploadId,s=this.stores.uploads.get(t);if(!t||!s)return;if(!this.fields.get(s.field))return;let r={};r[e.target.name]=e.target.value,s.fields={...s.fields,...r},await this.setUpload(s.id,s);let i={};return i[s.attachmentId??s.id]=s.fields,await this.sendToQueue("uploads/meta",i,"Uploading Meta","",!0)}async handleOperationComplete(e,t){const s=e.response;if(s?.data){const e=Array.isArray(s.data)?s.data:Object.values(s.data);for(const t of e)if(t.upload_id&&t.attachment_id){const e=this.stores.uploads.get(t.upload_id);e&&(e.attachmentId=t.attachment_id,e.status="completed",await this.stores.uploads.save(e))}}const r=this.stores.uploads.filterByIndex({field:t}),i=this.stores.groups.filterByIndex({field:t});await Promise.all([...r.filter((e=>"completed"===e.status)).map((e=>this.clearUpload(e.id))),...i.map((e=>this.stores.groups.delete(e.id)))]),this.notify("uploads-complete",{fieldId:t,response:s})}scanFields(e,t=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t)))}registerField(e,t=!0,s=null){const r={element:e,id:s||this.determineFieldId(e),config:this.extractFieldConfig(e,t),uploads:new Set,operationId:null,groups:[],ui:window.uiFromSelectors(this.selectors.fields,e),groupUI:window.uiFromSelectors(this.selectors.groups,e)};return this.fields.set(r.id,r),e.dataset.uploader=r.id,this.getSelectionHandler(r.id),"single"!==r.config.type&&this.initSortable(r.id),r.id}extractFieldConfig(e,t){return{autoUpload:t,destination:e.dataset.destination||"meta",content:this.extractFieldContent(e),mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:this.extractFieldItemId(e)??0,maxFiles:parseInt(e.dataset.maxFiles)??25,subType:e.dataset.subtype??"image"}}extractFieldContent(e){return e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||null}extractFieldItemId(e){return e.dataset.itemId||e.closest("dialog")?.dataset.itemId||null}determineFieldId(e){let t=this.extractFieldContent(e);t=null===t?"":t+"_";let s=this.extractFieldItemId(e);s=null===s?"":s+"_";return`${t}${s}${e.dataset.field||""}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,r){const i=this.fields.get(e);i&&window.showProgress(i.ui.progress,t,s,r)}getWorker(){return this.workerState.worker||"undefined"==typeof OffscreenCanvas||(this.workerState.worker=new Worker("worker.js"),this.workerState.worker.onmessage=e=>this.handleWorkerMessage(e),this.workerState.worker.onerror=e=>this.handleWorkerError(e)),this.workerState.worker}handleWorkerMessage(e){const{id:t,blob:s}=e.data,r=this.workerState.tasks.get(t);r&&(clearTimeout(r.timeoutId),r.resolve(s),this.workerState.tasks.delete(t))}handleWorkerError(e){this.workerState.tasks.forEach((t=>{clearTimeout(t.timeoutId),t.reject(e)})),this.workerState.tasks.clear(),this.restartWorker()}restartWorker(){this.workerState.worker&&(this.workerState.worker.terminate(),this.workerState.worker=null),this.workerState.restart.count++}async processImages(e,t=2200,s=2200){const r=[],i=[...e],o=this.workerState.settings.maxConcurrent,a=async()=>{for(;i.length>0;){const e=i.shift();r.push(await this.processImage(e,t,s))}};return await Promise.all(Array.from({length:Math.min(o,e.length)},(()=>a()))),r}async processImage(e,t=2200,s=2200,r=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),r)}catch(r){return this.resizeImage(e,t,s)}}withTimeout(e,t){return Promise.race([e,new Promise(((e,s)=>setTimeout((()=>s(new Error("Timeout"))),t)))])}async workerImage(e,t=2200,s=2200){const{settings:r,restart:i}=this.workerState;if(i.count>=i.max)throw new Error("Worker max restarts exceeded");const o=await createImageBitmap(e);let{width:a,height:l}=o;if(a>t||l>s){const e=Math.min(t/a,s/l);a=Math.round(a*e),l=Math.round(l*e)}const n=this.getWorker(),d=crypto.randomUUID();return new Promise(((t,s)=>{const i=setTimeout((()=>{this.workerState.tasks.delete(d),r.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),r.timeout);this.workerState.tasks.set(d,{resolve:t,reject:s,timeoutId:i}),n.postMessage({id:d,imageBitmap:o,width:a,height:l,type:e.type,quality:.9},[o])}))}resizeImage(e,t,s){return new Promise((r=>{const i=new Image;i.onload=()=>{URL.revokeObjectURL(i.src);let{width:o,height:a}=i;if(o>t||a>s){const e=Math.min(t/o,s/a);o=Math.round(o*e),a=Math.round(a*e)}const l=document.createElement("canvas");l.width=o,l.height=a,l.getContext("2d").drawImage(i,0,0,o,a),l.toBlob(r,e.type,.9)},i.src=URL.createObjectURL(e)}))}async processFiles(e,t){let s=this.fields.get(e);if(!s)return;s.groupUI.container&&(s.groupUI.container.hidden=!1);const r=t.length;let i=0;this.updateFieldProgress(e,0,r,"Processing files...");const o=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),r=await this.setUpload(s,{id:s,field:e,status:"local_processing",blob:null,fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),i=await this.createUpload(s,t,e);return this.uploads.set(s,{element:i,ui:window.uiFromSelectors(this.selectors.items,i)}),await this.addToGroup(s,null),{uploadId:s,upload:r,file:t}}))),a=o.filter((e=>e.file.type.startsWith("image/"))),l=o.filter((e=>!e.file.type.startsWith("image/"))),n=await this.processImages(a.map((e=>e.file)));for(let t=0;t<a.length;t++){const{uploadId:s,upload:o}=a[t];o.blob=n[t],o.fields.size=n[t].size,o.status="queued",await this.setUpload(s,o),i++,this.updateFieldProgress(e,i,r,"Processing files...")}for(const{uploadId:t,upload:s,file:o}of l)s.blob=o,s.status="queued",await this.setUpload(t,s),i++,this.updateFieldProgress(e,i,r,"Processing files...");this.maybeLockUploads(e),s.config.autoUpload&&"post_group"!==s.config.destination&&await this.queueUploads("uploads",e)}async checkRecovery(){const e=this.stores.uploads.filterByIndex({status:["local_processing","queued","uploading"]});if(0===e.length)return;let t=window.getTemplate("restoreNotification");if(!t)return void this.error.log("No restore notification",{component:"UploadManager",src:window.location.href});const s=new Map;e.forEach((e=>{const t=e.src||"unknown";s.has(t)||s.set(t,[]),s.get(t).push(e)}));const r=window.location.href;let i=s.size>1?` across ${s.size} pages`:"",o=e.length>1?"uploads":"upload",a=`${e.length} ${o} can be recovered${i}`,l=t.querySelector(".details");l&&(l.textContent=a);let n=1;for(const[e,i]of s){let s=window.getTemplate("restoreField");if(!s)continue;let o=this.registerField(s,!1,"recovery_"+n),a=this.fields.get(o);n++;let l=e===r,[d,u,c]=[s.querySelector("h3"),s.querySelector("h3 a"),s.querySelector(".item-grid")];s.open=l,l?(u.remove(),d.textContent="From this page:"):[u.href,u.title,u.textContent]=[e,"Navigate to Page and Restore",e];let p=[...new Set(i.map((e=>e.group??"preview")))];for(let e of p){let t="preview"===e||this.stores.groups.get(e);if(!t)continue;let s=await this.createGroupElement(e,a.id),r=s.querySelector(".item-grid"),o=i.filter((t=>t.group===("preview"===e)?null:e));for(const[e,r]of Object.entries(t.fields??{})){let t=s.querySelector(`input[name*="${e}"]`);t&&(t.value=r)}for(let e of o){let t=await this.createUpload(e.id,this.formatFile(e),a.id);r.append(t)}c.append(s)}t.querySelector(".wrap").append(s)}document.body.append(t),t=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(t),this.restoreSelection=new window.jvbHandleSelection({container:t,wrapper:".restore-uploads .wrap",bulkControls:".selection-actions",selectAll:"#select-all-restore",count:".selection-count"}),this.restoreModal.handleOpen()}async handleRestoreSelected(){if(!this.restoreSelection)return;let e=Array.from(this.restoreSelection.selectedItems);0!==e.length&&await this.restoreSelectedUploads(e)}async handleRestoreAll(){if(!this.restoreModal)return;const e=Array.from(this.restoreModal.modal.querySelectorAll(".item.upload")).map((e=>e.dataset.uploadId));await this.restoreSelectedUploads(e)}async restoreSelectedUploads(e){let t=window.location.href,s=Array.from(this.stores.uploads.data.values()).filter((s=>e.includes(s.id)&&s.src===t)),r=[...new Set(s.map((e=>e.group)))].filter(Boolean),i=s[0].field;if(!document.querySelector(`[data-uploader="${i}"]`))return void console.log("No field found for "+i);let o=this.fields.get(i);o.groupUI.container&&(o.groupUI.container.hidden=!1);let a=[];for(let e of r){let t=this.stores.groups.get(e);await this.createGroup(i,e);let r=this.groups.get(e),o=s.filter((t=>t.group===e));if(t&&this.groups.has(e)){let e=t.fields;for(const[t,s]of Object.entries(e)){let e=r.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of o){let s=await this.createUpload(t.id,this.formatFile(t),i);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),a.push(t.id)}}let l=s.filter((e=>!a.includes(e.id)));for(let e of l){let t=await this.createUpload(e.id,this.formatFile(e),i);this.uploads.set(e.id,{element:t,ui:window.uiFromSelectors(this.selectors.items,t)}),await this.addToGroup(e.id,null)}this.cleanupRestore()}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}getStatusText(e){return{received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"}[e]||e}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]??0}async createUpload(e,t,s){let r=window.getTemplate("uploadItem");if(!r)return null;let i=this.fields.get(s);if(!i)return null;r.dataset.uploadId=e;let o=this.getSubtypeFromMime(t.type)||"image";r.dataset.subtype=o;let[a,l,n,d,u]=[r.querySelector('[name="featured"]'),r.querySelector("img"),r.querySelector("video"),r.querySelector("label > span"),r.querySelector("details")];switch(a&&(a.value=e),o){case"image":if(l){const e=this.createPreviewUrl(t);l.src=e,l.alt=t.name||"",l.dataset.previewUrl=e}n?.remove(),d?.remove();break;case"video":if(n){const e=this.createPreviewUrl(t);n.src=e,n.dataset.previewUrl=e}l?.remove(),d?.remove();break;case"document":let e=t.name.split(".").pop()?.toLowerCase()??"",s={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},r=window.getIcon(s[e]??"file");d&&(d.innerText=t.name,d.prepend(r)),l?.remove(),n?.remove()}if(u){let e=window.getTemplate("uploadMeta");e&&u.append(e)}return r.draggable="single"!==i.config.type??!1,r.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e,i=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,i&&(i.htmlFor=r)}})),r}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}async handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId;confirm("Remove this item?")&&(await this.removeUpload(s),this.a11y.announce("Item removed"))}async setBulkUpload(e,t,s){const r=Array.from(e).map((async e=>("status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e))));await Promise.all(r)}async setUploadStatus(e,t){e.progress&&window.showProgress(e.progress,this.getStatusProgress(t),100,this.getStatusText(t),this.queue.icons[t]??"")}async removeUpload(e){let t=this.stores.uploads.get(e);if(!t)return;if(t.group){let s=this.stores.groups.get(t.group);s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length&&await this.removeGroup(s.id,!1)}await this.clearUpload(e),this.maybeLockUploads(t.field);let s=this.selectionHandlers.get(t.field);s&&s.deselect(e),this.a11y.announce("Upload removed")}async clearUpload(e){const t=this.uploads.get(e);if(t&&(this.revokePreviewUrl(t.preview),t.element)){const e=t.element.dataset.previewUrl;this.revokePreviewUrl(e),t.element.remove()}this.uploads.delete(e),await this.stores.uploads.delete(e)}async handleAddToGroup(e){const t=this.selected.get(e);if(!t||0===t.size)return;let s=await this.createGroup(e);s&&(await Promise.all(Array.from(t).map((e=>this.addToGroup(e,s)))),this.selectionHandlers.get(e)?.clearSelection(),this.a11y.announce(`Created group with ${t.size} items`))}async createGroup(e,t=null){let s=this.fields.get(e);if(!s)return;t||(t=window.generateID("group"));const r=this.createGroupElement(t,e);if(!r)return null;s.groupUI.grid.append(r);const i=r.querySelector(".item-grid");i&&(i.dataset.groupId=t,this.createSortable(e,i,t));let o=this.stores.groups.data.has(t)?this.stores.groups.data.get(t):{};return await this.setGroup(t,{...o,id:t,field:e}),t}createGroupElement(e,t=null){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,t&&(s.dataset.fieldId=t);const r=s.querySelector("[data-select-all]");if(r){const t=`select-all-${e}`,i=s.querySelector(`label[for="${r.id}"]`);r.id=t,r.name=t,i&&(i.htmlFor=t)}let i=window.getTemplate("groupMetadata"),o=s.querySelector(".fields");if(i&&o){o.append(i);let t=o.querySelector('[name="post_title"]'),s=o.querySelector('[name="post_excerpt"]');t&&(t.id=`${e}_title`,t.name=`${e}[post_title]`),s&&(s.id=`${e}_excerpt`,s.name=`${e}[post_excerpt]`)}else s.querySelector("details")?.remove();const a=s.querySelector(".item-grid");return a&&(a.dataset.groupId=e),this.groups.set(e,{element:s,ui:window.uiFromSelectors(this.selectors.group,s)}),s}async setGroup(e,t){const s={...{id:e,src:window.location.href,uploads:[],operationId:null,field:null,fields:{}},...t};Object.preventExtensions(s),await this.stores.groups.save(s)}async setBulkGroup(e,t,s){let r=this.stores.groups.filterByIndex({field:e});if(0===r.length)return;let i=r.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(i)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),r=this.uploads.get(e);if(!s||!r)return;const i=this.fields.get(s.field);if(!i)return;if(null!==r.element?.parentElement&&(!t&&null===s.group||t===s.group))return void this.handleReorder(s.field,t);if(s.group){const t=this.stores.groups.get(s.group);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length&&await this.removeGroup(t.id,!1))}if(r.ui.checkbox&&(r.ui.checkbox.checked=!1),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),r.ui.featured&&(r.ui.featured.hidden=!t),t){r.ui.featured&&(r.ui.featured.name=`${t}_featured`);let i=this.stores.groups.get(t);i&&(i.uploads.push(e),s.group=t,this.stores.groups.save(i))}else s.group=null;let o=t?this.groups.get(t)?.ui.grid:i.ui.grid;o&&o.append(r.element),this.stores.uploads.save(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.group.item);if(!t)return;let s=t.dataset.groupId;if(!confirm("Delete this group? Items will be moved back to the upload area."))return;let r=this.stores.uploads.filterByIndex({group:s});Promise.all(r.map((e=>this.addToGroup(e.id,null)))).then((()=>{this.removeGroup(s,!1).then((()=>{})),this.a11y.announce("Group deleted. Items returned to upload area")}))}async removeGroup(e,t=!0){let s=this.groups.get(e),r=this.stores.groups.get(e);if(!r)return;let i=!0;t&&r.uploads.length>0&&(i=window.confirm("Keep uploads in this group?")),await Promise.all(r.uploads.map((e=>i?this.addToGroup(e,null):this.removeUpload(e))));const o=this.getGroupKey(r.field,e),a=this.sortables.get(o);a?.destroy&&a.destroy(),this.sortables.delete(o),s?.element&&s.element.remove(),this.groups.delete(e),await this.stores.groups.delete(e),this.a11y.announce("Group removed")}maybeLockUploads(e){let t=this.fields.get(e);if(!t||!t.ui.dropZone)return;let s=this.stores.uploads.filterByIndex({field:e}).length,r=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=r}async handleOperationCancelled(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all([...t.map((e=>this.removeUpload(e.id))),...s.map((e=>this.removeGroup(e.id,!1)))]),this.a11y.announce("Upload Cancelled")}async handleOperationFailed(e,t){await this.setBulkUpload(this.stores.uploads.filterByIndex({field:t}),"status","failed")}async handleFieldStatus(e,t){let s=t.status,r=this.stores.uploads.filterByIndex({field:e});await this.setBulkUpload(r,"status",s)}getGroupKey(e,t=null){return t?`${e}_${t}`:`${e}`}getSelectionHandler(e){let t=this.getGroupKey(e);if(!this.selectionHandlers.has(t)){let s=this.fields.get(e);if(!s)return;let r=new window.jvbHandleSelection({container:s.element,item:this.selectors.items.item,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions,checkbox:this.selectors.items.checkbox,selectAll:this.selectors.fields.selectAll,wrapper:`${this.selectors.fields.preview}, ${this.selectors.group.item}`});r.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems),console.log(Array.from(this.selected)),this.syncSortableSelection(e,s.selectedItems)})),this.selectionHandlers.set(t,r)}return this.selectionHandlers.get(t)}initSortable(e){if(!window.Sortable)return;const t=this.fields.get(e);t&&(!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0),this.createSortable(e,t.ui.grid,null),this.initEmptyGroupDropZone(e))}createSortable(e,t,s){if(!t)return null;const r=this.getGroupKey(e,s);if(this.sortables.has(r))return this.sortables.get(r);const i=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},ghostClass:"ghost",chosenClass:"chosen",dragClass:"dragging",onStart:t=>{const s=t.item,r=s?.dataset.uploadId,i=this.selected.get(e);if(r&&(!i||!i.has(r))){const t=this.selectionHandlers.get(e);t&&t.select(r)}this.syncSortableSelection(e)},onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(r,i),i}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),e.dataTransfer.dropEffect="move",s.classList.add("drag-over")})),s.addEventListener("dragleave",(e=>{s.contains(e.relatedTarget)||s.classList.remove("drag-over")})),s.addEventListener("drop",(async t=>{t.preventDefault(),s.classList.remove("drag-over");const r=this.selected.get(e);if(!r||0===r.size)return;const i=await this.createGroup(e);i&&(await Promise.all(Array.from(r).map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,r=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===r.length)return;const i=s.dataset.groupId||null;await Promise.all(r.map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(t)?.clearSelection()}syncSortableSelection(e){const t=this.selected.get(e)||new Set;for(const[s,r]of this.uploads){const i=this.stores.uploads.get(s);if(!i||i.field!==e)continue;const o=r.element;if(!o)continue;const a=t.has(s);a&&!o.classList.contains("selected")?Sortable.utils.select(o):!a&&o.classList.contains("selected")&&Sortable.utils.deselect(o)}}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(!s)return void console.log("Couldn't Reorder items...");let r=Array.from(s.querySelectorAll(this.selectors.items.item+":not(.ghost)")).map((e=>e.dataset.uploadId)).filter((e=>e));if(t){let e=this.groups.get(t);e&&(e.uploads=r)}else{let t=this.fields.get(e)?.ui.hidden;t&&(t.value=r.join(","))}this.a11y.announce("Items reordered")}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.previewUrls.forEach((e=>{this.revokePreviewUrl(e)})),this.previewUrls.clear()}cleanupAllPreviewUrls(){this.previewUrls.forEach((e=>this.revokePreviewUrl(e))),this.previewUrls.clear()}async handleClearCache(){const e=window.location.href,t=this.stores.uploads.filterByIndex({src:e}),s=this.stores.groups.filterByIndex({src:e});await Promise.all([...t.map((e=>this.clearUpload(e.id))),...s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id))))]),this.restoreModal&&this.cleanupRestore(),this.a11y.announce("Cache cleared for this page")}async getFilesForForm(e){const t=e.querySelectorAll(this.selectors.fields.field),s=[];for(const e of t){const t=this.determineFieldId(e),r=e.dataset.field,i=this.stores.uploads.filterByIndex({field:t});for(const e of i){const t=this.formatFile(e);t&&s.push({file:t,fieldName:r,uploadId:e.id,meta:e.fields||{}})}}return s}async clearFieldFromStores(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all(t.map((e=>this.clearUpload(e.id)))),await Promise.all(s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id)))))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUploads=new e)}))}))})(); |
| | |
| | | (()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t,e="default"){const n=t instanceof Date?t:new Date(t),i=n-new Date,o=i<0,r=Math.floor(Math.abs(i)/1e3),a=Math.floor(r/60),s=Math.floor(a/60),c=Math.floor(s/24);if(0===a)return"Just now";let l="";if(r<10)l="a moment";else if(r<60)l="less than a minute";else if(a<5)l="a few minutes";else if(s<24)l=0===s?`${a} ${1===a?"minute":"minutes"}`:`about ${s} ${1===s?"hour":"hours"}`;else{if(!(c<7)){if("default"===e)return n.toLocaleDateString();const t={Y:n.getFullYear(),y:String(n.getFullYear()).slice(-2),F:n.toLocaleDateString("en-CA",{month:"long"}),M:n.toLocaleDateString("en-CA",{month:"short"}),m:String(n.getMonth()+1).padStart(2,"0"),n:n.getMonth()+1,d:String(n.getDate()).padStart(2,"0"),j:n.getDate(),D:n.toLocaleDateString("en-CA",{weekday:"short"}),l:n.toLocaleDateString("en-CA",{weekday:"long"}),H:String(n.getHours()).padStart(2,"0"),i:String(n.getMinutes()).padStart(2,"0"),s:String(n.getSeconds()).padStart(2,"0"),h:String(n.getHours()%12||12).padStart(2,"0"),g:n.getHours()%12||12,A:n.getHours()>=12?"PM":"AM",a:n.getHours()>=12?"pm":"am"};return e.replace(/[YyFMmnjDlHishgAa]/g,(e=>t[e]))}if(1===c)return o?"yesterday":"tomorrow";l=`about ${c} days`,l=`${c} ${1===c?"day":"days"}`}return o?`${l} ago`:`in ${l}`},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.icon=null,window.getIcon=function(t,e=""){if(void 0===t)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return e=""!==e&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${e.slice(0,2)}`:"",n.classList.add(`icon-${t}${e}`),n},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-CA",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})}`},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.generateID=function(t="jvb"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(t,e,n,i="",o=""){const r=e<n;t.progress&&r&&window.fade(t.progress,!0);const a=n>0?e/n*100:0;t.fill&&(t.fill.style.width=`${a}%`),t.details&&(t.details.textContent=i),t.count&&(t.count.textContent=`${e}/${n}`),t.icon&&(t.icon.className=""===o?"icon":"icon icon-"+o),t.progress&&e===n&&window.fade(t.progress,!1)},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(t){return t instanceof Date&&!isNaN(t)||(t=new Date(t)),window.dateFormatter.format(t)},window.typeText=function(t,e,n=50){return t.classList.add("typeText"),new Promise((i=>{let o=0;t.textContent="";const r=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(r),i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{let i=t.textContent,o=i.length;const r=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(r),n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){let a=!0;return async function(){for(;a;)await window.typeText(t,e,n),await new Promise((t=>setTimeout(t,o))),await window.eraseText(t,i),await new Promise((t=>setTimeout(t,r)))}(),function(){a=!1}},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return Array.isArray(e)&&(e=e.join(",")),"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const a=this.map(t[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())clearTimeout(t);this.timeouts.clear()}};document.body;const t=document.documentElement,e=document.querySelector(".scroll-progress .bar");let n=window.scrollY||t.scrollTop||0,i=-1,o=!1,r=0;function a(){r=Math.max(0,t.scrollHeight-window.innerHeight)}function s(t){if(!e)return;const n=r>0?t/r:0,i=Math.max(0,Math.min(1,n));e.style.transform=`scaleX(${i})`}function c(){const e=window.scrollY||t.scrollTop||0;e>n?i=1:e<n&&(i=-1),n=e,document.body.classList.toggle("scroll-up",i<0&&e>0),s(e),o=!1}window.addEventListener("scroll",(()=>{o||(o=!0,requestAnimationFrame(c))}),{passive:!0}),window.addEventListener("resize",(()=>{window.debouncer.schedule("recalc-max-scroll",(()=>{a(),s(window.scrollY||t.scrollTop||0)}),20)})),a(),s(n)})(); |
| | | (()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t,e="default"){const n=t instanceof Date?t:new Date(t),i=n-new Date,o=i<0,r=Math.floor(Math.abs(i)/1e3),a=Math.floor(r/60),s=Math.floor(a/60),l=Math.floor(s/24);if(0===a)return"Just now";let c="";if(r<10)c="a moment";else if(r<60)c="less than a minute";else if(a<5)c="a few minutes";else if(s<24)c=0===s?`${a} ${1===a?"minute":"minutes"}`:`about ${s} ${1===s?"hour":"hours"}`;else{if(!(l<7)){if("default"===e)return n.toLocaleDateString();const t={Y:n.getFullYear(),y:String(n.getFullYear()).slice(-2),F:n.toLocaleDateString("en-CA",{month:"long"}),M:n.toLocaleDateString("en-CA",{month:"short"}),m:String(n.getMonth()+1).padStart(2,"0"),n:n.getMonth()+1,d:String(n.getDate()).padStart(2,"0"),j:n.getDate(),D:n.toLocaleDateString("en-CA",{weekday:"short"}),l:n.toLocaleDateString("en-CA",{weekday:"long"}),H:String(n.getHours()).padStart(2,"0"),i:String(n.getMinutes()).padStart(2,"0"),s:String(n.getSeconds()).padStart(2,"0"),h:String(n.getHours()%12||12).padStart(2,"0"),g:n.getHours()%12||12,A:n.getHours()>=12?"PM":"AM",a:n.getHours()>=12?"pm":"am"};return e.replace(/[YyFMmnjDlHishgAa]/g,(e=>t[e]))}if(1===l)return o?"yesterday":"tomorrow";c=`about ${l} days`,c=`${l} ${1===l?"day":"days"}`}return o?`${c} ago`:`in ${c}`},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.icon=null,window.getIcon=function(t,e=""){if(void 0===t)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return e=""!==e&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${e.slice(0,2)}`:"",n.classList.add(`icon-${t}${e}`),n},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-CA",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})}`},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.generateID=function(t="jvb"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(t,e,n,i="",o=""){const r=e<n;t.progress&&r&&window.fade(t.progress,!0);const a=n>0?e/n*100:0;t.fill&&(t.fill.style.width=`${a}%`),t.details&&(t.details.textContent=i),t.count&&(t.count.textContent=`${e}/${n}`),t.icon&&(t.icon.className=""===o?"icon":"icon icon-"+o),t.progress&&e===n&&window.fade(t.progress,!1)},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(t){return t instanceof Date&&!isNaN(t)||(t=new Date(t)),window.dateFormatter.format(t)},window.typeText=function(t,e,n=50){return new Promise((i=>{t._typeInterval&&(clearInterval(t._typeInterval),delete t._typeInterval);let o=0;t.textContent="",t._typeInterval=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(t._typeInterval),delete t._typeInterval,i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{t._eraseInterval&&(clearInterval(t._eraseInterval),delete t._eraseInterval);let i=t.textContent,o=i.length;t._eraseInterval=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(t._eraseInterval),delete t._eraseInterval,n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){const a=t.id||t.dataset.typeKey||`type-${Date.now()}`;t.dataset.typeKey||(t.dataset.typeKey=a),t._stopTyping&&t._stopTyping();let s=!0;const l=function(){s=!1,t._typeInterval&&(clearInterval(t._typeInterval),delete t._typeInterval),t._eraseInterval&&(clearInterval(t._eraseInterval),delete t._eraseInterval)};return t._stopTyping=l,async function(){for(;s&&(await window.typeText(t,e,n),s)&&(await new Promise((t=>setTimeout(t,o))),s)&&(await window.eraseText(t,i),s);)await new Promise((t=>setTimeout(t,r)))}(),l},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return Array.isArray(e)&&(e=e.join(",")),"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const a=this.map(t[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())clearTimeout(t);this.timeouts.clear()}};document.body;const t=document.documentElement,e=document.querySelector(".scroll-progress .bar");let n=window.scrollY||t.scrollTop||0,i=-1,o=!1,r=0;function a(){r=Math.max(0,t.scrollHeight-window.innerHeight)}function s(t){if(!e)return;const n=r>0?t/r:0,i=Math.max(0,Math.min(1,n));e.style.transform=`scaleX(${i})`}function l(){const e=window.scrollY||t.scrollTop||0;e>n?i=1:e<n&&(i=-1),n=e,document.body.classList.toggle("scroll-up",i<0&&e>0),s(e),o=!1}window.addEventListener("scroll",(()=>{o||(o=!0,requestAnimationFrame(l))}),{passive:!0}),window.addEventListener("resize",(()=>{window.debouncer.schedule("recalc-max-scroll",(()=>{a(),s(window.scrollY||t.scrollTop||0)}),20)})),a(),s(n)})(); |
| | |
| | | .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} |
| | |
| | | .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} |
| | |
| | | <?php return array('dependencies' => array(), 'version' => '1469437e42d138dbcd90'); |
| | | <?php return array('dependencies' => array(), 'version' => 'b17f126267647a51eaa7'); |
| | |
| | | (()=>{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.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()}),{timeout:2e3}):setTimeout((()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),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.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.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}})),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach((e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(e)}))}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}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}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.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach((i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)}));let i=!1;return e.forEach(((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}})),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach((t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.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.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach((e=>{const t=`${this.config.source}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)}));const e=`${this.config.source}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}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)=>{var i;"data-loaded"===e&&(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))}))}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)}))}))})(); |
| | |
| | | <?php |
| | | |
| | | use JVBase\managers\CacheManager; |
| | | use JVBase\utility\Features; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | |
| | | function jvbUserIsVerified():bool |
| | | { |
| | | return (Features::forMembership()->has('member_verified')) ? |
| | | current_user_can('skip_moderation') : |
| | | true; |
| | | return !(Features::forMembership()->has('member_verified')) || current_user_can('skip_moderation'); |
| | | } |
| | | |
| | | function jvbUserTypes():array |
| | |
| | | |
| | | function jvbTermHasPosts(int $termID, string $taxonomy):bool |
| | | { |
| | | $cache = new \JVBase\managers\CacheManager('termCheck', 60*30); |
| | | $cache = CacheManager::for('terms', 30*60)->connectTo('taxonomy'); |
| | | $key = $termID.$taxonomy; |
| | | $cached = $cache->get($key); |
| | | if ($cached) { |
| | |
| | | <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> |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | 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> |
| | |
| | | |
| | | $this->title = JVB_TAXONOMY[$this->name]['plural']; |
| | | $this->base = $config['base']??''; |
| | | |
| | | $this->config = wp_parse_args($config, [ |
| | | 'types' => false, // for feed block implementation |
| | | 'max' => 0, // 0 = unlimited |
| | |
| | | <h3 id="modal-title">Select Taxonomy</h3> |
| | | </header> |
| | | |
| | | <div class="selected-items-section"> |
| | | <div class="selected-items row" role="region" aria-label="Selected items"> |
| | | <!-- Selected items will be populated here --> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="selected-items row" role="region" aria-label="Selected items"></div> |
| | | |
| | | <div class="items-wrap"> |
| | | <!-- Common/Favorite terms section --> |
| | |
| | | <!-- Terms will be populated here --> |
| | | </ul> |
| | | |
| | | <!-- Loading indicator --> |
| | | <p class="loading" hidden aria-live="polite"> |
| | | <span>loading items</span> |
| | | <p class="message" hidden aria-live="polite"> |
| | | { <span>loading items</span> } |
| | | </p> |
| | | <button class="submit-term" hidden data-ignore><strong>Create: </strong> "<span></span>"</button> |
| | | |
| | | <!-- Infinite scroll sentinel --> |
| | | <div class="scroll-sentinel" aria-hidden="true"></div> |
| | |
| | | <!-- Search section --> |
| | | <div class="search-wrapper"> |
| | | <div class="search-bar"> |
| | | <?= jvbSearch('Search terms') ?> |
| | | <?= jvbSearch('Search terms', 'search-terms') ?> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | <option value="0">None (Top Level)</option> |
| | | </select> |
| | | </div> |
| | | |
| | | <button type="button" class="submit-term">Add Term</button> |
| | | </form> |
| | | <div class="term-suggestions" hidden><h4></h4><ul class="term-suggestion-list"></ul></div> |
| | | <div class="loading-message create-term" hidden> |
| | | <span id="typed-text"></span> |
| | | <span class="cursor">|</span> |
| | | </div> |
| | | |
| | | </div> |
| | | </details> |
| | | <?= jvbModalActions(); ?> |
| | |
| | | <template class="loadingItems"> |
| | | <p>{ <span>loading items</span> }</p> |
| | | </template> |
| | | <template class="autocompleteButton"> |
| | | <button class="autocomplete submit-term" type="button"><strong>Create: </strong><span></span></button> |
| | | </template> |
| | | <template class="autocompleteItem"> |
| | | <button class="autocomplete item" type="button" data-autocomplete-select></button> |
| | | <li class="autocomplete item btn"></li> |
| | | </template> |
| | | <template class="noTermResults"> |
| | | <p>{ <span>nothing found</span> }</p> |
| | | </template> |
| | | <template class="termListItem"> |
| | | <li> |
| | | <input type ="checkbox"> |
| | | <input type="checkbox"> |
| | | <label> |
| | | <span class="term-name"></span> |
| | | </label> |
| | |
| | | <template class="selectedTerm"> |
| | | <div class="selected-item row"> |
| | | <span class="item-name"></span> |
| | | <button type="button" class="remove-item row"><?=jvbIcon('x')?></button> |
| | | <button type="button" class="remove-term row"><?=jvbIcon('x')?></button> |
| | | </div> |
| | | </template> |
| | | <template class="termBreadcrumb"> |
| | |
| | | </button> |
| | | <?php if ($hasAutocomplete !== '') { ?> |
| | | <input type="text" id="<?= $this->base ?><?= esc_attr($this->config['name']) ?>-autocomplete" autocomplete="off" data-ignore data-autocomplete> |
| | | <ul class="search-results" hidden> |
| | | </ul> |
| | | <div class="auto-wrapper" hidden> |
| | | <ul class="search-results"> |
| | | </ul> |
| | | <p class="message" hidden aria-live="polite"> |
| | | { <span>loading items</span> } |
| | | </p> |
| | | <button class="submit-term" hidden data-ignore><strong>Create: </strong> "<span></span>"</button> |
| | | </div> |
| | | |
| | | <?php } ?> |
| | | </div> |
| | | |
| | | <div class="selected-items row" role="region" aria-label="Selected <?= esc_attr($this->plural) ?>"> |
| | | <?php if (!empty($selected)): ?> |
| | | <?php foreach ($selected as $termId): ?> |
| | | <?php $this->renderSelectedTerm($termId); ?> |
| | | <?php endforeach; ?> |
| | | <?php endif; ?> |
| | | </div> |
| | | |
| | | <?php |
| | | $selectedItems = ''; |
| | | if (!empty($selected)): |
| | | ob_start(); |
| | | foreach ($selected as $termId): |
| | | $this->renderSelectedTerm($termId); |
| | | endforeach; |
| | | $selectedItems = ob_get_clean(); |
| | | endif; |
| | | ?> |
| | | <div class="selected-items row" role="region" aria-label="Selected <?= esc_attr($this->plural) ?>"><?=$selectedItems?></div> |
| | | <?= $extra ?> |
| | | </div> |
| | | <?php |
| | |
| | | |
| | | 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 |
| | | ); |
| | | } |
| | | |
| | | /** |
| | |
| | | <div class="selected-item row" data-id="<?= esc_attr($termId) ?>"> |
| | | <span><?= esc_html($termPath) ?></span> |
| | | <button type="button" |
| | | class="remove-item row" |
| | | class="remove-term row" |
| | | aria-label="Remove <?= esc_attr($term->name) ?>"> |
| | | <?= jvbIcon('x') ?> |
| | | </button> |
| | |
| | | } |
| | | |
| | | ?> |
| | | <aside id="queue" class="left col start btw main" aria-expanded="false" hidden> |
| | | <aside id="queue" class="left col start btw main" aria-expanded="false" > |
| | | <div class="status-actions row start nowrap"> |
| | | <div class="refresh row btw"> |
| | | <span class="countdown row" title="Will refresh again...">5</span> |
| | |
| | | foreach($filters as $filter => $title) { |
| | | $active = ($i === 0) ? ' active': ''; |
| | | ?> |
| | | <button class="filter<?=$active?>" data-filter="<?=$filter?>" data-count="0"><span class="count row"></span><?=$title?></button> |
| | | <input type="radio" id="qfilter-<?=$filter?>" name="qfilter" class="btn filter<?=$active?>" data-filter="<?=$filter?>"> |
| | | <label for="qfilter-<?=$filter?>" data-count="0"><span class="count row"></span><?=$title?></label> |
| | | <?php |
| | | $i++; |
| | | } |
| | |
| | | <button class="retry-all">Retry Failed</button> |
| | | </div> |
| | | </aside> |
| | | <button class="qtoggle row" title="Show Queue" aria-controls="queue" hidden> |
| | | <button class="qtoggle row" title="Show Queue" aria-controls="queue" > |
| | | <?= jvbIcon('floppy-disk') ?> |
| | | <span class="screen-reader-text"></span> |
| | | <span class="indicator"></span> |
| | |
| | | <div class="item"> |
| | | <div class="header row btw"> |
| | | <span class="type"></span> |
| | | <span class="status row"><span class="screen-reader-text"></span></span> |
| | | <span class="status row"><?= jvbIcon('arrows-clockwise') ?><span class="screen-reader-text"></span></span> |
| | | </div> |
| | | <?php jvbRenderProgressBar() ?> |
| | | <?php jvbRenderProgressBar('',false,false) ?> |
| | | <div class="info"> |
| | | <div class="details"></div> |
| | | <div class="time row start"> |
| | | <?= jvbIcon('clock') ?> |
| | | <span class="started">Started: <span class="time"></span> |
| | | <span class="completed"></span> |
| | | <span class="started">Started: <time></time></span> |
| | | <span class="completed" hidden><span>Completed: </span><time></time></span> |
| | | </div> |
| | | </div> |
| | | <div class="actions row end"> |
| | | <button class="retry" data-action="retry"><span>Retry</span><?= jvbIcon('arrows-clockwise')?></button> |
| | | <button class="cancel" data-action="cancel"><span>Cancel</span><?= jvbIcon('x-square')?></button> |
| | | <button class="dismiss" data-action="dismiss"><span>Dismiss</span><?= jvbIcon('eye-closed')?></button> |
| | | |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | * Outputs the search bar (likely don't need anymore) |
| | | * @return string |
| | | */ |
| | | function jvbSearch(string $placeholder = 'Search...'):string |
| | | function jvbSearch(string $placeholder = 'Search...', string $id = 'search'):string |
| | | { |
| | | return '<div class="search-container row start nowrap"> |
| | | <input type="search" id="search" placeholder="'.$placeholder.'"> |
| | | <button |
| | | title="Clear Search" |
| | | type="button" |
| | | class="clear-search" |
| | | aria-label="Clear search" |
| | | onclick="this.previousElementSibling.value = \'\'; this.previousElementSibling.focus();" |
| | | >'.jvbIcon('x', ['title'=> 'Clear Search']).'</button> |
| | | <button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">'.jvbIcon('magnifying-glass').'</button> |
| | | </div>'; |
| | | $id = sanitize_title($id); |
| | | return sprintf( |
| | | '<div class="search-container row start nowrap"> |
| | | <input type="search" id="%s" placeholder="%s"> |
| | | <button |
| | | title="Clear Search" |
| | | type="button" |
| | | class="clear-search" |
| | | aria-label="Clear search" |
| | | onclick="this.previousElementSibling.value = \'\'; this.previousElementSibling.focus();" |
| | | >%s</button> |
| | | <button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">%s</button> |
| | | </div>', |
| | | $id, |
| | | $placeholder, |
| | | jvbIcon('x', ['title'=> 'Clear Search']), |
| | | jvbIcon('magnifying-glass') |
| | | ); |
| | | } |
| | | |
| | | |
| | |
| | | return $out; |
| | | } |
| | | |
| | | function jvbRenderProgressBar(string $inside ='', $top = false) |
| | | function jvbRenderProgressBar(string $inside ='', $top = false, $icon = true) |
| | | { |
| | | $top = $top ? ' abs top' : ''; |
| | | ?> |
| | |
| | | <div class="fill"></div> |
| | | </div> |
| | | <div class="row btw"> |
| | | <i class="icon"></i> |
| | | <?php if ($icon) { ?> |
| | | <i class="icon"></i> |
| | | <?php } ?> |
| | | <div class="details"> |
| | | <?=$inside?> |
| | | </div> |
| | |
| | | $this->config = $this->constant[$content]; |
| | | $this->content = $content; |
| | | $this->cache = CacheManager::for($content); |
| | | |
| | | // Create and configure skeleton |
| | | $this->skeleton = new CRUDSkeleton(); |
| | | $this->configure(); |
| | |
| | | // Initialize meta |
| | | $this->skeleton->initMeta($this->type, $this->content); |
| | | |
| | | |
| | | $this->skeleton->addSearch(); |
| | | |
| | | // Timeline if applicable |
| | | if (Features::forContent($this->content)->has('is_timeline')) { |
| | |
| | | { |
| | | switch ($action) { |
| | | case 'unlock-operation-queue': |
| | | error_log('Unlocking Queue from admin action'); |
| | | $this->unlockQueue(); |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| | |
| | | return; |
| | | } |
| | | |
| | | error_log('[queue] Checking queue...'); |
| | | |
| | | // Peek at what operations we might process |
| | | $batch_size = $this->getAdaptiveBatchSize(); |
| | |
| | | { |
| | | try { |
| | | $table = $this->wpdb->prefix . $this->table; |
| | | error_log('QUEUING OPERATION: '.print_r([ |
| | | 'type' => $type, |
| | | 'user_id' => $user_id, |
| | | 'data' => $data, |
| | | 'options' => $options, |
| | | ],true)); |
| | | |
| | | // Extract options |
| | | $priority = (array_key_exists('priority', $options) && in_array($options['priority'], ['low', 'normal', 'high'])) ? $options['priority'] : 'normal'; |
| | | $delay = (array_key_exists('delay', $options)) ? (int) $options['delay'] : 0; |
| | |
| | | // Generate operation ID |
| | | $operation_id = $this->checkOperationId($operation_id, $type, $merge, $user_id); |
| | | |
| | | error_log('Checked OperationId: '.print_r($operation_id, true)); |
| | | // Check for existing operation |
| | | $existing = $this->getOperation($operation_id); |
| | | error_log('Existing: '.print_r($existing, true)); |
| | | |
| | | $dependencies = $options['depends_on'] ?? []; |
| | | if (is_string($dependencies)) { |
| | |
| | | } |
| | | |
| | | if ($existing) { |
| | | error_log('[queue]:queueOperation - Already Existing operation... updating'); |
| | | |
| | | if ($merge !== 'append' && ($existing->status === 'pending' || $existing->status === 'scheduled')) { |
| | | error_log('Not appending operation, checking for merge'); |
| | | |
| | | if ($merge === 'merge') { |
| | | $existing_data = json_decode($existing->request_data ?? '{}', true); |
| | | $data = $this->deepMerge($existing_data, $data); |
| | |
| | | } |
| | | |
| | | } else if ($merge === 'append') { |
| | | error_log('Append operation, creating a new one'); |
| | | $operation_id = $operation_id . '_' . time() . '_' . substr(uniqid(), -4); |
| | | $existing = null; |
| | | } |
| | | } |
| | | |
| | | if (!$existing) { |
| | | error_log('Inserting new operation into table'); |
| | | // Prepare metadata with chunk config |
| | | $metadata = $chunk_config ? ['chunk_config' => $chunk_config] : []; |
| | | |
| | |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | error_log("Error queueing operation: " . $e->getMessage()); |
| | | JVB()->error()->log('queue', $e->getMessage(), $data, 'high'); |
| | | return new WP_Error('queue_failed', $e->getMessage()); |
| | | } |
| | |
| | | $merged = $existing; |
| | | |
| | | if (!$this->isAssociativeArray($existing) && !$this->isAssociativeArray($new)) { |
| | | error_log('It is an associative array!'); |
| | | return array_merge($existing, $new); |
| | | } |
| | | error_log('Not an associative array... moving on!'); |
| | | foreach ($new as $key => $newValue) { |
| | | if (!array_key_exists($key, $existing)) { |
| | | $merged[$key] = $newValue; |
| | |
| | | 'limit' => 1 |
| | | ]); |
| | | if ($existing) { |
| | | error_log('Found existing User operations: '.print_r($existing, true)); |
| | | $operation_id = $existing[0]->id; |
| | | } |
| | | } |
| | | |
| | | if (!$operation_id) { |
| | | $operation_id = uniqid('op_'); |
| | | error_log('Generated operation ID: '.print_r($operation_id, true)); |
| | | } |
| | | |
| | | if (!str_starts_with($operation_id, 'u')) { |
| | | $operation_id = 'u' . $user_id . '_' . $operation_id; |
| | | error_log('Added user ID to operation ID: '.print_r($operation_id, true)); |
| | | } |
| | | |
| | | return $operation_id; |
| | |
| | | { |
| | | // Only add if not already scheduled AND if operation is high priority or small |
| | | if (!has_action('shutdown', [$this, 'processQueueOnShutdown'])) { |
| | | error_log('Adding shutdown call'); |
| | | add_action('shutdown', [$this, 'processQueueOnShutdown'], 100); |
| | | } |
| | | } |
| | |
| | | protected function canProcessOperation(object $operation): bool |
| | | { |
| | | if ($operation->retries >= $this->max_attempts) { |
| | | error_log("Operation {$operation->id} exceeded retry limit: {$operation->retries} >= {$this->max_attempts}"); |
| | | $this->markAsPermanentlyFailed($operation->id, 'Exceeded maximum retry attempts'); |
| | | return false; |
| | | } |
| | |
| | | |
| | | if ($result) { |
| | | $this->invalidateQueueCache('status'); |
| | | error_log("Marked operation {$operation_id} as permanently failed: {$final_error_message}"); |
| | | } |
| | | } |
| | | |
| | |
| | | ['%s'] |
| | | ); |
| | | |
| | | // Log retry schedule |
| | | error_log(sprintf( |
| | | "Operation %s scheduled for retry #%d at %s (delay: %ds)", |
| | | $operation_id, |
| | | $attempt, |
| | | $scheduled_at, |
| | | $delay |
| | | )); |
| | | } |
| | | |
| | | /** |
| | |
| | | ]; |
| | | } |
| | | |
| | | error_log('Filtered Result: '.print_r($filterResult, true)); |
| | | |
| | | $newCount = $progress_count + $chunk['progress']; |
| | | |
| | | if ($filterResult['success']) { |
| | |
| | | $filterResult['result'] = [$filterResult['result']]; |
| | | } |
| | | // Store the result data |
| | | error_log('Merging Old Result: '. print_r($oldResult, true)); |
| | | error_log('With Newer Result: '. print_r($filterResult['result'], true)); |
| | | $resultToStore = $this->deepMerge($oldResult, $filterResult['result']); |
| | | |
| | | error_log('Merged Result: '.print_r($resultToStore, true)); |
| | | |
| | | $resultToStore['processed_at'] = current_time('mysql'); |
| | | |
| | | // Check if operation is complete |
| | |
| | | ['%s'] |
| | | ); |
| | | |
| | | |
| | | |
| | | |
| | | error_log('Completion result: '.print_r($result, true)); |
| | | // Now do post-completion tasks |
| | | $this->invalidateQueueCache('status'); |
| | | $this->updateLastModified($operation->user_id); |
| | |
| | | ", $this->max_attempts)); |
| | | |
| | | if (!empty($stuck_operations)) { |
| | | error_log("Found " . count($stuck_operations) . " stuck operations to clean up"); |
| | | |
| | | foreach ($stuck_operations as $operation) { |
| | | $this->markAsPermanentlyFailed( |
| | | $operation->id, |
| | |
| | | ", date('Y-m-d H:i:s', strtotime('-1 hour')))); |
| | | |
| | | if (!empty($long_processing)) { |
| | | error_log("Found " . count($long_processing) . " long-running operations to reset"); |
| | | |
| | | foreach ($long_processing as $operation) { |
| | | $this->wpdb->update($table, [ |
| | | 'status' => 'pending', |
| | |
| | | * Generic selector field renderer |
| | | * Handles both taxonomy and post selectors with consistent structure |
| | | */ |
| | | private function renderSelectorField(string $name, mixed $value, array $field, string $type): void |
| | | public function renderSelectorField(string $name, mixed $value, array $field, string $type): void |
| | | { |
| | | $conditional = $this->handleConditionalField($field); |
| | | $validationAttrs = $this->buildValidationAttributes($field); |
| | | $describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : ''; |
| | | |
| | | $isSimple = (array_key_exists('mode', $field) && $field['mode']==='simple'); |
| | | // Parse selected values |
| | | $value = (is_array($value)) ? array_filter(array_map('absint', $value)): $value; |
| | | $selected = ($value === '') ? [] : (is_array($value) ? $value : explode(',', $value)); |
| | |
| | | case 'cancel': |
| | | // Can only cancel pending/queued operations |
| | | if (!in_array($operation_status, ['pending', 'queued'])) { |
| | | $errors[] = "Operation {$operation_id} cannot be cancelled (status: {$operation_status})"; |
| | | // $errors[] = "Operation {$operation_id} cannot be cancelled (status: {$operation_status})"; |
| | | continue 2; |
| | | } |
| | | |
| | |
| | | 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); |
| | | } |
| | |
| | | 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) { |
| | |
| | | if (str_contains($taxonomy, ',')) { |
| | | return $this->handleBatchTermRequest($taxonomy, $data, $request); |
| | | } |
| | | $taxonomy = jvbCheckBase($taxonomy); |
| | | |
| | | if (array_key_exists('termIDs', $data)) { |
| | | $args = [ |
| | |
| | | unset($context['upload_ids']); |
| | | |
| | | $config = $this->getFieldConfig($args); |
| | | error_log('secureFiles: '.print_r($files, true)); |
| | | $file_array = $files['files'] ?? $files; |
| | | $tmp_names = isset($file_array['tmp_name'][0]) && is_array($file_array['tmp_name'][0]) |
| | | ? $file_array['tmp_name'][0] |
| | |
| | | $args = $this->buildUploadArgs($request); |
| | | $data = $request->get_params(); |
| | | |
| | | error_log('get_file_params: '.print_r($files, true)); |
| | | global $_FILES; |
| | | error_log('Global Files: '.print_r($_FILES, true)); |
| | | |
| | | if (!$args['content'] || !$args['user'] || !$args['posts']) { |
| | | |
| | |
| | | $created_posts = []; |
| | | $used_upload_ids = []; |
| | | |
| | | error_log('Processing Group Data: '.print_r($data, true)); |
| | | // Create posts from groups |
| | | foreach ($data['posts'] as $index => $post) { |
| | | $post_title = !empty($post['fields']['post_title']) |
| | |
| | | $created_posts[] = $new_post_id; |
| | | |
| | | // Get featured image upload_id |
| | | $featured_upload_id = (int)$post['fields']['featured'] ?? null; |
| | | |
| | | |
| | | $featured_upload_id = array_key_exists('featured', $post['fields']) ? (int)$post['fields']['featured'] : null; |
| | | $featured_attachment_id = null; |
| | | $gallery_attachment_ids = []; |
| | | |
| | |
| | | set_post_thumbnail($new_post_id, (int)$gallery_attachment_ids[0]); |
| | | array_shift($gallery_attachment_ids); |
| | | } |
| | | error_log('Remaining Gallery images: '.print_r($gallery_attachment_ids, true)); |
| | | |
| | | // Set gallery images |
| | | if (!empty($gallery_attachment_ids)) { |
| | | $meta = new MetaManager($new_post_id, 'post'); |
| | |
| | | $used_upload_ids = []; |
| | | |
| | | $content = jvbCheckBase($data['content']); |
| | | error_log('[processTimelineUploads]Got content: '.print_r($content, true)); |
| | | |
| | | $config = Features::getConfig($content); |
| | | error_log('[processTimelineUploads]Got config: '.print_r($config, true)); |
| | | |
| | | $defaultTitle = 'New '.$config['singular']. ' '; |
| | | foreach ($data['posts'] as $index=> $post) { |
| | | $title = !empty($post['fields']['post_title']) |
| | |
| | | return $this; |
| | | } |
| | | |
| | | protected function taxConfig(string $taxonomy, string $label = ''):array |
| | | { |
| | | $isVerified = jvbUserIsVerified(); |
| | | $label = ($label === '') ? JVB_TAXONOMY[$taxonomy]['plural'] : $label; |
| | | return [ |
| | | 'type' => 'taxonomy', |
| | | 'label' => $label, |
| | | 'taxonomy' => $taxonomy, |
| | | 'createNew' => $isVerified, |
| | | 'multiple' => true, |
| | | 'mode' => 'append', |
| | | ]; |
| | | } |
| | | |
| | | public function addSearch():self |
| | | { |
| | | $this->hasSearch = true; |
| | |
| | | $temp = array_filter($fields, function ($field) { |
| | | return in_array($field, $this->timelineUniqueFields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | $config = [ |
| | | 'type' => 'gallery', |
| | | 'subtype' => 'timeline', |
| | |
| | | $content = ''; |
| | | foreach ($fields as $slug=> $field) { |
| | | if (in_array($slug, $this->timelineSharedFields)) { |
| | | if (in_array($field['type'], ['taxonomy', 'selector'])) { |
| | | $field = array_merge($field, $this->taxConfig($field['taxonomy'], $field['label'])); |
| | | } |
| | | $content .= $this->form->render($slug, null, $field, false, true); |
| | | } |
| | | } |
| | |
| | | $section = (array_key_exists('section', $config)) ? $config['section'] : 'basic'; |
| | | $tabs[$section]['content'] .= $this->meta->render('form', $n, $config, false, true); |
| | | } else { |
| | | if (in_array($config['type'], ['taxonomy', 'selector'])) { |
| | | $config = array_merge($config, $this->taxConfig($config['taxonomy'], $config['label'])); |
| | | } |
| | | $this->meta->render('form', $n, $config); |
| | | } |
| | | } |
| | |
| | | <div class="taxonomies"> |
| | | <?php |
| | | foreach ($this->taxonomies as $taxonomy => $config) { |
| | | $this->meta->render( |
| | | 'form', |
| | | $this->form->renderSelectorField( |
| | | 'bulk-edit-'.$taxonomy, |
| | | [ |
| | | 'type' => 'taxonomy', |
| | | 'label' => $config['label'], |
| | | 'taxonomy' => $taxonomy, |
| | | 'createNew' => jvbUserIsVerified(), |
| | | 'multiple' => true, |
| | | 'mode' => 'append' |
| | | ] |
| | | '', |
| | | $this->taxConfig($taxonomy, $config['label']), |
| | | 'taxonomy' |
| | | ); |
| | | } |
| | | ?> |
| | |
| | | */ |
| | | class Navigation { |
| | | private string $id; |
| | | private array $items = []; |
| | | public array $items = []; |
| | | private array $classes = []; |
| | | protected array $defaultMenuClasses = []; |
| | | private bool $isNav = true; |
| | |
| | | $menuItem->url($item['url']); |
| | | } |
| | | |
| | | |
| | | if (!empty($item['submenu'])) { |
| | | $submenu = $menuItem->submenu(); |
| | | $submenu->populateFromArray($item['submenu']); |
| | |
| | | * @return string |
| | | */ |
| | | public function render(): string { |
| | | if (!$this->url && (!$this->submenu || empty($this->submenu->items))) { |
| | | return ''; |
| | | } |
| | | $classes = $this->classes; |
| | | if ($this->submenu) { |
| | | $classes[] = 'has-submenu'; |
| | |
| | | padding: 1rem 0; |
| | | max-width:var(--wide); |
| | | margin: 0 auto; |
| | | |
| | | .remove-term.remove-term { |
| | | width: max-content; |
| | | height: max-content |
| | | } |
| | | } |
| | | .filter-group { |
| | | position: relative; |
| | |
| | | |
| | | img { |
| | | object-fit: cover; |
| | | width: 100%; |
| | | height: 100%; |
| | | //opacity: .7; |
| | | //filter: grayscale(.5) sepia(.3); |
| | | &:hover { |
| | |
| | | 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; |
| | | } |
| | |
| | | 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: '', |
| | |
| | | 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.processCachedFilters(); |
| | | this.processURLFilters(); |
| | | this.updateFilterUI(); |
| | | this.initGallery(); |
| | | }, { timeout: 2000 }); |
| | | } else { |
| | | setTimeout(() => { |
| | | this.initTaxonomies(); |
| | | this.initStore(); |
| | | this.initListeners(); |
| | | this.initTaxonomies(); |
| | | |
| | | this.processCachedFilters(); |
| | | this.processURLFilters(); |
| | | this.updateFilterUI(); |
| | | this.initGallery(); |
| | | }, 100); |
| | | } |
| | | } |
| | | |
| | | initElements() { |
| | | this.currentTaxonomies = new Set(); // Allowed Taxonomies, grabbed from active buttons |
| | | this.taxonomyFilters = {}; |
| | | this.elements = { |
| | | this.selectors = { |
| | | filterTrigger: '[data-filter]', |
| | | filters: { |
| | | content: '[data-filter="content"]', |
| | | orderby: '[data-filter="orderby"]', |
| | | order: '[data-filter="order"]', |
| | | match: '[data-filter="match"]', |
| | | actions: '.filter-actions .toggle-text', |
| | | container: '.filters', |
| | | content: '[data-filter="content"]', |
| | | orderby: '[data-filter="orderby"]', |
| | | order: '[data-filter="order"]', |
| | | match: '[data-filter="match"]', |
| | | favourites: '[data-filter="favourites"]', |
| | | taxonomy: '[data-filter^="taxonomy"]' |
| | | taxonomy: '[data-filter^="taxonomy"]', |
| | | }, |
| | | selectedTax: '.selected-items', |
| | | clearFilter: 'button.clear-filters', |
| | | loadMore: 'button.load-more', |
| | | filterContainer: '.filters', |
| | | grid: '.item-grid', |
| | | grid: '.item-grid', |
| | | selected: '.selected-items', |
| | | buttons: { |
| | | loadMore: 'button.load-more', |
| | | remove: '.remove-term', |
| | | clearFilters: 'button.clear-filters' |
| | | } |
| | | }; |
| | | this.ui = window.uiFromSelectors(this.elements); |
| | | this.ui = window.uiFromSelectors(this.selectors, this.container); |
| | | |
| | | //Add content and taxonomies |
| | | this.ui.content = this.ui.filters.container.querySelectorAll('[name="content"]'); |
| | | if (this.ui.content.length === 0) this.ui.content = false; |
| | | this.ui.taxonomies = this.ui.filters.container.querySelectorAll('[data-taxonomy]'); |
| | | if (this.ui.taxonomies.length === 0) this.ui.content = false; |
| | | this.ui.orderbyWrap = this.ui.filters.container.querySelector('[data-for-order]'); |
| | | if (this.ui.orderbyWrap.length === 0) this.ui.content = false; |
| | | this.ui.order = this.ui.filters.container.querySelectorAll('[data-filter="order"]'); |
| | | if (this.ui.order.length === 0) this.ui.content = false; |
| | | this.ui.orderby = this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'); |
| | | if (this.ui.orderby.length === 0) this.ui.content = false; |
| | | |
| | | this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false; |
| | | this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]'); |
| | | if (this.ui.content && this.ui.content.length > 0) { |
| | | this.contentTypes = Array.from( |
| | | this.ui.content |
| | | ).map(content => content.value); |
| | | } else { |
| | | this.contentTypes = [this.container.dataset['content']]; |
| | | } |
| | | |
| | | if (this.ui.taxonomies.length>0) { |
| | | this.taxonomies = Array.from( |
| | | this.ui.taxonomies, |
| | | ).map(content => content.dataset.taxonomy); |
| | | } else { |
| | | this.taxonomies = []; |
| | | } |
| | | |
| | | |
| | | this.contentTypes = (this.ui.content) |
| | | ? Array.from(this.ui.content).map(c => c.value) |
| | | : [this.container.dataset.content]; |
| | | this.taxonomies = (this.ui.taxonomies?.length > 0) |
| | | ? Array.from(this.ui.taxonomies).map(t => t.dataset.taxonomy) |
| | | : []; |
| | | } |
| | | |
| | | async initTaxonomies() { |
| | | this.selector = window.jvbSelector; |
| | | const buttons = document.querySelectorAll('[data-filter="taxonomy"]'); |
| | | initListeners() { |
| | | this.popStateHandler = this.handlePopState.bind(this); |
| | | this.clickHandler = this.handleClick.bind(this); |
| | | this.changeHandler = this.handleChange.bind(this); |
| | | |
| | | this.selector.isInitializing = true; |
| | | buttons.forEach((button) => { |
| | | const taxonomy = button.dataset.taxonomy; |
| | | this.currentTaxonomies.add(taxonomy); |
| | | |
| | | this.selector.registerFilterButton(button, { |
| | | button: button, |
| | | buttonSelector: '[data-filter="taxonomy"]', |
| | | selected: this.ui.selectedTax |
| | | }); |
| | | |
| | | // Add preload listeners |
| | | this.addTaxonomyPreloadListeners(button, taxonomy); |
| | | }); |
| | | |
| | | this.selector.isInitializing = false; |
| | | |
| | | this.selector.subscribe((event, data) => { |
| | | if (event === 'selected-terms') this.handleTaxonomyChange(data); |
| | | }); |
| | | } |
| | | |
| | | addTaxonomyPreloadListeners(button, taxonomy) { |
| | | const preload = () => { |
| | | this.selector.preloadTaxonomy(taxonomy); |
| | | }; |
| | | |
| | | // Desktop hover |
| | | button.addEventListener('mouseenter', preload, { once: true }); |
| | | |
| | | // Touch/keyboard (fires before click) |
| | | button.addEventListener('pointerdown', preload, { once: true }); |
| | | |
| | | // Keyboard focus |
| | | button.addEventListener('focus', preload, { once: true }); |
| | | } |
| | | |
| | | handleTaxonomyChange(data) { |
| | | const { terms, taxonomy } = data; |
| | | |
| | | // Update only the current taxonomy's terms |
| | | if (terms.size > 0) { |
| | | this.taxonomyFilters[taxonomy] = Array.from(terms.keys()); |
| | | } else { |
| | | // Remove taxonomy if no terms selected |
| | | delete this.taxonomyFilters[taxonomy]; |
| | | } |
| | | |
| | | // Build filters object with all taxonomies |
| | | let filters = { |
| | | page: 1 |
| | | }; |
| | | |
| | | // Add taxonomy filters if any exist |
| | | if (Object.keys(this.taxonomyFilters).length > 0) { |
| | | filters.taxonomy = this.taxonomyFilters; |
| | | } |
| | | |
| | | this.updateFilter(filters); |
| | | } |
| | | |
| | | clearAllTaxonomies() { |
| | | this.taxonomyFilters = {}; |
| | | window.removeChildren(this.ui.selectedTax); |
| | | |
| | | this.updateFilter({ |
| | | taxonomy: null, |
| | | page: 1 |
| | | }); |
| | | window.addEventListener('popstate', this.popStateHandler); |
| | | document.addEventListener('click', this.clickHandler); |
| | | document.addEventListener('change', this.changeHandler); |
| | | } |
| | | |
| | | initFilters() { |
| | | //defaults |
| | | this.filters = { |
| | | content: this.contentTypes[0], |
| | | orderby: 'date', |
| | | order: 'desc', |
| | | page: 1 |
| | | this.allowedFilters = ['content', 'order', 'orderby', 'favourites', 'match']; |
| | | let defaults = { |
| | | content: this.contentTypes[0], |
| | | orderby: 'date', |
| | | order: 'desc', |
| | | page: 1, |
| | | }; |
| | | if (this.config.context) this.filters.context = this.config.context; |
| | | if (this.config.source) this.filters.source = this.config.source; |
| | | if (this.config.context) defaults.context = this.config.context; |
| | | if (this.config.source) defaults.source = this.config.source; |
| | | |
| | | //check the cache |
| | | this.processCachedFilters(); |
| | | //check url |
| | | this.processURLFilters(); |
| | | |
| | | // Set initial UI state |
| | | this.syncUIToFilters(); |
| | | this.filters = defaults; |
| | | 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.store.filters, filter)) break; |
| | | let doit = this.store.filters[filter] === value; |
| | | if (doit) { |
| | | input.checked = doit; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | }); |
| | | |
| | | if (Object.hasOwn(this.store.filters, 'taxonomy')) { |
| | | for (let [taxonomy, terms] of Object.entries(this.store.filters.taxonomy)) { |
| | | terms.forEach(termId => { |
| | | termId = parseInt(termId); |
| | | const term = this.selector.store.get(termId); |
| | | if (term) { |
| | | this.createTermElement(termId); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | 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 |
| | | }); |
| | | |
| | | this.updateURL(); |
| | | this.saveToCacheFilters(); |
| | | } |
| | | |
| | | resetFilters(filters) { |
| | | filters = { |
| | | ...this.store.filters, |
| | | page: 1, |
| | | ... filters |
| | | } |
| | | this.store.setFilters(filters); |
| | | |
| | | this.updateURL(); |
| | | this.saveToCacheFilters(); |
| | | } |
| | | |
| | | getFieldId(taxonomy) { |
| | | return this.selector.getFieldId(Array.from(this.ui.taxonomies).filter(tax => tax.dataset.taxonomy === taxonomy)[0]??null); |
| | | } |
| | | removeSelectedTerm(button) { |
| | | const termId = parseInt(button.dataset.id); |
| | | const taxonomy = button.dataset.taxonomy; |
| | | |
| | | if (Object.hasOwn(this.taxFilters, taxonomy)){ |
| | | this.taxFilters[taxonomy] = this.taxFilters[taxonomy] |
| | | .filter(id => id !== termId); |
| | | if (this.taxFilters[taxonomy].length === 0) { |
| | | delete this.taxFilters[taxonomy]; |
| | | } |
| | | } |
| | | button.remove(); |
| | | |
| | | // Find the fieldId for this taxonomy |
| | | const field = this.getFieldId(taxonomy); |
| | | if (field) { |
| | | this.selector.activeField = field; |
| | | // Notify selector to remove from its selectedTerms |
| | | this.selector.removeSelected(termId, field); |
| | | } |
| | | |
| | | this.resetFilters({ |
| | | taxonomy: Object.keys(this.taxFilters).length > 0 |
| | | ? this.taxFilters |
| | | : null |
| | | }); |
| | | } |
| | | |
| | | updateContentFor(content) { |
| | | let checkIt = [ |
| | | this.ui.taxonomies, |
| | | this.ui.orderby |
| | | ]; |
| | | checkIt.forEach(check => { |
| | | if (!check) return; |
| | | check.forEach(button => { |
| | | const forTypes = button.dataset.for?.split(',')??[]; |
| | | button.hidden = forTypes.length > 0 && !forTypes.includes(content); |
| | | if (button.hidden && button.checked) { |
| | | button.checked = false; |
| | | } |
| | | }); |
| | | }); |
| | | } |
| | | updateOrderOptions(order) { |
| | | if (this.ui.orderbyWrap) { |
| | | let options = this.ui.orderbyWrap.dataset.forOrder.split(',')??[]; |
| | | this.ui.orderbyWrap.hidden = !options.includes(order); |
| | | } |
| | | } |
| | | |
| | | updateFilterControls() { |
| | | const isHidden = Object.keys(this.taxFilters).length === 0; |
| | | if (this.ui.buttons.clearFilters) { |
| | | this.ui.buttons.clearFilters.hidden = isHidden; |
| | | } |
| | | if (this.ui.filters.actions) { |
| | | this.ui.filters.actions.hidden = isHidden; |
| | | } |
| | | } |
| | | |
| | | async initTaxonomies() { |
| | | this.taxFilters = {}; |
| | | this.selector = window.jvbSelector; |
| | | // this.selector.scanExistingFields(this.ui.filters.container); |
| | | // this.taxonomies.map(tax => this.selector.batchFetch.add(tax)); |
| | | // this.selector.batchFetchTaxonomies(); |
| | | this.selector.subscribe((event, data) => { |
| | | switch (event) { |
| | | case 'selected-terms': |
| | | |
| | | this.handleTaxonomyChange(data); |
| | | break; |
| | | |
| | | } |
| | | }); |
| | | } |
| | | handleTaxonomyChange(data) { |
| | | const {terms, taxonomy } = data; |
| | | if (terms.size === 0) return; |
| | | this.taxFilters[taxonomy] = Array.from(terms); |
| | | this.resetFilters({ taxonomy: this.taxFilters }); |
| | | |
| | | terms.forEach(t => { |
| | | this.createTermElement(t); |
| | | }); |
| | | this.updateFilterControls(); |
| | | } |
| | | getTaxonomyIcon(taxonomy) { |
| | | let iconButton = Array.from(this.ui.taxonomies) |
| | | .find(t => t.dataset.taxonomy === taxonomy); |
| | | return iconButton?.dataset.icon.trim() || 'tag'; |
| | | } |
| | | createTermElement(termId) { |
| | | const term = this.selector.store.get(termId); |
| | | if (!term) return; |
| | | if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return; |
| | | let icon = this.getTaxonomyIcon(term.taxonomy); |
| | | let template = window.getTemplate('feedTerm'); |
| | | if (!template) return; |
| | | let [iconEl,span] = [template.querySelector('.icon'), template.querySelector('span')]; |
| | | if (!iconEl || !span) return; |
| | | template.dataset.id = term.id; |
| | | template.dataset.taxonomy = term.taxonomy; |
| | | iconEl.className = `icon icon-${icon}`; |
| | | span.textContent = term.name; |
| | | |
| | | this.ui.selected.append(template); |
| | | } |
| | | |
| | | processCachedFilters() { |
| | | Object.keys(this.filters).forEach(filter => { |
| | | let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`); |
| | | if (cached && cached !== this.filters[filter]) { |
| | | this.filters[filter] = cached; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | processURLFilters() { |
| | | if (!this.isFirstPage()) return false; |
| | | const params = new URLSearchParams(window.location.search); |
| | | if (!params.toString()) return false; |
| | | let shouldUpdate = false; |
| | | this.allowedFilters.forEach(filter => { |
| | | let value = params.get(`f_${filter}`); |
| | | if (value) { |
| | | shouldUpdate = true; |
| | | this.filters[filter] = value; |
| | | } |
| | | }); |
| | | |
| | | let hasTax = false; |
| | | params.forEach((value, key) => { |
| | | if (key.startsWith('f_tax_')) { |
| | | hasTax = true; |
| | | shouldUpdate = true; |
| | | const taxonomy = key.replace('f_tax_',''); |
| | | this.taxFilters[taxonomy] = value.split(',').map(Number); |
| | | } |
| | | }); |
| | | if (shouldUpdate) { |
| | | if (hasTax) { |
| | | this.filters.taxonomy = this.taxFilters; |
| | | } |
| | | this.resetFilters(this.filters); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | updateURL() { |
| | | const params = new URLSearchParams(); |
| | | this.allowedFilters.forEach(key => { |
| | | if (Object.hasOwn(this.store.filters, key) && this.store.filters[key] !== this.defaults[key]) { |
| | | params.set(`f_${key}`, this.store.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() : ''}`; |
| | | const currentURL = window.location.pathname + window.location.search; // Change this line |
| | | |
| | | if (newURL !== currentURL) { |
| | | window.history.pushState({filters:this.store.filters}, '', newURL); |
| | | } |
| | | } |
| | | |
| | | saveToCacheFilters() { |
| | | Object.keys(this.store.filters).forEach(filter => { |
| | | const cacheKey = `${this.config.source}_${this.config.context}_${filter}`; |
| | | |
| | | if (this.store.filters[filter] !== this.defaults[filter]) { |
| | | this.cache.set(cacheKey, this.store.filters[filter]); |
| | | } else { |
| | | this.cache.remove(cacheKey); |
| | | } |
| | | }); |
| | | |
| | | const taxCacheKey = `${this.config.source}_${this.config.context}_taxonomy`; |
| | | if (Object.keys(this.taxFilters).length > 0) { |
| | | this.cache.set(taxCacheKey, this.taxFilters); |
| | | } else { |
| | | this.cache.remove(taxCacheKey); |
| | | } |
| | | } |
| | | |
| | | initGallery() { |
| | | this.gallery = (this.config.gallery) ? window.jvbGallery : false; |
| | | if (this.gallery) { |
| | | this.gallery.subscribe((event, data) => { |
| | | if (event === 'load-more' && this.store.lastResponse?.has_more) { |
| | | this.nextPage(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // Update content-specific visibility |
| | | this.updateContentFor(this.filters.content); |
| | | } |
| | | nextPage() { |
| | | this.store.setFilter('page', this.store.filters.page++); |
| | | } |
| | | |
| | | initStore() { |
| | | const store = window.jvbStore.register( |
| | | 'feed', |
| | |
| | | { 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; |
| | |
| | | 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; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | initGallery() { |
| | | this.gallery = (this.config.gallery) ? window.jvbGallery : false; |
| | | if (this.gallery) { |
| | | this.gallery.subscribe((event, data) => { |
| | | if (event === 'load-more' && this.store.lastResponse) { |
| | | if (this.store.lastResponse['has_more']) { |
| | | this.nextPage(); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | |
| | | processCachedFilters() { |
| | | Object.keys(this.filters).forEach(filter => { |
| | | let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`); |
| | | if (cached && cached !== this.filters[filter]){ |
| | | this.filters[filter] = cached; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | processURLFilters() { |
| | | if (this.filters.page > 1) { |
| | | return false; |
| | | } |
| | | const params = new URLSearchParams(window.location.search); |
| | | |
| | | if (!params.toString()) { |
| | | return false; |
| | | } |
| | | let filters = ['content', 'order', 'orderby', 'favourites', 'match']; |
| | | filters.forEach(filter => { |
| | | let value = params.get(`f_${filter}`); |
| | | if (value) { |
| | | this.filters[filter] = value; |
| | | let input = this.ui.filters[filter]; |
| | | if (input) { |
| | | input.checked = true; |
| | | } |
| | | } |
| | | }); |
| | | |
| | | let hasTaxonomy = false; |
| | | // Load taxonomy filters from URL |
| | | params.forEach((value, key) => { |
| | | if (key.startsWith('f_tax_')) { |
| | | hasTaxonomy = true; |
| | | const taxonomy = key.replace('f_tax_', ''); |
| | | if (!this.taxonomyFilters[taxonomy]) { |
| | | this.taxonomyFilters[taxonomy] = []; |
| | | } |
| | | this.taxonomyFilters[taxonomy] = value.split(',').map(Number); |
| | | } |
| | | }); |
| | | if (this.ui.filterContainer && hasTaxonomy) { |
| | | for (let [tax, ids] in Object.entries(this.taxonomyFilters)) { |
| | | let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`); |
| | | if (button) { |
| | | if (button.dataset.fieldId) { |
| | | let field = this.selector.get(button.dataset.fieldId); |
| | | field.selectedTerms = new Set(ids); |
| | | this.selector.initFieldDisplay(button.dataset.fieldId); |
| | | } else { |
| | | this.selector.registerField(button, { |
| | | button: button, |
| | | buttonSelector: '[data-filter="taxonomy"]', |
| | | selected: this.ui.selectedTax, |
| | | selectedItems: ids |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Update URL with current filters (for sharing/bookmarking) |
| | | */ |
| | | updateURL() { |
| | | const params = new URLSearchParams(); |
| | | |
| | | // Add simple filters |
| | | ['content', 'order', 'orderby', 'match'].forEach(key => { |
| | | if (this.filters[key]) { |
| | | params.set(`f_${key}`, this.filters[key]); |
| | | } |
| | | }); |
| | | |
| | | // Add taxonomy filters |
| | | Object.entries(this.taxonomyFilters).forEach(([taxonomy, terms]) => { |
| | | if (terms.length > 0) { |
| | | params.set(`f_tax_${taxonomy}`, terms.join(',')); |
| | | } |
| | | }); |
| | | |
| | | // Update URL without reload |
| | | const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`; |
| | | window.history.pushState({ filters: this.filters }, '', newURL); |
| | | isFirstPage() { |
| | | return this.store.filters.page === 1; |
| | | } |
| | | |
| | | renderItems() { |
| | | let items = this.store.getFiltered(); |
| | | if (this.store.filters['page'] === 1) { |
| | | if (this.isFirstPage()) { |
| | | window.removeChildren(this.ui.grid); |
| | | } |
| | | |
| | | if (items.length === 0) { |
| | | this.a11y.announceItems(0, this.store.filters['page'] > 0); |
| | | return; |
| | | } |
| | | |
| | | const fragment = document.createDocumentFragment(); |
| | | const batchSize = 10; |
| | | |
| | | const processBatch = (startIndex) => { |
| | | const endIndex = Math.min(startIndex + batchSize, items.length); |
| | | |
| | | for (let i = startIndex; i < endIndex; i++) { |
| | | const item = items[i]; |
| | | const element = this.createItemElement(item); |
| | | |
| | | fragment.appendChild(element); |
| | | } |
| | | |
| | | if (endIndex < items.length) { |
| | | requestAnimationFrame(() => processBatch(endIndex)); |
| | | } else { |
| | | this.removePlaceholders(); |
| | | this.ui.grid.append(fragment); |
| | | |
| | | if (this.config.gallery) { |
| | | this.gallery.updateGalleryItems(this.gallery.getGalleryItems()); |
| | | } |
| | | |
| | | this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])')); |
| | | this.a11y.announceItems(items.length, this.store.filters['page'] > 1, this.store.hasMore); |
| | | } |
| | | }; |
| | | |
| | | if (items.length > 0) { |
| | | processBatch(0); |
| | | this.showEmptyState(); |
| | | this.a11y.announceItems(0, this.isFirstPage()); |
| | | } else { |
| | | this.a11y.announceItems(0, this.store.filters['page'] >1, false); |
| | | const fragment = document.createDocumentFragment(); |
| | | const processBatch = (startIndex) => { |
| | | const endIndex = Math.min(startIndex + 10, items.length); |
| | | for (let i = startIndex; i < endIndex; i++) { |
| | | const item = items[i]; |
| | | const element = this.createItemElement(item); |
| | | fragment.append(element); |
| | | } |
| | | if (endIndex < items.length) { |
| | | requestAnimationFrame(() => processBatch(endIndex)); |
| | | } else { |
| | | this.removePlaceholders(); |
| | | this.ui.grid.append(fragment); |
| | | |
| | | if (this.config.gallery) this.gallery.buildGalleryItems('.item img'); |
| | | this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])')); |
| | | this.a11y.announceItems(items.length, !this.isFirstPage(), this.store.lastResponse?.has_more??false); |
| | | } |
| | | }; |
| | | processBatch(0); |
| | | } |
| | | |
| | | if (this.ui.filters.match) { |
| | | this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0; |
| | | } |
| | | if (this.ui.clearFilter) { |
| | | this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0; |
| | | } |
| | | this.updateFilterControls(); |
| | | } |
| | | |
| | | /** |
| | | * |
| | | * @param {object} item |
| | | */ |
| | | showEmptyState() { |
| | | window.removeChildren(this.ui.grid); |
| | | let template = window.getTemplate('emptyState'); |
| | | if (!template) return; |
| | | this.ui.grid.append(template); |
| | | |
| | | } |
| | | |
| | | createItemElement(item) { |
| | | let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`); |
| | | |
| | | const isTimeline = Object.hasOwn(template.dataset, 'timeline'); |
| | | |
| | | // Format fields using helpers |
| | | for (let [fieldName, value] of Object.entries(item.fields)) { |
| | | if (isTimeline && ['timeline', 'number'].includes(fieldName)) continue; |
| | | |
| | | let el = template.querySelector(`[data-field="${fieldName}"]`); |
| | | if (!el) continue; |
| | | |
| | | if (value === '') { |
| | | el.remove(); |
| | | continue; |
| | | } |
| | | |
| | | if (this.isImageField(item, value)) { |
| | | this.formatImageFields(el, value, item); |
| | | this.formatImageField(el, value, item); |
| | | } else if (this.isTaxonomyField(item, fieldName)) { |
| | | this.formatTaxonomyField(el, item, fieldName, value); |
| | | } else if (this.isTimeField(el)) { |
| | |
| | | } |
| | | } |
| | | |
| | | // 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) { |
| | |
| | | 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; |
| | |
| | | this.formatImageField(element, values[0], item); |
| | | } |
| | | } |
| | | formatImageField(element, value, item) { |
| | | let imgData = item.images[value]??false; |
| | | if (!imgData) return; |
| | | [ |
| | | element.src, |
| | | element.srcset, |
| | | element.alt |
| | | ] = [ |
| | | imgData.tiny, |
| | | `${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`, |
| | | imgData['image-alt-text'] |
| | | ] |
| | | } |
| | | formatImageField(element, value, item) { |
| | | let imgData = item.images[value]??false; |
| | | if (!imgData) return; |
| | | [ |
| | | element.src, |
| | | element.srcset, |
| | | element.alt |
| | | ] = [ |
| | | imgData.tiny, |
| | | `${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`, |
| | | imgData['image-alt-text'] |
| | | ] |
| | | } |
| | | isTaxonomyField(item, field) { |
| | | if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) { |
| | | return false; |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | 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() { |
| | |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | |
| | |
| | | class FeedBlock { |
| | | class FeedBlockOld { |
| | | constructor() { |
| | | this.cache = window.jvbCache; |
| | | this.a11y = window.jvbA11y; |
| | | this.loading = window.jvbLoading; |
| | | this.error = window.jvbError; |
| | | |
| | | |
| | | this.container = document.querySelector('section.feed-block'); |
| | | if (!this.container) { |
| | | return; |
| | | } |
| | | |
| | | this.openGallery = false; |
| | | this.a11y = window.jvbA11y; |
| | | this.cache = new window.jvbCache('feed'); |
| | | this.error = window.jvbError; |
| | | |
| | | this.initElements(); |
| | | this.addPlaceholders(); |
| | | this.config = { |
| | | api: feedSettings.apiUrl, |
| | | nonce: feedSettings.nonce, |
| | | user: jvbSettings.currentUser || null, |
| | | source: '', |
| | | context: '', |
| | | highlight: null, |
| | | gallery: false, |
| | | showAuthor: true, |
| | | showDate: false, |
| | | view: localStorage.getItem('feedViewMode') || 'grid', |
| | | view: this.cache.get('feedView') || 'grid', |
| | | ... this.container.dataset |
| | | }; |
| | | this.taxonomies = {}; |
| | | this.rendered = {}; |
| | | this.initElements(); |
| | | this.initFilters(); |
| | | |
| | | this.feed = { |
| | | imageLoadThreshold: 5, |
| | | lazyLoadOffset: '100px', |
| | | gallery: [], |
| | | loaded: 0, |
| | | intsersectionObserver: null, |
| | | templates: new Map() |
| | | }; |
| | | |
| | | this.isLoading = false; |
| | | this.hasMore = true; |
| | | this.retries = { |
| | | count: 0, |
| | | max: 3, |
| | | delay: 1000 |
| | | }; |
| | | this.page = 1; |
| | | this.order = 'DESC'; |
| | | this.orderby = 'date'; |
| | | |
| | | this.gallery = (this.config.gallery) ? new window.jvbGallery(document.querySelector('dialog.gallery'), { |
| | | imageWrapper: '.item', |
| | | loadMore: ()=>this.fetchFeed.bind(this) |
| | | }) : false; |
| | | this.initListeners(); |
| | | if (this.page === 1) { |
| | | this.processURLFilters(); |
| | | } else { |
| | | this.updateFilters(); |
| | | } |
| | | |
| | | this.loadWhenAble(); |
| | | } |
| | | |
| | | loadWhenAble() { |
| | | if ('requestIdleCallback' in window) { |
| | | requestIdleCallback(() => { |
| | | this.initTaxonomies(); |
| | | this.initStore(); |
| | | this.initListeners(); |
| | | this.initGallery(); |
| | | }, { timeout: 2000 }); |
| | | } else { |
| | | setTimeout(() => { |
| | | this.initTaxonomies(); |
| | | this.initStore(); |
| | | this.initListeners(); |
| | | this.initGallery(); |
| | | }, 100); |
| | | } |
| | | } |
| | | |
| | | initElements() { |
| | | this.filterSelector = 'form.feed-filters'; |
| | | this.filterForm = this.container.querySelector(this.filterSelector); |
| | | this.grid = this.container.querySelector('.item-grid'); |
| | | this.loadMore = this.container.querySelector('.load-more'); |
| | | this.filterControls = this.container.querySelector('.filter-actions'); |
| | | this.contentTypes = Array.from(this.filterForm.querySelectorAll('input[name="content"]')).map( |
| | | content => { |
| | | return content.value; |
| | | }); |
| | | this.selectedTerms = this.container.querySelector('.selected-items-section .selected-items'); |
| | | this.currentTaxonomies = new Set(); // Allowed Taxonomies, grabbed from active buttons |
| | | this.taxonomyFilters = {}; |
| | | this.elements = { |
| | | filterTrigger: '[data-filter]', |
| | | filters: { |
| | | content: '[data-filter="content"]', |
| | | orderby: '[data-filter="orderby"]', |
| | | order: '[data-filter="order"]', |
| | | match: '[data-filter="match"]', |
| | | favourites: '[data-filter="favourites"]', |
| | | taxonomy: '[data-filter^="taxonomy"]' |
| | | }, |
| | | selectedTax: '.selected-items', |
| | | clearFilter: 'button.clear-filters', |
| | | loadMore: 'button.load-more', |
| | | filterContainer: '.filters', |
| | | grid: '.item-grid', |
| | | }; |
| | | this.ui = window.uiFromSelectors(this.elements); |
| | | |
| | | |
| | | this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false; |
| | | this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]'); |
| | | if (this.ui.content && this.ui.content.length > 0) { |
| | | this.contentTypes = Array.from( |
| | | this.ui.content |
| | | ).map(content => content.value); |
| | | } else { |
| | | this.contentTypes = [this.container.dataset['content']]; |
| | | } |
| | | |
| | | if (this.ui.taxonomies.length>0) { |
| | | this.taxonomies = Array.from( |
| | | this.ui.taxonomies, |
| | | ).map(content => content.dataset.taxonomy); |
| | | } else { |
| | | this.taxonomies = []; |
| | | } |
| | | |
| | | |
| | | } |
| | | |
| | | initListeners() { |
| | | window.addEventListener('popstate', this.handlePopState.bind(this)); |
| | | document.addEventListener('click', this.handleClick.bind(this)); |
| | | document.addEventListener('change', this.handleChange.bind(this)); |
| | | async initTaxonomies() { |
| | | this.selector = window.jvbSelector; |
| | | const buttons = document.querySelectorAll('[data-filter="taxonomy"]'); |
| | | |
| | | this.selector.isInitializing = true; |
| | | buttons.forEach((button) => { |
| | | const taxonomy = button.dataset.taxonomy; |
| | | this.currentTaxonomies.add(taxonomy); |
| | | |
| | | // Intersection observer for lazy loading |
| | | if ('IntersectionObserver' in window) { |
| | | this.imageObserver = new IntersectionObserver(entries => { |
| | | entries.forEach(entry => { |
| | | if (entry.isIntersecting) { |
| | | this.loadImage(entry.target); |
| | | this.imageObserver.unobserve(entry.target); |
| | | } |
| | | }); |
| | | }, { |
| | | rootMargin: '100px', |
| | | threshold: 0.1 |
| | | this.selector.registerFilterButton(button, { |
| | | button: button, |
| | | buttonSelector: '[data-filter="taxonomy"]', |
| | | selected: this.ui.selectedTax |
| | | }); |
| | | |
| | | // Add preload listeners |
| | | this.addTaxonomyPreloadListeners(button, taxonomy); |
| | | }); |
| | | |
| | | this.selector.isInitializing = false; |
| | | |
| | | this.selector.subscribe((event, data) => { |
| | | if (event === 'selected-terms') this.handleTaxonomyChange(data); |
| | | }); |
| | | } |
| | | |
| | | addTaxonomyPreloadListeners(button, taxonomy) { |
| | | const preload = () => { |
| | | this.selector.preloadTaxonomy(taxonomy); |
| | | }; |
| | | |
| | | // Desktop hover |
| | | button.addEventListener('mouseenter', preload, { once: true }); |
| | | |
| | | // Touch/keyboard (fires before click) |
| | | button.addEventListener('pointerdown', preload, { once: true }); |
| | | |
| | | // Keyboard focus |
| | | button.addEventListener('focus', preload, { once: true }); |
| | | } |
| | | |
| | | handleTaxonomyChange(data) { |
| | | const { terms, taxonomy } = data; |
| | | |
| | | // Update only the current taxonomy's terms |
| | | if (terms.size > 0) { |
| | | this.taxonomyFilters[taxonomy] = Array.from(terms.keys()); |
| | | } else { |
| | | // Remove taxonomy if no terms selected |
| | | delete this.taxonomyFilters[taxonomy]; |
| | | } |
| | | |
| | | // Build filters object with all taxonomies |
| | | let filters = { |
| | | page: 1 |
| | | }; |
| | | |
| | | // Add taxonomy filters if any exist |
| | | if (Object.keys(this.taxonomyFilters).length > 0) { |
| | | filters.taxonomy = this.taxonomyFilters; |
| | | } |
| | | |
| | | this.updateFilter(filters); |
| | | } |
| | | |
| | | clearAllTaxonomies() { |
| | | this.taxonomyFilters = {}; |
| | | window.removeChildren(this.ui.selectedTax); |
| | | |
| | | this.updateFilter({ |
| | | taxonomy: null, |
| | | page: 1 |
| | | }); |
| | | } |
| | | |
| | | initFilters() { |
| | | //defaults |
| | | this.filters = { |
| | | content: this.contentTypes[0], |
| | | orderby: 'date', |
| | | order: 'desc', |
| | | page: 1 |
| | | }; |
| | | if (this.config.context) this.filters.context = this.config.context; |
| | | if (this.config.source) this.filters.source = this.config.source; |
| | | |
| | | //check the cache |
| | | this.processCachedFilters(); |
| | | //check url |
| | | this.processURLFilters(); |
| | | |
| | | // Set initial UI state |
| | | this.syncUIToFilters(); |
| | | } |
| | | syncUIToFilters() { |
| | | if (this.ui.filterContainer) { |
| | | // Check radio buttons |
| | | Object.entries(this.filters).forEach(([key, value]) => { |
| | | const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`); |
| | | if (input) { |
| | | input.checked = true; |
| | | } |
| | | }); |
| | | } |
| | | // Resize observer for responsive images |
| | | if ('ResizeObserver' in window) { |
| | | this.resizeObserver = new ResizeObserver(window.debounce(() => { |
| | | this.updateImageSizes(); |
| | | }, 250)); |
| | | |
| | | // Observe the container |
| | | this.resizeObserver.observe(this.container); |
| | | } else { |
| | | // Fallback to window resize |
| | | window.addEventListener('resize', window.debounce(() => { |
| | | this.updateImageSizes(); |
| | | }, 250)); |
| | | } |
| | | // Update content-specific visibility |
| | | this.updateContentFor(this.filters.content); |
| | | } |
| | | nextPage() { |
| | | this.store.setFilter('page', this.store.filters.page++); |
| | | } |
| | | |
| | | this.taxonomies = {}; |
| | | this.container.querySelectorAll('.jvb-selector:not([hidden])').forEach(selector => { |
| | | let taxonomy = selector.dataset.taxonomy; |
| | | if (!Object.hasOwn(this.taxonomies, taxonomy)) { |
| | | this.taxonomies[taxonomy] = new window.jvbTaxonomySelector( |
| | | selector, |
| | | { |
| | | multiple: true, |
| | | feed: true, |
| | | selected: {}, |
| | | onClose: () => this.setSelectedTerms(taxonomy), |
| | | initStore() { |
| | | const store = window.jvbStore.register( |
| | | 'feed', |
| | | { |
| | | storeName: 'feed', |
| | | endpoint: 'feed', |
| | | keyPath: 'id', |
| | | indexes: [ |
| | | { name: 'content', keyPath: 'content'}, |
| | | { name: 'taxonomy', keyPath: 'taxonomy'}, |
| | | { name: 'user', keyPath: 'user'}, |
| | | { name: 'date', keyPath: 'modified'}, |
| | | { name: 'title', keyPath: 'title'} |
| | | ], |
| | | filters: this.filters, |
| | | TTL: 6 * 60 * 60 * 1000, |
| | | showLoading: true, |
| | | required: 'content', |
| | | delayFetch: true |
| | | } |
| | | ); |
| | | this.store = store.feed; |
| | | |
| | | this.store.subscribe((event, data) => { |
| | | switch (event) { |
| | | case 'data-loaded': |
| | | this.renderItems(); |
| | | this.ui.loadMore.hidden = true; |
| | | if (this.store.lastResponse && this.store.lastResponse['has_more']) { |
| | | this.ui.loadMore.hidden = !this.store.lastResponse['has_more']; |
| | | } |
| | | ); |
| | | break; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Handle browser history navigation |
| | | */ |
| | | handlePopState(e) { |
| | | if (e.state && e.state.filters) { |
| | | if(this.processURLFilters()){ |
| | | // Load items with updated filters |
| | | this.resetPage(); |
| | | this.fetchFeed(); |
| | | |
| | | // Announce to screen readers |
| | | this.a11y.announce('Feed filters updated from browser history.'); |
| | | } |
| | | initGallery() { |
| | | this.gallery = (this.config.gallery) ? window.jvbGallery : false; |
| | | if (this.gallery) { |
| | | this.gallery.subscribe((event, data) => { |
| | | if (event === 'load-more' && this.store.lastResponse) { |
| | | if (this.store.lastResponse['has_more']) { |
| | | this.nextPage(); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | |
| | | processCachedFilters() { |
| | | Object.keys(this.filters).forEach(filter => { |
| | | let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`); |
| | | if (cached && cached !== this.filters[filter]){ |
| | | this.filters[filter] = cached; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | processURLFilters() { |
| | | const params = new URLSearchParams(window.location.search); |
| | | //No parameters to process |
| | | if (!params.toString()) { |
| | | this.updateFilters(); |
| | | return; |
| | | if (this.filters.page > 1) { |
| | | return false; |
| | | } |
| | | const params = new URLSearchParams(window.location.search); |
| | | |
| | | let filters = ['content', 'order', 'orderby', 'favourites','match']; |
| | | |
| | | if (!params.toString()) { |
| | | return false; |
| | | } |
| | | let filters = ['content', 'order', 'orderby', 'favourites', 'match']; |
| | | filters.forEach(filter => { |
| | | let value = params.get('f_'+filter); |
| | | params.delete('f_'+filter); |
| | | if (value && this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`)) { |
| | | this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`).checked = true; |
| | | let value = params.get(`f_${filter}`); |
| | | if (value) { |
| | | this.filters[filter] = value; |
| | | let input = this.ui.filters[filter]; |
| | | if (input) { |
| | | input.checked = true; |
| | | } |
| | | } |
| | | }); |
| | | |
| | | let unprocessed = {}; |
| | | for (var [key, value] of Object.entries(Object.fromEntries(params))) { |
| | | |
| | | key = key.replace('f_',''); |
| | | if (this.contentTypes.includes(key)) { |
| | | this.openGallery = value; |
| | | } else { |
| | | this.taxonomies[key].addTermsFromURL(value); |
| | | this.setSelectedTerms(key); |
| | | let hasTaxonomy = false; |
| | | // Load taxonomy filters from URL |
| | | params.forEach((value, key) => { |
| | | if (key.startsWith('f_tax_')) { |
| | | hasTaxonomy = true; |
| | | const taxonomy = key.replace('f_tax_', ''); |
| | | if (!this.taxonomyFilters[taxonomy]) { |
| | | this.taxonomyFilters[taxonomy] = []; |
| | | } |
| | | this.taxonomyFilters[taxonomy] = value.split(',').map(Number); |
| | | } |
| | | } |
| | | |
| | | this.updateFilters(); |
| | | } |
| | | |
| | | handleClick(e) { |
| | | if (e.target.classList.contains('load-more') || e.target.closest('.load-more')) { |
| | | this.fetchFeed(false); |
| | | e.target.disabled = true; |
| | | } else if (e.target.classList.contains('clear-filters') || e.target.closest('.clear-filters')) { |
| | | this.resetFilters(); |
| | | } else if (this.config.gallery && e.target.closest('.feed-image')) { |
| | | this.gallery.handleGalleryOpen(e); |
| | | } else if (e.target.classList.contains('.remove-item') || e.target.closest('.remove-item')) { |
| | | let tag = e.target.closest('.selected-item'); |
| | | let taxonomy = tag.dataset.taxonomy; |
| | | this.taxonomies[taxonomy].removeSelectedTerm(tag.dataset.id); |
| | | this.setSelectedTerms(taxonomy); |
| | | this.updateFilters(); |
| | | } |
| | | } |
| | | handleChange(e) { |
| | | if (e.target.closest(this.filterSelector)) { |
| | | this.resetPage(); |
| | | window.removeChildren(this.grid); |
| | | this.addPlaceholders(); |
| | | //update filters |
| | | this.updateFilters(); |
| | | } |
| | | } |
| | | |
| | | updateFilters() { |
| | | this.page = 1; |
| | | const params = new URLSearchParams(window.location.search); |
| | | |
| | | let filters = Object.fromEntries(new FormData(this.filterForm)); |
| | | |
| | | let contents = []; |
| | | for (let [key, value] of Object.entries(filters)) { |
| | | let set = false; |
| | | switch (key) { |
| | | case 'content': |
| | | if (value !== this.contentTypes[0]) { |
| | | set = true; |
| | | }); |
| | | if (this.ui.filterContainer && hasTaxonomy) { |
| | | for (let [tax, ids] in Object.entries(this.taxonomyFilters)) { |
| | | let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`); |
| | | if (button) { |
| | | if (button.dataset.fieldId) { |
| | | let field = this.selector.get(button.dataset.fieldId); |
| | | field.selectedTerms = new Set(ids); |
| | | this.selector.initFieldDisplay(button.dataset.fieldId); |
| | | } else { |
| | | params.delete('f_'+key); |
| | | this.selector.registerField(button, { |
| | | button: button, |
| | | buttonSelector: '[data-filter="taxonomy"]', |
| | | selected: this.ui.selectedTax, |
| | | selectedItems: ids |
| | | }); |
| | | } |
| | | break; |
| | | case 'orderby': |
| | | if (value !== 'date') { |
| | | set = true; |
| | | } |
| | | break; |
| | | case 'order': |
| | | if (value !== 'desc') { |
| | | set = true; |
| | | } |
| | | break; |
| | | default: |
| | | set = true; |
| | | } |
| | | } |
| | | if (!set) { |
| | | params.delete('f_'+key); |
| | | } |
| | | |
| | | |
| | | if (set && value !== false && value !== '') { |
| | | params.set('f_'+key, value); |
| | | } |
| | | if (value !== '') { |
| | | contents.push(value); |
| | | } |
| | | |
| | | const newURL = `${window.location.pathname}?${params.toString()}`; |
| | | history.pushState(filters, '', newURL); |
| | | |
| | | } |
| | | |
| | | this.filters = filters; |
| | | this.updateContentFor(filters.content); |
| | | |
| | | this.updateFilterControls(); |
| | | |
| | | this.loading.setContent(contents); |
| | | this.fetchFeed(true); |
| | | } |
| | | |
| | | updateFilterControls() { |
| | | this.filterControls.hidden = this.selectedTerms.children.length < 2; |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Toggles taxonomy selectors and certain order/orderby options |
| | | * depending on current content |
| | | * @param content |
| | | * Update URL with current filters (for sharing/bookmarking) |
| | | */ |
| | | updateContentFor(content) { |
| | | this.filterForm.querySelectorAll('.jvb-selector').forEach(tax => { |
| | | let hasContent = tax.dataset.for.includes(content); |
| | | tax.hidden = !hasContent; |
| | | if (!hasContent) { |
| | | let t = tax.dataset.taxonomy; |
| | | this.clearSelectedTerms(t); |
| | | updateURL() { |
| | | const params = new URLSearchParams(); |
| | | |
| | | // Add simple filters |
| | | ['content', 'order', 'orderby', 'match'].forEach(key => { |
| | | if (this.filters[key]) { |
| | | params.set(`f_${key}`, this.filters[key]); |
| | | } |
| | | }); |
| | | this.filterForm.querySelectorAll('input[data-for]').forEach(toggle => { |
| | | toggle.hidden = !toggle.dataset.for.includes(content); |
| | | |
| | | // Add taxonomy filters |
| | | Object.entries(this.taxonomyFilters).forEach(([taxonomy, terms]) => { |
| | | if (terms.length > 0) { |
| | | params.set(`f_tax_${taxonomy}`, terms.join(',')); |
| | | } |
| | | }); |
| | | this.filterForm.querySelectorAll('input[name="order"]').forEach(order => { |
| | | order.hidden = this.filters.order === 'random'; |
| | | }); |
| | | |
| | | // Update URL without reload |
| | | const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`; |
| | | window.history.pushState({ filters: this.filters }, '', newURL); |
| | | } |
| | | |
| | | clearSelectedTerms(taxonomy) { |
| | | this.filterForm.querySelector(`input[name="${taxonomy}"]`).value = ''; |
| | | if (Object.hasOwn(this.taxonomies, taxonomy)) { |
| | | this.taxonomies[taxonomy].selectedItems = {}; |
| | | } |
| | | } |
| | | |
| | | setSelectedTerms(taxonomy) { |
| | | let input = this.filterForm.querySelector(`input[name="${taxonomy}"]`); |
| | | input.value = ''; |
| | | let selected = this.taxonomies[taxonomy].selectedTerms; |
| | | if (!window.isEmptyObject(selected)) { |
| | | let ids = Object.keys(selected); |
| | | input.value = ids.join(','); |
| | | } |
| | | this.updateFilters(); |
| | | } |
| | | |
| | | |
| | | nextPage() { |
| | | if (this.hasMore) { |
| | | this.page++; |
| | | } |
| | | } |
| | | resetPage() { |
| | | this.page = 1; |
| | | this.hasMore = true; |
| | | } |
| | | resetState() { |
| | | this.resetPage(true); |
| | | this.isLoading = false; |
| | | this.retries = { |
| | | count: 0, |
| | | max: 3, |
| | | delay: 1000 |
| | | }; |
| | | } |
| | | |
| | | resetFilters() { |
| | | this.filterForm.reset(); |
| | | //check the first content |
| | | this.filterForm.querySelector('input[name="content"]').checked = true; |
| | | this.filterForm.querySelector('input[name="orderby"][value="date"]').checked = true; |
| | | this.page = 1; |
| | | this.updateFilters(); |
| | | } |
| | | |
| | | |
| | | buildFilterRequest() { |
| | | |
| | | let filters = {}; |
| | | |
| | | for (let [filter, value] of Object.entries(this.filters)) { |
| | | if (value !== false && value !== '') { |
| | | filters[filter] = value; |
| | | } |
| | | } |
| | | filters.page = parseInt(this.page); |
| | | if (this.container.dataset.context) { |
| | | filters.context = this.container.dataset.context; |
| | | } |
| | | if (this.container.dataset.source) { |
| | | filters.source = this.container.dataset.source; |
| | | } |
| | | return new URLSearchParams(filters).toString(); |
| | | } |
| | | |
| | | async fetchFeed(reset = false, force = false) { |
| | | if (this.isLoading) { |
| | | return false; |
| | | } |
| | | this.loading.showLoading(this.filters); |
| | | try { |
| | | if (this.page === 1) { |
| | | window.removeChildren(this.grid); |
| | | this.addPlaceholders(); |
| | | } |
| | | |
| | | const data = await this.cache.fetchWithCache( |
| | | `${this.config.api}feed?${this.buildFilterRequest()}`, |
| | | { |
| | | method: 'GET', |
| | | }, |
| | | { |
| | | context: 'feed', |
| | | forceRefresh: true |
| | | // forceRefresh: force |
| | | } |
| | | ); |
| | | |
| | | //Handle empty results |
| | | if (!data || !data.items || data.items.length === 0) { |
| | | if (this.page === 1) { |
| | | this.showEmptyState(); |
| | | } |
| | | this.hasMore = false; |
| | | return false; |
| | | } else { |
| | | this.hasMore = data['has_more']; |
| | | |
| | | this.renderItems(data.items, this.page > 1); |
| | | |
| | | if (this.hasMore) { |
| | | this.nextPage(); |
| | | } |
| | | return true; |
| | | } |
| | | } catch (error) { |
| | | this.handleError(error); |
| | | } finally { |
| | | this.loading.hideLoading(); |
| | | if (this.openGallery !== false) { |
| | | this.gallery.openWhenReady = this.openGallery; |
| | | this.openGallery = false; |
| | | } |
| | | this.loadMore.disabled = false; |
| | | this.loadMore.hidden = !this.hasMore; |
| | | } |
| | | } |
| | | |
| | | removePlaceholders() { |
| | | if (this.grid.querySelector('.placeholder')) { |
| | | window.removeChildren(this.grid); |
| | | } |
| | | } |
| | | showEmptyState() { |
| | | window.removeChildren(this.grid); |
| | | let template = window.getTemplate('emptyState'); |
| | | let isFavourite = Object.hasOwn(this.filters, 'favourites') && this.filters.favourites === true; |
| | | if (isFavourite) { |
| | | [ |
| | | template.querySelector('h3').textContent, |
| | | template.querySelector('p:first-of-type').textContent, |
| | | template.querySelector('p:last-of-type').textContent, |
| | | ] = [ |
| | | '♡ BLANK CANVAS ♡', |
| | | 'You haven\'t fallen in love with any pieces... yet!', |
| | | 'Hit that heart icon when something stops your scroll — your dream collection is waiting to start.' |
| | | ]; |
| | | } |
| | | this.grid.append(template); |
| | | this.a11y.announceEmpty(isFavourite); |
| | | |
| | | } |
| | | handleError(error){ |
| | | return this.error.handleApiError( |
| | | error, |
| | | { |
| | | component: 'Feed Block', |
| | | action: 'loaditems' |
| | | }, |
| | | () => this.fetchFeed() |
| | | ); |
| | | } |
| | | |
| | | addPlaceholders() { |
| | | let total = this.contentTypes.length - 1; |
| | | for (let i = 0; i < 9; i++) { |
| | | let template = window.getTemplate('placeholderTemplate'); |
| | | let rand = Math.floor(Math.random()*total+1); |
| | | let icon = window.getIcon(this.contentTypes[rand]).cloneNode(true); |
| | | |
| | | template.append(icon); |
| | | this.grid.append(template); |
| | | } |
| | | } |
| | | renderItems(items, append = false) { |
| | | //Clear the grid if we aren't appending |
| | | if (!append) { |
| | | window.removeChildren(this.grid); |
| | | this.addPlaceholders(); |
| | | renderItems() { |
| | | let items = this.store.getFiltered(); |
| | | if (this.store.filters['page'] === 1) { |
| | | window.removeChildren(this.ui.grid); |
| | | } |
| | | |
| | | |
| | | //Bail early if no items |
| | | if (items.length === 0) { |
| | | this.a11y.announceUpdate(0, append); |
| | | this.a11y.announceItems(0, this.store.filters['page'] > 0); |
| | | return; |
| | | } |
| | | |
| | | //Use DocumentFragment for better performance |
| | | const fragment = document.createDocumentFragment(); |
| | | |
| | | const batchSize = 10; |
| | | |
| | | const processBatch = (startIndex) => { |
| | | const endIndex = Math.min(startIndex + batchSize, items.length); |
| | | |
| | | for (let i = startIndex; i < endIndex; i++) { |
| | | const item = items[i]; |
| | | const element = this.createItemElement(item); |
| | | fragment.appendChild(element); |
| | | |
| | | this.imageObserver.observe(element); |
| | | fragment.appendChild(element); |
| | | } |
| | | |
| | | if (endIndex < items.length) { |
| | | requestAnimationFrame(() => { |
| | | processBatch(endIndex); |
| | | }); |
| | | requestAnimationFrame(() => processBatch(endIndex)); |
| | | } else { |
| | | this.removePlaceholders(); |
| | | //all batches are processed, append fragment |
| | | this.grid.appendChild(fragment); |
| | | this.ui.grid.append(fragment); |
| | | |
| | | if (this.config.gallery) { |
| | | this.gallery.updateGalleryItems(this.gallery.getGalleryItems()); |
| | | } |
| | | this.a11y.makeNavigable(this.grid.querySelectorAll('.item:not([data-keyboard-nav])')); |
| | | this.a11y.announceItems(items.length, append, this.hasMore); |
| | | |
| | | this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])')); |
| | | this.a11y.announceItems(items.length, this.store.filters['page'] > 1, this.store.hasMore); |
| | | } |
| | | }; |
| | | |
| | | if (items.length > 0) { |
| | | processBatch(0); |
| | | } else { |
| | | this.a11y.announceUpdate(0, append); |
| | | this.a11y.announceItems(0, this.store.filters['page'] >1, false); |
| | | } |
| | | |
| | | if (this.ui.filters.match) { |
| | | this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0; |
| | | } |
| | | if (this.ui.clearFilter) { |
| | | this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Creates a feed-item. Used by RenderItems |
| | | * |
| | | * @param {object} item |
| | | */ |
| | | createItemElement(item) { |
| | | if(!this.rendered[item.icon]) { |
| | | this.rendered[item.icon] = new Map(); |
| | | } |
| | | if (this.rendered[item.icon].has(item.id)) { |
| | | return this.rendered[item.icon].get(item.id); |
| | | } |
| | | let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`); |
| | | |
| | | const favourited = window.isFavourited(item.icon, item.id)??false; |
| | | const template = window.getTemplate('feed-item'); |
| | | const isTimeline = Object.hasOwn(template.dataset, 'timeline'); |
| | | |
| | | template.id = `${item.icon}-${item.id}`; |
| | | template.dataset.id = item.id; |
| | | template.classList.add(item.icon); |
| | | // Format fields using helpers |
| | | for (let [fieldName, value] of Object.entries(item.fields)) { |
| | | if (isTimeline && ['timeline', 'number'].includes(fieldName)) continue; |
| | | let el = template.querySelector(`[data-field="${fieldName}"]`); |
| | | if (!el) continue; |
| | | |
| | | if (item['umami_view']) { |
| | | this.buildUmamiData(template, item['umami_view']); |
| | | } |
| | | if (value === '') { |
| | | el.remove(); |
| | | continue; |
| | | } |
| | | |
| | | let favouriteButton = template.querySelector('button.favourite'); |
| | | [ |
| | | favouriteButton.dataset.id, |
| | | favouriteButton.dataset.type, |
| | | favouriteButton.dataset.artist, |
| | | favouriteButton.title |
| | | ] = [ |
| | | item.id, |
| | | item.icon, |
| | | item['user_id'], |
| | | (favourited) ? 'Remove from Favourites' : 'Add to Favourites' |
| | | ]; |
| | | |
| | | let order = item.order; |
| | | let single = template.querySelector('.item'); |
| | | let list = template.querySelector('.item-list'); |
| | | let img = template.querySelector('.feed-images'); |
| | | let summary = template.querySelector('summary'); |
| | | let info = template.querySelector('.item-info'); |
| | | |
| | | for (let [index, id] of Object.entries(order)) { |
| | | let target; |
| | | let config = item[id]; |
| | | if (id === 'title') { |
| | | target = template.querySelector('h3 a'); |
| | | if (item.title !== '') { |
| | | [ |
| | | target.textContent, |
| | | target.href, |
| | | target.url |
| | | ] = [ |
| | | item.title, |
| | | item.url, |
| | | `Learn more about this ${item.icon}` |
| | | ]; |
| | | if (item.icon !== '') { |
| | | target.closest('h3').prepend(window.getIcon(item.icon)); |
| | | } |
| | | if (item.umami_click) { |
| | | this.buildUmamiData(target, item.umami_click); |
| | | } |
| | | } else { |
| | | target.remove(); |
| | | } |
| | | } else if (Object.hasOwn(config, 'terms')) { |
| | | //Taxonomy list |
| | | if (config.terms.length === 0) { |
| | | continue; |
| | | } |
| | | let taxonomy = list.cloneNode(true); |
| | | let label = taxonomy.querySelector('.label'); |
| | | let termList = taxonomy.querySelector('ul'); |
| | | let listItem = taxonomy.querySelector('li'); |
| | | |
| | | if (config.label) { |
| | | label.textContent = config.label; |
| | | } |
| | | if (config.icon) { |
| | | label.prepend(window.getIcon(config.icon)); |
| | | } |
| | | if (!config.label && !config.icon){ |
| | | label.remove(); |
| | | } |
| | | |
| | | config.terms.forEach(term => { |
| | | let termItem = listItem.cloneNode(true); |
| | | let link = termItem.querySelector('a'); |
| | | [ |
| | | link.href, |
| | | link.title, |
| | | link.textContent |
| | | ] = [ |
| | | term.url, |
| | | `Learn more about ${term.title}`, |
| | | term.title |
| | | ]; |
| | | if (term.umami_click.length > 0) { |
| | | this.buildUmamiData(link, term.umami_click); |
| | | } |
| | | termList.append(termItem); |
| | | }); |
| | | |
| | | listItem.remove(); |
| | | info.appendChild(taxonomy); |
| | | } else if (Object.hasOwn(config, 'value') && config.value !== '') { |
| | | let itemInfo = single.cloneNode(true); |
| | | let label = itemInfo.querySelector('.label'); |
| | | let link = itemInfo.querySelector('a'); |
| | | let p = itemInfo.querySelector('p'); |
| | | if (Object.hasOwn(config, 'label')) { |
| | | label.textContent = config.label; |
| | | } |
| | | if (Object.hasOwn(config, 'icon')) { |
| | | label.prepend(window.getIcon(config.icon)); |
| | | } |
| | | if (!Object.hasOwn(config, 'icon') && !Object.hasOwn(config, 'label')) { |
| | | label.remove(); |
| | | } |
| | | if (Object.hasOwn(config, 'url')) { |
| | | p.remove(); |
| | | [ |
| | | link.textContent, |
| | | link.href, |
| | | link.title |
| | | ] = [ |
| | | config.value, |
| | | config.url, |
| | | `Learn more about ${config.value}` |
| | | ]; |
| | | } else { |
| | | link.remove(); |
| | | p.textContent = config.value; |
| | | } |
| | | info.appendChild(itemInfo); |
| | | } else if (id === 'image') { |
| | | let images = summary.querySelector('.feed-images'); |
| | | let img = images.querySelector('a'); |
| | | |
| | | let main = img.cloneNode(true); |
| | | if (!this.config.gallery) { |
| | | main.href = item.url; |
| | | } |
| | | |
| | | main.classList.add('feed-image'); |
| | | this.buildImageData(main.querySelector('img'), item.image); |
| | | images.append(main); |
| | | |
| | | if (item.content?.length > 0) { |
| | | images.classList.add('multi'); |
| | | item.content.forEach(c => { |
| | | let image = img.cloneNode(true); |
| | | if (!this.config.gallery) { |
| | | image.href = c.url; |
| | | } |
| | | let itemImg = image.querySelector('img'); |
| | | itemImg.src = c.image.small; |
| | | itemImg.alt = c.image.alt; |
| | | images.append(image); |
| | | }); |
| | | } |
| | | img.remove(); |
| | | if (this.isImageField(item, value)) { |
| | | this.formatImageFields(el, value, item); |
| | | } else if (this.isTaxonomyField(item, fieldName)) { |
| | | this.formatTaxonomyField(el, item, fieldName, value); |
| | | } else if (this.isTimeField(el)) { |
| | | this.formatTimeField(el, value); |
| | | } else { |
| | | this.formatField(el, value); |
| | | } |
| | | } |
| | | single.remove(); |
| | | list.remove(); |
| | | |
| | | this.rendered[item.icon].set(item.id, template); |
| | | // Handle link |
| | | let link = template.querySelector('a'); |
| | | if (link && item.url !== '') { |
| | | [ |
| | | link.href, |
| | | link.title |
| | | ] = [ |
| | | item.url, |
| | | `View ${item.fields['post_title']??'Item'}` |
| | | ]; |
| | | } |
| | | |
| | | if (isTimeline) { |
| | | this.addTimelineElements(item, template); |
| | | } |
| | | |
| | | return template; |
| | | } |
| | | |
| | | buildImageData(img, data){ |
| | | if (typeof data.tiny !== 'string') { |
| | | return; |
| | | splitIDs(value) { |
| | | return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value); |
| | | } |
| | | isImageField(item, value) { |
| | | if (!Object.hasOwn(item, 'images') || Object.keys(item.images).length === 0) { |
| | | return false; |
| | | } |
| | | let values = this.splitIDs(value); |
| | | |
| | | return values.some(v => |
| | | Object.keys(item.images).map(k => parseInt(k)).includes(parseInt(v)) |
| | | ); |
| | | } |
| | | formatImageFields(element, value, item) { |
| | | let values = this.splitIDs(value); // Convert string to array first |
| | | if (values.length === 0) return; |
| | | |
| | | if (values.length > 1) { |
| | | let image = element.querySelector('img'); |
| | | if (!image) return; |
| | | values.forEach(imgID => { |
| | | let img = image.cloneNode(true); |
| | | this.formatImageField(img, imgID, item); |
| | | element.append(img); |
| | | }); |
| | | image.remove(); |
| | | } else { |
| | | if (element.tagName !== 'IMG') { |
| | | element = element.querySelector('img'); |
| | | if (!element) return; |
| | | } |
| | | this.formatImageField(element, values[0], item); |
| | | } |
| | | } |
| | | formatImageField(element, value, item) { |
| | | let imgData = item.images[value]??false; |
| | | if (!imgData) return; |
| | | [ |
| | | img.src, |
| | | img.dataset.small, |
| | | img.dataset.medium, |
| | | img.dataset.large, |
| | | img.alt |
| | | ] = |
| | | element.src, |
| | | element.srcset, |
| | | element.alt |
| | | ] = [ |
| | | imgData.tiny, |
| | | `${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`, |
| | | imgData['image-alt-text'] |
| | | ] |
| | | } |
| | | isTaxonomyField(item, field) { |
| | | if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) { |
| | | return false; |
| | | } |
| | | |
| | | return Object.keys(item.taxonomies).includes(field); |
| | | } |
| | | formatTaxonomyField(element, item, field, value) { |
| | | if (element.tagName !== 'UL' || !element.querySelector('li')) return; |
| | | let values = this.splitIDs(value); |
| | | if (values.length === 0) { |
| | | element.remove(); |
| | | } |
| | | let listItem = element.querySelector('li'); |
| | | for (let termID of values) { |
| | | let term = item.taxonomies[field][termID]??false; |
| | | if (!term) continue; |
| | | let termItem = listItem.cloneNode(true); |
| | | let link = termItem.querySelector('a'); |
| | | if (!link) continue; |
| | | |
| | | [ |
| | | data.tiny, |
| | | data.small, |
| | | data.medium, |
| | | data.large, |
| | | data.alt |
| | | link.href, |
| | | link.title, |
| | | link.textContent |
| | | ] = [ |
| | | term.url, |
| | | `See more ${term.title}`, |
| | | term.title |
| | | ]; |
| | | element.append(termItem); |
| | | } |
| | | listItem.remove(); |
| | | } |
| | | isTimeField(el) { |
| | | return el.tagName === 'TIME' || el.querySelector('time') !== null; |
| | | } |
| | | formatTimeField(element, value) { |
| | | if (element.tagName !== 'TIME') { |
| | | element = element.querySelector('time'); |
| | | if (!element) return; |
| | | } |
| | | element.setAttribute('datetime', value); |
| | | element.textContent = window.formatTimeAgo(value, 'F Y'); |
| | | } |
| | | formatField(element, value) { |
| | | element.textContent = value; |
| | | } |
| | | |
| | | buildUmamiData(item, data){ |
| | | for(let [key, value] of Object.entries(data)){ |
| | | item.dataset[key] = value; |
| | | addTimelineElements(item, template) { |
| | | let [ |
| | | afterEl, |
| | | number, |
| | | started, |
| | | last |
| | | ] = [ |
| | | template.querySelector('span.after-text'), |
| | | template.querySelector('[data-field="number"] b'), |
| | | template.querySelector('[data-field="started"] time'), |
| | | template.querySelector('[data-field="updated"] time') |
| | | ]; |
| | | |
| | | if (afterEl) { |
| | | afterEl.textContent = `After ${item.fields.number} Tx`; |
| | | } |
| | | if (number) { |
| | | number.textContent = item.fields.number; |
| | | } |
| | | if (started) { |
| | | this.formatTimeField(started, item.fields.timeline[0]['post_date']); |
| | | } |
| | | if (last) { |
| | | this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Load Image, used by renderItems |
| | | * @param element |
| | | */ |
| | | loadImage(element) { |
| | | const img = element.querySelector('img'); |
| | | if (!img) return; |
| | | const size = this.getImageSize(); |
| | | |
| | | img.src = img.dataset[size] || img.dataset.src; |
| | | element.setAttribute('data-loaded', 'true'); |
| | | removePlaceholders() { |
| | | const placeholders = this.ui.grid.querySelectorAll('.placeholder'); |
| | | if (placeholders.length > 0) { |
| | | placeholders.forEach(p => p.remove()); |
| | | } |
| | | } |
| | | |
| | | |
| | | addPlaceholders() { |
| | | let total = this.contentTypes.length; |
| | | const fragment = document.createDocumentFragment(); |
| | | for (let i = 0; i < 12; i++) { |
| | | let template = window.getTemplate('placeholderTemplate'); |
| | | |
| | | let rand = Math.floor(Math.random() * total); |
| | | let icon; |
| | | if (this.ui.content && this.ui.content.length > 0) { |
| | | icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true); |
| | | } else { |
| | | icon = window.getIcon(this.container.dataset.icon); |
| | | } |
| | | template.append(icon); |
| | | fragment.append(template); |
| | | } |
| | | this.ui.grid.append(fragment); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Updates the image size according to screen size |
| | | * |
| | | * @param {object} filters {name: value} |
| | | */ |
| | | updateImageSizes() { |
| | | const size = this.getImageSize(); |
| | | const items = this.grid.querySelectorAll('.item'); |
| | | items.forEach(item => { |
| | | const img = item.querySelector('img'); |
| | | if (img && img.dataset[size] && img.src !== img.dataset[size]) { |
| | | img.src = img.dataset[size]; |
| | | updateFilter(filters) { |
| | | //double check filters are what we're expecting |
| | | let allowed = ['taxonomy','favourites','match', ... Object.keys(this.filters)]; |
| | | |
| | | filters = Object.keys(filters) |
| | | .filter(key => allowed.includes(key)) |
| | | .reduce((obj, key) => { |
| | | obj[key] = filters[key]; |
| | | return obj; |
| | | }, {}); |
| | | |
| | | if (window.getDifferences.map(this.filters, filters)) { |
| | | this.filters = { ...this.filters, ...filters }; // Merge instead of replace |
| | | this.updateURL(); |
| | | this.store.setFilters(filters); |
| | | } |
| | | } |
| | | /** |
| | | * Update visible filters based on selected content type |
| | | */ |
| | | updateContentFor(contentType) { |
| | | // Update taxonomy filter visibility |
| | | const taxonomyButtons = this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]'); |
| | | taxonomyButtons.forEach(button => { |
| | | const forTypes = button.dataset.for?.split(',') || []; |
| | | button.hidden = forTypes.length > 0 && !forTypes.includes(contentType); |
| | | }); |
| | | |
| | | // Update ordering options |
| | | const orderButtons = this.ui.filterContainer.querySelectorAll('[data-for]'); |
| | | orderButtons.forEach(button => { |
| | | const forTypes = button.dataset.for?.split(',') || []; |
| | | if (forTypes.length > 0) { |
| | | button.hidden = !forTypes.includes(contentType); |
| | | // Uncheck if hiding |
| | | if (button.hidden && button.checked) { |
| | | button.checked = false; |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | /** |
| | | * Get image size based on screen width |
| | | */ |
| | | getImageSize() { |
| | | const width = window.innerWidth; |
| | | if (width > 1024) return 'medium'; |
| | | if (width > 500) return 'medium'; |
| | | return 'small'; |
| | | |
| | | // Update order direction visibility based on selected orderby |
| | | const orderBy = this.ui.filterContainer.querySelector('[name="orderby"]:checked'); |
| | | this.updateOrderDirectionVisibility(orderBy?.value); |
| | | } |
| | | |
| | | /** |
| | | * Show/hide order direction based on orderby selection |
| | | */ |
| | | updateOrderDirectionVisibility(orderBy) { |
| | | const orderDirection = this.ui.filterContainer.querySelector('.order-direction'); |
| | | if (orderDirection) { |
| | | const forOrders = orderDirection.dataset.forOrder?.split(',') || []; |
| | | orderDirection.hidden = forOrders.length > 0 && !forOrders.includes(orderBy); |
| | | } |
| | | } |
| | | /********************************************************************* |
| | | LISTENERS |
| | | *********************************************************************/ |
| | | initListeners() { |
| | | this.popStateHandler = this.handlePopState.bind(this); |
| | | this.clickHandler = this.handleClick.bind(this); |
| | | this.changeHandler = this.handleChange.bind(this); |
| | | this.imageObserver = null; |
| | | this.resizeObserver = null; |
| | | if ('IntersectionObserver' in window) { |
| | | this.imageObserver = new IntersectionObserver(entries => { |
| | | entries.forEach(entry => { |
| | | this.loadImage(entry.target); |
| | | this.imageObserver.unobserve(entry.target); |
| | | }); |
| | | }, { |
| | | rootMargin: '100px', |
| | | threshold: .1 |
| | | }); |
| | | } |
| | | |
| | | if ('ResizeObserver' in window) { |
| | | this.resizeObserver = new ResizeObserver(() => { |
| | | window.debouncer.schedule( |
| | | 'feed-update-images', |
| | | () => this.updateImageSizes(), |
| | | 250 |
| | | ); |
| | | }); |
| | | } else { |
| | | window.addEventListener('resize', () => { |
| | | window.debouncer.schedule( |
| | | 'feed-update-images', |
| | | () => this.updateImageSizes(), |
| | | 250 |
| | | ); |
| | | }); |
| | | } |
| | | |
| | | window.addEventListener('popstate', this.popStateHandler); |
| | | document.addEventListener('click', this.clickHandler); |
| | | document.addEventListener('change', this.changeHandler); |
| | | } |
| | | |
| | | handlePopState(e) { |
| | | if (e.state?.filters) { |
| | | if (this.processURLFilters()) { |
| | | this.store.setFilters(this.filters); |
| | | this.a11y.announce('Feed filters updated from browser history'); |
| | | } |
| | | } |
| | | } |
| | | |
| | | handleClick(e) { |
| | | if (window.targetCheck(e, this.elements.loadMore)) { |
| | | this.nextPage(); |
| | | } else if (window.targetCheck(e, this.elements.clearFilter)) { |
| | | this.clearAllTaxonomies(); |
| | | } else if (window.targetCheck(e, '.remove-item')) { |
| | | this.handleRemoveSelectedTerm(e); |
| | | } |
| | | } |
| | | |
| | | handleRemoveSelectedTerm(e) { |
| | | const selectedItem = e.target.closest('.selected-item'); |
| | | if (!selectedItem) return; |
| | | |
| | | const termId = parseInt(selectedItem.dataset.id); |
| | | const taxonomy = selectedItem.dataset.taxonomy; |
| | | |
| | | // Remove from filters |
| | | if (this.taxonomyFilters[taxonomy]) { |
| | | this.taxonomyFilters[taxonomy] = this.taxonomyFilters[taxonomy] |
| | | .filter(id => id !== termId); |
| | | |
| | | if (this.taxonomyFilters[taxonomy].length === 0) { |
| | | delete this.taxonomyFilters[taxonomy]; |
| | | } |
| | | } |
| | | |
| | | // Remove from UI |
| | | selectedItem.remove(); |
| | | |
| | | // Update filters |
| | | this.updateFilter({ |
| | | taxonomy: Object.keys(this.taxonomyFilters).length > 0 |
| | | ? this.taxonomyFilters |
| | | : null, |
| | | page: 1 |
| | | }); |
| | | } |
| | | |
| | | handleChange(e) { |
| | | let target = e.target; |
| | | if (Object.hasOwn(target.dataset, 'filter')) { |
| | | if (target.dataset.filter === 'content') { |
| | | this.updateContentFor(target.value); |
| | | this.updateFilter({ content: target.value, page: 1 }); |
| | | } else if (target.dataset.filter === 'orderby') { |
| | | this.updateOrderDirectionVisibility(target.value); |
| | | this.updateFilter({ orderby: target.value, page: 1 }); |
| | | } else if (target.dataset.filter === 'order') { |
| | | this.updateFilter({ order: target.value, page: 1 }); |
| | | } else if (target.dataset.filter === 'match') { |
| | | this.updateFilter({ match: target.checked ? 'all' : 'any', page: 1 }); |
| | | } else if (target.dataset.filter === 'favourites') { |
| | | this.updateFilter({ favourites: target.checked, page: 1 }); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | document.addEventListener('DOMContentLoaded', () => { |
| | | window.feedBlock = new FeedBlock(); |
| | | |
| | | document.addEventListener('DOMContentLoaded', async function() { |
| | | window.auth.subscribe(event => { |
| | | if (event === 'auth-loaded') { |
| | | window.feedBlock = new FeedBlock(); |
| | | } |
| | | }); |
| | | }); |