=Seems to be working, huzzah! Added some changes for on-this-page nav
54 files modified
1 files added
| | |
| | | use JVBase\rest\routes\AdminRoutes; |
| | | use JVBase\rest\routes\IntegrationsRoutes; |
| | | use JVBase\utility\Features; |
| | | use JVBase\base\SchemaHelper; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | 'admin' => new AdminPages(), |
| | | 'seoAdmin' => new SEOAdmin(), |
| | | 'seo' => new SchemaOutput(), |
| | | 'schemaHelper' => new SchemaHelper(), |
| | | // 'uploads' => new UploadManager(), |
| | | 'userTerms' => new UserTermsManager(), |
| | | 'email' => new EmailManager(), |
| | |
| | | { |
| | | return $this->customBlocks??false; |
| | | } |
| | | public function schemaHelper():SchemaHelper |
| | | { |
| | | return $this->managers['schemaHelper']; |
| | | } |
| | | } |
| | |
| | | nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;font-family:var(--heading);padding:0;margin:0}nav li{display:flex;align-items:center;height:max(var(--btn),max-content);width:100%;max-inline-size:none;padding:0}nav a,nav button{display:flex;text-decoration:none;align-items:center;justify-content:center;height:var(--btn);width:100%;white-space:nowrap;text-transform:uppercase;transition:var(--trans-color)}nav a{height:var(--btn);padding:var(--padding)}nav button{justify-content:center;aspect-ratio:1;padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon{transform:rotate(0);transition:transform var(--trans-base)}.has-submenu.open>button .icon{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max(100%,max-content);background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:all var(--trans-t) var(--trans-fn);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:2px solid var(--action-0);outline-offset:2px}nav.always{--dir:column;--wrap:nowrap;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{--justify:flex-end;width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px)}nav.always>ul{--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;position:relative;right:-300vw;width:100vw;max-height:100%;padding:1rem 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{flex-wrap:wrap;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;max-width:calc(100% - var(--btn));text-align:center}nav.always .has-submenu{display:flex}nav.always .has-submenu>a{flex:1}nav.always .has-submenu>button{flex:0 0 var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;height:max-content;top:0;width:100%;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>button{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>button .icon-list,nav.always>button .icon-x{display:none}nav.always.open>button .icon-x,nav.always>button .icon-list{display:block;width:32px;height:32px}@media (min-width:768px){nav.always>ul{padding-top:var(--btn)}}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-7)}#breadcrumbs ol{height:max-content;--wrap:wrap!important;--justify:flex-start!important}#breadcrumbs li{width:max-content}#breadcrumbs a{height:var(--chip)}#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}#breadcrumbs li:last-of-type::after{display:none}#breadcrumbs :is(a,span){padding:0 .125rem;color:var(--contrast);text-transform:none}#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed.bottom{position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed.bottom li{flex:1;justify-content:center}nav.fixed.bottom a{gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed.bottom a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));max-width:none;padding:0 .5rem;background-color:rgba(var(--base-rgb),var(--op-4));color:var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-6)}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}body:has(.additional-actionsbutton) nav.on-this-page{width:calc(100vw - var(--btn_) - 1rem)}.on-this-page ul{width:100%;gap:0}.on-this-page li{justify-content:center}.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}.on-this-page a{height:var(--chip);padding:0}nav.letters li{height:var(--chip);max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}nav.letters,nav.letters ul{height:var(--chipchip)}@media (min-width:768px){nav.letters,nav.letters ul{height:var(--chip)}nav.letters ul{--wrap:nowrap}nav.letters li{max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:flex-start;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}.index ul{width:max-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--trans-base)}.index li.active,.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;align-items:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}.index.open ul{--dir:column;--justify:flex-end;height:100%;width:100%}.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}.index.open a{justify-content:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed{height:max-content;--wrap:wrap;--gap:0 .25rem}nav.condensed ul{min-height:var(--chip_);height:max-content;--justify:center;--wrap:wrap}.condensed li{width:max-content;min-height:var(--chip)}.condensed li+li::before{content:'·';padding:0 .25em}.condensed a{height:max-content;min-height:var(--chip);font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}.condensed a:focus{border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials,.always ul.socials a,.always ul.socials li{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{position:fixed;bottom:var(--btn);left:var(--btnbtn);right:var(--btnbtn);padding-bottom:2px;z-index:var(--z-6);touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button{aspect-ratio:unset}nav.tabs button.active{cursor:default}nav.tabs button.active:hover{background-color:var(--base-100);color:var(--contrast)}nav.tabs button h2{--wrap:nowrap;margin:0;font-size:var(--txt-x-small)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content.active{padding:1rem 0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}:where(body>header,.wp-site-blocks>header){--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}nav.term-navigation:has([hidden]){display:none}.dashboard-nav{--justify:flex-start;width:100%}nav.filters{--dir:row;--justify:flex-start;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem} |
| | | nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;font-family:var(--heading);padding:0;margin:0}nav li{display:flex;align-items:center;height:max(var(--btn),max-content);width:100%;max-inline-size:none;padding:0}nav a,nav button{display:flex;text-decoration:none;align-items:center;justify-content:center;height:var(--btn);width:100%;white-space:nowrap;text-transform:uppercase;transition:var(--trans-color)}nav a{height:var(--btn);padding:var(--padding)}nav button{justify-content:center;aspect-ratio:1;padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon{transform:rotate(0);transition:transform var(--trans-base)}.has-submenu.open>button .icon{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max(100%,max-content);background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:all var(--trans-t) var(--trans-fn);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:2px solid var(--action-0);outline-offset:2px}nav.always{--dir:column;--wrap:nowrap;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{--justify:flex-end;width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px)}nav.always>ul{--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;position:relative;right:-300vw;width:100vw;max-height:100%;padding:1rem 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{flex-wrap:wrap;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;max-width:calc(100% - var(--btn));text-align:center}nav.always .has-submenu{display:flex}nav.always .has-submenu>a{flex:1}nav.always .has-submenu>button{flex:0 0 var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;height:max-content;top:0;width:100%;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>button{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>button .icon-list,nav.always>button .icon-x{display:none}nav.always.open>button .icon-x,nav.always>button .icon-list{display:block;width:32px;height:32px}@media (min-width:768px){nav.always>ul{padding-top:var(--btn)}}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-7)}#breadcrumbs ol{height:max-content;--wrap:wrap!important;--justify:flex-start!important}#breadcrumbs li{width:max-content}#breadcrumbs a{height:var(--chip)}#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}#breadcrumbs li:last-of-type::after{display:none}#breadcrumbs :is(a,span){padding:0 .125rem;color:var(--contrast);text-transform:none}#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed.bottom{position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed.bottom li{flex:1;justify-content:center}nav.fixed.bottom a{gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed.bottom a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));max-width:none;padding:0 .5rem;background-color:rgba(var(--base-rgb),var(--op-4));color:var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-6)}.on-this-page a,.on-this-page li{width:100%;height:100%}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}body:has(.additional-actionsbutton) nav.on-this-page{width:calc(100vw - var(--btn_) - 1rem)}.on-this-page button{order:3;padding:0 1rem;width:max-content;aspect-ratio:unset;height:var(--btn)}.on-this-page.open button{order:0}.on-this-page ul{width:100%;gap:0}.on-this-page li{justify-content:center}.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}.on-this-page a{padding:0}.on-this-page #back-to-top span{display:none}.on-this-page.open #back-to-top span{display:block}nav.letters li{height:var(--chip);max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}nav.letters,nav.letters ul{height:var(--chipchip)}@media (min-width:768px){nav.letters,nav.letters ul{height:var(--chip)}nav.letters ul{--wrap:nowrap}nav.letters li{max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:space-between;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}.index ul{width:100%}.index li{flex-shrink:0;transform:scaleX(0);max-width:0;overflow:hidden}.index li.active,.index li.adj{transform:scaleX(1);width:calc(100% - var(--btn_));flex-shrink:1;max-width:none}.index li:first-of-type{flex-shrink:1;transform:scaleX(1);order:9999;width:var(--btn);height:var(--btn);max-width:none}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;align-items:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}.index.open ul{--dir:column;--justify:flex-end;height:100%;width:100%}.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}.index.open a{justify-content:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed{height:max-content;--wrap:wrap;--gap:0 .25rem}nav.condensed ul{min-height:var(--chip_);height:max-content;--justify:center;--wrap:wrap}.condensed li{width:max-content;min-height:var(--chip)}.condensed li+li::before{content:'·';padding:0 .25em}.condensed a{height:max-content;min-height:var(--chip);font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}.condensed a:focus{border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials,.always ul.socials a,.always ul.socials li{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{position:fixed;bottom:var(--btn);left:var(--btnbtn);right:var(--btnbtn);padding-bottom:2px;z-index:var(--z-6);touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button{aspect-ratio:unset}nav.tabs button.active{cursor:default}nav.tabs button.active:hover{background-color:var(--base-100);color:var(--contrast)}nav.tabs button h2{--wrap:nowrap;margin:0;font-size:var(--txt-x-small)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content.active{padding:1rem 0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}:where(body>header,.wp-site-blocks>header){--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}nav.term-navigation:has([hidden]){display:none}.dashboard-nav{--justify:flex-start;width:100%}nav.filters{--dir:row;--justify:flex-start;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem} |
| | |
| | | if (refs.trash) refs.trash.dataset.id = data.id; |
| | | }; |
| | | const imageSetup = function(el, refs, data) { |
| | | if (data?.fields?.post_thumbnail) { |
| | | const thumbnail = data.images[data.fields.post_thumbnail] ?? {}; |
| | | let hasThumbnail = data?.fields?.post_thumbnail || data?.fields?.thumbnail; |
| | | if (hasThumbnail) { |
| | | const thumbnail = data.images[hasThumbnail] ?? {}; |
| | | refs.img.src = thumbnail.medium??''; |
| | | refs.img.alt = thumbnail.alt??data.fields.post_title??''; |
| | | } |
| | |
| | | } |
| | | this.store.clearCache(); |
| | | } |
| | | |
| | | if (event === 'operation-status' |
| | | && data.status === 'completed' |
| | | && data.type === 'content_update') { |
| | | |
| | | this.store.clearCache(); |
| | | |
| | | // Check for result data (from ContentExecutor) |
| | | if (!data.result || !data.result.posts) { |
| | | console.warn('Content update completed but no result.posts', data); |
| | | if (!data.result || !data.result.success || !data.result.errors) |
| | | { |
| | | console.warn('Content update completed but no results', data); |
| | | return; |
| | | } |
| | | |
| | | // Get successfully processed post IDs |
| | | const successfulIds = Object.keys(data.result.posts); |
| | | |
| | | if (successfulIds.length === 0) { |
| | | if (Object.keys(data.result.success).length > 0) { |
| | | this.checkCompletedChanges(Object.entries(data.result.success)); |
| | | } |
| | | if (Object.keys(data.result.errors).length > 0) { |
| | | this.checkFailedChanges(Object.entries(data.result.errors)); |
| | | return; |
| | | } |
| | | |
| | | // Clear from both persistent and in-memory storage |
| | | this.changesStore.deleteMany(successfulIds); |
| | | successfulIds.forEach(id => this.changes.delete(id)); |
| | | if (Object.keys(data.result.success).length === 0) { |
| | | this.changesStore.delete(id); |
| | | this.store.clearCache(); |
| | | } |
| | | } |
| | | |
| | | }); |
| | | } |
| | | checkCompletedChanges(items) { |
| | | for (let [id, data] of items) { |
| | | |
| | | let stored = this.changesStore.get(id); |
| | | if (!stored) continue; |
| | | |
| | | for (let [field, value] of Object.entries(data)) { |
| | | if (Object.hasOwn(stored, field)) { |
| | | let changes = window.getDifferences.map(stored[field], value); |
| | | |
| | | if (!changes) { |
| | | delete stored[field]; |
| | | } |
| | | |
| | | } |
| | | } |
| | | |
| | | //It'll have the id and the content still |
| | | if (Object.values(stored).length === 2) { |
| | | this.changesStore.delete(id); |
| | | this.store.clearCache(); |
| | | } else { |
| | | this.changesStore.save(stored); |
| | | } |
| | | } |
| | | } |
| | | checkFailedChanges(items) { |
| | | //TODO do something. |
| | | } |
| | | |
| | | initSettings() { |
| | | this.defaults = { |
| | |
| | | this.activeItem = item.id; |
| | | this.ui.modals.edit.modal.dataset.itemId = itemID; |
| | | this.ui.modals.edit.modal.dataset.content = this.content; |
| | | this.ui.modals.edit.h2.textContent = `Editing ${item.fields.post_title === '' ? this.singular : item.fields.post_title}`; |
| | | let title; |
| | | if (Object.hasOwn(item.fields, 'post_title')) { |
| | | title = item.fields.post_title; |
| | | } else if (Object.hasOwn(item.fields, 'name')) { |
| | | title = item.fields.name; |
| | | } |
| | | this.ui.modals.edit.h2.textContent = `Editing ${title === '' ? this.singular : title}`; |
| | | this.ui.modals.edit.form.dataset.formId = `edit-${itemID}`; |
| | | |
| | | |
| | |
| | | headers, |
| | | signal: controller.signal |
| | | }); |
| | | if (!response.ok) { |
| | | // Access the error details from the response body |
| | | const errorBody = await response.text(); |
| | | // Throw a new error with a descriptive message |
| | | throw new Error(`HTTP error! status: ${response.status}, message: ${errorBody}`); |
| | | } |
| | | |
| | | if (response.status === 304) { |
| | | // 304 means "Not Modified" - use cached data if available |
| | |
| | | if (!response.ok) { |
| | | throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
| | | } |
| | | |
| | | const data = await response.json(); |
| | | |
| | | await this.processFetchedData(name, data, cacheKey, response); |
| | |
| | | const isAbortError = error?.name === 'AbortError'; |
| | | |
| | | if (!isAbortError) { |
| | | console.error(`Fetch error for store "${name}":`, error); |
| | | console.error(`Fetch error for store "${name}":`, error.message); |
| | | console.dir(error); |
| | | this.notify(name, 'fetch-error', { error }); |
| | | throw error; |
| | | } |
| | |
| | | let fieldId = uploads[0].field; |
| | | let field = document.querySelector(`[data-uploader="${fieldId}"]`); |
| | | if (!field) { |
| | | console.log('No field found for '+fieldId); |
| | | return; |
| | | if ('crudManager' in window && fieldId.startsWith(window.crudManager.content)) { |
| | | let [content, itemId, fieldName] = fieldId.split('_'); |
| | | if (parseInt(itemId) > 0) { |
| | | window.crudManager.openEditModal(itemId); |
| | | field = document.querySelector(`[data-uploader="${fieldId}"]`); |
| | | } else { |
| | | console.log('No field found for '+fieldId); |
| | | return; |
| | | } |
| | | } else { |
| | | console.log('No field found for '+fieldId); |
| | | return; |
| | | } |
| | | |
| | | } |
| | | let fieldData = this.fields.get(fieldId); |
| | | if (fieldData.groupUI.container) { |
| | |
| | | return ui; |
| | | } |
| | | |
| | | window.sleep = async function (ms = 50) { |
| | | return new Promise(resolve => setTimeout(resolve, ms)); |
| | | }; |
| | | |
| | | class DebouncedActions { |
| | | constructor() { |
| | |
| | | return; |
| | | } |
| | | if (this.openNav && e.target.closest(`#${this.openNav}`) === null) { |
| | | console.log('Closing nav', this.openNav); |
| | | this.toggleNav(false, this.openNav); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | document.addEventListener('DOMContentLoaded', function() { |
| | | |
| | | window.jvbNav = new Navigation(); |
| | | }); |
| | |
| | | class OnThisPage extends window.UIHandler { |
| | | constructor() { |
| | | super(); |
| | | class OnThisPage { |
| | | constructor() { |
| | | this.initElements(); |
| | | this.initListeners(); |
| | | } |
| | | |
| | | // Initialize state tracking |
| | | this.navOpen = false; |
| | | initElements() { |
| | | this.selectors = { |
| | | nav: 'nav.on-this-page', |
| | | toggle: 'button.toggle', |
| | | icon: 'button.toggle .icon' |
| | | } |
| | | this.ui = window.uiFromSelectors(this.selectors); |
| | | |
| | | // Bind methods |
| | | this.toggleNav = this.toggleNav.bind(this); |
| | | let items = this.ui.nav.querySelectorAll('li a'); |
| | | this.ui.items = {}; |
| | | this.ui.sections = {}; |
| | | this.selectors.items = []; |
| | | |
| | | // Bind elements first |
| | | this.bindElements(); |
| | | for (let item of items) { |
| | | let id = item.getAttribute('href'); |
| | | this.ui.items[id.replace('#', '')] = item.closest('li'); |
| | | this.selectors.items.push(id); |
| | | this.ui.sections[id.replace('#', '')] = document.querySelector(id); |
| | | } |
| | | this.selectors.items = this.selectors.items.join(','); |
| | | } |
| | | |
| | | initListeners() { |
| | | this.ui.toggle.addEventListener('click', () => { |
| | | let icon = this.ui.nav.classList().contains('open') ? 'icon-x-square' : 'icon-plus-square'; |
| | | console.log('Changing icon to: '+icon); |
| | | this.ui.icon.className = 'icon '+icon; |
| | | }); |
| | | |
| | | if (this.elements.nav) { |
| | | if(this.elements.toggle){ |
| | | // Bind click directly |
| | | this.elements.toggle.addEventListener('click', this.toggleNav); |
| | | this.observer = new IntersectionObserver( |
| | | (entries) => { |
| | | entries.forEach(entry => { |
| | | if (entry.isIntersecting) { |
| | | |
| | | // Bind UIHandler events for escape and outside clicks |
| | | this.bindEvents(); |
| | | } |
| | | let index = Object.keys(this.ui.items).indexOf(entry.target.id); |
| | | let i = 0; |
| | | for (let item of Object.values(this.ui.items)) { |
| | | item.classList.toggle('active', index === i); |
| | | // item.classList.toggle('adj', i === index +1 || i === index -1); |
| | | i++; |
| | | } |
| | | } |
| | | }); |
| | | }, |
| | | { |
| | | rootMargin: '-50% 0px -50% 0px', |
| | | threshold: 0 |
| | | } |
| | | ); |
| | | |
| | | |
| | | // Set up section observer |
| | | this.setupSectionObserver(); |
| | | } |
| | | } |
| | | |
| | | bindElements() { |
| | | const nav = document.querySelector('nav.on-this-page'); |
| | | |
| | | if (!nav) return; |
| | | this.elements = { |
| | | nav, |
| | | links: nav.querySelectorAll('a'), |
| | | sections: Array.from(nav.querySelectorAll('a')) |
| | | .map(a => { |
| | | const id = a.getAttribute('href'); |
| | | return document.querySelector(id); |
| | | }) |
| | | .filter(Boolean) |
| | | }; |
| | | if(nav.querySelector('button.toggle')){ |
| | | this.elements.toggle = nav.querySelector('button.toggle'); |
| | | } |
| | | } |
| | | |
| | | // Override to prevent UIHandler's component event binding |
| | | bindComponentEvents() { |
| | | // Intentionally empty |
| | | } |
| | | |
| | | toggleNav(event) { |
| | | event?.preventDefault(); |
| | | event?.stopPropagation(); |
| | | |
| | | const { nav, toggle } = this.elements; |
| | | if (!nav || !toggle) return; |
| | | |
| | | // Toggle state |
| | | this.navOpen = !this.navOpen; |
| | | |
| | | // Update DOM |
| | | if (this.navOpen) { |
| | | nav.classList.add('open'); |
| | | toggle.setAttribute('aria-label', 'Hide Index'); |
| | | toggle.setAttribute('aria-expanded', 'true'); |
| | | this.bindLinkHandlers(); |
| | | } else { |
| | | nav.classList.remove('open'); |
| | | toggle.setAttribute('aria-label', 'Show Index'); |
| | | toggle.setAttribute('aria-expanded', 'false'); |
| | | this.cleanupLinkHandlers(); |
| | | } |
| | | } |
| | | |
| | | bindLinkHandlers() { |
| | | const { links } = this.elements; |
| | | links?.forEach(link => { |
| | | link._boundHandler = () => { |
| | | this.navOpen = false; |
| | | this.elements.nav.classList.remove('open'); |
| | | this.elements.toggle.setAttribute('aria-label', 'Show Index'); |
| | | this.elements.toggle.setAttribute('aria-expanded', 'false'); |
| | | this.cleanupLinkHandlers(); |
| | | }; |
| | | link.addEventListener('click', link._boundHandler); |
| | | }); |
| | | } |
| | | |
| | | cleanupLinkHandlers() { |
| | | const { links } = this.elements; |
| | | links?.forEach(link => { |
| | | if (link._boundHandler) { |
| | | link.removeEventListener('click', link._boundHandler); |
| | | delete link._boundHandler; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | setupSectionObserver() { |
| | | const { sections } = this.elements; |
| | | |
| | | if (!sections?.length) return; |
| | | |
| | | this.initializeObserver( |
| | | 'sections', |
| | | sections, |
| | | { |
| | | rootMargin: '-50% 0% -50% 0%', |
| | | threshold: 0 |
| | | }, |
| | | (entries) => { |
| | | entries.forEach(entry => { |
| | | if (!entry.isIntersecting) return; |
| | | |
| | | const id = entry.target.id; |
| | | const link = this.elements.nav?.querySelector(`a[href="#${id}"]`); |
| | | if (link) { |
| | | this.updateActiveClasses(link); |
| | | } |
| | | }); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | updateActiveClasses(activeLink) { |
| | | const listItem = activeLink.closest('li'); |
| | | if (!listItem) return; |
| | | |
| | | // Remove existing active and adjacent classes |
| | | const allItems = this.elements.nav.querySelectorAll('li'); |
| | | allItems.forEach(item => { |
| | | item.classList.remove('active', 'adj'); |
| | | }); |
| | | |
| | | // Add new classes |
| | | listItem.classList.add('active'); |
| | | |
| | | // Add adjacent classes |
| | | if (listItem.previousElementSibling) { |
| | | listItem.previousElementSibling.classList.add('adj'); |
| | | } |
| | | if (listItem.nextElementSibling) { |
| | | listItem.nextElementSibling.classList.add('adj'); |
| | | } |
| | | } |
| | | |
| | | // Use local state for component active check |
| | | isComponentActive(componentKey) { |
| | | if (componentKey === 'nav') { |
| | | return this.navOpen; |
| | | } |
| | | return super.isComponentActive(componentKey); |
| | | } |
| | | |
| | | // UIHandler event handlers |
| | | handleOutsideClick(event) { |
| | | if (this.navOpen && !this.elements.nav.contains(event.target)) { |
| | | this.toggleNav(event); |
| | | } |
| | | } |
| | | |
| | | handleEscapeKey(event) { |
| | | if (event.key === 'Escape' && this.navOpen) { |
| | | this.toggleNav(event); |
| | | event.preventDefault(); |
| | | } |
| | | } |
| | | |
| | | cleanup() { |
| | | this.cleanupLinkHandlers(); |
| | | super.cleanup(); |
| | | } |
| | | for (let section of Object.values(this.ui.sections)) { |
| | | console.log('Observing section: ', section); |
| | | this.observer.observe(section); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Initialize |
| | |
| | | (()=>{class e{constructor(){this.container=document.querySelector(".crud[data-content]:not([data-ignore])"),this.container&&(this.content=this.container.dataset.content,this.endpoint=this.container.dataset.endpoint??"content",this.singular=this.container.dataset.singular,this.plural=this.container.dataset.plural,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.populate=window.jvbPopulate,this.cache=new window.jvbCache(this.content),this.activeItem=null,this.isTimeline=!1,this.isPopulating=!1,this.changes=new Map,this.items=new Map,this.init())}init(){this.initElements(),this.initListeners(),this.defineTemplates();let e=this.initSettings();this.initStore(e),this.checkHideFilters(),this.initIntegrations(),this.initUploader(),this.initModals()}defineTemplates(){const e=window.jvbTemplates,t=this,i=(e,i,s)=>{e.dataset.itemId=s.id;let a=i.checkbox.closest(".preview");window.prefixInput(i.checkbox,`select-${s.id}`,a,!0),i.checkbox.value=s.id,i.checkbox.checked=t.selected.has(parseInt(s.id)),i.selectLabel&&(i.selectLabel.htmlFor=`select-${s.id}`),i.edit&&(i.edit.dataset.id=s.id),i.trash&&(i.trash.dataset.id=s.id)},s=function(e,t,i){if(i?.fields?.post_thumbnail){const e=i.images[i.fields.post_thumbnail]??{};t.img.src=e.medium??"",t.img.alt=e.alt??i.fields.post_title??""}};e.define("gridView",{refs:{img:"img",checkbox:".select-item",selectLabel:"label.select-item-label",edit:'[data-action="edit"]',trash:'[data-action="trash"]'},setup({el:e,refs:t,manyRefs:a,data:l}){i(e,t,l),s(0,t,l)}}),e.define("listView",{refs:{img:"img",checkbox:".select-item",selectLabel:"label.select-item-label",edit:'[data-action="edit"]',trash:'[data-action="trash"]'},manyRefs:{attrs:"[data-attr]",fields:"[data-field]"},setup({el:e,refs:t,manyRefs:a,data:l}){i(e,t,l),s(0,t,l),a?.attrs?.forEach((e=>{const t=l[e.dataset.attr];t&&""!==t?e.textContent=t:e.remove()})),a?.fields?.forEach((e=>{const t=l.fields?.[e.dataset.field];t&&""!==t?"DIV"===e.tagName?e.innerHTML=t:e.textContent=t:e.remove()}))}});let a={};this.isTimeline&&(a.sharedRow="tr.shared",a.point="tr.timeline-point"),e.define("tableView",{refs:{checkbox:".select-item",selectLabel:"label.select-item-label",...a},manyRefs:{inputs:"input,select,textarea",status:'input[name="post_status"]',selectors:'[data-type="selector"]',fields:"[data-field]"},setup({el:e,refs:s,manyRefs:a,data:l}){if(i(e,s,l),a?.inputs?.forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${l.id}-`,t)})),a?.status?.forEach((e=>{e.value===l.status&&(e.checked=!0)})),t.isTimeline)s.sharedRow&&(s.sharedRow.querySelectorAll("input,select,textarea").forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${l.id}-`,t)})),t.populate.populate(s.sharedRow,l),s.sharedRow.querySelectorAll('input[name="post_status"]').forEach((e=>{e.value===l.status&&(e.checked=!0)}))),s.point&&l.fields?.timeline&&(Object.entries(l.fields.timeline).forEach((([i,a],n)=>{const o=s.point.cloneNode(!0);o.dataset.index=`${n}`,o.dataset.itemId=a.id,o.querySelectorAll("input,select,textarea").forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${a.id}-`,t)})),t.populate.populate(o,{fields:a,images:l.images,taxonomies:l.taxonomies});const d=l.images?.[a.post_thumbnail];d&&o.querySelector(".field.upload")?.setAttribute("title",d["image-title"]??""),e.insertBefore(o,s.point)})),s.point.remove());else if(void 0!==t.ui.table.form?.dataset.edit)a?.inputs?.forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${l.id}-`,t)})),a?.status?.forEach((e=>{e.value===l.status&&(e.checked=!0)})),t.populate.populate(e,l);else{const e=Object.hasOwn(l,"fields")?l.fields:l;a?.fields?.forEach((t=>{if(Object.hasOwn(e,t.dataset.field)&&""!==e[t.dataset.field]){let i=e[t.dataset.field],s=e.children[0];s&&(s.textContent="date"===t.dataset.field?window.formatTimeAgo(i):i)}}))}a?.selectors?.forEach((e=>e.setAttribute("data-lazy","")))}}),e.define("emptyState"),e.define("bulkItem",{refs:{checkbox:"input",img:"img",label:"label"},setup({el:e,refs:t,manyRefs:i,data:s}){t.checkbox&&(t.checkbox.id=`bulk_${s.id}`,t.checkbox.value=s.id,t.checkbox.checked=!0,t.checkbox.name="selected[]");let a=s?.images[s?.fields?.post_thumnbail]??{};t.img&&Object.keys(a).length>0&&(t.img.src=a.medium??"",t.img.alt=a.alt??""),t.label&&(t.label.title=item.fields.post_title)}}),e.define("trashOptions"),e.define("notTrashOptions"),e.define("contentTable")}initElements(){this.allowedFilters=["status","orderby","order","search","date-filter","dateFrom","dateTo"],this.selectors={buttons:{create:".create-item",clearFilters:'[data-action="clear-filters"]'},views:{grid:'input[data-view="grid"]',list:'input[data-view="list"]',table:'input[data-view="table"]'},modals:{create:{modal:"dialog.create",form:"dialog.create form",h2:"dialog.create h2"},edit:{modal:"dialog.edit",form:"dialog.edit form",h2:"dialog.edit h2"},bulkEdit:{modal:"dialog.bulkEdit",selected:"dialog.bulkEdit .selected",h2:"dialog.bulkEdit h2 span",form:"dialog.bulkEdit form"},date:{modal:"dialog.date-range",start:"dialog.date-range .date-start",end:"dialog.date-range .date-end",month:"dialog.date-range .month-select"}},grid:`.${this.content}.item-grid`,table:{nav:"#vertical",form:"form.table",table:"form.table table",body:"form.table body",head:"form.table thead",foot:"form.table tfoot",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{action:".bulk-action-select",count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"},filters:{container:"details.all-filters",search:'.all-filters input[type="search"]',status:{all:'[name="status"]#all',publish:'[name="status"]#publish',draft:'[name="status"]#draft',trash:'[name="status"]#trash'},orderby:{date:'[name="orderby"]#date',alphabetical:'[name="orderby"]#alphabetical'},order:{asc:'[name="order"][value="asc"]',desc:'[name="order"][value="desc"]'},date:'[data-filter="date"]'},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.selectors);const e=document.querySelectorAll('[data-filter="taxonomies"]');e.length>0&&(this.ui.filters.taxonomies={},e.forEach((e=>{const t=e.dataset.taxonomy;this.ui.filters.taxonomies[t]=e,this.allowedFilters.push(`tax_${t}`)}))),this.isTimeline=!!document.querySelector("[data-timeline]")}initUploader(){this.ui.uploader&&(window.jvbUploads.scanFields(this.ui.uploader),window.jvbUploads.subscribe(((e,t)=>{if("sent-to-queue"===e&&t===this.ui.uploader.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearCache()})),"sent-to-queue"===e&&t.field){const e=t.field.config.name,i=t.field.config.itemID;i&&e&&this.changes.has(i)&&delete this.changes.get(i)[e]}})))}initModals(){this.modals={};for(let[e,t]of Object.entries(this.ui.modals))t.modal&&(this.modals[e]=new window.jvbModal(t.modal),this.modals[e].subscribe(((t,i)=>{if("modal-close"===t){const t=this.ui.modals[e].form.dataset.formId;t&&this.forms.clearForm(t),this.resetForm(this.ui.modals[e].form),"date"===e&&this.handleCustomDateSelection(),["edit","bulkEdit","create"].includes(e)&&window.debouncer.timeouts.has(`save-${this.content}`)&&this.scheduleSave(0)}})))}initStore(e){let t={...this.defaults,...e};const i=window.jvbStore.register(this.content,[{storeName:this.content,keyPath:"id",endpoint:this.endpoint??"content",headers:{"X-Action-Nonce":window.auth.getNonce("dash")},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:t,ignore:["content","user"],TTL:36e5,showLoading:!0},{storeName:"changes",keyPath:"id"}]);this.changesStore=i.changes,this.store=i[this.content],this.store.subscribe(((e,t)=>{if("data-loaded"===e)this.render(),this.selectionHandler.collectItems()})),this.changesStore.subscribe(((e,t)=>{if("data-ready"===e){let e=this.changesStore.getAll();e.length>0&&(e.forEach((e=>{this.changes.set(e.id,e)})),this.savePosts("",!1).then((()=>{})))}}))}initIntegrations(){this.selected=new Set,this.selectionHandler=new window.jvbHandleSelection(this.container,{selectAll:{checkbox:"#select-all",label:".bulk-select label",span:".bulk-select label span"},wrapper:{wrapper:".wrap"},item:{idAttribute:"itemId"}}),this.selectionHandler.subscribe(((e,t)=>{this.selected=new Set([...t.selectedItems].map((e=>parseInt(e)))),this.ui.bulk.control.hidden=0===this.selected.size,this.ui.bulk.count.hidden=0===this.selected.size,this.ui.bulk.count.textContent=`${this.selected.size} ${this.plural} selected`})),this.forms=window.jvbForm,window.jvbUploads&&window.jvbUploads.subscribe(((e,t)=>{"groups_uploaded"===e&&t.content===this.content&&this.handleGroupsUploaded(t)})),this.queue.subscribe(((e,t)=>{if(["image_upload","video_upload","document_upload"].includes(t.type)&&"operation-status"===e&&"completed"===t.status&&this.store.clearCache(),"operation-status"===e&&"completed"===t.status&&"uploads/groups"===t.endpoint&&(t.result&&t.result.group_mappings&&this.handleGroupMappings(t.result.group_mappings),this.store.clearCache()),"operation-status"===e&&"completed"===t.status&&"content_update"===t.type){if(this.store.clearCache(),!t.result||!t.result.posts)return void console.warn("Content update completed but no result.posts",t);const e=Object.keys(t.result.posts);if(0===e.length)return;this.changesStore.deleteMany(e),e.forEach((e=>this.changes.delete(e)))}}))}initSettings(){this.defaults={content:this.content,user:window.auth.getUser(),page:1,status:"all",orderby:"date",order:"desc",search:""};let e={},t=this.container.dataset.view??"grid";this.view=this.cache.get("view")??t,this.view!==t&&(this.ui.views[this.view].checked=!0),this.status=this.cache.get("status")??this.defaults.status,this.status!==this.defaults.status&&(this.ui.filters.status[this.status].checked=!0,e.status=this.status),this.orderby=this.cache.get("orderby")??this.defaults.orderby,this.orderby!==this.defaults.orderby&&(this.ui.filters.orderby[this.orderby].checked=!0,e.orderBy=this.orderby),this.order=this.cache.get("order")??this.defaults.order,this.order!==this.defaults.order&&(this.ui.filters.order[this.order].checked=!0,e.order=this.order),this.ui.filters.taxonomies&&Object.entries(this.ui.filters.taxonomies).forEach((([t,i])=>{const s=`tax_${t}`,a=this.cache.get(s);a&&(i.value=a,e[s]=a)}));let i=this.cache.get("tabNav")??"horizontal";this.ui.table.nav&&"vertical"===i&&(this.ui.table.nav.checked=!0);let s={showFilters:{element:this.ui.filters.container,default:"closed"},showUploader:{element:this.ui.uploader,default:"open"}};for(let[e,t]of Object.entries(s))if(t.element){let i=this.cache.get(e)??t.default;t.element.open="open"===i,t.element.addEventListener("toggle",(()=>{this.cache.set(e,t.element.open?"open":"closed")}))}return e}initListeners(){this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleModalSubmit.bind(this),document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler),this.ui.filters.search&&this.ui.filters.search.addEventListener("input",this.inputHandler);for(let[e,t]of Object.entries(this.ui.modals))t.form&&t.form.addEventListener("submit",this.submitHandler)}handleModalSubmit(e){e.preventDefault();const t=e.target.closest("dialog");if(!t)return;if(t.classList.contains("create"))return void this.handleCreateSubmit(t);this.plural;this.scheduleSave(0)}async handleCreateSubmit(e){e.dataset.itemId;this.changes.size>0&&(this.cancelBackup(),await this.handleBackup());const t=await this.changesStore.getAll();if(0===t.length)return;let i={};t.forEach((e=>{const{id:t,...s}=e;i[t]=s}));let s=this.queue.addToQueue({endpoint:this.endpoint,headers:{"X-Action-Nonce":window.auth.getNonce("dash")},data:{posts:i},popup:`Creating your new ${this.singular}`,title:`Creating your new ${this.singular}`});if(!s)return;const a=e.querySelectorAll("[data-upload-field]");for(const e of a){const t=e.dataset.uploader;if(!t)continue;0!==window.jvbUploads.stores.uploads.filterByIndex({field:t}).length&&await window.jvbUploads.queueUploads("uploads",t,s)}}handleChange(e){const t=e.target.closest("[data-item-id]"),i=e.target.matches("[data-filter]"),s=e.target.matches(".bulk-action-select"),a=e.target.matches("[data-view]");if(t||i||s||a)if(this.isPopulating||!t||e.target.closest("[data-ignore], .select-item")){if(a)return this.items.clear(),void this.handleViewChange(e.target);if(s)this.handleBulkAction(e.target);else if(i)this.handleFilterChange(e.target);else if("table"===this.view){if(e.target.matches("details.multi-select"))return void this.toggleColumn(e.target.id,e.target.checked);e.target.matches(this.selectors.table.nav)&&(this.tabNav=e.target.checked,this.cache.set("tabNav",e.target.checked?"vertical":"horizontal"))}}else this.handleItemUpdate(e)}handleBulkAction(e){if(e.value.startsWith("tax-")){const t=e.options[e.selectedIndex],i=t.dataset.taxonomy,s=t.dataset.single,a=t.dataset.plural;return window.jvbSelector.openEmpty(i,s,a,(e=>this.handleBulkTaxonomy(e))),void(e.value="")}switch(e.value){case"edit":this.openBulkEditModal();break;case"publish":case"trash":case"delete":this.setBulkStatus(e.value);break;case"draft":case"restore":this.setBulkStatus("draft")}}handleBulkTaxonomy(e){e.termIds.length&&this.selected.size&&(this.selected.forEach((t=>{const i=this.store.get(t);if(!i)return;const s=(i.taxonomies?.[e.taxonomy]||[]).map((e=>e.id)),a=[...new Set([...s,...e.termIds])];this.updateItem(t,e.taxonomy,a)})),this.savePosts(`Adding ${e.terms.length} ${e.taxonomy} to ${this.selected.size} ${this.plural}...`).then((()=>{})),this.selectionHandler.clearSelection())}handleItemUpdate(e){let t=window.targetCheck(e,"[data-item-id]");if(!t)return;const i=e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');let s,a;if(i)s=i.dataset.field,a=this.forms.getFieldValue(i);else{let t=e.target.closest("[data-field]");s=t.dataset.field,a=this.forms.getFieldValue(e.target)}t.dataset.itemId.split(",").forEach((e=>{this.updateItem(e,s,a)}))}updateItem(e,t,i){if(this.isPopulating)return;const s=this.store.get(e);if(s){const a=s.fields?.[t]??s[t];if(null===window.getDifferences.map(a,i)){if(this.changes.has(e)){delete this.changes.get(e)[t];0===Object.keys(this.changes.get(e)).filter((e=>"id"!==e&&"content"!==e)).length&&(this.changes.delete(e),this.changesStore.delete(e))}return}}this.changes.has(e)||this.changes.set(e,{id:e,content:this.content}),this.changes.get(e)[t]=i,this.scheduleBackup(),"number"!=typeof e&&String(e).includes("group")||this.scheduleSave()}scheduleBackup(){window.debouncer.schedule(`changes-${this.content}`,(async()=>{this.changes.size>0&&await this.handleBackup()}),2e3)}cancelBackup(){window.debouncer.cancel(`changes-${this.content}`)}async handleBackup(){const e=Array.from(this.changes.values());this.changes.clear();const t=e.map((e=>e.id)),i=await Promise.all(t.map((e=>this.changesStore.get(e)))),s=e.map(((e,t)=>i[t]?window.deepMerge(i[t],e):e));await this.changesStore.saveMany(s)}scheduleSave(e=1e4){window.debouncer.schedule(`save-${this.content}`,(async()=>{this.changes.size>0&&(this.cancelBackup(),await this.handleBackup()),await this.savePosts("",!1)}),e)}handleFilterChange(e){let t=e.dataset.filter;return"date"===t&&"custom"===e.value?(e.value="",void this.modals.date.handleOpen()):"date"===t&&""!==e.value?(this.setFilter("date-filter",e.value),this.deleteFilter("dateFrom"),this.deleteFilter("dateTo"),void this.checkHideFilters()):("taxonomies"===t&&(t=`tax_${e.dataset.taxonomy}`),void this.setFilter(t,e.value))}checkHideFilters(){const e=this.store.filters,t=Object.entries(e).some((([e,t])=>!["content","user","page"].includes(e)&&(this.defaults[e]!==t&&""!==t&&null!==t)));this.ui.buttons.clearFilters.hidden=!t}clearAllFilters(){let e=this.store.filters;this.store.clearFilters();for(let[t,i]of Object.entries(e))this.cache.remove(t),this.deleteFilter(t,i);this.a11y.announce("All filters cleared")}handleCustomDateSelection(){if(this.ui.modals.date.month&&this.ui.modals.date.month.value){const[e,t]=this.ui.modals.date.month.value.split("-"),i=`${e}-${t}-01`,s=new Date(e,parseInt(t),0).getDate(),a=`${e}-${t}-${String(s).padStart(2,"0")}`;this.setFilter("dateFrom",i),this.setFilter("dateTo",a),this.deleteFilter("date-filter"),this.ui.modals.date.month.value=""}else this.ui.modals.date.start&&this.ui.modals.date.start.value&&this.ui.modals.date.end&&this.ui.modals.date.end.value&&(this.setFilter("dateFrom",this.ui.modals.date.start.value),this.setFilter("dateTo",this.ui.modals.date.end.value),this.deleteFilter("date-filter"),this.ui.modals.date.start.value="",this.ui.modals.date.end.value="");this.checkHideFilters()}handleViewChange(e){this.view=e.dataset.view,this.cache.set("view",this.view),this.render()}handleClick(e){if(e.target.matches(".clear-search"))return void this.deleteFilter("search","");const t=e.target.closest("[data-action]");return t?(e.preventDefault(),void this.handleActionButton(t)):e.target.matches(".apply-date-filter")?(this.handleCustomDateSelection(),void this.modals.date.handleClose()):void(e.target.matches(this.selectors.buttons.create)&&this.openCreateModal())}openCreateModal(){this.forms.registerForm(this.ui.modals.create.form,{cache:!1}),this.ui.modals.create.modal.dataset.itemId=window.generateID("new"),this.modals.create.handleOpen()}handleActionButton(e){const t=e.dataset.id;switch(e.dataset.action){case"edit":this.openEditModal(t);break;case"delete":confirm("Delete this item? This cannot be undone")&&(this.updateItem(t,"post_status","delete"),window.fade(e.closest(".item"),!1),this.savePosts(`Permanently deleting ${this.singular}...`).then((()=>{})),this.store.delete(t));break;case"trash":"trash"===this.status?confirm("Delete this item? This cannot be undone")&&(this.updateItem(t,"post_status","delete"),window.fade(e.closest(".item"),!1),this.savePosts(`Permanently deleting ${this.singular}...`).then((()=>{})),this.store.delete(t)):(this.updateItem(t,"post_status","trash"),window.fade(e.closest(".item"),!1),this.savePosts(`Sending ${this.singular} to trash...`).then((()=>{})));break;case"bulk-edit":this.selected.size>0&&this.openBulkEditModal();break;case"bulk-delete":this.handleBulkDelete();break;case"refresh":this.store.clearCache(),this.store.fetch();break;case"clear-filters":this.clearAllFilters()}}handleBulkDelete(){let e="trash"===this.status;if(this.selected.size>0&&confirm(`${e?"Permanently delete":"Send"} ${this.selected.size} ${1===this.selected.size?this.singular:this.plural}${e?"":"to trash"}?`)){this.selected.forEach((t=>{this.store.delete(t),this.updateItem(t,"post_status",e?"delete":"trash")}));let t=e?`Permanently deleting ${this.selected.size} ${1===this.selected.size?this.singular:this.plural}`:`Sending ${this.selected.size} ${1===this.selected.size?this.singular:this.plural} to trash`;this.savePosts(t).then((()=>{})),this.selectionHandler.clearSelection()}}handleInput(e){e.preventDefault(),e.stopPropagation();let t=e.target.value.trim(),i=`${this.content}-search`;0!==t.length?window.debouncer.schedule(i,(()=>{this.a11y.announce(`Searching for "${t}"...`),this.store.setFilters({search:t,page:1})}),300):this.deleteFilter("search","")}handleKeys(e){if(this.tabNav&&"Tab"===e.key){e.preventDefault();const t=e.target.closest("[data-field]"),i=e.target.closest("tr");if(!t||!i)return;const s=t.dataset.field,a=e.shiftKey;let l=this.findNextEditableRow(i,a);l||(l=this.wrapToRow(i,a)),l&&this.focusFieldInRow(l,s,a)}}findNextEditableRow(e,t=!1){let i=t?e.previousElementSibling:e.nextElementSibling;for(;i&&!this.isEditableRow(i);)i=t?i.previousElementSibling:i.nextElementSibling;return i}wrapToRow(e,t=!1){if(this.isTimeline){const i=e.closest("tbody");if(!i)return null;const s=Array.from(i.querySelectorAll("tr")).filter((e=>this.isEditableRow(e)));return t?s[s.length-1]:s[0]}{if(!this.ui.table.body)return null;const e=Array.from(this.ui.table.body.querySelectorAll("tr")).filter((e=>this.isEditableRow(e)));return t?e[e.length-1]:e[0]}}isEditableRow(e){return!e.closest("thead")&&!e.closest("tfoot")&&(this.isTimeline?e.classList.contains("shared")||e.classList.contains("timeline-point"):!!e.dataset.itemId)}focusFieldInRow(e,t,i=!1){const s=e.querySelector(`[data-field="${t}"]`);if(!s)return;const a=this.findFocusableInput(s);if(a){a.focus(),a.select&&"text"===a.type&&a.select();const e=i?"next":"previous";this.a11y?.announce(`Moved to ${t} in ${e} row`)}}findFocusableInput(e){const t=['input:not([type="hidden"]):not([disabled])',"textarea:not([disabled])","select:not([disabled])","button:not([disabled])"];for(const i of t){const t=e.querySelector(i);if(t)return t}return null}openEditModal(e){let t=this.store.get(parseInt(e));t&&(this.activeItem=t.id,this.ui.modals.edit.modal.dataset.itemId=e,this.ui.modals.edit.modal.dataset.content=this.content,this.ui.modals.edit.h2.textContent=`Editing ${""===t.fields.post_title?this.singular:t.fields.post_title}`,this.ui.modals.edit.form.dataset.formId=`edit-${e}`,this.forms.registerForm(this.ui.modals.edit.form,{cache:!1,autoUpload:!0}),this.isPopulating=!0,this.populate.populate(this.ui.modals.edit.form,t),requestAnimationFrame((()=>{requestAnimationFrame((()=>{this.isPopulating=!1}))})),this.modals.edit.handleOpen())}openBulkEditModal(){window.removeChildren(this.ui.modals.bulkEdit.selected),this.ui.modals.edit.form.reset(),window.chunkIt(this.selected,(t=>{let i=this.store.get(parseInt(t));if(i)return e.push(i.id),window.jvbTemplates.create("bulkItem",i)}),(e=>this.ui.modals.bulkEdit.selected.append(e))).then((()=>{}));let e=Array.from(this.selected).map((e=>this.store.get(parseInt(e)))).filter(Boolean);this.ui.modals.bulkEdit.modal.dataset.itemId=e.join(","),this.ui.modals.bulkEdit.h2&&(this.ui.modals.bulkEdit.h2.textContent=this.selected.size),this.modals.bulkEdit.handleOpen(),this.forms.registerForm(this.ui.modals.bulkEdit.form,{cache:!1}),this.isPopulating=!0,this.populate.populate(this.ui.modals.edit.form,item),requestAnimationFrame((()=>{requestAnimationFrame((()=>{this.isPopulating=!1}))}))}async savePosts(e="",t=!1){this.changes.size>0&&(this.cancelBackup(),await this.handleBackup());let i=await this.changesStore.getAll();if(0===i.length)return;if(i=this.validateChanges(i),0===i.length)return;""===e&&(e=`Saving ${i.length} ${1===i.length?this.singular:this.plural}`);let s={},a=[];i.forEach((e=>{let t=e.id;const{id:i,...l}=e;s[t]=l,e.post_status&&this.shouldRemoveItemUI(e.post_status)&&a.push(t)})),a.length>0&&this.removeItems(a);let l={endpoint:this.endpoint,headers:{"X-Action-Nonce":window.auth.getNonce("dash")},data:{posts:s},delay:t,popup:"Saving changes",title:e};this.queue.addToQueue(l)}validateChanges(e){return e.reduce(((e,t)=>{const{id:i,content:s,...a}=t,l=this.store.get(i);if(!l)return e.push(t),e;const n={id:i,content:s};let o=!1;for(const[e,t]of Object.entries(a)){const i=l.fields?.[e]??l[e];null!==window.getDifferences.map(i,t)&&(n[e]=t,o=!0)}return o?e.push(n):(this.changes.delete(i),this.changesStore.delete(i)),e}),[])}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,i=[];if(this.selected.forEach((t=>{i.push(t),this.updateItem(t,"post_status",e)})),"delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";this.shouldRemoveItemUI(e)&&this.removeItems(i),this.selectionHandler.clearSelection(),this.savePosts(`${t} ${i.length} ${1===i.length?this.singular:this.plural}...`).then((()=>{}))}render(){const e=this.store.getFiltered();if(0!==e.length){switch(this.view){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e).then((()=>{}));break;case"list":this.renderList(e)}this.updateUI()}else this.renderEmpty()}updateUI(){if(this.ui.bulk.action){let e=!1,t=this.ui.bulk.action.querySelector('[value="edit"]'),i=this.status;"trash"===i&&t?(window.removeChildren(this.ui.bulk.action),e=window.jvbTemplates.create("trashOptions")):"trash"===i||t||(window.removeChildren(this.ui.bulk.action),e=window.jvbTemplates.create("notTrashOptions")),e&&e.querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulk.action.append(e)})),this.ui.bulk.action.value=""}this.selected.size>0&&this.selectionHandler.updateSelectionUI()}renderEmpty(){this.toggleTable(!1),window.removeChildren(this.ui.grid);const e=window.jvbTemplates.create("emptyState");e&&(this.ui.grid.append(e),this.a11y.announceItems(0,!1,!1))}toggleTable(e=!0){if(this.ui.table.selectedColumns&&(this.ui.table.selectedColumns.hidden=!e),e&&!this.ui.table.form){let e=window.jvbTemplates.create("contentTable");this.container.append(e),this.ui.table=window.uiFromSelectors(this.selectors.table),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.form&&(this.ui.table.form.hidden=!e,e||this.forms.clearForm(this.ui.table.form.dataset.formId),this.ui.table.body&&window.removeChildren(this.ui.table.body)),this.keyHandler=this.handleKeys.bind(this),e?document.addEventListener("keydown",this.keyHandler):document.removeEventListener("keydown",this.keyHandler)}renderGrid(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view"),window.chunkIt(e,(e=>this.renderGridItem(e)),(e=>this.ui.grid.append(e))).then((()=>{}))}renderList(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),window.chunkIt(e,(e=>this.renderListItem(e)),(e=>this.ui.grid.append(e))).then((()=>{}))}async renderTable(e){this.toggleTable(),window.removeChildren(this.ui.grid),await window.chunkIt(e,(e=>this.renderTableItem(e)),(e=>{this.ui.table.body?this.ui.table.body.append(e):this.ui.table.table.insertBefore(e,this.ui.table.foot)}),5),requestAnimationFrame((()=>{window.jvbSelector?.scanExistingFields(this.ui.table.table)}))}renderGridItem(e){let t=window.jvbTemplates.create("gridView",e);return this.items.set(e.id,t),t}renderListItem(e){let t=window.jvbTemplates.create("listView",e);return this.items.set(e.id,t),t}renderTableItem(e){let t=window.jvbTemplates.create("tableView",e);return this.items.set(e.id,t),t}toggleColumn(e,t){this.ui.table.table.querySelectorAll(`.${e}`).forEach((e=>{e.hidden=!t}))}handleGroupsUploaded(e){const{posts:t,fieldId:i}=e;let s=window.jvbUploads,a=(s.fields.get(i),[]);t.forEach((e=>{const t={id:e.groupId,title:e.fields.post_title||`New ${this.singular}`,status:"draft",date:(new Date).toISOString(),modified:(new Date).toISOString(),thumbnail:null,icon:this.content,taxonomies:{},fields:e.fields,images:{}};e.images.forEach(((e,i)=>{let a=e.upload_id;0===i&&(t.fields.post_thumbnail=e);let l=s.stores.uploads.get(a);l&&(t.images[a]={"image-alt-text":"","image-caption":"","image-title":l.fields.originalName,medium:s.createPreviewUrl(s.formatFile(l))})})),a.push(t)})),this.store.saveMany(a).then((()=>this.render())),this.a11y.announce(`${t.length} ${1===t.length?this.singular:this.plural} created. Waiting for server confirmation...`)}handleGroupMappings(e){for(const[t,i]of Object.entries(e)){let e={};this.changes.has(t)&&(e=this.changes.get(t),this.changes.delete(t));let s=this.changesStore.get(t)??{};(e.size>0||s.size>0)&&(e=window.deepMerge(s,e),this.changes.set(i,e),this.scheduleBackup())}}shouldRemoveItemUI(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.store.filters.status}removeItems(e){e.forEach((e=>{if(this.items.has(e)){let t=this.items.get(e);t&&window.fade(t,!1)}}))}setFilters(e){for(let[t,i]of Object.entries(e)){if(!this.allowedFilters.includes(t)){delete e[t];continue}this.cache.set(t,i);let s=this.findFilterEl(t);this.setElValue(s,i)}this.store.setFilters(e)}setFilter(e,t){if(!this.allowedFilters.includes(e))return;this.cache.set(e,t),"status"===e&&(this.status=t),"orderby"===e&&(this.orderby=t),"order"===e&&(this.order=t);let i=this.findFilterEl(e,t);this.setElValue(i,t),this.store.setFilter(e,t)}deleteFilter(e,t){if(!this.allowedFilters.includes(e))return;if(Object.hasOwn(this.defaults,e))return void this.setFilter(e,this.defaults[e]);let i=this.findFilterEl(e,t);this.setElValue(i,!1),this.cache.remove(e),this.setFilter(e,"")}setElValue(e,t){if(e){if(!t)return["SELECT","TEXTAREA"].includes(e.tagName)&&(e.value=""),["text","search"].includes(e.type)&&(e.value=""),void("radio"===e.type&&(e.checked=!1));["SELECT","TEXTAREA"].includes(e.tagName)&&(e.value=t),["text","search"].includes(e.type)&&(e.value=t),"radio"===e.type&&(e.checked=!0)}}findFilterEl(e,t){if(["date-filter","dateFrom","dateTo"].includes(e)){switch(e){case"date-filter":e="month";break;case"dateFrom":e="start";break;case"dateTo":e="end"}return this.ui.modals.date[e]}if(e.includes("tax_")){const t=e.replace("tax_",""),i=this.ui.filters.taxonomies?.[t];return i||(console.warn("Taxonomy filter element not found:",t),null)}if(!Object.hasOwn(this.ui.filters,e))return console.warn("Filter el not found: ",e),!1;let i=this.ui.filters[e];if("object"==typeof i){if(!Object.hasOwn(this.ui.filters[e],t))return!1;i=this.ui.filters[e][t]}return i}resetForm(e){e.querySelectorAll('input[type="hidden"], input[type="text"], input[type="number"], input[type="email"], input[type="url"], textarea').forEach((e=>{e.value=""})),e.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach((e=>{e.checked=!1})),e.querySelectorAll("select").forEach((e=>{e.selectedIndex=0})),e.querySelectorAll(".selected-items").forEach((e=>{window.removeChildren(e)})),e.querySelectorAll(".item-grid.preview").forEach((e=>{window.removeChildren(e)}))}destroy(){window.debouncer.cancel(`changes-${this.content}`),this.changes.size>0&&(this.changesStore.saveMany(this.changes).then((()=>{})),this.changes.clear()),this.timelineSortables&&(this.timelineSortables.forEach((e=>e.destroy())),this.timelineSortables=[]);for(let[e,t]of Object.entries(this.ui.modals))t.form&&t.form.removeEventListener("submit",this.submitHandler);document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),this.ui.filters.search&&this.ui.filters.search.removeEventListener("input",this.handleInput)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{if("auth-loaded"===t){let t=document.querySelector("[data-content]");t&&!Object.hasOwn(t.dataset,"ignore")&&(window.crudManager=new e({content:t.dataset.content}))}}))}))})(); |
| | | (()=>{class e{constructor(){this.container=document.querySelector(".crud[data-content]:not([data-ignore])"),this.container&&(this.content=this.container.dataset.content,this.endpoint=this.container.dataset.endpoint??"content",this.singular=this.container.dataset.singular,this.plural=this.container.dataset.plural,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.populate=window.jvbPopulate,this.cache=new window.jvbCache(this.content),this.activeItem=null,this.isTimeline=!1,this.isPopulating=!1,this.changes=new Map,this.items=new Map,this.init())}init(){this.initElements(),this.initListeners(),this.defineTemplates();let e=this.initSettings();this.initStore(e),this.checkHideFilters(),this.initIntegrations(),this.initUploader(),this.initModals()}defineTemplates(){const e=window.jvbTemplates,t=this,s=(e,s,i)=>{e.dataset.itemId=i.id;let a=s.checkbox.closest(".preview");window.prefixInput(s.checkbox,`select-${i.id}`,a,!0),s.checkbox.value=i.id,s.checkbox.checked=t.selected.has(parseInt(i.id)),s.selectLabel&&(s.selectLabel.htmlFor=`select-${i.id}`),s.edit&&(s.edit.dataset.id=i.id),s.trash&&(s.trash.dataset.id=i.id)},i=function(e,t,s){let i=s?.fields?.post_thumbnail||s?.fields?.thumbnail;if(i){const e=s.images[i]??{};t.img.src=e.medium??"",t.img.alt=e.alt??s.fields.post_title??""}};e.define("gridView",{refs:{img:"img",checkbox:".select-item",selectLabel:"label.select-item-label",edit:'[data-action="edit"]',trash:'[data-action="trash"]'},setup({el:e,refs:t,manyRefs:a,data:l}){s(e,t,l),i(0,t,l)}}),e.define("listView",{refs:{img:"img",checkbox:".select-item",selectLabel:"label.select-item-label",edit:'[data-action="edit"]',trash:'[data-action="trash"]'},manyRefs:{attrs:"[data-attr]",fields:"[data-field]"},setup({el:e,refs:t,manyRefs:a,data:l}){s(e,t,l),i(0,t,l),a?.attrs?.forEach((e=>{const t=l[e.dataset.attr];t&&""!==t?e.textContent=t:e.remove()})),a?.fields?.forEach((e=>{const t=l.fields?.[e.dataset.field];t&&""!==t?"DIV"===e.tagName?e.innerHTML=t:e.textContent=t:e.remove()}))}});let a={};this.isTimeline&&(a.sharedRow="tr.shared",a.point="tr.timeline-point"),e.define("tableView",{refs:{checkbox:".select-item",selectLabel:"label.select-item-label",...a},manyRefs:{inputs:"input,select,textarea",status:'input[name="post_status"]',selectors:'[data-type="selector"]',fields:"[data-field]"},setup({el:e,refs:i,manyRefs:a,data:l}){if(s(e,i,l),a?.inputs?.forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${l.id}-`,t)})),a?.status?.forEach((e=>{e.value===l.status&&(e.checked=!0)})),t.isTimeline)i.sharedRow&&(i.sharedRow.querySelectorAll("input,select,textarea").forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${l.id}-`,t)})),t.populate.populate(i.sharedRow,l),i.sharedRow.querySelectorAll('input[name="post_status"]').forEach((e=>{e.value===l.status&&(e.checked=!0)}))),i.point&&l.fields?.timeline&&(Object.entries(l.fields.timeline).forEach((([s,a],n)=>{const o=i.point.cloneNode(!0);o.dataset.index=`${n}`,o.dataset.itemId=a.id,o.querySelectorAll("input,select,textarea").forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${a.id}-`,t)})),t.populate.populate(o,{fields:a,images:l.images,taxonomies:l.taxonomies});const r=l.images?.[a.post_thumbnail];r&&o.querySelector(".field.upload")?.setAttribute("title",r["image-title"]??""),e.insertBefore(o,i.point)})),i.point.remove());else if(void 0!==t.ui.table.form?.dataset.edit)a?.inputs?.forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${l.id}-`,t)})),a?.status?.forEach((e=>{e.value===l.status&&(e.checked=!0)})),t.populate.populate(e,l);else{const e=Object.hasOwn(l,"fields")?l.fields:l;a?.fields?.forEach((t=>{if(Object.hasOwn(e,t.dataset.field)&&""!==e[t.dataset.field]){let s=e[t.dataset.field],i=e.children[0];i&&(i.textContent="date"===t.dataset.field?window.formatTimeAgo(s):s)}}))}a?.selectors?.forEach((e=>e.setAttribute("data-lazy","")))}}),e.define("emptyState"),e.define("bulkItem",{refs:{checkbox:"input",img:"img",label:"label"},setup({el:e,refs:t,manyRefs:s,data:i}){t.checkbox&&(t.checkbox.id=`bulk_${i.id}`,t.checkbox.value=i.id,t.checkbox.checked=!0,t.checkbox.name="selected[]");let a=i?.images[i?.fields?.post_thumnbail]??{};t.img&&Object.keys(a).length>0&&(t.img.src=a.medium??"",t.img.alt=a.alt??""),t.label&&(t.label.title=item.fields.post_title)}}),e.define("trashOptions"),e.define("notTrashOptions"),e.define("contentTable")}initElements(){this.allowedFilters=["status","orderby","order","search","date-filter","dateFrom","dateTo"],this.selectors={buttons:{create:".create-item",clearFilters:'[data-action="clear-filters"]'},views:{grid:'input[data-view="grid"]',list:'input[data-view="list"]',table:'input[data-view="table"]'},modals:{create:{modal:"dialog.create",form:"dialog.create form",h2:"dialog.create h2"},edit:{modal:"dialog.edit",form:"dialog.edit form",h2:"dialog.edit h2"},bulkEdit:{modal:"dialog.bulkEdit",selected:"dialog.bulkEdit .selected",h2:"dialog.bulkEdit h2 span",form:"dialog.bulkEdit form"},date:{modal:"dialog.date-range",start:"dialog.date-range .date-start",end:"dialog.date-range .date-end",month:"dialog.date-range .month-select"}},grid:`.${this.content}.item-grid`,table:{nav:"#vertical",form:"form.table",table:"form.table table",body:"form.table body",head:"form.table thead",foot:"form.table tfoot",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{action:".bulk-action-select",count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"},filters:{container:"details.all-filters",search:'.all-filters input[type="search"]',status:{all:'[name="status"]#all',publish:'[name="status"]#publish',draft:'[name="status"]#draft',trash:'[name="status"]#trash'},orderby:{date:'[name="orderby"]#date',alphabetical:'[name="orderby"]#alphabetical'},order:{asc:'[name="order"][value="asc"]',desc:'[name="order"][value="desc"]'},date:'[data-filter="date"]'},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.selectors);const e=document.querySelectorAll('[data-filter="taxonomies"]');e.length>0&&(this.ui.filters.taxonomies={},e.forEach((e=>{const t=e.dataset.taxonomy;this.ui.filters.taxonomies[t]=e,this.allowedFilters.push(`tax_${t}`)}))),this.isTimeline=!!document.querySelector("[data-timeline]")}initUploader(){this.ui.uploader&&(window.jvbUploads.scanFields(this.ui.uploader),window.jvbUploads.subscribe(((e,t)=>{if("sent-to-queue"===e&&t===this.ui.uploader.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearCache()})),"sent-to-queue"===e&&t.field){const e=t.field.config.name,s=t.field.config.itemID;s&&e&&this.changes.has(s)&&delete this.changes.get(s)[e]}})))}initModals(){this.modals={};for(let[e,t]of Object.entries(this.ui.modals))t.modal&&(this.modals[e]=new window.jvbModal(t.modal),this.modals[e].subscribe(((t,s)=>{if("modal-close"===t){const t=this.ui.modals[e].form.dataset.formId;t&&this.forms.clearForm(t),this.resetForm(this.ui.modals[e].form),"date"===e&&this.handleCustomDateSelection(),["edit","bulkEdit","create"].includes(e)&&window.debouncer.timeouts.has(`save-${this.content}`)&&this.scheduleSave(0)}})))}initStore(e){let t={...this.defaults,...e};const s=window.jvbStore.register(this.content,[{storeName:this.content,keyPath:"id",endpoint:this.endpoint??"content",headers:{"X-Action-Nonce":window.auth.getNonce("dash")},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:t,ignore:["content","user"],TTL:36e5,showLoading:!0},{storeName:"changes",keyPath:"id"}]);this.changesStore=s.changes,this.store=s[this.content],this.store.subscribe(((e,t)=>{if("data-loaded"===e)this.render(),this.selectionHandler.collectItems()})),this.changesStore.subscribe(((e,t)=>{if("data-ready"===e){let e=this.changesStore.getAll();e.length>0&&(e.forEach((e=>{this.changes.set(e.id,e)})),this.savePosts("",!1).then((()=>{})))}}))}initIntegrations(){this.selected=new Set,this.selectionHandler=new window.jvbHandleSelection(this.container,{selectAll:{checkbox:"#select-all",label:".bulk-select label",span:".bulk-select label span"},wrapper:{wrapper:".wrap"},item:{idAttribute:"itemId"}}),this.selectionHandler.subscribe(((e,t)=>{this.selected=new Set([...t.selectedItems].map((e=>parseInt(e)))),this.ui.bulk.control.hidden=0===this.selected.size,this.ui.bulk.count.hidden=0===this.selected.size,this.ui.bulk.count.textContent=`${this.selected.size} ${this.plural} selected`})),this.forms=window.jvbForm,window.jvbUploads&&window.jvbUploads.subscribe(((e,t)=>{"groups_uploaded"===e&&t.content===this.content&&this.handleGroupsUploaded(t)})),this.queue.subscribe(((e,t)=>{if(["image_upload","video_upload","document_upload"].includes(t.type)&&"operation-status"===e&&"completed"===t.status&&this.store.clearCache(),"operation-status"===e&&"completed"===t.status&&"uploads/groups"===t.endpoint&&(t.result&&t.result.group_mappings&&this.handleGroupMappings(t.result.group_mappings),this.store.clearCache()),"operation-status"===e&&"completed"===t.status&&"content_update"===t.type){if(this.store.clearCache(),!t.result||!t.result.success||!t.result.errors)return void console.warn("Content update completed but no results",t);if(Object.keys(t.result.success).length>0&&this.checkCompletedChanges(Object.entries(t.result.success)),Object.keys(t.result.errors).length>0)return void this.checkFailedChanges(Object.entries(t.result.errors));0===Object.keys(t.result.success).length&&(this.changesStore.delete(id),this.store.clearCache())}}))}checkCompletedChanges(e){for(let[t,s]of e){let e=this.changesStore.get(t);if(e){for(let[t,i]of Object.entries(s))if(Object.hasOwn(e,t)){window.getDifferences.map(e[t],i)||delete e[t]}2===Object.values(e).length?(this.changesStore.delete(t),this.store.clearCache()):this.changesStore.save(e)}}}checkFailedChanges(e){}initSettings(){this.defaults={content:this.content,user:window.auth.getUser(),page:1,status:"all",orderby:"date",order:"desc",search:""};let e={},t=this.container.dataset.view??"grid";this.view=this.cache.get("view")??t,this.view!==t&&(this.ui.views[this.view].checked=!0),this.status=this.cache.get("status")??this.defaults.status,this.status!==this.defaults.status&&(this.ui.filters.status[this.status].checked=!0,e.status=this.status),this.orderby=this.cache.get("orderby")??this.defaults.orderby,this.orderby!==this.defaults.orderby&&(this.ui.filters.orderby[this.orderby].checked=!0,e.orderBy=this.orderby),this.order=this.cache.get("order")??this.defaults.order,this.order!==this.defaults.order&&(this.ui.filters.order[this.order].checked=!0,e.order=this.order),this.ui.filters.taxonomies&&Object.entries(this.ui.filters.taxonomies).forEach((([t,s])=>{const i=`tax_${t}`,a=this.cache.get(i);a&&(s.value=a,e[i]=a)}));let s=this.cache.get("tabNav")??"horizontal";this.ui.table.nav&&"vertical"===s&&(this.ui.table.nav.checked=!0);let i={showFilters:{element:this.ui.filters.container,default:"closed"},showUploader:{element:this.ui.uploader,default:"open"}};for(let[e,t]of Object.entries(i))if(t.element){let s=this.cache.get(e)??t.default;t.element.open="open"===s,t.element.addEventListener("toggle",(()=>{this.cache.set(e,t.element.open?"open":"closed")}))}return e}initListeners(){this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleModalSubmit.bind(this),document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler),this.ui.filters.search&&this.ui.filters.search.addEventListener("input",this.inputHandler);for(let[e,t]of Object.entries(this.ui.modals))t.form&&t.form.addEventListener("submit",this.submitHandler)}handleModalSubmit(e){e.preventDefault();const t=e.target.closest("dialog");if(!t)return;if(t.classList.contains("create"))return void this.handleCreateSubmit(t);this.plural;this.scheduleSave(0)}async handleCreateSubmit(e){e.dataset.itemId;this.changes.size>0&&(this.cancelBackup(),await this.handleBackup());const t=await this.changesStore.getAll();if(0===t.length)return;let s={};t.forEach((e=>{const{id:t,...i}=e;s[t]=i}));let i=this.queue.addToQueue({endpoint:this.endpoint,headers:{"X-Action-Nonce":window.auth.getNonce("dash")},data:{posts:s},popup:`Creating your new ${this.singular}`,title:`Creating your new ${this.singular}`});if(!i)return;const a=e.querySelectorAll("[data-upload-field]");for(const e of a){const t=e.dataset.uploader;if(!t)continue;0!==window.jvbUploads.stores.uploads.filterByIndex({field:t}).length&&await window.jvbUploads.queueUploads("uploads",t,i)}}handleChange(e){const t=e.target.closest("[data-item-id]"),s=e.target.matches("[data-filter]"),i=e.target.matches(".bulk-action-select"),a=e.target.matches("[data-view]");if(t||s||i||a)if(this.isPopulating||!t||e.target.closest("[data-ignore], .select-item")){if(a)return this.items.clear(),void this.handleViewChange(e.target);if(i)this.handleBulkAction(e.target);else if(s)this.handleFilterChange(e.target);else if("table"===this.view){if(e.target.matches("details.multi-select"))return void this.toggleColumn(e.target.id,e.target.checked);e.target.matches(this.selectors.table.nav)&&(this.tabNav=e.target.checked,this.cache.set("tabNav",e.target.checked?"vertical":"horizontal"))}}else this.handleItemUpdate(e)}handleBulkAction(e){if(e.value.startsWith("tax-")){const t=e.options[e.selectedIndex],s=t.dataset.taxonomy,i=t.dataset.single,a=t.dataset.plural;return window.jvbSelector.openEmpty(s,i,a,(e=>this.handleBulkTaxonomy(e))),void(e.value="")}switch(e.value){case"edit":this.openBulkEditModal();break;case"publish":case"trash":case"delete":this.setBulkStatus(e.value);break;case"draft":case"restore":this.setBulkStatus("draft")}}handleBulkTaxonomy(e){e.termIds.length&&this.selected.size&&(this.selected.forEach((t=>{const s=this.store.get(t);if(!s)return;const i=(s.taxonomies?.[e.taxonomy]||[]).map((e=>e.id)),a=[...new Set([...i,...e.termIds])];this.updateItem(t,e.taxonomy,a)})),this.savePosts(`Adding ${e.terms.length} ${e.taxonomy} to ${this.selected.size} ${this.plural}...`).then((()=>{})),this.selectionHandler.clearSelection())}handleItemUpdate(e){let t=window.targetCheck(e,"[data-item-id]");if(!t)return;const s=e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');let i,a;if(s)i=s.dataset.field,a=this.forms.getFieldValue(s);else{let t=e.target.closest("[data-field]");i=t.dataset.field,a=this.forms.getFieldValue(e.target)}t.dataset.itemId.split(",").forEach((e=>{this.updateItem(e,i,a)}))}updateItem(e,t,s){if(this.isPopulating)return;const i=this.store.get(e);if(i){const a=i.fields?.[t]??i[t];if(null===window.getDifferences.map(a,s)){if(this.changes.has(e)){delete this.changes.get(e)[t];0===Object.keys(this.changes.get(e)).filter((e=>"id"!==e&&"content"!==e)).length&&(this.changes.delete(e),this.changesStore.delete(e))}return}}this.changes.has(e)||this.changes.set(e,{id:e,content:this.content}),this.changes.get(e)[t]=s,this.scheduleBackup(),"number"!=typeof e&&String(e).includes("group")||this.scheduleSave()}scheduleBackup(){window.debouncer.schedule(`changes-${this.content}`,(async()=>{this.changes.size>0&&await this.handleBackup()}),2e3)}cancelBackup(){window.debouncer.cancel(`changes-${this.content}`)}async handleBackup(){const e=Array.from(this.changes.values());this.changes.clear();const t=e.map((e=>e.id)),s=await Promise.all(t.map((e=>this.changesStore.get(e)))),i=e.map(((e,t)=>s[t]?window.deepMerge(s[t],e):e));await this.changesStore.saveMany(i)}scheduleSave(e=1e4){window.debouncer.schedule(`save-${this.content}`,(async()=>{this.changes.size>0&&(this.cancelBackup(),await this.handleBackup()),await this.savePosts("",!1)}),e)}handleFilterChange(e){let t=e.dataset.filter;return"date"===t&&"custom"===e.value?(e.value="",void this.modals.date.handleOpen()):"date"===t&&""!==e.value?(this.setFilter("date-filter",e.value),this.deleteFilter("dateFrom"),this.deleteFilter("dateTo"),void this.checkHideFilters()):("taxonomies"===t&&(t=`tax_${e.dataset.taxonomy}`),void this.setFilter(t,e.value))}checkHideFilters(){const e=this.store.filters,t=Object.entries(e).some((([e,t])=>!["content","user","page"].includes(e)&&(this.defaults[e]!==t&&""!==t&&null!==t)));this.ui.buttons.clearFilters.hidden=!t}clearAllFilters(){let e=this.store.filters;this.store.clearFilters();for(let[t,s]of Object.entries(e))this.cache.remove(t),this.deleteFilter(t,s);this.a11y.announce("All filters cleared")}handleCustomDateSelection(){if(this.ui.modals.date.month&&this.ui.modals.date.month.value){const[e,t]=this.ui.modals.date.month.value.split("-"),s=`${e}-${t}-01`,i=new Date(e,parseInt(t),0).getDate(),a=`${e}-${t}-${String(i).padStart(2,"0")}`;this.setFilter("dateFrom",s),this.setFilter("dateTo",a),this.deleteFilter("date-filter"),this.ui.modals.date.month.value=""}else this.ui.modals.date.start&&this.ui.modals.date.start.value&&this.ui.modals.date.end&&this.ui.modals.date.end.value&&(this.setFilter("dateFrom",this.ui.modals.date.start.value),this.setFilter("dateTo",this.ui.modals.date.end.value),this.deleteFilter("date-filter"),this.ui.modals.date.start.value="",this.ui.modals.date.end.value="");this.checkHideFilters()}handleViewChange(e){this.view=e.dataset.view,this.cache.set("view",this.view),this.render()}handleClick(e){if(e.target.matches(".clear-search"))return void this.deleteFilter("search","");const t=e.target.closest("[data-action]");return t?(e.preventDefault(),void this.handleActionButton(t)):e.target.matches(".apply-date-filter")?(this.handleCustomDateSelection(),void this.modals.date.handleClose()):void(e.target.matches(this.selectors.buttons.create)&&this.openCreateModal())}openCreateModal(){this.forms.registerForm(this.ui.modals.create.form,{cache:!1}),this.ui.modals.create.modal.dataset.itemId=window.generateID("new"),this.modals.create.handleOpen()}handleActionButton(e){const t=e.dataset.id;switch(e.dataset.action){case"edit":this.openEditModal(t);break;case"delete":confirm("Delete this item? This cannot be undone")&&(this.updateItem(t,"post_status","delete"),window.fade(e.closest(".item"),!1),this.savePosts(`Permanently deleting ${this.singular}...`).then((()=>{})),this.store.delete(t));break;case"trash":"trash"===this.status?confirm("Delete this item? This cannot be undone")&&(this.updateItem(t,"post_status","delete"),window.fade(e.closest(".item"),!1),this.savePosts(`Permanently deleting ${this.singular}...`).then((()=>{})),this.store.delete(t)):(this.updateItem(t,"post_status","trash"),window.fade(e.closest(".item"),!1),this.savePosts(`Sending ${this.singular} to trash...`).then((()=>{})));break;case"bulk-edit":this.selected.size>0&&this.openBulkEditModal();break;case"bulk-delete":this.handleBulkDelete();break;case"refresh":this.store.clearCache(),this.store.fetch();break;case"clear-filters":this.clearAllFilters()}}handleBulkDelete(){let e="trash"===this.status;if(this.selected.size>0&&confirm(`${e?"Permanently delete":"Send"} ${this.selected.size} ${1===this.selected.size?this.singular:this.plural}${e?"":"to trash"}?`)){this.selected.forEach((t=>{this.store.delete(t),this.updateItem(t,"post_status",e?"delete":"trash")}));let t=e?`Permanently deleting ${this.selected.size} ${1===this.selected.size?this.singular:this.plural}`:`Sending ${this.selected.size} ${1===this.selected.size?this.singular:this.plural} to trash`;this.savePosts(t).then((()=>{})),this.selectionHandler.clearSelection()}}handleInput(e){e.preventDefault(),e.stopPropagation();let t=e.target.value.trim(),s=`${this.content}-search`;0!==t.length?window.debouncer.schedule(s,(()=>{this.a11y.announce(`Searching for "${t}"...`),this.store.setFilters({search:t,page:1})}),300):this.deleteFilter("search","")}handleKeys(e){if(this.tabNav&&"Tab"===e.key){e.preventDefault();const t=e.target.closest("[data-field]"),s=e.target.closest("tr");if(!t||!s)return;const i=t.dataset.field,a=e.shiftKey;let l=this.findNextEditableRow(s,a);l||(l=this.wrapToRow(s,a)),l&&this.focusFieldInRow(l,i,a)}}findNextEditableRow(e,t=!1){let s=t?e.previousElementSibling:e.nextElementSibling;for(;s&&!this.isEditableRow(s);)s=t?s.previousElementSibling:s.nextElementSibling;return s}wrapToRow(e,t=!1){if(this.isTimeline){const s=e.closest("tbody");if(!s)return null;const i=Array.from(s.querySelectorAll("tr")).filter((e=>this.isEditableRow(e)));return t?i[i.length-1]:i[0]}{if(!this.ui.table.body)return null;const e=Array.from(this.ui.table.body.querySelectorAll("tr")).filter((e=>this.isEditableRow(e)));return t?e[e.length-1]:e[0]}}isEditableRow(e){return!e.closest("thead")&&!e.closest("tfoot")&&(this.isTimeline?e.classList.contains("shared")||e.classList.contains("timeline-point"):!!e.dataset.itemId)}focusFieldInRow(e,t,s=!1){const i=e.querySelector(`[data-field="${t}"]`);if(!i)return;const a=this.findFocusableInput(i);if(a){a.focus(),a.select&&"text"===a.type&&a.select();const e=s?"next":"previous";this.a11y?.announce(`Moved to ${t} in ${e} row`)}}findFocusableInput(e){const t=['input:not([type="hidden"]):not([disabled])',"textarea:not([disabled])","select:not([disabled])","button:not([disabled])"];for(const s of t){const t=e.querySelector(s);if(t)return t}return null}openEditModal(e){let t,s=this.store.get(parseInt(e));s&&(this.activeItem=s.id,this.ui.modals.edit.modal.dataset.itemId=e,this.ui.modals.edit.modal.dataset.content=this.content,Object.hasOwn(s.fields,"post_title")?t=s.fields.post_title:Object.hasOwn(s.fields,"name")&&(t=s.fields.name),this.ui.modals.edit.h2.textContent=`Editing ${""===t?this.singular:t}`,this.ui.modals.edit.form.dataset.formId=`edit-${e}`,this.forms.registerForm(this.ui.modals.edit.form,{cache:!1,autoUpload:!0}),this.isPopulating=!0,this.populate.populate(this.ui.modals.edit.form,s),requestAnimationFrame((()=>{requestAnimationFrame((()=>{this.isPopulating=!1}))})),this.modals.edit.handleOpen())}openBulkEditModal(){window.removeChildren(this.ui.modals.bulkEdit.selected),this.ui.modals.edit.form.reset(),window.chunkIt(this.selected,(t=>{let s=this.store.get(parseInt(t));if(s)return e.push(s.id),window.jvbTemplates.create("bulkItem",s)}),(e=>this.ui.modals.bulkEdit.selected.append(e))).then((()=>{}));let e=Array.from(this.selected).map((e=>this.store.get(parseInt(e)))).filter(Boolean);this.ui.modals.bulkEdit.modal.dataset.itemId=e.join(","),this.ui.modals.bulkEdit.h2&&(this.ui.modals.bulkEdit.h2.textContent=this.selected.size),this.modals.bulkEdit.handleOpen(),this.forms.registerForm(this.ui.modals.bulkEdit.form,{cache:!1}),this.isPopulating=!0,this.populate.populate(this.ui.modals.edit.form,item),requestAnimationFrame((()=>{requestAnimationFrame((()=>{this.isPopulating=!1}))}))}async savePosts(e="",t=!1){this.changes.size>0&&(this.cancelBackup(),await this.handleBackup());let s=await this.changesStore.getAll();if(0===s.length)return;if(s=this.validateChanges(s),0===s.length)return;""===e&&(e=`Saving ${s.length} ${1===s.length?this.singular:this.plural}`);let i={},a=[];s.forEach((e=>{let t=e.id;const{id:s,...l}=e;i[t]=l,e.post_status&&this.shouldRemoveItemUI(e.post_status)&&a.push(t)})),a.length>0&&this.removeItems(a);let l={endpoint:this.endpoint,headers:{"X-Action-Nonce":window.auth.getNonce("dash")},data:{posts:i},delay:t,popup:"Saving changes",title:e};this.queue.addToQueue(l)}validateChanges(e){return e.reduce(((e,t)=>{const{id:s,content:i,...a}=t,l=this.store.get(s);if(!l)return e.push(t),e;const n={id:s,content:i};let o=!1;for(const[e,t]of Object.entries(a)){const s=l.fields?.[e]??l[e];null!==window.getDifferences.map(s,t)&&(n[e]=t,o=!0)}return o?e.push(n):(this.changes.delete(s),this.changesStore.delete(s)),e}),[])}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,s=[];if(this.selected.forEach((t=>{s.push(t),this.updateItem(t,"post_status",e)})),"delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";this.shouldRemoveItemUI(e)&&this.removeItems(s),this.selectionHandler.clearSelection(),this.savePosts(`${t} ${s.length} ${1===s.length?this.singular:this.plural}...`).then((()=>{}))}render(){const e=this.store.getFiltered();if(0!==e.length){switch(this.view){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e).then((()=>{}));break;case"list":this.renderList(e)}this.updateUI()}else this.renderEmpty()}updateUI(){if(this.ui.bulk.action){let e=!1,t=this.ui.bulk.action.querySelector('[value="edit"]'),s=this.status;"trash"===s&&t?(window.removeChildren(this.ui.bulk.action),e=window.jvbTemplates.create("trashOptions")):"trash"===s||t||(window.removeChildren(this.ui.bulk.action),e=window.jvbTemplates.create("notTrashOptions")),e&&e.querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulk.action.append(e)})),this.ui.bulk.action.value=""}this.selected.size>0&&this.selectionHandler.updateSelectionUI()}renderEmpty(){this.toggleTable(!1),window.removeChildren(this.ui.grid);const e=window.jvbTemplates.create("emptyState");e&&(this.ui.grid.append(e),this.a11y.announceItems(0,!1,!1))}toggleTable(e=!0){if(this.ui.table.selectedColumns&&(this.ui.table.selectedColumns.hidden=!e),e&&!this.ui.table.form){let e=window.jvbTemplates.create("contentTable");this.container.append(e),this.ui.table=window.uiFromSelectors(this.selectors.table),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.form&&(this.ui.table.form.hidden=!e,e||this.forms.clearForm(this.ui.table.form.dataset.formId),this.ui.table.body&&window.removeChildren(this.ui.table.body)),this.keyHandler=this.handleKeys.bind(this),e?document.addEventListener("keydown",this.keyHandler):document.removeEventListener("keydown",this.keyHandler)}renderGrid(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view"),window.chunkIt(e,(e=>this.renderGridItem(e)),(e=>this.ui.grid.append(e))).then((()=>{}))}renderList(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),window.chunkIt(e,(e=>this.renderListItem(e)),(e=>this.ui.grid.append(e))).then((()=>{}))}async renderTable(e){this.toggleTable(),window.removeChildren(this.ui.grid),await window.chunkIt(e,(e=>this.renderTableItem(e)),(e=>{this.ui.table.body?this.ui.table.body.append(e):this.ui.table.table.insertBefore(e,this.ui.table.foot)}),5),requestAnimationFrame((()=>{window.jvbSelector?.scanExistingFields(this.ui.table.table)}))}renderGridItem(e){let t=window.jvbTemplates.create("gridView",e);return this.items.set(e.id,t),t}renderListItem(e){let t=window.jvbTemplates.create("listView",e);return this.items.set(e.id,t),t}renderTableItem(e){let t=window.jvbTemplates.create("tableView",e);return this.items.set(e.id,t),t}toggleColumn(e,t){this.ui.table.table.querySelectorAll(`.${e}`).forEach((e=>{e.hidden=!t}))}handleGroupsUploaded(e){const{posts:t,fieldId:s}=e;let i=window.jvbUploads,a=(i.fields.get(s),[]);t.forEach((e=>{const t={id:e.groupId,title:e.fields.post_title||`New ${this.singular}`,status:"draft",date:(new Date).toISOString(),modified:(new Date).toISOString(),thumbnail:null,icon:this.content,taxonomies:{},fields:e.fields,images:{}};e.images.forEach(((e,s)=>{let a=e.upload_id;0===s&&(t.fields.post_thumbnail=e);let l=i.stores.uploads.get(a);l&&(t.images[a]={"image-alt-text":"","image-caption":"","image-title":l.fields.originalName,medium:i.createPreviewUrl(i.formatFile(l))})})),a.push(t)})),this.store.saveMany(a).then((()=>this.render())),this.a11y.announce(`${t.length} ${1===t.length?this.singular:this.plural} created. Waiting for server confirmation...`)}handleGroupMappings(e){for(const[t,s]of Object.entries(e)){let e={};this.changes.has(t)&&(e=this.changes.get(t),this.changes.delete(t));let i=this.changesStore.get(t)??{};(e.size>0||i.size>0)&&(e=window.deepMerge(i,e),this.changes.set(s,e),this.scheduleBackup())}}shouldRemoveItemUI(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.store.filters.status}removeItems(e){e.forEach((e=>{if(this.items.has(e)){let t=this.items.get(e);t&&window.fade(t,!1)}}))}setFilters(e){for(let[t,s]of Object.entries(e)){if(!this.allowedFilters.includes(t)){delete e[t];continue}this.cache.set(t,s);let i=this.findFilterEl(t);this.setElValue(i,s)}this.store.setFilters(e)}setFilter(e,t){if(!this.allowedFilters.includes(e))return;this.cache.set(e,t),"status"===e&&(this.status=t),"orderby"===e&&(this.orderby=t),"order"===e&&(this.order=t);let s=this.findFilterEl(e,t);this.setElValue(s,t),this.store.setFilter(e,t)}deleteFilter(e,t){if(!this.allowedFilters.includes(e))return;if(Object.hasOwn(this.defaults,e))return void this.setFilter(e,this.defaults[e]);let s=this.findFilterEl(e,t);this.setElValue(s,!1),this.cache.remove(e),this.setFilter(e,"")}setElValue(e,t){if(e){if(!t)return["SELECT","TEXTAREA"].includes(e.tagName)&&(e.value=""),["text","search"].includes(e.type)&&(e.value=""),void("radio"===e.type&&(e.checked=!1));["SELECT","TEXTAREA"].includes(e.tagName)&&(e.value=t),["text","search"].includes(e.type)&&(e.value=t),"radio"===e.type&&(e.checked=!0)}}findFilterEl(e,t){if(["date-filter","dateFrom","dateTo"].includes(e)){switch(e){case"date-filter":e="month";break;case"dateFrom":e="start";break;case"dateTo":e="end"}return this.ui.modals.date[e]}if(e.includes("tax_")){const t=e.replace("tax_",""),s=this.ui.filters.taxonomies?.[t];return s||(console.warn("Taxonomy filter element not found:",t),null)}if(!Object.hasOwn(this.ui.filters,e))return console.warn("Filter el not found: ",e),!1;let s=this.ui.filters[e];if("object"==typeof s){if(!Object.hasOwn(this.ui.filters[e],t))return!1;s=this.ui.filters[e][t]}return s}resetForm(e){e.querySelectorAll('input[type="hidden"], input[type="text"], input[type="number"], input[type="email"], input[type="url"], textarea').forEach((e=>{e.value=""})),e.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach((e=>{e.checked=!1})),e.querySelectorAll("select").forEach((e=>{e.selectedIndex=0})),e.querySelectorAll(".selected-items").forEach((e=>{window.removeChildren(e)})),e.querySelectorAll(".item-grid.preview").forEach((e=>{window.removeChildren(e)}))}destroy(){window.debouncer.cancel(`changes-${this.content}`),this.changes.size>0&&(this.changesStore.saveMany(this.changes).then((()=>{})),this.changes.clear()),this.timelineSortables&&(this.timelineSortables.forEach((e=>e.destroy())),this.timelineSortables=[]);for(let[e,t]of Object.entries(this.ui.modals))t.form&&t.form.removeEventListener("submit",this.submitHandler);document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),this.ui.filters.search&&this.ui.filters.search.removeEventListener("input",this.handleInput)}}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.25){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`${jvbBase.base}${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},ignore:[],required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.ignoreFilters=new Set(["search","page","per_page","orderby","order","context","source",...i.config.ignore]),i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),saveMany:t=>this.saveMany(e,t),delete:t=>this.delete(e,t),deleteMany:t=>this.deleteMany(e,t),get:t=>this.get(e,t),getMany:t=>this.getMany(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=()=>{const e=o.error||new Error("Transaction failed with unknown error");a(e)};try{h=r(c,o)}catch(e){a(e||new Error("Callback failed with unknown error"))}}))):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)){let t=r.items.map((t=>this.get(e,t)));return this.notify(e,"data-loaded",{cached:!0,items:t??[]}),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){if(!("AbortError"===t?.name))throw 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||[]).filter((e=>e&&"object"==typeof e)),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 l={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,l),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.put(l)}))}))}_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}async saveMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Map?Array.from(t.values()):Array.isArray(t)?t:Object.values(t);if(0===r.length)return[];const i=[];return r.forEach((t=>{const s=this._saveItem(e,t);i.push(s)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{i.forEach((t=>{e.put(t.processed)}))})),this.notify(e,"items-saved",{count:i.length,keys:i.map((e=>e.key))}),i.map((e=>e.key))}processForStorage(e,t=!0,s="root"){if(null===e)return{valid:!0,data:null};if(void 0===e)return t?{valid:!1,error:`Undefined value at ${s}`}:{valid:!0,data:void 0};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:void 0};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:void 0};if(e instanceof FormData)return{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0===e.data&&null!==a||(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:void 0}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}async deleteMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===r.length?[]:(r.forEach((e=>{s.data.delete(e)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{r.forEach((t=>{e.delete(t)}))})),this.notify(e,"items-deleted",{count:r.length,ids:r}),r)}get(e,t){return this.stores.get(e).data.get(t)}getMany(e,t,s=!0){const r=this.stores.get(e);if(!r)return[];const i=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===i.length?[]:s?i.reduce(((e,t)=>{const s=r.data.get(t);return s&&e.push(s),e}),[]):i.map((e=>r.data.get(e)??null))}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=>!(!e||"object"!=typeof e)&&Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r?.items){const e=r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]);return this.applyOrdering(e,t)}const i=Array.from(t.data.values()),a=t.filters.search?.toLowerCase().trim()||"",o=[];t.filters.taxonomy&&"object"==typeof t.filters.taxonomy&&Object.entries(t.filters.taxonomy).forEach((([e,t])=>{const s=Array.isArray(t)?t:[t];o.push((t=>{if(!t.taxonomies||!t.taxonomies[e])return!1;const r=Object.keys(t.taxonomies[e]).map((e=>parseInt(e)));return s.some((e=>r.includes(parseInt(e))))}))}));for(const[e,s]of Object.entries(t.filters))if("taxonomy"!==e&&!t.ignoreFilters.has(e)&&null!=s&&""!==s&&"all"!==s)if("string"==typeof s&&s.includes(",")){const t=s.split(",").map((e=>e.trim()));o.push((s=>t.includes(String(s[e]))))}else o.push((t=>String(t[e])===String(s)));const n=i.filter((e=>{for(const t of o)if(!t(e))return!1;return!(a&&!this.searchObject(e,a))}));return this.applyOrdering(n,t)}applyOrdering(e,t){if(Array.isArray(e)||(e=Array.from(e)),0===e.length)return e;const s=t.filters.orderby||"date",r=(t.filters.order||"desc").toLowerCase();return["random","rand"].includes(s)||["random","rand"].includes(r)?this.shuffle(e):(e.sort(((e,t)=>{let i,a;switch(s){case"alphabetical":case"title":i=(e.title||e.name||"").toLowerCase(),a=(t.title||t.name||"").toLowerCase();break;case"modified":i=new Date(e.modified||e.date||0),a=new Date(t.modified||t.date||0);break;default:i=new Date(e.date||e.modified||0),a=new Date(t.date||t.modified||0)}return i<a?"asc"===r?-1:1:i>a?"asc"===r?1:-1:0})),e)}shuffle(e){const t=e.slice();for(let e=t.length-1;e>0;e--){const s=Math.floor(Math.random()*(e+1));[t[e],t[s]]=[t[s],t[e]]}return t}searchObject(e,t){if(!e||"object"!=typeof e)return"string"==typeof e&&e.toLowerCase().includes(t);for(const s of Object.values(e))if(null!=s)if("object"!=typeof s){if("string"==typeof s&&s.toLowerCase().includes(t))return!0}else if(this.searchObject(s,t))return!0;return!1}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t});const a=await this.shouldFetchWithFilters(e,t,i);if(r.config.endpoint&&a)await this.fetch(e);else{const t=this.getFiltered(e);this.notify(e,"data-loaded",{cached:!0,items:t})}}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if(!1===r.lastResponse.has_more){if(Object.entries(t).every((([e,t])=>(r.ignoreFilters.has(e),!0))))return!1}if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e]))||Object.keys(s.filters).some((e=>!(e in t)&&t!==s.config.filters)))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})(); |
| | | (()=>{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.25){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`${jvbBase.base}${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},ignore:[],required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.ignoreFilters=new Set(["search","page","per_page","orderby","order","context","source",...i.config.ignore]),i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),saveMany:t=>this.saveMany(e,t),delete:t=>this.delete(e,t),deleteMany:t=>this.deleteMany(e,t),get:t=>this.get(e,t),getMany:t=>this.getMany(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=()=>{const e=o.error||new Error("Transaction failed with unknown error");a(e)};try{h=r(c,o)}catch(e){a(e||new Error("Callback failed with unknown error"))}}))):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)){let t=r.items.map((t=>this.get(e,t)));return this.notify(e,"data-loaded",{cached:!0,items:t??[]}),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(!n.ok){const e=await n.text();throw new Error(`HTTP error! status: ${n.status}, message: ${e}`)}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){if(!("AbortError"===t?.name))throw console.error(`Fetch error for store "${e}":`,t.message),console.dir(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||[]).filter((e=>e&&"object"==typeof e)),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 l={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,l),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.put(l)}))}))}_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}async saveMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Map?Array.from(t.values()):Array.isArray(t)?t:Object.values(t);if(0===r.length)return[];const i=[];return r.forEach((t=>{const s=this._saveItem(e,t);i.push(s)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{i.forEach((t=>{e.put(t.processed)}))})),this.notify(e,"items-saved",{count:i.length,keys:i.map((e=>e.key))}),i.map((e=>e.key))}processForStorage(e,t=!0,s="root"){if(null===e)return{valid:!0,data:null};if(void 0===e)return t?{valid:!1,error:`Undefined value at ${s}`}:{valid:!0,data:void 0};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:void 0};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:void 0};if(e instanceof FormData)return{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0===e.data&&null!==a||(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:void 0}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}async deleteMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===r.length?[]:(r.forEach((e=>{s.data.delete(e)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{r.forEach((t=>{e.delete(t)}))})),this.notify(e,"items-deleted",{count:r.length,ids:r}),r)}get(e,t){return this.stores.get(e).data.get(t)}getMany(e,t,s=!0){const r=this.stores.get(e);if(!r)return[];const i=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===i.length?[]:s?i.reduce(((e,t)=>{const s=r.data.get(t);return s&&e.push(s),e}),[]):i.map((e=>r.data.get(e)??null))}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=>!(!e||"object"!=typeof e)&&Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r?.items){const e=r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]);return this.applyOrdering(e,t)}const i=Array.from(t.data.values()),a=t.filters.search?.toLowerCase().trim()||"",o=[];t.filters.taxonomy&&"object"==typeof t.filters.taxonomy&&Object.entries(t.filters.taxonomy).forEach((([e,t])=>{const s=Array.isArray(t)?t:[t];o.push((t=>{if(!t.taxonomies||!t.taxonomies[e])return!1;const r=Object.keys(t.taxonomies[e]).map((e=>parseInt(e)));return s.some((e=>r.includes(parseInt(e))))}))}));for(const[e,s]of Object.entries(t.filters))if("taxonomy"!==e&&!t.ignoreFilters.has(e)&&null!=s&&""!==s&&"all"!==s)if("string"==typeof s&&s.includes(",")){const t=s.split(",").map((e=>e.trim()));o.push((s=>t.includes(String(s[e]))))}else o.push((t=>String(t[e])===String(s)));const n=i.filter((e=>{for(const t of o)if(!t(e))return!1;return!(a&&!this.searchObject(e,a))}));return this.applyOrdering(n,t)}applyOrdering(e,t){if(Array.isArray(e)||(e=Array.from(e)),0===e.length)return e;const s=t.filters.orderby||"date",r=(t.filters.order||"desc").toLowerCase();return["random","rand"].includes(s)||["random","rand"].includes(r)?this.shuffle(e):(e.sort(((e,t)=>{let i,a;switch(s){case"alphabetical":case"title":i=(e.title||e.name||"").toLowerCase(),a=(t.title||t.name||"").toLowerCase();break;case"modified":i=new Date(e.modified||e.date||0),a=new Date(t.modified||t.date||0);break;default:i=new Date(e.date||e.modified||0),a=new Date(t.date||t.modified||0)}return i<a?"asc"===r?-1:1:i>a?"asc"===r?1:-1:0})),e)}shuffle(e){const t=e.slice();for(let e=t.length-1;e>0;e--){const s=Math.floor(Math.random()*(e+1));[t[e],t[s]]=[t[s],t[e]]}return t}searchObject(e,t){if(!e||"object"!=typeof e)return"string"==typeof e&&e.toLowerCase().includes(t);for(const s of Object.values(e))if(null!=s)if("object"!=typeof s){if("string"==typeof s&&s.toLowerCase().includes(t))return!0}else if(this.searchObject(s,t))return!0;return!1}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t});const a=await this.shouldFetchWithFilters(e,t,i);if(r.config.endpoint&&a)await this.fetch(e);else{const t=this.getFiltered(e);this.notify(e,"data-loaded",{cached:!0,items:t})}}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if(!1===r.lastResponse.has_more){if(Object.entries(t).every((([e,t])=>(r.ignoreFilters.has(e),!0))))return!1}if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e]))||Object.keys(s.filters).some((e=>!(e in t)&&t!==s.config.filters)))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})(); |
| | |
| | | (()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach((e=>{let t=e.id;""===t&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.hoverOnListener),e.addEventListener("mouseleave",this.hoverOffListener));let[s,n,i]=[e.querySelectorAll("nav .toggle"),e.querySelectorAll(".has-submenu"),e.querySelectorAll(".toggle:not(.main)")],a={nav:e,toggles:s,submenus:n,submenuToggles:i};this.navs.set(t,a),this.counter++}))}initListeners(){this.clickListener=this.handleClick.bind(this),this.escapeListener=this.handleEscape.bind(this),this.hoverOnListener=this.handleHoverOn.bind(this),this.hoverOffListener=this.handleHoverOff.bind(this),document.addEventListener("click",this.clickListener)}handleClick(e){if(0===this.navs.size)return;this.openNav&&null===e.target.closest(`#${this.openNav}`)&&this.toggleNav(!1,this.openNav);let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav");this.toggleNav(!e.classList.contains("open"),e.id)}let s=e.target.closest('[data-action="toggle-submenu"], .has-submenu .a');if(s){let e=s.closest("li");this.toggleSubmenu(!e.classList.contains("open"),e)}}handleHoverOn(e){let t=e.target.closest("nav");t&&this.toggleNav(!0,t.id);let s=e.target.closest(".has-submenu");s&&this.toggleSubmenu(!0,s)}handleHoverOff(e){let t=e.target.closest("nav");t&&this.toggleNav(!1,t.id)}handleEscape(e){this.openNav&&"Escape"===e.key&&this.toggleNav(!1,this.openNav)}toggleNav(e,t){let s=this.navs.get(t);s&&(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.escapeListener)):(this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.escapeListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach((e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}))),s.nav.ariaExpanded=e,s.nav.classList.toggle("open",e),s.ariaHidden=!e,e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus())}toggleSubmenu(e,t){let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];t.classList.toggle("open",e),t.ariaHidden=!e,s.ariaExpanded=e,e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbNav=new e}))})(); |
| | | (()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach((e=>{let t=e.id;""===t&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.hoverOnListener),e.addEventListener("mouseleave",this.hoverOffListener));let[s,n,i]=[e.querySelectorAll("nav .toggle"),e.querySelectorAll(".has-submenu"),e.querySelectorAll(".toggle:not(.main)")],a={nav:e,toggles:s,submenus:n,submenuToggles:i};this.navs.set(t,a),this.counter++}))}initListeners(){this.clickListener=this.handleClick.bind(this),this.escapeListener=this.handleEscape.bind(this),this.hoverOnListener=this.handleHoverOn.bind(this),this.hoverOffListener=this.handleHoverOff.bind(this),document.addEventListener("click",this.clickListener)}handleClick(e){if(0===this.navs.size)return;this.openNav&&null===e.target.closest(`#${this.openNav}`)&&(console.log("Closing nav",this.openNav),this.toggleNav(!1,this.openNav));let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav");this.toggleNav(!e.classList.contains("open"),e.id)}let s=e.target.closest('[data-action="toggle-submenu"], .has-submenu .a');if(s){let e=s.closest("li");this.toggleSubmenu(!e.classList.contains("open"),e)}}handleHoverOn(e){let t=e.target.closest("nav");t&&this.toggleNav(!0,t.id);let s=e.target.closest(".has-submenu");s&&this.toggleSubmenu(!0,s)}handleHoverOff(e){let t=e.target.closest("nav");t&&this.toggleNav(!1,t.id)}handleEscape(e){this.openNav&&"Escape"===e.key&&this.toggleNav(!1,this.openNav)}toggleNav(e,t){let s=this.navs.get(t);s&&(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.escapeListener)):(this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.escapeListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach((e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}))),s.nav.ariaExpanded=e,s.nav.classList.toggle("open",e),s.ariaHidden=!e,e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus())}toggleSubmenu(e,t){let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];t.classList.toggle("open",e),t.ariaHidden=!e,s.ariaExpanded=e,e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbNav=new e}))})(); |
| | |
| | | (()=>{class e extends window.UIHandler{constructor(){super(),this.navOpen=!1,this.toggleNav=this.toggleNav.bind(this),this.bindElements(),this.elements.nav&&(this.elements.toggle&&(this.elements.toggle.addEventListener("click",this.toggleNav),this.bindEvents()),this.setupSectionObserver())}bindElements(){const e=document.querySelector("nav.on-this-page");e&&(this.elements={nav:e,links:e.querySelectorAll("a"),sections:Array.from(e.querySelectorAll("a")).map((e=>{const t=e.getAttribute("href");return document.querySelector(t)})).filter(Boolean)},e.querySelector("button.toggle")&&(this.elements.toggle=e.querySelector("button.toggle")))}bindComponentEvents(){}toggleNav(e){e?.preventDefault(),e?.stopPropagation();const{nav:t,toggle:n}=this.elements;t&&n&&(this.navOpen=!this.navOpen,this.navOpen?(t.classList.add("open"),n.setAttribute("aria-label","Hide Index"),n.setAttribute("aria-expanded","true"),this.bindLinkHandlers()):(t.classList.remove("open"),n.setAttribute("aria-label","Show Index"),n.setAttribute("aria-expanded","false"),this.cleanupLinkHandlers()))}bindLinkHandlers(){const{links:e}=this.elements;e?.forEach((e=>{e._boundHandler=()=>{this.navOpen=!1,this.elements.nav.classList.remove("open"),this.elements.toggle.setAttribute("aria-label","Show Index"),this.elements.toggle.setAttribute("aria-expanded","false"),this.cleanupLinkHandlers()},e.addEventListener("click",e._boundHandler)}))}cleanupLinkHandlers(){const{links:e}=this.elements;e?.forEach((e=>{e._boundHandler&&(e.removeEventListener("click",e._boundHandler),delete e._boundHandler)}))}setupSectionObserver(){const{sections:e}=this.elements;e?.length&&this.initializeObserver("sections",e,{rootMargin:"-50% 0% -50% 0%",threshold:0},(e=>{e.forEach((e=>{if(!e.isIntersecting)return;const t=e.target.id,n=this.elements.nav?.querySelector(`a[href="#${t}"]`);n&&this.updateActiveClasses(n)}))}))}updateActiveClasses(e){const t=e.closest("li");if(!t)return;this.elements.nav.querySelectorAll("li").forEach((e=>{e.classList.remove("active","adj")})),t.classList.add("active"),t.previousElementSibling&&t.previousElementSibling.classList.add("adj"),t.nextElementSibling&&t.nextElementSibling.classList.add("adj")}isComponentActive(e){return"nav"===e?this.navOpen:super.isComponentActive(e)}handleOutsideClick(e){this.navOpen&&!this.elements.nav.contains(e.target)&&this.toggleNav(e)}handleEscapeKey(e){"Escape"===e.key&&this.navOpen&&(this.toggleNav(e),e.preventDefault())}cleanup(){this.cleanupLinkHandlers(),super.cleanup()}}document.addEventListener("DOMContentLoaded",(()=>{document.querySelector("nav.on-this-page")&&(window.onThisPage=new e)}))})(); |
| | | (()=>{class e{constructor(){this.initElements(),this.initListeners()}initElements(){this.selectors={nav:"nav.on-this-page",toggle:"button.toggle",icon:"button.toggle .icon"},this.ui=window.uiFromSelectors(this.selectors);let e=this.ui.nav.querySelectorAll("li a");this.ui.items={},this.ui.sections={},this.selectors.items=[];for(let t of e){let e=t.getAttribute("href");this.ui.items[e.replace("#","")]=t.closest("li"),this.selectors.items.push(e),this.ui.sections[e.replace("#","")]=document.querySelector(e)}this.selectors.items=this.selectors.items.join(",")}initListeners(){this.ui.toggle.addEventListener("click",(()=>{let e=this.ui.nav.classList().contains("open")?"icon-x-square":"icon-plus-square";console.log("Changing icon to: "+e),this.ui.icon.className="icon "+e})),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{if(e.isIntersecting){let t=Object.keys(this.ui.items).indexOf(e.target.id),s=0;for(let e of Object.values(this.ui.items))e.classList.toggle("active",t===s),s++}}))}),{rootMargin:"-50% 0px -50% 0px",threshold:0});for(let e of Object.values(this.ui.sections))console.log("Observing section: ",e),this.observer.observe(e)}}document.addEventListener("DOMContentLoaded",(()=>{document.querySelector("nav.on-this-page")&&(window.onThisPage=new e)}))})(); |
| | |
| | | (()=>{class e{constructor(){this.a11y=window.jvbA11y,this.queue=window.jvbQueue,this.error=window.jvbError,this.templates=window.jvbTemplates,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.changes=new Map,this.previewUrls=new Set,this.initElements(),this.initListeners(),this.defineTemplates()}defineTemplates(){const e=this.templates,t=this;e.define("uploadItem",{refs:{select:'[name="select-item"]',featured:'[name="featured"]',img:"img",video:"video",file:"label > span",details:"details",alt:'[name="image-alt-text"]',title:'[name="image-title"]',description:'[name="image-caption"]'},manyRefs:{inputs:"input, select, textarea"},setup({el:e,refs:s,manyRefs:i,data:r}){let a,o,l,d=!1;switch(Object.hasOwn(r,"file")?(e.dataset.uploadId=r.uploadId,a=t.getSubtypeFromMime(r.file.type)||"image",o="document"!==a&&t.createPreviewUrl(r.file),d=o,l=r.file.name||""):(e.dataset.id=r.id,a=t.getSubtypeFromURL(r.medium??r.src),o=r.medium??r.src,l=r["image-alt-text"]??""),e.dataset.subtype=a,s.featured&&(s.featured.value=r.uploadId),a){case"image":s.img&&(s.img.src=o,s.img.alt=l,d&&(s.img.dataset.previewUrl=d)),s.video&&s.video.remove(),s.file&&s.file.remove();break;case"video":s.video&&(s.video.src=o,s.video.alt=l,d&&(s.video.dataset.previewUrl=d)),s.img&&s.img.remove(),s.file&&s.file.remove();break;case"document":if(s.preview){let e=r.file.name.split(".").pop()?.toLowerCase()??"",t={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},i=window.getIcon(t[e]??"file");s.preview.innerText=r.file.name??r.title,s.preview.prepend(i)}s.img&&s.img.remove(),s.video&&s.video.remove()}if(s.details&&(Object.hasOwn(r,"field")&&Object.hasOwn(r.field,"config")&&Object.hasOwn(r.field.config,"showMeta")&&!r.field.config.showMeta?s.details.remove():(Object.hasOwn(r,"id")?s.details.dataset.attachmentId=r.id:Object.hasOwn(r,"uploadId")&&(s.details.dataset.uploadId=r.uploadId),s.details.setAttribute("data-ignore",""),"image"!==a&&s.alt?s.alt.closest(".field")?.remove():Object.hasOwn(r,"image-alt-text")&&s.alt&&(s.alt.value=r["image-alt-text"]),(Object.hasOwn(r,"title")||Object.hasOwn(r,"file"))&&s.title&&(s.title.value=r.title||r.file.name),Object.hasOwn(r,"image-caption")&&s.description&&(s.description.value=r["image-caption"]))),e.draggable="single"!==e.dataset.mode,i.inputs)for(let t of i.inputs){let s=t.closest("[data-field]")??e;window.prefixInput(t,`${r.id??r.uploadId}-`,s)}}}),e.define("imageGroup",{refs:{selectAll:"[data-select-all]",fields:".fields",details:"details",grid:".item-grid"},setup({el:t,refs:s,manyRefs:i,data:r}){if(t.dataset.groupId=r.groupId,s.selectAll){let e=s.selectAll.closest(".field");window.prefixInput(s.selectAll,`select-all-${r.groupId}`,e,!0)}let a=e.create("groupMetadata",{groupId:r.groupId});a?s.fields.append(a):s.details.remove(),s.grid&&(s.grid.dataset.groupId=r.groupId)}}),e.define("groupMetadata",{manyRefs:{inputs:"input,textarea,select"},setup({el:e,refs:t,manyRefs:s,data:i}){t.inputs&&t.inputs.forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${i.groupId}-`,t)}))}}),e.define("restoreNotification",{refs:{details:".details",wrap:".wrap"},setup({el:t,refs:s,manyRefs:i,data:r}){if(s.details){let e=r.bySource.size>1?` across ${r.bySource.size} pages`:"",t=r.pendingUploads.length>1?"uploads":"upload";s.details.textContent=`${r.pendingUploads.length} ${t} can be recovered${e}`}if(!s.wrap)return void console.warn("No wrap element in template");let a=1;for(const[t,i]of r.bySource){let r={index:a,isCurrent:t===window.location.href,src:t,uploads:i};s.wrap.append(e.create("restoreField",r)),a++}}}),e.define("restoreField",{refs:{h3:"h3",a:"h3 a",grid:".item-grid"},async setup({el:e,refs:s,manyRefs:i,data:r}){let a=t.registerField(e,!1,!1,`recovery_${r.index}`);r.isCurrent?(e.open=!0,s.a?.remove(),s.h3&&(s.h3.textContent="From this page:")):s.a&&(s.a.href=r.src,s.a.title="Navigate to page and restore",s.a.textContent=r.src);let o=[...new Set(r.uploads.map((e=>e.group??"preview")))];for(let e of o){let i="preview"===e||t.stores.groups.get(e);if(!i)continue;let o=await t.createGroupElement(e,a),l=o.querySelector(".item-grid"),d=r.uploads.filter((t=>t.group===("preview"===e)?null:e));for(const[e,t]of Object.entries(i.fields??{})){let s=o.querySelector(`input[name*="${e}"]`);s&&(s.value=t)}for(let e of d){let s=await t.createUpload(e.id,t.formatFile(e),a);l.append(s)}s.grid.append(o)}}})}initStores(){const{uploads:e,groups:t}=window.jvbStore.register("uploads",[{storeName:"uploads",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"status",keyPath:"status"},{name:"group",keyPath:"group"},{name:"src",keyPath:"src"}]},{storeName:"groups",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"src",keyPath:"src"}]}]);this.stores={uploads:e,groups:t,ready:[]},this.stores.uploads.subscribe(this.handleStores.bind(this,"uploads")),this.stores.groups.subscribe(this.handleStores.bind(this,"groups")),this.queue.subscribe(((e,t)=>{if(("operation-status"===e||"cancel-operation"===e)&&["image_upload","video_upload","document_upload"].includes(t.type)){let s=[];if(t.data)if(t.data instanceof FormData){const e=this.stores.uploads.formDataToObject(t.data);s=e.upload_ids||[]}else s=t.data.upload_ids||[];if(0===s.length&&t.result&&t.result.upload_ids&&(s=t.result.upload_ids),!s||0===s.length)return void console.warn("[UploadManager] No upload_ids found for operation:",{id:t.id,type:t.type,status:t.status,hasData:!!t.data,hasResult:!!t.result});if("cancel-operation"===e)return this.handleOperationCancelled(s);this.setBulkUpload(s,"status",t.status).then((()=>{console.log(`[UploadManager] Updated ${s.length} uploads to status: ${t.status}`)})),"completed"===t.status&&("process_upload_groups"===t.type?(s.forEach((e=>{this.setBulkUpload([e],"serverProcessed",!0).then((()=>{}))})),t.result&&t.result.created_posts&&console.log("[UploadManager] Created posts:",t.result.created_posts),setTimeout((()=>{s.forEach((e=>{this.removeUpload(e).then((()=>{}))}))}),2e3)):s.forEach((e=>{this.removeUpload(e).then((()=>{}))}))),"failed"!==t.status&&"failed_permanent"!==t.status||console.error("[UploadManager] Operation failed:",{id:t.id,type:t.type,uploadIds:s,error:t.error_message})}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery().then((()=>{})))}initWorker(){this.worker=null,this.workerState={worker:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:3e3,maxConcurrent:3,restartAfterTimeout:!0}}}initElements(){this.selectors={fields:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-wrapper",preview:".preview-wrap",grid:".item-grid.preview",progress:{progress:".file-upload-container .progress",fill:".file-upload-container .progress .fill",details:".file-upload-container .progress .details",icon:".file-upload-container .progress .icon"},selectAll:"[data-select-all]",actions:".selection-actions",count:".selected .info",hidden:'input[type="hidden"]'},groups:{container:".group-display",grid:".item-grid.groups",empty:".empty-group",header:".sidebar .header"},group:{item:".upload-group",actions:".selection-actions",selectAll:'[name="select-all-group"]',count:".group-header .info",fields:"details .fields",grid:".item-grid.group",total:".group-content .group-count"},items:{item:".item.upload",checkbox:'[name="select-item"]',featured:'[name="featured"]',image:"img",details:"details",progress:{progress:".progress",fill:".fill",details:".details",icon:".icon"}}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dropHandler=this.handleDrop.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}async setUpload(e,t){const s={...{id:e,attachment:null,group:null,field:null,src:window.location.href,blob:null,status:"local_processing",operationId:null,fields:{}},...t};return Object.preventExtensions(s),await this.stores.uploads.save(s),s}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls.delete(e))}formatFile(e){return e.blob?new File([e.blob],e.fields.originalName||"file",{type:e.fields.type||e.blob.type,lastModified:e.fields.lastModified||Date.now()}):null}handleClick(e){let t=window.targetCheck(e,this.selectors.fields.dropZone);t&&!e.target.matches("input, button, a")&&t.querySelector(this.selectors.fields.input)?.click();const s=window.targetCheck(e,"[data-action]");s&&this.handleAction(s)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(s).then((()=>{}));break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e).then((()=>{}));break;case"upload":this.queueUploads("uploads/groups",s).then((()=>{}));break;case"restore":this.handleRestoreSelected().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":this.handleClearCache().then((()=>{}))}}handleChange(e){let t=this.getFieldIdFromElement(e.target);if(t)if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);s.length>0&&this.processFiles(t,s).then((()=>{}))}else e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]')||("post_group"===this.fields.get(t).config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e));else{e.target.closest("[data-upload-id], [data-attachment-id]")&&this.queueUploadMeta(e)}}handleGroupMetaChange(e){const t=e.dataset.groupId;if(!t)return;const s=e.name;if(!s)return;const i=e.value,r=s.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");window.debouncer.schedule(`group-meta-${t}-${r}`,(async()=>{const e=this.stores.groups.get(t);e&&(e.fields||(e.fields={}),e.fields[r]=i,await this.setGroup(t,e))}),300)}handleDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.fields.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleDragLeave(e){const t=e.target.closest(this.selectors.fields.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.fields.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleDrop(e){const t=e.target.closest(this.selectors.fields.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover"),t.classList.add("uploading");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const i=this.getFieldIdFromElement(t);i&&(this.processFiles(i,s).then((()=>{this.updateHandlerItems(i)})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t,s=null){let i=new FormData;const r=this.fields.get(t);if(!r)return;let a=this.stores.uploads.filterByIndex({field:t});if(0===a.length)return;const[o,l]=["uploads"===e,"uploads/groups"===e];let d,n,u,p,c;i.append("fieldId",r.id),i.append("content",r.config.content),o&&(i.append("mode",r.config.mode),i.append("field_name",r.config.repeaterPath||r.config.name),i.append("fieldId",r.id),i.append("field_type",r.config.type),i.append("subtype",r.config.subtype),i.append("item_id",r.config.itemID),i.append("destination",r.config.destination),s&&i.append("depends_on",s)),l?({posts:d,uploadMap:n,files:u}=this.collectGroups(t)):o&&({uploadMap:n,files:u}=this.collectUploads(t)),l&&i.append("posts",JSON.stringify(d)),u.forEach((e=>{i.append("files[]",e)})),i.append("upload_ids",JSON.stringify(n)),o?(p=`Uploading ${a.length} file${a.length>1?"s":""} to server...`,c=`Uploading ${a.length} file${a.length>1?"s":""}...`):l&&(p=`Creating ${d.length} ${r.config.content}${d.length>1?"s":""} from uploads...`,c=`Creating ${d.length} post${d.length>1?"s":""}...`),await this.setBulkUpload(a,"status","queued");let h=this.sendToQueue(e,i,p,c);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1),this.notify("groups_uploaded",{fieldId:t,posts:d,content:r.config.content})}return h?(r.operationId=h,await this.setBulkUpload(a,"operationId",h),await this.setBulkUpload(a,"status","uploading"),await this.setBulkGroup(t,"operationId",h),this.fields.set(r.id,r),this.notify("sent-to-queue",{field:r,operation:h})):await this.setBulkUpload(a,"status","failed"),h}async sendToQueue(e,t,s="",i="",r=!1){""===i&&(i=s);const a={endpoint:e,method:"POST",data:t,title:s,popup:i,canMerge:r,sendNow:"uploads/groups"===e,headers:{"X-Action-Nonce":window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(a)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=[],i=[],r=[];const a=this.stores.groups.filterByIndex({field:e}).filter((e=>{const t=this.getGroupUploadsInOrder(e);return t.length>0&&t.some((e=>this.formatFile(e)))}));for(const e of a){const t=this.groups.get(e.id)?.element,a=this.collectGroupFieldsFromDOM(t,e.id),o={groupId:e.id,images:[],fields:a},l=this.getGroupUploadsInOrder(e);for(const t of l){const s=this.formatFile(t);if(s){r.push(s);const a={upload_id:t.id,index:i.length},l=this.uploads.get(t.id),d=l?.element?.querySelector(`input[name="${e.id}_featured"]`);d?.checked&&(o.fields.featured=t.id),o.images.push(a),i.push(t.id)}}o.images.length>0&&s.push(o)}const o=t.filter((e=>!e.group));for(const e of o){const t={groupId:window.generateID("group"),images:[],fields:{}},a=this.formatFile(e);if(a){r.push(a);const s={upload_id:e.id,index:i.length};t.images.push(s),i.push(e.id)}t.images.length>0&&s.push(t)}return{posts:s,uploadMap:i,files:r}}getGroupUploadsInOrder(e){return e.uploads&&0!==e.uploads.length?e.uploads.map((e=>this.stores.uploads.get(e))).filter(Boolean):[]}collectGroupFieldsFromDOM(e,t){if(!e)return{};const s={};return e.querySelectorAll("input, textarea, select").forEach((e=>{const i=e.name.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");["featured","select-all"].some((e=>i.includes(e)))||e.value&&(s[i]=e.value)})),s}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],i=[];for(const e of t){const t=this.formatFile(e);t&&(i.push(t),s.push(e.id))}return{uploadMap:s,files:i}}queueUploadMeta(e){let t=e.target.closest("[data-attachment-id]")?.dataset.attachmentId,s=!1;if(!t&&(t=e.target.closest("[data-upload-id]")?.dataset.uploadId,s=!0,!t))return;if(!this.changes.has(t)){let e={};s?e.uploadId=t:e.attachmentId=t,this.changes.set(t,e)}let i=e.target.closest("[data-field]").dataset.field;this.changes.get(t)[i]=e.target.value,this.scheduleSave()}scheduleSave(){window.debouncer.schedule("upload-meta",(async()=>{if(this.changes.size>0){let e={};for(let[t,s]of this.changes.entries())console.log(t,s),e[t]=s;let t={user:window.auth.getUser(),items:e};await this.sendToQueue("uploads/meta",t,"Uploading Meta","Uploading Meta",!0),this.changes.clear()}}),2e3)}scanFields(e,t=!0,s=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t,s)))}registerField(e,t=!0,s=!0,i=null){const r={element:e,id:i||this.determineFieldId(e),config:this.extractFieldConfig(e,t,s),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,s){const i={autoUpload:t,showMeta:s,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",repeaterPath:null},r=e.closest("[data-index]"),a=r?.closest("[data-field][data-repeater-id]");return a&&r&&(i.repeaterPath=`${a.dataset.field}:${r.dataset.index}:${i.name}`),i}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+"_";const i=e.dataset.field||"",r=e.closest("[data-index]"),a=r?.closest("[data-field][data-repeater-id]");return a&&r?`${t}${s}${a.dataset.field}_${r.dataset.index}_${i}`:`${t}${s}${i}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,i){const r=this.fields.get(e);r&&window.showProgress(r.ui.progress,t,s,i)}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,i=this.workerState.tasks.get(t);i&&(clearTimeout(i.timeoutId),i.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 i=[],r=[...e],a=this.workerState.settings.maxConcurrent,o=async()=>{for(;r.length>0;){const e=r.shift(),a=await this.processImage(e.file,t,s);i.push({uploadId:e.uploadId,blob:a})}};return await Promise.all(Array.from({length:Math.min(a,e.length)},(()=>o()))),i}async processImage(e,t=2200,s=2200,i=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),i)}catch(i){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:i,restart:r}=this.workerState;if(r.count>=r.max)throw new Error("Worker max restarts exceeded");const a=await createImageBitmap(e);let{width:o,height:l}=a;if(o>t||l>s){const e=Math.min(t/o,s/l);o=Math.round(o*e),l=Math.round(l*e)}const d=this.getWorker(),n=crypto.randomUUID();return new Promise(((t,s)=>{const r=setTimeout((()=>{this.workerState.tasks.delete(n),i.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),i.timeout);this.workerState.tasks.set(n,{resolve:t,reject:s,timeoutId:r}),d.postMessage({id:n,imageBitmap:a,width:o,height:l,type:e.type,quality:.9},[a])}))}resizeImage(e,t,s){return new Promise((i=>{const r=new Image;r.onload=()=>{URL.revokeObjectURL(r.src);let{width:a,height:o}=r;if(a>t||o>s){const e=Math.min(t/a,s/o);a=Math.round(a*e),o=Math.round(o*e)}const l=document.createElement("canvas");l.width=a,l.height=o,l.getContext("2d").drawImage(r,0,0,a,o),l.toBlob(i,e.type,.9)},r.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 i=t.length;let r=0;this.updateFieldProgress(e,0,i,"Processing files...");const a=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),i=await this.setUpload(s,{id:s,field:e,status:"local_processing",fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),r=await this.createUpload(s,t,e);return this.uploads.set(s,{element:r,ui:window.uiFromSelectors(this.selectors.items,r)}),await this.addToGroup(s,null),{uploadId:s,upload:i,file:t}}))),o=a.filter((e=>e.file.type.startsWith("image/"))),l=a.filter((e=>!e.file.type.startsWith("image/"))),d=await this.processImages(o.map((e=>({file:e.file,uploadId:e.uploadId}))));for(const{uploadId:t,blob:s}of d){const a=o.find((e=>e.uploadId===t));a&&(a.upload.blob=s,a.upload.fields.size=s.size,a.upload.status="queued",await this.setUpload(t,a.upload),r++,this.updateFieldProgress(e,r,i,"Processing files..."))}for(const{uploadId:t,upload:s,file:a}of l)s.blob=a,s.status="queued",await this.setUpload(t,s),r++,this.updateFieldProgress(e,r,i,"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"]}),t=Array.from(this.stores.groups.data.values());for(const e of t){this.stores.uploads.filterByIndex({group:e.id}).length>0||await this.stores.groups.delete(e.id)}if(0===e.length)return;const s=new Map;e.forEach((e=>{const t=e.src||"unknown";s.has(t)||s.set(t,[]),s.get(t).push(e)}));let i={bySource:s,pendingUploads:e};document.body.append(this.templates.create("restoreNotification",i));let r=document.querySelector("dialog.restore-uploads");this.restoreModal=new window.jvbModal(r),this.restoreSelection=new window.jvbHandleSelection(r,{wrapper:{wrapper:".restore-field",id:"selection"},items:".item-grid.restore",selectAll:{bulkControls:".selection-actions",checkbox:"#select-all-restore",count:".selection-count"}}),this.restoreModal.handleOpen()}async handleRestoreSelected(){if(!this.restoreSelection)return;let e=Array.from(this.restoreSelection.selectedItems);0!==e.length&&await this.restoreSelectedUploads(e)}async handleRestoreAll(){if(!this.restoreModal)return;const e=Array.from(this.restoreModal.modal.querySelectorAll(".item.upload")).map((e=>e.dataset.uploadId));await this.restoreSelectedUploads(e)}async restoreSelectedUploads(e){let t=window.location.href,s=Array.from(this.stores.uploads.data.values()).filter((s=>e.includes(s.id)&&s.src===t)),i=[...new Set(s.map((e=>e.group)))].filter(Boolean),r=s[0].field;if(!document.querySelector(`[data-uploader="${r}"]`))return void console.log("No field found for "+r);let a=this.fields.get(r);a.groupUI.container&&(a.groupUI.container.hidden=!1);let o=[];for(let e of i){let t=this.stores.groups.get(e);await this.createGroup(r,e);let i=this.groups.get(e),a=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=i.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of a){let s=await this.createUpload(t.id,this.formatFile(t),r);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),o.push(t.id)}}let l=s.filter((e=>!o.includes(e.id)));for(let e of l){let t=await this.createUpload(e.id,this.formatFile(e),r);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 i=this.fields.get(s);if(!i)return null;let r={uploadId:e,file:t,field:i};return this.templates.create("uploadItem",r)}getSubtypeFromURL(e){if(!e||""===e)return"";const t=e.split("?")[0].toLowerCase();return[".webp",".jpg",".jpeg",".png",".gif",".svg"].some((e=>t.endsWith(e)))?"image":[".mp4",".ogg",".mov",".webm",".avi"].some((e=>t.endsWith(e)))?"video":"document"}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,i=t.dataset.id;if((s||i)&&confirm("Remove this item?")){if(s)await this.removeUpload(s);else{const s=this.getFieldIdFromElement(e);t.remove(),s&&(this.updateHiddenInput(s),this.maybeLockUploads(s))}this.a11y.announce("Item removed")}}updateHiddenInput(e){const t=this.fields.get(e);if(!t?.ui.hidden)return;const s=Array.from(t.ui.grid?.querySelectorAll(this.selectors.items.item)||[]).map((e=>e.dataset.id||e.dataset.uploadId)).filter(Boolean).join(",");t.ui.hidden.value!==s&&(t.ui.hidden.value=s,t.ui.hidden.dispatchEvent(new Event("change",{bubbles:!0})))}async setBulkUpload(e,t,s){const i=Array.from(e).map((async e=>{if("string"==typeof e&&(e=await this.stores.uploads.get(e)),e)return"status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e)}));await Promise.all(i)}async setUploadStatus(e,t){"string"==typeof e&&(e=await this.stores.uploads.get(e)),e&&e.progress&&window.showProgress(e.progress,this.getStatusProgress(t),100,this.getStatusText(t),this.queue.icons[t]??"")}async removeUpload(e){let t=this.stores.uploads.get(e);if(!t)return;const s=t.field;if(t.group){let s=this.stores.groups.get(t.group);s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length?await this.removeGroup(s.id,!1):await this.stores.groups.save(s)}await this.clearUpload(e),this.updateHiddenInput(s),this.maybeLockUploads(s);let i=this.selectionHandlers.get(s);i&&i.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 i=this.createGroupElement(t,e);if(!i)return null;const r=s.groupUI.empty;r?.nextSibling?s.groupUI.grid.insertBefore(i,r.nextSibling):s.groupUI.grid.append(i);const a=i.querySelector(".item-grid");a&&(a.dataset.groupId=t,this.createSortable(e,a,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={groupId:e,fieldId:t},i=this.templates.create("imageGroup",s);return this.groups.set(e,{element:i,ui:window.uiFromSelectors(this.selectors.group,i)}),this.getSelectionHandler(t)?.addWrapper(i),i}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 i=this.stores.groups.filterByIndex({field:e});if(0===i.length)return;let r=i.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(r)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),i=this.uploads.get(e);if(!s||!i)return;const r=this.fields.get(s.field);if(!r)return;if(null!==i.element?.parentElement&&(!t&&null===s.group||t===s.group))return void this.handleReorder(s.field,t);if(s.group){const t=this.stores.groups.get(s.group);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.removeGroup(t.id,!1):await this.stores.groups.save(t))}i.ui.checkbox&&(i.ui.checkbox.checked=!1);const a=this.selectionHandlers.get(s.field);if(a&&a.isSelected(e)&&a.deselect(e),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),i.ui.featured&&(i.ui.featured.hidden=!t),t){i.ui.featured&&(i.ui.featured.name=`${t}_featured`);let r=this.stores.groups.get(t);r&&(r.uploads.push(e),s.group=t,await this.stores.groups.save(r))}else s.group=null;let o=t?this.groups.get(t)?.ui.grid:r.ui.grid;o&&(o.append(i.element),t&&await this.handleReorder(s.field,t)),await this.stores.uploads.save(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.group.item);if(!t)return;let s=t.dataset.groupId;if(!confirm("Delete this group? Items will be moved back to the upload area."))return;let i=this.stores.uploads.filterByIndex({group:s});Promise.all(i.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),i=this.stores.groups.get(e);if(!i)return;let r=!0;t&&i.uploads.length>0&&(r=window.confirm("Keep uploads in this group?")),await Promise.all(i.uploads.map((e=>r?this.addToGroup(e,null):this.removeUpload(e))));if(this.fields.get(i.field)){const t=this.getGroupKey(i.field,e),r=this.selectionHandlers.get(t);r?.destroy&&r.destroy(),this.selectionHandlers.get(i.field)?.removeWrapper(s.element);const a=this.sortables.get(t);a?.destroy&&a.destroy(),this.sortables.delete(t)}s?.element&&s.element.remove(),this.groups.delete(e),await this.stores.groups.delete(e),this.a11y.announce("Group removed")}maybeLockUploads(e){let t=this.fields.get(e);if(!t||!t.ui.dropZone)return;let s=this.stores.uploads.filterByIndex({field:e}).length,i=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=i}async handleOperationCancelled(e){0!==e.length&&e.forEach((e=>{this.removeUpload(e)}))}getGroupKey(e,t=null){return t?`${e}_${t}`:`${e}`}getSelectionHandler(e){let t=this.getGroupKey(e);if(!this.selectionHandlers.has(t)){let s=this.fields.get(e);if(!s)return;if("post_group"!==s.config.destination)return;let i=new window.jvbHandleSelection(s.element,{selectAll:{checkbox:this.selectors.fields.selectAll,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions},item:{item:this.selectors.items.item,checkbox:this.selectors.items.checkbox,idAttribute:"uploadId"},wrapper:{wrapper:".preview-wrap, .upload-group",id:"groupId"}});i.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems)})),this.selectionHandlers.set(t,i)}return this.selectionHandlers.get(t)}updateHandlerItems(e){let t=this.getSelectionHandler(e);t&&t.collectItems()}initSortable(e){if(!window.Sortable)return;const t=this.fields.get(e);t&&(!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0),this.createSortable(e,t.ui.grid,null),this.initEmptyGroupDropZone(e))}createSortable(e,t,s){if(!t)return null;const i=this.getGroupKey(e,s);if(this.sortables.has(i))return this.sortables.get(i);const r=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},dragClass:"dragging",ignore:".empty-group",onStart:t=>{const s=t.item,i=s?.dataset.uploadId,r=this.selected.get(e);if(i&&(!r||!r.has(i))){const t=this.selectionHandlers.get(e);t&&t.select(i)}},onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(i,r),r}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),e.stopPropagation(),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(),t.stopPropagation(),s.classList.remove("drag-over");const i=this.selected.get(e);if(!i||0===i.size)return;const r=await this.createGroup(e);r&&(await Promise.all(Array.from(i).map((e=>this.addToGroup(e,r)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,i=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===i.length)return;const r=s.dataset.groupId||null;for(const e of i)await this.addToGroup(e,r);await this.handleReorder(t,r),this.selectionHandlers.get(t)?.clearSelection()}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(s){if(t){let e=Array.from(s.children).filter((e=>e.matches(this.selectors.items.item)&&!e.classList.contains("ghost"))).map((e=>e.dataset.uploadId)).filter((e=>e)),i=this.stores.groups.get(t);i&&(i.uploads=e,this.stores.groups.save(i).then((()=>{})))}else this.updateHiddenInput(e);this.a11y.announce("Items reordered")}else console.log("Couldn't Reorder items...")}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),i=e.dataset.field,r=this.stores.uploads.filterByIndex({field:t});for(const e of r){const t=this.formatFile(e);t&&s.push({file:t,fieldName:i,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.templates=window.jvbTemplates,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.changes=new Map,this.previewUrls=new Set,this.initElements(),this.initListeners(),this.defineTemplates()}defineTemplates(){const e=this.templates,t=this;e.define("uploadItem",{refs:{select:'[name="select-item"]',featured:'[name="featured"]',img:"img",video:"video",file:"label > span",details:"details",alt:'[name="image-alt-text"]',title:'[name="image-title"]',description:'[name="image-caption"]'},manyRefs:{inputs:"input, select, textarea"},setup({el:e,refs:s,manyRefs:i,data:r}){let a,o,l,d=!1;switch(Object.hasOwn(r,"file")?(e.dataset.uploadId=r.uploadId,a=t.getSubtypeFromMime(r.file.type)||"image",o="document"!==a&&t.createPreviewUrl(r.file),d=o,l=r.file.name||""):(e.dataset.id=r.id,a=t.getSubtypeFromURL(r.medium??r.src),o=r.medium??r.src,l=r["image-alt-text"]??""),e.dataset.subtype=a,s.featured&&(s.featured.value=r.uploadId),a){case"image":s.img&&(s.img.src=o,s.img.alt=l,d&&(s.img.dataset.previewUrl=d)),s.video&&s.video.remove(),s.file&&s.file.remove();break;case"video":s.video&&(s.video.src=o,s.video.alt=l,d&&(s.video.dataset.previewUrl=d)),s.img&&s.img.remove(),s.file&&s.file.remove();break;case"document":if(s.preview){let e=r.file.name.split(".").pop()?.toLowerCase()??"",t={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},i=window.getIcon(t[e]??"file");s.preview.innerText=r.file.name??r.title,s.preview.prepend(i)}s.img&&s.img.remove(),s.video&&s.video.remove()}if(s.details&&(Object.hasOwn(r,"field")&&Object.hasOwn(r.field,"config")&&Object.hasOwn(r.field.config,"showMeta")&&!r.field.config.showMeta?s.details.remove():(Object.hasOwn(r,"id")?s.details.dataset.attachmentId=r.id:Object.hasOwn(r,"uploadId")&&(s.details.dataset.uploadId=r.uploadId),s.details.setAttribute("data-ignore",""),"image"!==a&&s.alt?s.alt.closest(".field")?.remove():Object.hasOwn(r,"image-alt-text")&&s.alt&&(s.alt.value=r["image-alt-text"]),(Object.hasOwn(r,"title")||Object.hasOwn(r,"file"))&&s.title&&(s.title.value=r.title||r.file.name),Object.hasOwn(r,"image-caption")&&s.description&&(s.description.value=r["image-caption"]))),e.draggable="single"!==e.dataset.mode,i.inputs)for(let t of i.inputs){let s=t.closest("[data-field]")??e;window.prefixInput(t,`${r.id??r.uploadId}-`,s)}}}),e.define("imageGroup",{refs:{selectAll:"[data-select-all]",fields:".fields",details:"details",grid:".item-grid"},setup({el:t,refs:s,manyRefs:i,data:r}){if(t.dataset.groupId=r.groupId,s.selectAll){let e=s.selectAll.closest(".field");window.prefixInput(s.selectAll,`select-all-${r.groupId}`,e,!0)}let a=e.create("groupMetadata",{groupId:r.groupId});a?s.fields.append(a):s.details.remove(),s.grid&&(s.grid.dataset.groupId=r.groupId)}}),e.define("groupMetadata",{manyRefs:{inputs:"input,textarea,select"},setup({el:e,refs:t,manyRefs:s,data:i}){t.inputs&&t.inputs.forEach((e=>{let t=e.closest("[data-field]");window.prefixInput(e,`${i.groupId}-`,t)}))}}),e.define("restoreNotification",{refs:{details:".details",wrap:".wrap"},setup({el:t,refs:s,manyRefs:i,data:r}){if(s.details){let e=r.bySource.size>1?` across ${r.bySource.size} pages`:"",t=r.pendingUploads.length>1?"uploads":"upload";s.details.textContent=`${r.pendingUploads.length} ${t} can be recovered${e}`}if(!s.wrap)return void console.warn("No wrap element in template");let a=1;for(const[t,i]of r.bySource){let r={index:a,isCurrent:t===window.location.href,src:t,uploads:i};s.wrap.append(e.create("restoreField",r)),a++}}}),e.define("restoreField",{refs:{h3:"h3",a:"h3 a",grid:".item-grid"},async setup({el:e,refs:s,manyRefs:i,data:r}){let a=t.registerField(e,!1,!1,`recovery_${r.index}`);r.isCurrent?(e.open=!0,s.a?.remove(),s.h3&&(s.h3.textContent="From this page:")):s.a&&(s.a.href=r.src,s.a.title="Navigate to page and restore",s.a.textContent=r.src);let o=[...new Set(r.uploads.map((e=>e.group??"preview")))];for(let e of o){let i="preview"===e||t.stores.groups.get(e);if(!i)continue;let o=await t.createGroupElement(e,a),l=o.querySelector(".item-grid"),d=r.uploads.filter((t=>t.group===("preview"===e)?null:e));for(const[e,t]of Object.entries(i.fields??{})){let s=o.querySelector(`input[name*="${e}"]`);s&&(s.value=t)}for(let e of d){let s=await t.createUpload(e.id,t.formatFile(e),a);l.append(s)}s.grid.append(o)}}})}initStores(){const{uploads:e,groups:t}=window.jvbStore.register("uploads",[{storeName:"uploads",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"status",keyPath:"status"},{name:"group",keyPath:"group"},{name:"src",keyPath:"src"}]},{storeName:"groups",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"src",keyPath:"src"}]}]);this.stores={uploads:e,groups:t,ready:[]},this.stores.uploads.subscribe(this.handleStores.bind(this,"uploads")),this.stores.groups.subscribe(this.handleStores.bind(this,"groups")),this.queue.subscribe(((e,t)=>{if(("operation-status"===e||"cancel-operation"===e)&&["image_upload","video_upload","document_upload"].includes(t.type)){let s=[];if(t.data)if(t.data instanceof FormData){const e=this.stores.uploads.formDataToObject(t.data);s=e.upload_ids||[]}else s=t.data.upload_ids||[];if(0===s.length&&t.result&&t.result.upload_ids&&(s=t.result.upload_ids),!s||0===s.length)return void console.warn("[UploadManager] No upload_ids found for operation:",{id:t.id,type:t.type,status:t.status,hasData:!!t.data,hasResult:!!t.result});if("cancel-operation"===e)return this.handleOperationCancelled(s);this.setBulkUpload(s,"status",t.status).then((()=>{console.log(`[UploadManager] Updated ${s.length} uploads to status: ${t.status}`)})),"completed"===t.status&&("process_upload_groups"===t.type?(s.forEach((e=>{this.setBulkUpload([e],"serverProcessed",!0).then((()=>{}))})),t.result&&t.result.created_posts&&console.log("[UploadManager] Created posts:",t.result.created_posts),setTimeout((()=>{s.forEach((e=>{this.removeUpload(e).then((()=>{}))}))}),2e3)):s.forEach((e=>{this.removeUpload(e).then((()=>{}))}))),"failed"!==t.status&&"failed_permanent"!==t.status||console.error("[UploadManager] Operation failed:",{id:t.id,type:t.type,uploadIds:s,error:t.error_message})}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery().then((()=>{})))}initWorker(){this.worker=null,this.workerState={worker:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:3e3,maxConcurrent:3,restartAfterTimeout:!0}}}initElements(){this.selectors={fields:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-wrapper",preview:".preview-wrap",grid:".item-grid.preview",progress:{progress:".file-upload-container .progress",fill:".file-upload-container .progress .fill",details:".file-upload-container .progress .details",icon:".file-upload-container .progress .icon"},selectAll:"[data-select-all]",actions:".selection-actions",count:".selected .info",hidden:'input[type="hidden"]'},groups:{container:".group-display",grid:".item-grid.groups",empty:".empty-group",header:".sidebar .header"},group:{item:".upload-group",actions:".selection-actions",selectAll:'[name="select-all-group"]',count:".group-header .info",fields:"details .fields",grid:".item-grid.group",total:".group-content .group-count"},items:{item:".item.upload",checkbox:'[name="select-item"]',featured:'[name="featured"]',image:"img",details:"details",progress:{progress:".progress",fill:".fill",details:".details",icon:".icon"}}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dropHandler=this.handleDrop.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}async setUpload(e,t){const s={...{id:e,attachment:null,group:null,field:null,src:window.location.href,blob:null,status:"local_processing",operationId:null,fields:{}},...t};return Object.preventExtensions(s),await this.stores.uploads.save(s),s}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls.delete(e))}formatFile(e){return e.blob?new File([e.blob],e.fields.originalName||"file",{type:e.fields.type||e.blob.type,lastModified:e.fields.lastModified||Date.now()}):null}handleClick(e){let t=window.targetCheck(e,this.selectors.fields.dropZone);t&&!e.target.matches("input, button, a")&&t.querySelector(this.selectors.fields.input)?.click();const s=window.targetCheck(e,"[data-action]");s&&this.handleAction(s)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(s).then((()=>{}));break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e).then((()=>{}));break;case"upload":this.queueUploads("uploads/groups",s).then((()=>{}));break;case"restore":this.handleRestoreSelected().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":this.handleClearCache().then((()=>{}))}}handleChange(e){let t=this.getFieldIdFromElement(e.target);if(t)if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);s.length>0&&this.processFiles(t,s).then((()=>{}))}else e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]')||("post_group"===this.fields.get(t).config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e));else{e.target.closest("[data-upload-id], [data-attachment-id]")&&this.queueUploadMeta(e)}}handleGroupMetaChange(e){const t=e.dataset.groupId;if(!t)return;const s=e.name;if(!s)return;const i=e.value,r=s.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");window.debouncer.schedule(`group-meta-${t}-${r}`,(async()=>{const e=this.stores.groups.get(t);e&&(e.fields||(e.fields={}),e.fields[r]=i,await this.setGroup(t,e))}),300)}handleDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.fields.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleDragLeave(e){const t=e.target.closest(this.selectors.fields.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.fields.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleDrop(e){const t=e.target.closest(this.selectors.fields.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover"),t.classList.add("uploading");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const i=this.getFieldIdFromElement(t);i&&(this.processFiles(i,s).then((()=>{this.updateHandlerItems(i)})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t,s=null){let i=new FormData;const r=this.fields.get(t);if(!r)return;let a=this.stores.uploads.filterByIndex({field:t});if(0===a.length)return;const[o,l]=["uploads"===e,"uploads/groups"===e];let d,n,u,p,c;i.append("fieldId",r.id),i.append("content",r.config.content),o&&(i.append("mode",r.config.mode),i.append("field_name",r.config.repeaterPath||r.config.name),i.append("fieldId",r.id),i.append("field_type",r.config.type),i.append("subtype",r.config.subtype),i.append("item_id",r.config.itemID),i.append("destination",r.config.destination),s&&i.append("depends_on",s)),l?({posts:d,uploadMap:n,files:u}=this.collectGroups(t)):o&&({uploadMap:n,files:u}=this.collectUploads(t)),l&&i.append("posts",JSON.stringify(d)),u.forEach((e=>{i.append("files[]",e)})),i.append("upload_ids",JSON.stringify(n)),o?(p=`Uploading ${a.length} file${a.length>1?"s":""} to server...`,c=`Uploading ${a.length} file${a.length>1?"s":""}...`):l&&(p=`Creating ${d.length} ${r.config.content}${d.length>1?"s":""} from uploads...`,c=`Creating ${d.length} post${d.length>1?"s":""}...`),await this.setBulkUpload(a,"status","queued");let h=this.sendToQueue(e,i,p,c);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1),this.notify("groups_uploaded",{fieldId:t,posts:d,content:r.config.content})}return h?(r.operationId=h,await this.setBulkUpload(a,"operationId",h),await this.setBulkUpload(a,"status","uploading"),await this.setBulkGroup(t,"operationId",h),this.fields.set(r.id,r),this.notify("sent-to-queue",{field:r,operation:h})):await this.setBulkUpload(a,"status","failed"),h}async sendToQueue(e,t,s="",i="",r=!1){""===i&&(i=s);const a={endpoint:e,method:"POST",data:t,title:s,popup:i,canMerge:r,sendNow:"uploads/groups"===e,headers:{"X-Action-Nonce":window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(a)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=[],i=[],r=[];const a=this.stores.groups.filterByIndex({field:e}).filter((e=>{const t=this.getGroupUploadsInOrder(e);return t.length>0&&t.some((e=>this.formatFile(e)))}));for(const e of a){const t=this.groups.get(e.id)?.element,a=this.collectGroupFieldsFromDOM(t,e.id),o={groupId:e.id,images:[],fields:a},l=this.getGroupUploadsInOrder(e);for(const t of l){const s=this.formatFile(t);if(s){r.push(s);const a={upload_id:t.id,index:i.length},l=this.uploads.get(t.id),d=l?.element?.querySelector(`input[name="${e.id}_featured"]`);d?.checked&&(o.fields.featured=t.id),o.images.push(a),i.push(t.id)}}o.images.length>0&&s.push(o)}const o=t.filter((e=>!e.group));for(const e of o){const t={groupId:window.generateID("group"),images:[],fields:{}},a=this.formatFile(e);if(a){r.push(a);const s={upload_id:e.id,index:i.length};t.images.push(s),i.push(e.id)}t.images.length>0&&s.push(t)}return{posts:s,uploadMap:i,files:r}}getGroupUploadsInOrder(e){return e.uploads&&0!==e.uploads.length?e.uploads.map((e=>this.stores.uploads.get(e))).filter(Boolean):[]}collectGroupFieldsFromDOM(e,t){if(!e)return{};const s={};return e.querySelectorAll("input, textarea, select").forEach((e=>{const i=e.name.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");["featured","select-all"].some((e=>i.includes(e)))||e.value&&(s[i]=e.value)})),s}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],i=[];for(const e of t){const t=this.formatFile(e);t&&(i.push(t),s.push(e.id))}return{uploadMap:s,files:i}}queueUploadMeta(e){let t=e.target.closest("[data-attachment-id]")?.dataset.attachmentId,s=!1;if(!t&&(t=e.target.closest("[data-upload-id]")?.dataset.uploadId,s=!0,!t))return;if(!this.changes.has(t)){let e={};s?e.uploadId=t:e.attachmentId=t,this.changes.set(t,e)}let i=e.target.closest("[data-field]").dataset.field;this.changes.get(t)[i]=e.target.value,this.scheduleSave()}scheduleSave(){window.debouncer.schedule("upload-meta",(async()=>{if(this.changes.size>0){let e={};for(let[t,s]of this.changes.entries())console.log(t,s),e[t]=s;let t={user:window.auth.getUser(),items:e};await this.sendToQueue("uploads/meta",t,"Uploading Meta","Uploading Meta",!0),this.changes.clear()}}),2e3)}scanFields(e,t=!0,s=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t,s)))}registerField(e,t=!0,s=!0,i=null){const r={element:e,id:i||this.determineFieldId(e),config:this.extractFieldConfig(e,t,s),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,s){const i={autoUpload:t,showMeta:s,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",repeaterPath:null},r=e.closest("[data-index]"),a=r?.closest("[data-field][data-repeater-id]");return a&&r&&(i.repeaterPath=`${a.dataset.field}:${r.dataset.index}:${i.name}`),i}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+"_";const i=e.dataset.field||"",r=e.closest("[data-index]"),a=r?.closest("[data-field][data-repeater-id]");return a&&r?`${t}${s}${a.dataset.field}_${r.dataset.index}_${i}`:`${t}${s}${i}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,i){const r=this.fields.get(e);r&&window.showProgress(r.ui.progress,t,s,i)}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,i=this.workerState.tasks.get(t);i&&(clearTimeout(i.timeoutId),i.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 i=[],r=[...e],a=this.workerState.settings.maxConcurrent,o=async()=>{for(;r.length>0;){const e=r.shift(),a=await this.processImage(e.file,t,s);i.push({uploadId:e.uploadId,blob:a})}};return await Promise.all(Array.from({length:Math.min(a,e.length)},(()=>o()))),i}async processImage(e,t=2200,s=2200,i=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),i)}catch(i){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:i,restart:r}=this.workerState;if(r.count>=r.max)throw new Error("Worker max restarts exceeded");const a=await createImageBitmap(e);let{width:o,height:l}=a;if(o>t||l>s){const e=Math.min(t/o,s/l);o=Math.round(o*e),l=Math.round(l*e)}const d=this.getWorker(),n=crypto.randomUUID();return new Promise(((t,s)=>{const r=setTimeout((()=>{this.workerState.tasks.delete(n),i.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),i.timeout);this.workerState.tasks.set(n,{resolve:t,reject:s,timeoutId:r}),d.postMessage({id:n,imageBitmap:a,width:o,height:l,type:e.type,quality:.9},[a])}))}resizeImage(e,t,s){return new Promise((i=>{const r=new Image;r.onload=()=>{URL.revokeObjectURL(r.src);let{width:a,height:o}=r;if(a>t||o>s){const e=Math.min(t/a,s/o);a=Math.round(a*e),o=Math.round(o*e)}const l=document.createElement("canvas");l.width=a,l.height=o,l.getContext("2d").drawImage(r,0,0,a,o),l.toBlob(i,e.type,.9)},r.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 i=t.length;let r=0;this.updateFieldProgress(e,0,i,"Processing files...");const a=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),i=await this.setUpload(s,{id:s,field:e,status:"local_processing",fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),r=await this.createUpload(s,t,e);return this.uploads.set(s,{element:r,ui:window.uiFromSelectors(this.selectors.items,r)}),await this.addToGroup(s,null),{uploadId:s,upload:i,file:t}}))),o=a.filter((e=>e.file.type.startsWith("image/"))),l=a.filter((e=>!e.file.type.startsWith("image/"))),d=await this.processImages(o.map((e=>({file:e.file,uploadId:e.uploadId}))));for(const{uploadId:t,blob:s}of d){const a=o.find((e=>e.uploadId===t));a&&(a.upload.blob=s,a.upload.fields.size=s.size,a.upload.status="queued",await this.setUpload(t,a.upload),r++,this.updateFieldProgress(e,r,i,"Processing files..."))}for(const{uploadId:t,upload:s,file:a}of l)s.blob=a,s.status="queued",await this.setUpload(t,s),r++,this.updateFieldProgress(e,r,i,"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"]}),t=Array.from(this.stores.groups.data.values());for(const e of t){this.stores.uploads.filterByIndex({group:e.id}).length>0||await this.stores.groups.delete(e.id)}if(0===e.length)return;const s=new Map;e.forEach((e=>{const t=e.src||"unknown";s.has(t)||s.set(t,[]),s.get(t).push(e)}));let i={bySource:s,pendingUploads:e};document.body.append(this.templates.create("restoreNotification",i));let r=document.querySelector("dialog.restore-uploads");this.restoreModal=new window.jvbModal(r),this.restoreSelection=new window.jvbHandleSelection(r,{wrapper:{wrapper:".restore-field",id:"selection"},items:".item-grid.restore",selectAll:{bulkControls:".selection-actions",checkbox:"#select-all-restore",count:".selection-count"}}),this.restoreModal.handleOpen()}async handleRestoreSelected(){if(!this.restoreSelection)return;let e=Array.from(this.restoreSelection.selectedItems);0!==e.length&&await this.restoreSelectedUploads(e)}async handleRestoreAll(){if(!this.restoreModal)return;const e=Array.from(this.restoreModal.modal.querySelectorAll(".item.upload")).map((e=>e.dataset.uploadId));await this.restoreSelectedUploads(e)}async restoreSelectedUploads(e){let t=window.location.href,s=Array.from(this.stores.uploads.data.values()).filter((s=>e.includes(s.id)&&s.src===t)),i=[...new Set(s.map((e=>e.group)))].filter(Boolean),r=s[0].field,a=document.querySelector(`[data-uploader="${r}"]`);if(!a){if(!("crudManager"in window)||!r.startsWith(window.crudManager.content))return void console.log("No field found for "+r);{let[e,t,s]=r.split("_");if(!(parseInt(t)>0))return void console.log("No field found for "+r);window.crudManager.openEditModal(t),a=document.querySelector(`[data-uploader="${r}"]`)}}let o=this.fields.get(r);o.groupUI.container&&(o.groupUI.container.hidden=!1);let l=[];for(let e of i){let t=this.stores.groups.get(e);await this.createGroup(r,e);let i=this.groups.get(e),a=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=i.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of a){let s=await this.createUpload(t.id,this.formatFile(t),r);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),l.push(t.id)}}let d=s.filter((e=>!l.includes(e.id)));for(let e of d){let t=await this.createUpload(e.id,this.formatFile(e),r);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 i=this.fields.get(s);if(!i)return null;let r={uploadId:e,file:t,field:i};return this.templates.create("uploadItem",r)}getSubtypeFromURL(e){if(!e||""===e)return"";const t=e.split("?")[0].toLowerCase();return[".webp",".jpg",".jpeg",".png",".gif",".svg"].some((e=>t.endsWith(e)))?"image":[".mp4",".ogg",".mov",".webm",".avi"].some((e=>t.endsWith(e)))?"video":"document"}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,i=t.dataset.id;if((s||i)&&confirm("Remove this item?")){if(s)await this.removeUpload(s);else{const s=this.getFieldIdFromElement(e);t.remove(),s&&(this.updateHiddenInput(s),this.maybeLockUploads(s))}this.a11y.announce("Item removed")}}updateHiddenInput(e){const t=this.fields.get(e);if(!t?.ui.hidden)return;const s=Array.from(t.ui.grid?.querySelectorAll(this.selectors.items.item)||[]).map((e=>e.dataset.id||e.dataset.uploadId)).filter(Boolean).join(",");t.ui.hidden.value!==s&&(t.ui.hidden.value=s,t.ui.hidden.dispatchEvent(new Event("change",{bubbles:!0})))}async setBulkUpload(e,t,s){const i=Array.from(e).map((async e=>{if("string"==typeof e&&(e=await this.stores.uploads.get(e)),e)return"status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e)}));await Promise.all(i)}async setUploadStatus(e,t){"string"==typeof e&&(e=await this.stores.uploads.get(e)),e&&e.progress&&window.showProgress(e.progress,this.getStatusProgress(t),100,this.getStatusText(t),this.queue.icons[t]??"")}async removeUpload(e){let t=this.stores.uploads.get(e);if(!t)return;const s=t.field;if(t.group){let s=this.stores.groups.get(t.group);s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length?await this.removeGroup(s.id,!1):await this.stores.groups.save(s)}await this.clearUpload(e),this.updateHiddenInput(s),this.maybeLockUploads(s);let i=this.selectionHandlers.get(s);i&&i.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 i=this.createGroupElement(t,e);if(!i)return null;const r=s.groupUI.empty;r?.nextSibling?s.groupUI.grid.insertBefore(i,r.nextSibling):s.groupUI.grid.append(i);const a=i.querySelector(".item-grid");a&&(a.dataset.groupId=t,this.createSortable(e,a,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={groupId:e,fieldId:t},i=this.templates.create("imageGroup",s);return this.groups.set(e,{element:i,ui:window.uiFromSelectors(this.selectors.group,i)}),this.getSelectionHandler(t)?.addWrapper(i),i}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 i=this.stores.groups.filterByIndex({field:e});if(0===i.length)return;let r=i.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(r)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),i=this.uploads.get(e);if(!s||!i)return;const r=this.fields.get(s.field);if(!r)return;if(null!==i.element?.parentElement&&(!t&&null===s.group||t===s.group))return void this.handleReorder(s.field,t);if(s.group){const t=this.stores.groups.get(s.group);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.removeGroup(t.id,!1):await this.stores.groups.save(t))}i.ui.checkbox&&(i.ui.checkbox.checked=!1);const a=this.selectionHandlers.get(s.field);if(a&&a.isSelected(e)&&a.deselect(e),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),i.ui.featured&&(i.ui.featured.hidden=!t),t){i.ui.featured&&(i.ui.featured.name=`${t}_featured`);let r=this.stores.groups.get(t);r&&(r.uploads.push(e),s.group=t,await this.stores.groups.save(r))}else s.group=null;let o=t?this.groups.get(t)?.ui.grid:r.ui.grid;o&&(o.append(i.element),t&&await this.handleReorder(s.field,t)),await this.stores.uploads.save(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.group.item);if(!t)return;let s=t.dataset.groupId;if(!confirm("Delete this group? Items will be moved back to the upload area."))return;let i=this.stores.uploads.filterByIndex({group:s});Promise.all(i.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),i=this.stores.groups.get(e);if(!i)return;let r=!0;t&&i.uploads.length>0&&(r=window.confirm("Keep uploads in this group?")),await Promise.all(i.uploads.map((e=>r?this.addToGroup(e,null):this.removeUpload(e))));if(this.fields.get(i.field)){const t=this.getGroupKey(i.field,e),r=this.selectionHandlers.get(t);r?.destroy&&r.destroy(),this.selectionHandlers.get(i.field)?.removeWrapper(s.element);const a=this.sortables.get(t);a?.destroy&&a.destroy(),this.sortables.delete(t)}s?.element&&s.element.remove(),this.groups.delete(e),await this.stores.groups.delete(e),this.a11y.announce("Group removed")}maybeLockUploads(e){let t=this.fields.get(e);if(!t||!t.ui.dropZone)return;let s=this.stores.uploads.filterByIndex({field:e}).length,i=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=i}async handleOperationCancelled(e){0!==e.length&&e.forEach((e=>{this.removeUpload(e)}))}getGroupKey(e,t=null){return t?`${e}_${t}`:`${e}`}getSelectionHandler(e){let t=this.getGroupKey(e);if(!this.selectionHandlers.has(t)){let s=this.fields.get(e);if(!s)return;if("post_group"!==s.config.destination)return;let i=new window.jvbHandleSelection(s.element,{selectAll:{checkbox:this.selectors.fields.selectAll,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions},item:{item:this.selectors.items.item,checkbox:this.selectors.items.checkbox,idAttribute:"uploadId"},wrapper:{wrapper:".preview-wrap, .upload-group",id:"groupId"}});i.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems)})),this.selectionHandlers.set(t,i)}return this.selectionHandlers.get(t)}updateHandlerItems(e){let t=this.getSelectionHandler(e);t&&t.collectItems()}initSortable(e){if(!window.Sortable)return;const t=this.fields.get(e);t&&(!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0),this.createSortable(e,t.ui.grid,null),this.initEmptyGroupDropZone(e))}createSortable(e,t,s){if(!t)return null;const i=this.getGroupKey(e,s);if(this.sortables.has(i))return this.sortables.get(i);const r=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},dragClass:"dragging",ignore:".empty-group",onStart:t=>{const s=t.item,i=s?.dataset.uploadId,r=this.selected.get(e);if(i&&(!r||!r.has(i))){const t=this.selectionHandlers.get(e);t&&t.select(i)}},onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(i,r),r}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),e.stopPropagation(),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(),t.stopPropagation(),s.classList.remove("drag-over");const i=this.selected.get(e);if(!i||0===i.size)return;const r=await this.createGroup(e);r&&(await Promise.all(Array.from(i).map((e=>this.addToGroup(e,r)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,i=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===i.length)return;const r=s.dataset.groupId||null;for(const e of i)await this.addToGroup(e,r);await this.handleReorder(t,r),this.selectionHandlers.get(t)?.clearSelection()}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(s){if(t){let e=Array.from(s.children).filter((e=>e.matches(this.selectors.items.item)&&!e.classList.contains("ghost"))).map((e=>e.dataset.uploadId)).filter((e=>e)),i=this.stores.groups.get(t);i&&(i.uploads=e,this.stores.groups.save(i).then((()=>{})))}else this.updateHiddenInput(e);this.a11y.announce("Items reordered")}else console.log("Couldn't Reorder items...")}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),i=e.dataset.field,r=this.stores.uploads.filterByIndex({field:t});for(const e of r){const t=this.formatFile(e);t&&s.push({file:t,fieldName:i,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(e,t=!0){t?e.style.animation="fadeIn var(--transition-base)":(e.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${e.dataset.id??e.id??e.className.replace(" ","-")}`,(()=>{e.remove()}),500))},window.formatTimeAgo=function(e,t="default"){const n=e instanceof Date?e:new Date(e),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"===t)return n.toLocaleDateString();const e={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 t.replace(/[YyFMmnjDlHishgAa]/g,(t=>e[t]))}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(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((e=>{const t=Array.from(e.classList);if(t.length>0){const n=e.content.cloneNode(!0).firstElementChild;t.forEach((e=>{window.templates.has(e)||window.templates.set(e,n)}))}}))},window.getTemplate=function(e){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(e)&&window.templates.get(e).cloneNode(!0)};window.jvbTemplates=new class{constructor(){this.templates=new Map,this.definitions=new Map}registerAll(e=document){e.querySelectorAll("template").forEach((e=>{e.classList.forEach((t=>{this.templates.has(t)||this.templates.set(t,e)}))}))}define(e,t={},n=null){this.definitions.set(e,{refs:t.refs||null,manyRefs:t.manyRefs||null,setup:t.setup||null,context:n})}create(e,t={}){const n=this.templates.get(e);if(!n)return console.warn(`[TemplateRegistry] Template "${e}" not found`),null;const i=n.content.cloneNode(!0).firstElementChild;if(!i)return null;const o=this.definitions.get(e),r=o?.refs?this.#e(i,o.refs):{},a=o?.manyRefs?this.#e(i,o.manyRefs,!1):{};return o?.setup?.({el:i,refs:r,manyRefs:a,data:t}),i}#e(e,t,n=!0){const i={};for(const[o,r]of Object.entries(t)){let t,a=!1;"string"==typeof r?t=r:(t=r.selector,a=!!r.required);const s=n?e.querySelector(t):e.querySelectorAll(t);a&&(n&&!s&&console.warn(`[TemplateRegistry] Required ref "${o}" not found: ${t}`),n||0!==s.length||console.warn(`[TemplateRegistry] Required manyRef "${o}" not found: ${t}`)),i[o]=n?s:Array.from(s)}return i}},document.addEventListener("DOMContentLoaded",(()=>{window.jvbTemplates.registerAll()})),window.icon=null,window.getIcon=function(e,t=""){if(void 0===e)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return t=""!==t&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${t.slice(0,2)}`:"",n.classList.add(`icon-${e}${t}`),n},window.formatNumber=function(e){return e.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(e,t="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:t}).format(e)},window.escapeHtml=function(e){return e?("string"==typeof e||e instanceof String||(e=String(e)),e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.removeChildren=function(e){if(0!==e.children.length)for(;e.firstChild;)e.removeChild(e.firstChild)},window.formatDateRange=function(e,t){const n=new Date(e),i=new Date(t);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(e,t=300){let n;return function(...i){n||(e.apply(this,i),n=!0,setTimeout((()=>n=!1),t))}},window.chunkIt=async function(e,t,n,i=10){const o=[];for(let t=0;t<e.length;t+=i)o.push(e.slice(t,t+i));for(const e of o){const i=document.createDocumentFragment();e.forEach((e=>{const n=t(e);n&&i.append(n)})),n(i),await new Promise((e=>requestAnimationFrame(e)))}},window.prefixInput=function(e,t,n=null,i=!1,o=!1){if(!e)return void console.warn("prefixInput called with null/undefined input");const r=e.id,a=i?t:`${t}${e.name}`;let s=null;s=n?n.querySelector(`label[for="${r}"]`):e.labels&&e.labels.length>0?e.labels[0]:"LABEL"===e.previousElementSibling?.tagName?e.previousElementSibling:"LABEL"===e.nextElementSibling?.tagName?e.nextElementSibling:e.closest("[data-field]")?.querySelector(`label[for="${r}"]`),s&&(s.htmlFor=a),e.id=a,o&&(e.name=a)},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.sanitizeHtml=function(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML},window.generateID=function(e="jvb"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(e,t,n,i="",o=""){const r=t<n;e.progress&&r&&window.fade(e.progress,!0);const a=n>0?t/n*100:0;e.fill&&(e.fill.style.width=`${a}%`),e.details&&(e.details.textContent=i),e.count&&(e.count.textContent=`${t}/${n}`),e.icon&&(e.icon.className=""===o?"icon":"icon icon-"+o),e.progress&&t===n&&window.fade(e.progress,!1)},window.formatDate=function(e){if(!e)return"";const t=new Date(e),n=new Date,i=Math.floor((n-t)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:t.toLocaleDateString()},window.getPluralContent=function(e){return"artwork"===e?"artwork":e+"s"},window.showToast=function(e,t="success",n={}){window.jvbNotifications.showToast(e,t,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(e){return e instanceof Date&&!isNaN(e)||(e=new Date(e)),window.dateFormatter.format(e)},window.typeText=function(e,t,n=50){return new Promise((i=>{e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval);let o=0;e.textContent="",e._typeInterval=setInterval((()=>{o<t.length?(e.textContent+=t.charAt(o),o++):(clearInterval(e._typeInterval),delete e._typeInterval,i())}),n)}))},window.eraseText=function(e,t=10){return new Promise((n=>{e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval);let i=e.textContent,o=i.length;e._eraseInterval=setInterval((()=>{o>0?(o--,e.textContent=i.substring(0,o)):(clearInterval(e._eraseInterval),delete e._eraseInterval,n())}),t)}))},window.typeLoop=function(e,t,n=50,i=10,o=1e3,r=250){const a=e.id||e.dataset.typeKey||`type-${Date.now()}`;e.dataset.typeKey||(e.dataset.typeKey=a),e._stopTyping&&e._stopTyping();let s=!0;const l=function(){s=!1,e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval),e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval)};return e._stopTyping=l,async function(){for(;s&&(await window.typeText(e,t,n),s)&&(await new Promise((e=>setTimeout(e,o))),s)&&(await window.eraseText(e,i),s);)await new Promise((e=>setTimeout(e,r)))}(),l},window.toCamelCase=function(e){return e.replace(/-([a-z])/g,(function(e){return e[1].toUpperCase()}))},window.targetCheck=function(e,t){return Array.isArray(t)&&(t=t.join(",")),"string"==typeof t&&(e.target.closest(t)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(e,t){if(this.isFunction(e)||this.isFunction(t))throw"Invalid argument. Function given, object expected.";if(this.isFile(e)||this.isFile(t)){const n=this.compareFiles(e,t);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===e?t:e}}if(this.isValue(e)||this.isValue(t)){const n=this.compareValues(e,t);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=t;break;case this.VALUE_DELETED:i=this.getEmptyValue(e);break;case this.VALUE_UPDATED:default:i=t}return{type:n,data:i}}let n={},i=!1;for(let o in e)if(!this.isFunction(e[o])){let r;t&&void 0!==t[o]&&(r=t[o]);const a=this.map(e[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(t)for(let o in t)if(!this.isFunction(t[o])&&(void 0===e||void 0===e[o])){const e=this.map(void 0,t[o]);null!==e&&(e.hasOwnProperty("type")&&e.hasOwnProperty("data")?n[o]=e.data:n[o]=e,i=!0)}return i?n:null},getEmptyValue:function(e){return this.isArray(e)?[]:this.isObject(e)?{}:"number"==typeof e?0:"boolean"!=typeof e&&""},compareValues:function(e,t){return e===t||this.isDate(e)&&this.isDate(t)&&e.getTime()===t.getTime()?this.VALUE_UNCHANGED:void 0===e?this.VALUE_CREATED:void 0===t?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)},isDate:function(e){return"[object Date]"===Object.prototype.toString.call(e)},isObject:function(e){return"[object Object]"===Object.prototype.toString.call(e)},isFile:function(e){return e instanceof File},isValue:function(e){return!this.isObject(e)&&!this.isArray(e)},compareFiles:function(e,t){return!this.isFile(e)&&this.isFile(t)?this.VALUE_CREATED:this.isFile(e)&&!this.isFile(t)?this.VALUE_DELETED:this.isFile(e)&&this.isFile(t)?e.name===t.name&&e.size===t.size&&e.type===t.type&&e.lastModified===t.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(e,t){if(null==e)return t;if(null==t)return e;if(this.isFunction(e)||this.isFunction(t))return t;if(this.isFile(e)||this.isFile(t))return t;if(this.isValue(e)||this.isValue(t)||this.isArray(e)||this.isArray(t))return t;if(this.isObject(e)&&this.isObject(t)){let n={};for(let t in e)this.isFunction(e[t])||(n[t]=e[t]);for(let i in t)this.isFunction(t[i])||(void 0!==e[i]?n[i]=this.merge(e[i],t[i]):n[i]=t[i]);return n}return t}},window.deepMerge=function(e,t){return window.getDifferences.merge(e,t)},window.isInt=function(e){return!isNaN(parseInt(e))&&isFinite(e)},window.isNumeric=function(e){return!isNaN(parseFloat(e))&&isFinite(e)},window.uiFromSelectors=function(e,t=null,n=!1){let i={};for(let[o,r]of Object.entries(e))i[o]="object"==typeof r?window.uiFromSelectors(r,t):t?n?t.querySelectorAll(r):t.querySelector(r):n?document.querySelectorAll(r):document.querySelector(r);return i};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(e,t,n=1e3){this.cancel(e),this.timeouts.set(e,setTimeout((()=>{t(),this.timeouts.delete(e)}),n))}cancel(e){this.timeouts.has(e)&&(clearTimeout(this.timeouts.get(e)),this.timeouts.delete(e))}cleanup(){for(let e of this.timeouts.values())clearTimeout(e);this.timeouts.clear()}};document.body;const e=document.documentElement,t=document.querySelector(".scroll-progress .bar");let n=window.scrollY||e.scrollTop||0,i=-1,o=!1,r=0;function a(){r=Math.max(0,e.scrollHeight-window.innerHeight)}function s(e){if(!t)return;const n=r>0?e/r:0,i=Math.max(0,Math.min(1,n));t.style.transform=`scaleX(${i})`}function l(){const t=window.scrollY||e.scrollTop||0;t>n?i=1:t<n&&(i=-1),n=t,document.body.classList.toggle("scroll-up",i<0&&t>0),s(t),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||e.scrollTop||0)}),20)})),a(),s(n),window.decodeHTMLEntities=function(e){return window.decodeHelper||(window.decodeHelper=document.createElement("textarea")),window.decodeHelper.innerHTML=e,window.decodeHelper.value}})(); |
| | | (()=>{window.fade=function(e,t=!0){t?e.style.animation="fadeIn var(--transition-base)":(e.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${e.dataset.id??e.id??e.className.replace(" ","-")}`,(()=>{e.remove()}),500))},window.formatTimeAgo=function(e,t="default"){const n=e instanceof Date?e:new Date(e),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"===t)return n.toLocaleDateString();const e={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 t.replace(/[YyFMmnjDlHishgAa]/g,(t=>e[t]))}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(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((e=>{const t=Array.from(e.classList);if(t.length>0){const n=e.content.cloneNode(!0).firstElementChild;t.forEach((e=>{window.templates.has(e)||window.templates.set(e,n)}))}}))},window.getTemplate=function(e){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(e)&&window.templates.get(e).cloneNode(!0)};window.jvbTemplates=new class{constructor(){this.templates=new Map,this.definitions=new Map}registerAll(e=document){e.querySelectorAll("template").forEach((e=>{e.classList.forEach((t=>{this.templates.has(t)||this.templates.set(t,e)}))}))}define(e,t={},n=null){this.definitions.set(e,{refs:t.refs||null,manyRefs:t.manyRefs||null,setup:t.setup||null,context:n})}create(e,t={}){const n=this.templates.get(e);if(!n)return console.warn(`[TemplateRegistry] Template "${e}" not found`),null;const i=n.content.cloneNode(!0).firstElementChild;if(!i)return null;const o=this.definitions.get(e),r=o?.refs?this.#e(i,o.refs):{},a=o?.manyRefs?this.#e(i,o.manyRefs,!1):{};return o?.setup?.({el:i,refs:r,manyRefs:a,data:t}),i}#e(e,t,n=!0){const i={};for(const[o,r]of Object.entries(t)){let t,a=!1;"string"==typeof r?t=r:(t=r.selector,a=!!r.required);const s=n?e.querySelector(t):e.querySelectorAll(t);a&&(n&&!s&&console.warn(`[TemplateRegistry] Required ref "${o}" not found: ${t}`),n||0!==s.length||console.warn(`[TemplateRegistry] Required manyRef "${o}" not found: ${t}`)),i[o]=n?s:Array.from(s)}return i}},document.addEventListener("DOMContentLoaded",(()=>{window.jvbTemplates.registerAll()})),window.icon=null,window.getIcon=function(e,t=""){if(void 0===e)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return t=""!==t&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${t.slice(0,2)}`:"",n.classList.add(`icon-${e}${t}`),n},window.formatNumber=function(e){return e.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(e,t="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:t}).format(e)},window.escapeHtml=function(e){return e?("string"==typeof e||e instanceof String||(e=String(e)),e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.removeChildren=function(e){if(0!==e.children.length)for(;e.firstChild;)e.removeChild(e.firstChild)},window.formatDateRange=function(e,t){const n=new Date(e),i=new Date(t);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(e,t=300){let n;return function(...i){n||(e.apply(this,i),n=!0,setTimeout((()=>n=!1),t))}},window.chunkIt=async function(e,t,n,i=10){const o=[];for(let t=0;t<e.length;t+=i)o.push(e.slice(t,t+i));for(const e of o){const i=document.createDocumentFragment();e.forEach((e=>{const n=t(e);n&&i.append(n)})),n(i),await new Promise((e=>requestAnimationFrame(e)))}},window.prefixInput=function(e,t,n=null,i=!1,o=!1){if(!e)return void console.warn("prefixInput called with null/undefined input");const r=e.id,a=i?t:`${t}${e.name}`;let s=null;s=n?n.querySelector(`label[for="${r}"]`):e.labels&&e.labels.length>0?e.labels[0]:"LABEL"===e.previousElementSibling?.tagName?e.previousElementSibling:"LABEL"===e.nextElementSibling?.tagName?e.nextElementSibling:e.closest("[data-field]")?.querySelector(`label[for="${r}"]`),s&&(s.htmlFor=a),e.id=a,o&&(e.name=a)},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.sanitizeHtml=function(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML},window.generateID=function(e="jvb"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(e,t,n,i="",o=""){const r=t<n;e.progress&&r&&window.fade(e.progress,!0);const a=n>0?t/n*100:0;e.fill&&(e.fill.style.width=`${a}%`),e.details&&(e.details.textContent=i),e.count&&(e.count.textContent=`${t}/${n}`),e.icon&&(e.icon.className=""===o?"icon":"icon icon-"+o),e.progress&&t===n&&window.fade(e.progress,!1)},window.formatDate=function(e){if(!e)return"";const t=new Date(e),n=new Date,i=Math.floor((n-t)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:t.toLocaleDateString()},window.getPluralContent=function(e){return"artwork"===e?"artwork":e+"s"},window.showToast=function(e,t="success",n={}){window.jvbNotifications.showToast(e,t,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(e){return e instanceof Date&&!isNaN(e)||(e=new Date(e)),window.dateFormatter.format(e)},window.typeText=function(e,t,n=50){return new Promise((i=>{e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval);let o=0;e.textContent="",e._typeInterval=setInterval((()=>{o<t.length?(e.textContent+=t.charAt(o),o++):(clearInterval(e._typeInterval),delete e._typeInterval,i())}),n)}))},window.eraseText=function(e,t=10){return new Promise((n=>{e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval);let i=e.textContent,o=i.length;e._eraseInterval=setInterval((()=>{o>0?(o--,e.textContent=i.substring(0,o)):(clearInterval(e._eraseInterval),delete e._eraseInterval,n())}),t)}))},window.typeLoop=function(e,t,n=50,i=10,o=1e3,r=250){const a=e.id||e.dataset.typeKey||`type-${Date.now()}`;e.dataset.typeKey||(e.dataset.typeKey=a),e._stopTyping&&e._stopTyping();let s=!0;const l=function(){s=!1,e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval),e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval)};return e._stopTyping=l,async function(){for(;s&&(await window.typeText(e,t,n),s)&&(await new Promise((e=>setTimeout(e,o))),s)&&(await window.eraseText(e,i),s);)await new Promise((e=>setTimeout(e,r)))}(),l},window.toCamelCase=function(e){return e.replace(/-([a-z])/g,(function(e){return e[1].toUpperCase()}))},window.targetCheck=function(e,t){return Array.isArray(t)&&(t=t.join(",")),"string"==typeof t&&(e.target.closest(t)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(e,t){if(this.isFunction(e)||this.isFunction(t))throw"Invalid argument. Function given, object expected.";if(this.isFile(e)||this.isFile(t)){const n=this.compareFiles(e,t);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===e?t:e}}if(this.isValue(e)||this.isValue(t)){const n=this.compareValues(e,t);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=t;break;case this.VALUE_DELETED:i=this.getEmptyValue(e);break;case this.VALUE_UPDATED:default:i=t}return{type:n,data:i}}let n={},i=!1;for(let o in e)if(!this.isFunction(e[o])){let r;t&&void 0!==t[o]&&(r=t[o]);const a=this.map(e[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(t)for(let o in t)if(!this.isFunction(t[o])&&(void 0===e||void 0===e[o])){const e=this.map(void 0,t[o]);null!==e&&(e.hasOwnProperty("type")&&e.hasOwnProperty("data")?n[o]=e.data:n[o]=e,i=!0)}return i?n:null},getEmptyValue:function(e){return this.isArray(e)?[]:this.isObject(e)?{}:"number"==typeof e?0:"boolean"!=typeof e&&""},compareValues:function(e,t){return e===t||this.isDate(e)&&this.isDate(t)&&e.getTime()===t.getTime()?this.VALUE_UNCHANGED:void 0===e?this.VALUE_CREATED:void 0===t?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)},isDate:function(e){return"[object Date]"===Object.prototype.toString.call(e)},isObject:function(e){return"[object Object]"===Object.prototype.toString.call(e)},isFile:function(e){return e instanceof File},isValue:function(e){return!this.isObject(e)&&!this.isArray(e)},compareFiles:function(e,t){return!this.isFile(e)&&this.isFile(t)?this.VALUE_CREATED:this.isFile(e)&&!this.isFile(t)?this.VALUE_DELETED:this.isFile(e)&&this.isFile(t)?e.name===t.name&&e.size===t.size&&e.type===t.type&&e.lastModified===t.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(e,t){if(null==e)return t;if(null==t)return e;if(this.isFunction(e)||this.isFunction(t))return t;if(this.isFile(e)||this.isFile(t))return t;if(this.isValue(e)||this.isValue(t)||this.isArray(e)||this.isArray(t))return t;if(this.isObject(e)&&this.isObject(t)){let n={};for(let t in e)this.isFunction(e[t])||(n[t]=e[t]);for(let i in t)this.isFunction(t[i])||(void 0!==e[i]?n[i]=this.merge(e[i],t[i]):n[i]=t[i]);return n}return t}},window.deepMerge=function(e,t){return window.getDifferences.merge(e,t)},window.isInt=function(e){return!isNaN(parseInt(e))&&isFinite(e)},window.isNumeric=function(e){return!isNaN(parseFloat(e))&&isFinite(e)},window.uiFromSelectors=function(e,t=null,n=!1){let i={};for(let[o,r]of Object.entries(e))i[o]="object"==typeof r?window.uiFromSelectors(r,t):t?n?t.querySelectorAll(r):t.querySelector(r):n?document.querySelectorAll(r):document.querySelector(r);return i},window.sleep=async function(e=50){return new Promise((t=>setTimeout(t,e)))};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(e,t,n=1e3){this.cancel(e),this.timeouts.set(e,setTimeout((()=>{t(),this.timeouts.delete(e)}),n))}cancel(e){this.timeouts.has(e)&&(clearTimeout(this.timeouts.get(e)),this.timeouts.delete(e))}cleanup(){for(let e of this.timeouts.values())clearTimeout(e);this.timeouts.clear()}};document.body;const e=document.documentElement,t=document.querySelector(".scroll-progress .bar");let n=window.scrollY||e.scrollTop||0,i=-1,o=!1,r=0;function a(){r=Math.max(0,e.scrollHeight-window.innerHeight)}function s(e){if(!t)return;const n=r>0?e/r:0,i=Math.max(0,Math.min(1,n));t.style.transform=`scaleX(${i})`}function l(){const t=window.scrollY||e.scrollTop||0;t>n?i=1:t<n&&(i=-1),n=t,document.body.classList.toggle("scroll-up",i<0&&t>0),s(t),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||e.scrollTop||0)}),20)})),a(),s(n),window.decodeHTMLEntities=function(e){return window.decodeHelper||(window.decodeHelper=document.createElement("textarea")),window.decodeHelper.innerHTML=e,window.decodeHelper.value}})(); |
| | |
| | | require(JVB_DIR.'/base/login.php'); |
| | | require(JVB_DIR.'/base/membership.php'); |
| | | require(JVB_DIR.'/base/options.php'); |
| | | require(JVB_DIR.'/base/seo.php'); |
| | | |
| | | $base = apply_filters('jvb_base', 'jvb_'); |
| | | $base = (str_ends_with($base, '_')) ? $base : $base.'_'; |
| | |
| | | <?php |
| | | namespace JVBase\base; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\config\seo\Resolver; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | class SchemaHelper |
| | | { |
| | | protected static array $allowedTypes; |
| | | protected static array $allowedFormats = ['schema', 'archive', 'meta', 'reference']; |
| | | protected static array $schemas = []; |
| | | protected static array $metas = []; |
| | | protected static array $archives = []; |
| | | protected static array $references = []; |
| | | public function __construct() |
| | | { |
| | | self::$allowedTypes = array_merge(['website', 'organization'], Registrar::getRegistered()); |
| | | } |
| | | public static function checkType(string $type, string $reference = '[SchemaHelper] Invalid type'):string|false |
| | | { |
| | | $type = strtolower($type); |
| | | if (!in_array($type, self::$allowedTypes)) { |
| | | error_log($reference.': '.$type); |
| | | return false; |
| | | } |
| | | return $type; |
| | | } |
| | | public static function checkFormat(string $format, string $reference = '[SchemaHelper] Invalid format'):string|false |
| | | { |
| | | $format = strtolower($format); |
| | | if (!in_array($format, self::$allowedFormats)) { |
| | | error_log($reference.': '.$format); |
| | | return false; |
| | | } |
| | | return $format; |
| | | } |
| | | public static function getConfig(string $type, string $format):array |
| | | { |
| | | $reference = '[SchemaHelper]::getConfig'; |
| | | $type = self::checkType($type, $reference); |
| | | $format = self::checkFormat($format, $reference); |
| | | if (!$type || !$format) { |
| | | return []; |
| | | } |
| | | return match($format) { |
| | | 'schema' => self::schema($type), |
| | | 'archive' => self::archive($type), |
| | | 'meta' => self::meta($type), |
| | | 'reference' => self::reference($type), |
| | | }; |
| | | } |
| | | public static function schema(string $type): array |
| | | { |
| | | $type = self::checkType($type, '[SchemaHelper]::schema'); |
| | | if (!$type) { |
| | | return []; |
| | | } |
| | | |
| | | if (!array_key_exists($type, self::$schemas)) { |
| | | self::$schemas[$type] = get_option(BASE.ucfirst($type).'Schema', self::getDefault($type, 'schema')); |
| | | } |
| | | return self::$schemas[$type]; |
| | | } |
| | | public static function meta(string $type): array |
| | | { |
| | | $type = self::checkType($type, '[SchemaHelper]::meta'); |
| | | if (!$type) { |
| | | return []; |
| | | } |
| | | if (!array_key_exists($type, self::$metas)) { |
| | | self::$metas[$type] = get_option(BASE.ucfirst($type).'Meta', self::getDefault($type, 'meta')); |
| | | } |
| | | return self::$metas[$type]; |
| | | } |
| | | public static function archive(string $type): array |
| | | { |
| | | $type = self::checkType($type, '[SchemaHelper]::archive'); |
| | | error_log('[SchemaHelper]::archive type: '.print_r($type, true)); |
| | | if (!$type) { |
| | | return []; |
| | | } |
| | | |
| | | if (!array_key_exists($type, self::$archives)) { |
| | | self::$archives[$type] = get_option(BASE.ucfirst($type).'Archive', self::getDefault($type, 'archive')); |
| | | } |
| | | return self::$archives[$type]; |
| | | } |
| | | public static function reference(string $type): array |
| | | { |
| | | $type = self::checkType($type, '[SchemaHelper]::reference'); |
| | | if (!$type) { |
| | | return []; |
| | | } |
| | | if (!array_key_exists($type, self::$references)) { |
| | | self::$references[$type] = get_option(BASE.ucfirst($type).'Reference', self::getDefault($type, 'reference')); |
| | | } |
| | | return self::$references[$type]; |
| | | } |
| | | |
| | | public static function getDefault(string $type, string $format):array |
| | | { |
| | | $reference = '[SchemaHelper]::getDefault'; |
| | | $type = self::checkType($type, $reference); |
| | | $format = self::checkFormat($format, $reference); |
| | | if (!$type || !$format) { |
| | | return []; |
| | | } |
| | | |
| | | $defaults = match ($format) { |
| | | 'schema' => match ($type) { |
| | | 'website' => [ |
| | | 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebSite', |
| | | 'name' => get_bloginfo('name'), |
| | | 'url' => get_home_url(), |
| | | 'id' => get_home_url() . '#website', |
| | | 'description' => get_bloginfo('description'), |
| | | 'inLanguage' => 'en-CA' |
| | | ], |
| | | default => [] |
| | | }, |
| | | 'archive' => [ |
| | | 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage', |
| | | ], |
| | | default => [], |
| | | }; |
| | | return apply_filters(BASE.ucfirst($type).ucfirst($format).'Default', $defaults); |
| | | } |
| | | |
| | | public static function updateHistory(string $type, string $format, array $newest):bool |
| | | { |
| | | $reference = '[SchemaHelper]::updateHistory'; |
| | | $type = self::checkType($type, $reference); |
| | | $format = self::checkFormat($format, $reference); |
| | | if (!$type || !$format) { |
| | | return false; |
| | | } |
| | | |
| | | $historyOption = BASE.ucfirst($type).ucfirst($format).'History'; |
| | | $history = get_option($historyOption, []); |
| | | array_unshift($history, $newest); |
| | | if (count($history) > 5) { |
| | | array_pop($history); |
| | | } |
| | | return update_option($historyOption, $history); |
| | | } |
| | | |
| | | public static function update(string $type, string $format, array $config, ?Meta $meta = null):bool |
| | | { |
| | | $reference = '[SchemaHelper]::update'; |
| | | $type = self::checkType($type, $reference); |
| | | $format = self::checkFormat($format, $reference); |
| | | if (!$type || !$format) { |
| | | return false; |
| | | } |
| | | $method = 'update'.ucfirst($type); |
| | | return self::$method($config, $meta); |
| | | } |
| | | public static function updateSchema(string $type, array $config):bool |
| | | { |
| | | $reference = '[SchemaHelper]::updateSchema'; |
| | | $type = self::checkType($type, $reference); |
| | | if (!$type) { |
| | | return false; |
| | | } |
| | | if (!class_exists($config['type'])){ |
| | | error_log('[SchemaHelper]::updateSchema Config must be a valid schema type: '.$config['type']); |
| | | return false; |
| | | } |
| | | if (!in_array($type, self::$allowedTypes)) { |
| | | error_log('[SchemaHelper]::updateSchema Config must have a schema type'); |
| | | } |
| | | |
| | | return self::updateClassConfig($type, 'schema', $config); |
| | | } |
| | | |
| | | public static function updateClassConfig(string $type, string $format, array $config):bool |
| | | { |
| | | $reference = '[SchemaHelper]::updateClassConfig'; |
| | | $type = self::checkType($type, $reference); |
| | | if (!$type) { |
| | | return false; |
| | | } |
| | | if (!class_exists($config['type'])){ |
| | | error_log($reference.' Config must be a valid schema type: '.$config['type']); |
| | | return false; |
| | | } |
| | | if (!in_array($type, self::$allowedTypes)) { |
| | | error_log($reference.' Config must have a schema type'); |
| | | } |
| | | //Merge stored config with updates |
| | | $stored = self::schema($type); |
| | | $update = array_merge_recursive($stored, $config); |
| | | |
| | | //Validate Properties |
| | | $className = $update['type']; |
| | | unset($update['type']); |
| | | foreach ($update as $property => $value) { |
| | | if (!property_exists($className, $property)) { |
| | | error_log($reference.' invalid property attempted: '.$property.', with value: '.print_r($value, true).' for class: '.$className); |
| | | unset($update[$property]); |
| | | } |
| | | } |
| | | $update['type'] = $className; |
| | | |
| | | //Add changes to history (keeps last 5 changes) |
| | | self::updateHistory($type, $format, $update); |
| | | self::$schemas[$type] = $update; |
| | | return update_option(BASE.ucfirst($type).ucfirst($format), $update); |
| | | } |
| | | |
| | | public static function updateMeta(string $type, array $config):bool |
| | | { |
| | | $type = self::checkType($type, '[SchemaHelper]::updateMeta'); |
| | | $allowed = array_filter($config, function($key) { |
| | | $allowed = in_array($key, ['name', 'description']); |
| | | if (!$allowed) { |
| | | error_log('[SchemaHelper]::updateMeta invalid property attempted: '.$key); |
| | | } |
| | | return $allowed; |
| | | }); |
| | | if (empty($allowed)) { |
| | | error_log('[SchemaHelper]::updateMeta Name or Description must be set'); |
| | | return false; |
| | | } |
| | | $config = array_map('sanitize_text_field', $config); |
| | | self::updateHistory($type, 'meta', $config); |
| | | self::$metas[$type] = $config; |
| | | return update_option(BASE.ucfirst($type).'Meta', $config); |
| | | } |
| | | |
| | | public static function updateArchive(string $type, array $config):bool |
| | | { |
| | | $reference = '[SchemaHelper]::updateArchive'; |
| | | $type = self::checkType($type, $reference); |
| | | if (!$type) { |
| | | return false; |
| | | } |
| | | if (!class_exists($config['type'])){ |
| | | error_log('[SchemaHelper]::updateSchema Config must be a valid schema type: '.$config['type']); |
| | | return false; |
| | | } |
| | | if (!in_array($type, self::$allowedTypes)) { |
| | | error_log('[SchemaHelper]::updateSchema Config must have a schema type'); |
| | | } |
| | | |
| | | return self::updateClassConfig($type, 'schema', $config); |
| | | } |
| | | |
| | | public static function classFromConfig(array $config, ?Meta $meta = null):mixed |
| | | { |
| | | if (!array_key_exists('type', $config)) { |
| | | error_log('[SchemaHelper]::classFromConfig No class defined in config: '.print_r($config, true)); |
| | | return false; |
| | | } |
| | | $className = $config['type']; |
| | | unset($config['type']); |
| | | $class = new $className(); |
| | | |
| | | foreach ($config as $property => $value) { |
| | | if (is_array($value)) { |
| | | $value = self::classFromConfig($value, $meta); |
| | | } |
| | | $method = 'set'.ucfirst($property); |
| | | if (!method_exists($class, $method)) { |
| | | error_log('[SchemaHelper]::classFromConfig - method: '.$method.' does not exist in class: '.$className); |
| | | continue; |
| | | } |
| | | if (is_string($value) && str_contains($value, '{{')) { |
| | | $value = Resolver::resolveForSchema($property, $value, $config, $meta); |
| | | } |
| | | if (!empty($value)) { |
| | | $class->$method($value); |
| | | } |
| | | } |
| | | return $class; |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | /** |
| | | * JVB_SCHEMA: Site-wide schema configuration |
| | | * |
| | |
| | | * - attribution: Developer/maintainer info |
| | | */ |
| | | |
| | | use JVBase\managers\SEO\SchemaBuilder; |
| | | |
| | | $schema = apply_filters('jvb_schema', []); |
| | | $registry = SchemaBuilder::getInstance(); |
| | | $checked = []; |
| | | foreach ($schema as $key => $config) { |
| | | |
| | | if (array_key_exists('type', $config)) { |
| | | $type = $config['type']; |
| | | } elseif ($key === 'website') { |
| | | $type = 'WebSite'; |
| | | } |
| | | $exists = !is_null($registry->getTypeDefinition($type)); |
| | | if (!$exists) { |
| | | // error_log('[JVB_SCHEMA] No definitions for: '.print_r($type, true)); |
| | | continue; |
| | | } |
| | | $allowed = $registry->getFieldsForType($type); |
| | | $filtered = array_filter($config, function ($item) use ($allowed) { |
| | | return in_array($item, $allowed); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | if (empty($filtered)) { |
| | | // error_log('[JVB_SCHEMA] No valid filters for '.$type.'.'); |
| | | continue; |
| | | } |
| | | $removed = array_filter($config, function ($item) use ($allowed) { |
| | | return !in_array($item, $allowed); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | if (!empty($removed)) { |
| | | // error_log('[JVB_SCHEMA] Invalid fields detected for '.$type.': '.print_r($removed, true)); |
| | | } |
| | | $checked[$key] = $filtered; |
| | | } |
| | | |
| | | define('JVB_SCHEMA', $checked); |
| | | |
| | | |
| | | /** |
| | | JVB_CONTENT['artwork'] = [ |
| | |
| | | 'sponsor', |
| | | 'containsInPlace', |
| | | 'containsPlace', |
| | | 'openingHours' |
| | | 'openingHours', |
| | | 'id', |
| | | 'ignore', |
| | | ]; |
| | | |
| | | protected array $hints = [ |
| | |
| | | protected function setChecks():void |
| | | { |
| | | $checks = [ |
| | | 'website' |
| | | 'website', |
| | | 'organization' |
| | | ]; |
| | | $this->checks = array_merge($checks, Registrar::getRegistered()); |
| | | } |
| | |
| | | } |
| | | public function renderFieldsFor(string $class, array $stored):void |
| | | { |
| | | $fields = $this->getFieldsFor($class); |
| | | $fields = $this->getFieldsForClass($class); |
| | | $instance = new $class(); |
| | | foreach ($fields as $property => $value) { |
| | | $this->renderProperty($property, $stored[$property]??null, $instance); |
| | | } |
| | | } |
| | | |
| | | public function getFieldsFor(string $class):array |
| | | public function getFieldsForClass(string $class):array |
| | | { |
| | | if (!class_exists($class)) { |
| | | error_log('Class not found: '.$class); |
| | |
| | | } |
| | | |
| | | |
| | | |
| | | $_POST['type'] = $type; |
| | | $result = $this->saveFields($action, $type, $_POST); |
| | | |
| | | |
| | |
| | | |
| | | public function saveFields(string $action, string $class, array $data):array |
| | | { |
| | | $action = strtolower($action); |
| | | if (!in_array($action, $this->checks)) { |
| | | error_log('[SEOAdmin]Action is not allowed: '.$action); |
| | | return [ |
| | |
| | | ]; |
| | | } |
| | | |
| | | $allowed = $this->getFieldsFor($class); |
| | | if (empty($allowed)) { |
| | | return [ |
| | | 'jvb_notice' => 'error', |
| | | 'jvb_message' => 'Could not get fields from class' |
| | | ]; |
| | | } |
| | | |
| | | $checked = array_filter($data, function ($item) use ($allowed) { |
| | | return array_key_exists($item, $allowed); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | $stored = get_option(BASE.ucfirst($action).'Schema', []); |
| | | $updates = []; |
| | | foreach ($checked as $property => $value) { |
| | | $sanitized = Sanitizer::sanitize($value, $this->buildConfig($property)); |
| | | if (!array_key_exists($property, $stored) || $stored[$property] !== $sanitized) |
| | | $updates[$property] = $sanitized; |
| | | } |
| | | if (!empty($updates)) { |
| | | $history = get_option(BASE.ucfirst($action).'SchemaHistory', []); |
| | | array_unshift($history, $stored); |
| | | if (count($history) > 5){ |
| | | array_pop($history); |
| | | } |
| | | update_option(BASE.ucfirst($action).'SchemaHistory', $history); |
| | | |
| | | $update = array_merge($stored, $updates); |
| | | update_option(BASE.ucfirst($action).'Schema', $update); |
| | | } |
| | | return [ |
| | | $success = JVB()->schemaHelper()->updateSchema($action, $data); |
| | | return $success ? [ |
| | | 'jvb_notice' => 'success', |
| | | 'jvb_message' => 'Saved changes successfully' |
| | | ] : [ |
| | | 'jvb_notice' => 'error', |
| | | 'jvb_message' => 'Something went wrong...' |
| | | ]; |
| | | } |
| | | } |
| | |
| | | { |
| | | $this->cache = Cache::for('blocks', WEEK_IN_SECONDS); |
| | | $this->cache->connect('post')->connect('taxonomy'); |
| | | add_filter('render_block', [$this, 'render'], 990, 3); |
| | | add_filter('render_block', [$this, 'render'], 900, 3); |
| | | |
| | | add_action('init', [$this, 'registerBlockStyles']); |
| | | } |
| | |
| | | 'main'; |
| | | |
| | | if ($content == '') { |
| | | global $post; |
| | | if(is_singular()) { |
| | | global $post; |
| | | |
| | | $block['innerBlocks'] = parse_blocks($post->post_content); |
| | | $result = $this->innerBlocks($block); |
| | | $block['innerBlocks'] = parse_blocks($post->post_content); |
| | | $result = $this->innerBlocks($block); |
| | | }else { |
| | | $result = ''; |
| | | } |
| | | } else { |
| | | $result = $this->inside($block, $tag, $content); |
| | | } |
| | |
| | | protected string $image; |
| | | protected string $header; |
| | | protected string $headerExtra; |
| | | protected bool $isOpen = false; |
| | | protected string $beforeSummary; |
| | | protected string $detailsTitle; |
| | | protected array $details; |
| | | |
| | |
| | | { |
| | | $this->cache = Cache::for('summary_block', WEEK_IN_SECONDS); |
| | | add_action('init', [ $this, 'registerBlock' ]); |
| | | add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']); |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | |
| | | ]); |
| | | } |
| | | |
| | | public function enqueueScripts():void |
| | | { |
| | | wp_enqueue_script('jvb-page-nav'); |
| | | } |
| | | |
| | | protected function getConfig():string |
| | | { |
| | | return (is_tax()) ? 'tax' : 'content'; |
| | |
| | | ); |
| | | $this->headerExtra = ($headerExtra === '') ? '' : '<div>'.$headerExtra.'</div>'; |
| | | |
| | | $this->beforeSummary = apply_filters( |
| | | 'jvbBeforeSummary', |
| | | '', |
| | | $this->getType() |
| | | ); |
| | | /** |
| | | * The HTML string that appears within the <summary> block |
| | | */ |
| | |
| | | $this->getType() |
| | | ); |
| | | |
| | | $this->isOpen = apply_filters( |
| | | 'jvbSummaryIsOpen', |
| | | false, |
| | | $this->getType() |
| | | ); |
| | | |
| | | /** |
| | | * The content of the <details> block. |
| | | * This should return either an empty array for no information (so no details block) |
| | |
| | | <?php endif; ?> |
| | | </header> |
| | | <?php |
| | | if (!empty($this->beforeSummary)) { |
| | | echo $this->beforeSummary; |
| | | } |
| | | } |
| | | |
| | | protected function renderDetails():void |
| | |
| | | if (empty($details)) { |
| | | return; |
| | | } |
| | | $open = $this->isOpen ? ' open' : ''; |
| | | ?> |
| | | <details class="info"> |
| | | <details class="info"<?=$open?>> |
| | | <summary class="row btw"><?= $this->detailsTitle ?></summary> |
| | | <?php |
| | | foreach ($this->details as $key => $details) { |
| | |
| | | if (empty($this->details)) { |
| | | return; |
| | | } |
| | | echo jvbOnThisPage(array_keys($this->details)); |
| | | $IDs = apply_filters('jvbSummaryOnThisPage', array_keys($this->details), $this->getType()); |
| | | echo jvbOnThisPage($IDs); |
| | | } |
| | | |
| | | protected function getType():string |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | function jvbRenderTermList(array|bool|WP_Error $terms, string $label = ''):string { |
| | | if (!$terms || is_wp_error($terms) || empty($terms)) { |
| | | return ''; |
| | |
| | | <?php |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | |
| | | return $out; |
| | | } |
| | | |
| | | function jvbMetaTermList(string $value, string $tax, bool $icon = true):string |
| | | { |
| | | if ($value === '') { |
| | | return ''; |
| | | } |
| | | $tax = jvbCheckBase($tax); |
| | | $terms = array_map('absint', explode(',', $value)); |
| | | $out = []; |
| | | foreach ($terms as $t) { |
| | | $term = get_term($t, $tax); |
| | | if ($term && !is_wp_error($term)) { |
| | | $url = get_term_link($t, $tax); |
| | | $out[] = '<li><a href="'.$url.'" title="View more in '.$term->name.'" rel="tag">'.$term->name.'</a></li>'; |
| | | } |
| | | } |
| | | |
| | | $registrar = Registrar::getInstance($tax); |
| | | $icon = ($icon && $registrar) ? $registrar->getIcon() : ''; |
| | | $icon = ($icon === '') ? '' : jvbIcon($icon); |
| | | $title = $registrar ? '<li class="title">'.$icon.$registrar->getSingular().'</li>' : ''; |
| | | return (!empty($out)) ? '<ul class="term-list '.jvbNoBase($tax).'">'.$title.implode('',$out).'</ul>' : ''; |
| | | } |
| | | |
| | | /** |
| | | * @param int $artistID |
| | | * @param string $taxonomy |
| | |
| | | |
| | | ?> |
| | | <nav id="<?=$id?>" class="on-this-page index"> |
| | | <label>Jump to: |
| | | <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false"> |
| | | <?= jvbIcon('plus-square') ?> |
| | | </button> |
| | | </label> |
| | | <button type="button" aria-label="Show Index" title="Show Index" class="toggle main" aria-expanded="false"> |
| | | <span>Jump To:</span><?= jvbIcon('plus-square') ?> |
| | | </button> |
| | | <ul> |
| | | <li> |
| | | <li id="back-to-top"> |
| | | <a href="#top" title="Back to Top"> |
| | | <?= jvbIcon('caret-circle-up') ?> |
| | | <?= jvbIcon('caret-circle-up') ?><span>Back to Top</span> |
| | | </a> |
| | | </li> |
| | | <?php |
| | |
| | | } else { |
| | | $result = $this->createPost($data); |
| | | $meta->set("_{$this->service_name}_item_id", $result['name']); |
| | | |
| | | $meta->save(); |
| | | } |
| | | |
| | | return [ |
| | |
| | | } else { |
| | | $result = $this->createPost($data); |
| | | $meta->set("_{$this->service_name}_item_id", $result['name']); |
| | | $meta->save(); |
| | | } |
| | | |
| | | return [ |
| | |
| | | } else { |
| | | $result = $this->createPost($data); |
| | | $meta->set("_{$this->service_name}_item_id", $result['name']); |
| | | $meta->save(); |
| | | } |
| | | |
| | | return [ |
| | |
| | | */ |
| | | protected function getVariationMapping(string $post_type): array |
| | | { |
| | | $registrar = Registrar::getInstance($post_type)); |
| | | $registrar = Registrar::getInstance($post_type); |
| | | if (!$registrar) { |
| | | return []; |
| | | } |
| | |
| | | */ |
| | | protected function getFieldMapping(string $post_type): array |
| | | { |
| | | $registrar = Registrar::getInstance($post_type)); |
| | | $registrar = Registrar::getInstance($post_type); |
| | | if (!$registrar) { |
| | | return []; |
| | | } |
| | |
| | | } |
| | | |
| | | $meta->setAll($updates); |
| | | $meta->save(); |
| | | |
| | | // Trigger notification to customer if order is ready |
| | | if ($state === 'PREPARED') { |
| | |
| | | |
| | | // Save all values at once |
| | | $meta->setAll($values_to_save); |
| | | $meta->save(); |
| | | } |
| | | |
| | | /** |
| | |
| | | 'created_at' => current_time('mysql'), |
| | | 'updated_at' => current_time('mysql') |
| | | ]); |
| | | $meta->save(); |
| | | |
| | | // Index by Square order ID for quick webhook lookups |
| | | update_option(BASE . 'square_order_map_' . $order_data['square_order_id'], $order_post_id); |
| | |
| | | */ |
| | | public function renderTrackingScript(): void |
| | | { |
| | | // Skip on local environments |
| | | if (JVB_TESTING) { |
| | | return; |
| | | } |
| | | if (!$this->isSetUp() || is_admin()) { |
| | | return; |
| | | } |
| | | |
| | | // Skip on local environments |
| | | if (strpos(get_home_url(), JVB_LOCAL) !== false) { |
| | | return; |
| | | } |
| | | |
| | | $script_url = $this->getTrackingScriptUrl(); |
| | | $website_id = $this->getWebsiteId(); |
| | |
| | | $this->skeleton->setCalendar(); |
| | | } |
| | | |
| | | $this->skeleton->setDefaultStatus(); |
| | | if ($this->registrar && $this->registrar->getType() === 'post') { |
| | | $this->skeleton->setDefaultStatus(); |
| | | } else { |
| | | $this->skeleton->setStatuses([]); |
| | | } |
| | | |
| | | |
| | | // Views |
| | | $this->skeleton |
| | |
| | | * Initialize taxonomies from WordPress config |
| | | */ |
| | | protected function initTaxonomies(): void { |
| | | $this->taxonomies = $this->registrar->registrar->taxonomies; |
| | | $this->taxonomies = ($this->registrar->getType() === 'post') ? $this->registrar->registrar->taxonomies : []; |
| | | } |
| | | |
| | | /** |
| | |
| | | ****************************************************/ |
| | | public static function onTermChange(int $termId, int $ttId, string $taxonomy): void |
| | | { |
| | | // error_log('[Clearing cache for term change: '.$termId.']'); |
| | | error_log('[Clearing cache for term change: '.$termId.']'); |
| | | self::invalidateItem('taxonomy', $termId); |
| | | } |
| | | |
| | |
| | | |
| | | //Content |
| | | //content types |
| | | //Taxonomies |
| | | $availableContent = array_filter($pages, function($page, $key) { |
| | | return !is_numeric($key) && array_key_exists($key, Registrar::getRegistered('post')); |
| | | $all = array_merge( |
| | | Registrar::getRegistered('post'), |
| | | Registrar::getFeatured('is_content', 'term') |
| | | ); |
| | | $availableContent = array_filter($pages, function($page, $key) use($all) { |
| | | return !is_numeric($key) && in_array($key, $all) && JVB()->roles()->checkRole($this->user, $key); |
| | | }, ARRAY_FILTER_USE_BOTH); |
| | | if (!empty ($availableContent)){ |
| | | $content = $menu->addItem('Your Content', jvbDashIcon('book-bookmark')) |
| | |
| | | $item = $content->addItem($page, $registrar->getIcon()) |
| | | ->url($this->baseURL.'/'.$slug); |
| | | |
| | | $taxonomies = $registrar->registrar->taxonomies; |
| | | if (!empty ($taxonomies)) { |
| | | //TODO: If we add a dedicated 'create item' page, remove this from the empty check |
| | | $itemMenu = $item->submenu($slug); |
| | | foreach ($taxonomies as $s) { |
| | | $taxRegistrar = Registrar::getInstance($s); |
| | | $itemMenu->addItem($taxRegistrar->getPlural(), $taxRegistrar->getIcon()) |
| | | ->url($this->baseURL.'/'.$s); |
| | | if ($registrar->getType() === 'post') { |
| | | $taxonomies = $registrar->registrar->taxonomies; |
| | | if (!empty ($taxonomies)) { |
| | | //TODO: If we add a dedicated 'create item' page, remove this from the empty check |
| | | $itemMenu = $item->submenu($slug); |
| | | foreach ($taxonomies as $s) { |
| | | $taxRegistrar = Registrar::getInstance($s); |
| | | $itemMenu->addItem($taxRegistrar->getPlural(), $taxRegistrar->getIcon()) |
| | | ->url($this->baseURL.'/'.$s); |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | } |
| | | } |
| | | |
| | | //Taxonomies |
| | | |
| | | //Settings |
| | | $settings = $menu->addItem('Settings', jvbDashIcon('faders')) |
| | | ->submenu('settings') |
| | |
| | | if (!form || !window.jvbForm) return; |
| | | |
| | | window.jvbForm.registerForm(form, { |
| | | autosave: false, |
| | | endpoint: '<?= $action ?>', |
| | | formStatus: false, |
| | | showStatus: false, |
| | | cache: false, |
| | | }); |
| | | |
| | |
| | | use JVBase\managers\SEO\render\Thing\Intangible\OfferCatalog; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\Service; |
| | | use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\config\seo\Resolver; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | |
| | | public function buildSchema():array |
| | | { |
| | | $schema = []; |
| | | if (!is_front_page()) { |
| | | $schema[] = $this->buildBasicWebsiteSchema(); |
| | | |
| | | $schema[] = $this->buildWebsiteSchema(false); |
| | | if (is_front_page()) { |
| | | $test = $this->buildOrganizationSchema(); |
| | | if (!empty($test)) { |
| | | $schema[] = $test; |
| | | } |
| | | } |
| | | if (is_singular($this->types)) { |
| | | $type = get_post_type(); |
| | |
| | | } |
| | | |
| | | if (!empty($schema)) { |
| | | $website = get_option(BASE.'WebsiteSchema'); |
| | | $website = JVB()->schemaHelper()::schema('website'); |
| | | if (!empty($website)) { |
| | | if (JVB_TESTING) { |
| | | Cache::for('websiteSchema')->flush(); |
| | |
| | | |
| | | protected function websiteSchema():array |
| | | { |
| | | $stored = get_option(BASE.'WebsiteSchema', apply_filters(BASE.'WebsiteSchema', [])); |
| | | $stored = JVB()->schemaHelper()::schema('website'); |
| | | $website = JVB()->schemaHelper()::classFromConfig($stored); |
| | | |
| | | $seo = new WebSite(); |
| | | foreach ($stored as $property => $value) { |
| | | $method = 'set'.ucfirst($property); |
| | | $seo->$method($value); |
| | | if (!$website->getUrl() || empty($website->getUrl())){ |
| | | $website->setUrl(get_home_url()); |
| | | } |
| | | $seo->setUrl(get_home_url()); |
| | | $seo->setName(get_bloginfo('name')); |
| | | |
| | | $seo->setCreator($this->getCreator()); |
| | | return $seo->outputSchema(); |
| | | if (!$website->getName() || empty($website->getName())){ |
| | | $website->setName(get_bloginfo('name')); |
| | | } |
| | | if(!$website->getCreator() || empty($website->getCreator())) { |
| | | $website->setCreator($this->getCreator()); |
| | | } |
| | | return $website->outputSchema(); |
| | | } |
| | | |
| | | public function outputSchema():void |
| | |
| | | return BreadcrumbManager::getInstance()->toSchema(); |
| | | } |
| | | |
| | | public function buildBasicWebsiteSchema():array |
| | | public function buildWebsiteSchema(bool $full = true):array |
| | | { |
| | | if (JVB_TESTING){ |
| | | Cache::for('websiteSchema')->flush(); |
| | | } |
| | | $storedWebsite = JVB()->schemaHelper()::schema('website'); |
| | | |
| | | if (!$full) { |
| | | return Cache::for('websiteSchema')->remember( |
| | | 'reference', |
| | | function () use ($storedWebsite) { |
| | | $allowed = ['type','name', 'url', 'description', 'inLanguage']; |
| | | $storedWebsite = array_filter($storedWebsite, function ($key) use ($allowed) { |
| | | return in_array($key, $allowed); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | $website = JVB()->schemaHelper()::classFromConfig($storedWebsite); |
| | | if (!$website->getName() || empty($website->getName())) { |
| | | $website->setName(get_bloginfo('name')); |
| | | } |
| | | if (!$website->getUrl() || empty($website->getUrl())) { |
| | | $website->setUrl(get_home_url()); |
| | | } |
| | | if (!$website->getId() || empty($website->getId())) { |
| | | $website->setId(get_home_url().'/#website'); |
| | | } |
| | | if (!$website->getDescription() || empty($website->getDescription())) { |
| | | $website->setDescription(get_bloginfo('description')); |
| | | } |
| | | if (!$website->getInLanguage() || empty($website->getInLanguage())) { |
| | | $website->setInLanguage('en-CA'); |
| | | } |
| | | if (!$website->getPublisher() || empty($website->getPublisher())) { |
| | | $publisher = $this->getOptionSchemaReference('organization'); |
| | | if ($publisher){ |
| | | $website->setPublisher($publisher); |
| | | } |
| | | } |
| | | |
| | | $website->setCreator($this->getCreator(true)); |
| | | |
| | | return $website->outputSchema(); |
| | | } |
| | | ); |
| | | } |
| | | return Cache::for('websiteSchema')->remember( |
| | | 'reference', |
| | | function () { |
| | | $website = new WebSite(); |
| | | $website->setName(get_bloginfo('name')); |
| | | $website->setUrl(get_home_url()); |
| | | $website->setId(get_home_url().'/#website'); |
| | | $website->setDescription(get_bloginfo('description')); |
| | | $website->setInLanguage('en-CA'); |
| | | $publisher = $this->getOptionSchemaReference('organization'); |
| | | if ($publisher){ |
| | | $website->setPublisher($publisher); |
| | | 'full', |
| | | function () use ($storedWebsite) { |
| | | error_log('StoredWebsite: '.print_r($storedWebsite, true)); |
| | | $website = JVB()->schemaHelper()::classFromConfig($storedWebsite); |
| | | if (!$website->getName() || empty($website->getName())) { |
| | | $website->setName(get_bloginfo('name')); |
| | | } |
| | | if (!$website->getUrl() || empty($website->getUrl())) { |
| | | $website->setUrl(get_home_url()); |
| | | } |
| | | if (!$website->getId() || empty($website->getId())) { |
| | | $website->setId(get_home_url().'/#website'); |
| | | } |
| | | if (!$website->getDescription() || empty($website->getDescription())) { |
| | | $website->setDescription(get_bloginfo('description')); |
| | | } |
| | | if (!$website->getInLanguage() || empty($website->getInLanguage())) { |
| | | $website->setInLanguage('en-CA'); |
| | | } |
| | | if (!$website->getPublisher() || empty($website->getPublisher())) { |
| | | $publisher = $this->getOptionSchemaReference('organization'); |
| | | if ($publisher){ |
| | | $website->setPublisher($publisher); |
| | | } |
| | | } |
| | | |
| | | $website->setCreator($this->getCreator(true)); |
| | |
| | | return null; |
| | | } |
| | | $action = BASE.ucfirst($option).'Schema'; |
| | | $stored = get_option($action, apply_filters($action, [])); |
| | | // $stored = get_option($action, apply_filters($action, jvbDefaultSchema($option))); |
| | | $stored = JVB()->schemaHelper()::reference($action); |
| | | |
| | | if (empty($stored)){ |
| | | error_log('Attempted to get schema reference for: '.$option.', but defaults not set.'); |
| | | return null; |
| | |
| | | $value = array_key_exists($property, $stored) ? $stored[$property] : null; |
| | | if (!$value) {continue;} |
| | | |
| | | $class->$method(Resolver::resolve($property, $value)); |
| | | if (str_contains($value, '{{')) { |
| | | $value = Resolver::resolve($property, $value); |
| | | } |
| | | $class->$method($value); |
| | | } |
| | | return $class; |
| | | } |
| | | |
| | | public function buildOrganizationSchema():array |
| | | { |
| | | $config = JVB()->schemaHelper()::schema('organization'); |
| | | $class = JVB()->schemaHelper()::classFromConfig($config); |
| | | return ($class)? $class->outputSchema() : []; |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | public function getId():string { |
| | | return $this->id; |
| | | return $this->id??false; |
| | | } |
| | | public function setId(string $id):void |
| | | { |
| | |
| | | if (!in_array($operation->type, self::HANDLED_TYPES)) { |
| | | throw new Exception("ContentExecutor cannot handle type: {$operation->type}"); |
| | | } |
| | | error_log('Executing ContentExecutor.php'); |
| | | |
| | | try { |
| | | $data = $operation->requestData; |
| | |
| | | ); |
| | | } |
| | | |
| | | $results = []; |
| | | $results = [ |
| | | 'errors' => [], |
| | | 'success' => [], |
| | | 'newPosts' => [], |
| | | 'timelineParents' => [], |
| | | 'timelineStatus' => [], |
| | | 'timelineSharedFields' => [], |
| | | ]; |
| | | $errors = []; |
| | | $success = []; |
| | | $timelineParents = []; |
| | | $timelineStatus = []; |
| | | $timelineSharedFields = []; |
| | | $newPostsMap = []; |
| | | |
| | | foreach ($posts as $id => $postData) { |
| | | try { |
| | | $content = $postData['content'] ?? ''; |
| | | |
| | | // New post creation |
| | | if (str_starts_with((string)$id, 'new')) { |
| | | $newId = wp_insert_post([ |
| | | 'post_author' => $this->userId, |
| | | 'post_type' => jvbCheckBase($content), |
| | | 'post_title' => $postData['post_title'] ?? '', |
| | | 'post_status' => $postData['status'] ?? 'draft', |
| | | ]); |
| | | |
| | | if (!$newId || is_wp_error($newId)) { |
| | | $errors[$id] = 'Could not create post'; |
| | | $progress->failItem($id, 'Could not create post'); |
| | | continue; |
| | | } |
| | | |
| | | $newPostsMap[$id] = $newId; |
| | | $this->savePostFields($newId, $postData); |
| | | $success[$newId] = array_keys($postData); |
| | | $progress->advance(); |
| | | continue; |
| | | } |
| | | |
| | | // Existing post update |
| | | if (!$this->verifyOwnership((int)$id)) { |
| | | $progress->failItem($id, 'No permission to modify this post'); |
| | | $errors[$id] = 'No permission'; |
| | | continue; |
| | | } |
| | | |
| | | $this->savePostFields((int)$id, $postData); |
| | | $content = $postData['content'] ?? false; |
| | | if (!$content) continue; |
| | | $registrar = Registrar::getInstance($content); |
| | | if ($registrar && $registrar->hasFeature('is_timeline')) { |
| | | $post = get_post((int)$id); |
| | | $parentId = $post->post_parent > 0 ? $post->post_parent : $post->ID; |
| | | $fields = $registrar->getFields(); |
| | | $sharedFields = array_keys(array_filter($fields, function ($field) { |
| | | return !array_key_exists('for_all', $field) || !$field['for_all']; |
| | | })); |
| | | |
| | | if (array_key_exists('post_date', $postData) && !in_array($parentId, $timelineParents)) { |
| | | $timelineParents[] = $parentId; |
| | | } |
| | | if ($parentId === $id) { |
| | | if (array_key_exists('post_status', $postData) && !array_key_exists($parentId, $timelineStatus)) { |
| | | $timelineStatus[$parentId] = $postData['post_status']; |
| | | } |
| | | |
| | | if (count(array_intersect($sharedFields, array_keys($postData))) > 0) { |
| | | if (!array_key_exists($parentId, $timelineSharedFields)) { |
| | | $timelineSharedFields[$parentId] = []; |
| | | } |
| | | $temp = array_intersect($sharedFields, array_keys($postData)); |
| | | $timelineSharedFields[$parentId] = array_unique(array_merge($timelineSharedFields[$parentId], $temp)); |
| | | } |
| | | } |
| | | switch ($registrar->getType()) { |
| | | case 'post': |
| | | $results = $this->handlePost($id, $postData, $registrar, $results, $progress); |
| | | break; |
| | | case 'term': |
| | | $results = $this->handleTerm($id, $postData, $registrar, $results, $progress); |
| | | break; |
| | | case 'user': |
| | | $results = $this->handleUser($id, $postData, $registrar, $results, $progress); |
| | | break; |
| | | } |
| | | |
| | | $success[$id] = array_keys($postData); |
| | | $progress->advance(); |
| | | } catch (Exception $e) { |
| | | $progress->failItem($id, $e->getMessage()); |
| | | $errors[$id] = $e->getMessage(); |
| | | $results['errors'][$id] = $e->getMessage(); |
| | | } |
| | | } |
| | | error_log('Final Results: '.print_r($results, true)); |
| | | |
| | | try { |
| | | if (!empty($timelineSharedFields)) { |
| | | $this->checkSharedFields($timelineSharedFields); |
| | | if (!empty($results['timelineSharedFields'])) { |
| | | $this->checkSharedFields($results['timelineSharedFields']); |
| | | } |
| | | if (!empty($timelineStatus)) { |
| | | $this->handleTimelineStatusChange($timelineStatus); |
| | | if (!empty($results['timelineStatus'])) { |
| | | $this->handleTimelineStatusChange($results['timelineStatus']); |
| | | } |
| | | if (!empty($timelineParents)) { |
| | | $this->maybeReorderTimelines($timelineParents); |
| | | if (!empty($results['timelineParents'])) { |
| | | $this->maybeReorderTimelines($results['timelineParents']); |
| | | } |
| | | } catch (Exception $e) { |
| | | $errors[] = $e->getMessage(); |
| | | $results['errors'][] = $e->getMessage(); |
| | | } |
| | | |
| | | |
| | |
| | | |
| | | return new Result( |
| | | outcome: $outcome, |
| | | result: [ |
| | | 'posts' => $success, |
| | | 'errors' => $errors, |
| | | 'new_posts' => $newPostsMap, |
| | | 'updated_count' => count($success), |
| | | 'failed_count' => count($errors) |
| | | ] |
| | | result: $results, |
| | | ); |
| | | } |
| | | |
| | |
| | | return array_key_exists($key, $fields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | //Remove values that are already saved |
| | | $check = Meta::forPost($postId)->getAll(array_keys($allowedFields)); |
| | | error_log('Stored values: '.print_r($check, true)); |
| | | $allowedFields = array_filter($allowedFields, function ($key) use ($allowedFields, $check) { |
| | | return $allowedFields[$key] !== $check[$key]; |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | if (empty($allowedFields)) { |
| | | return true; |
| | | } |
| | | |
| | | return Meta::forPost($postId) |
| | | ->setAll($allowedFields) |
| | | ->save(); |
| | | ->setAll($allowedFields); |
| | | } |
| | | private function saveTermFields(int $termId, array $data): bool |
| | | { |
| | | $content = $data['content'] ?? ''; |
| | | error_log('Saving term fields: '.print_r($data, true)); |
| | | $fields = Registrar::getFieldsFor($content); |
| | | |
| | | $allowedFields = array_filter($data, function ($key) use ($fields) { |
| | | return array_key_exists($key, $fields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | |
| | | //Remove values that are already saved |
| | | $check = Meta::forTerm($termId)->getAll(array_keys($allowedFields)); |
| | | error_log('Stored values: '.print_r($check, true)); |
| | | $allowedFields = array_filter($allowedFields, function ($value, $key) use ($check) { |
| | | error_log('Sent value: '.print_r($value, true)); |
| | | error_log('Stored Value: '.print_r($check[$key], true)); |
| | | return $value !== $check[$key]; |
| | | }, ARRAY_FILTER_USE_BOTH); |
| | | |
| | | if (empty($allowedFields)) { |
| | | return true; |
| | | } |
| | | |
| | | error_log('Allowed fields: '.print_r($allowedFields, true)); |
| | | |
| | | return Meta::forTerm($termId) |
| | | ->setAll($allowedFields); |
| | | } |
| | | private function saveUserFields(int $userId, array $data): bool |
| | | { |
| | | $content = $data['content'] ?? ''; |
| | | $fields = Registrar::getFieldsFor($content); |
| | | |
| | | $allowedFields = array_filter($data, function ($key) use ($fields) { |
| | | return array_key_exists($key, $fields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | //Remove values that are already saved |
| | | $check = Meta::forUser($userId)->getAll(array_keys($allowedFields)); |
| | | $allowedFields = array_filter($allowedFields, function ($key) use ($allowedFields, $check) { |
| | | return $allowedFields[$key] !== $check[$key]; |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | if (empty($allowedFields)) { |
| | | return true; |
| | | } |
| | | |
| | | return Meta::forUser($userId) |
| | | ->setAll($allowedFields); |
| | | } |
| | | |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Helpers |
| | |
| | | if ($lastKey === $index) { |
| | | $latestTimestamp = strtotime($post->post_date); |
| | | } |
| | | $meta->save(); |
| | | $previousPost = $post; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | foreach ($children as $child) { |
| | | Meta::forPost($child)->setAll($values)->save(false); |
| | | Meta::forPost($child)->setAll($values); |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | protected function handlePost(string|int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array |
| | | { |
| | | // New post creation |
| | | if (str_starts_with((string)$ID, 'new')) { |
| | | |
| | | $newId = wp_insert_post([ |
| | | 'post_author' => $this->userId, |
| | | 'post_type' => $registrar->getBased(), |
| | | 'post_title' => $data['post_title'] ?? apply_filters('jvbDefaultTitle', '', $registrar->getSlug()), |
| | | 'post_status' => $data['status'] ?? 'draft', |
| | | ]); |
| | | |
| | | if (!$newId || is_wp_error($newId)) { |
| | | $results['errors'][$ID] = 'Could not create post'; |
| | | $progress->failItem($ID, 'Could not create post'); |
| | | return $results; |
| | | } |
| | | |
| | | $results['newPosts'][$ID] = $newId; |
| | | $this->savePostFields($newId, $data); |
| | | unset($data['content']); |
| | | $results['success'][$newId] = $data; |
| | | $progress->advance(); |
| | | return $results; |
| | | } |
| | | |
| | | //Existing post update |
| | | if (!$this->verifyOwnership((int)$ID)) { |
| | | $progress->failItem($ID, 'No permission to modify this post'); |
| | | $results['errors'][$ID] = 'No permission'; |
| | | return $results; |
| | | } |
| | | |
| | | $result = $this->savePostFields((int)$ID, $data); |
| | | unset($data['content']); |
| | | if ($result) { |
| | | $results['success'][$ID] = $data; |
| | | } else { |
| | | $results['errors'][$ID] = 'Could not update post data'; |
| | | } |
| | | if ($registrar && $registrar->hasFeature('is_timeline')) { |
| | | $post = get_post((int)$ID); |
| | | $parentId = $post->post_parent > 0 ? $post->post_parent : $post->ID; |
| | | $fields = $registrar->getFields(); |
| | | $sharedFields = array_keys(array_filter($fields, function ($field) { |
| | | return !array_key_exists('for_all', $field) || !$field['for_all']; |
| | | })); |
| | | |
| | | if (array_key_exists('post_date', $data) && !in_array($parentId, $results['timelineParents'])) { |
| | | $results['timelineParents'][] = $parentId; |
| | | } |
| | | if ($parentId === $ID) { |
| | | if (array_key_exists('post_status', $data) && !array_key_exists($parentId, $results['timelineStatus'])) { |
| | | $results['timelineStatus'][$parentId] = $data['post_status']; |
| | | } |
| | | |
| | | if (count(array_intersect($sharedFields, array_keys($data))) > 0) { |
| | | if (!array_key_exists($parentId, $results['timelineSharedFields'])) { |
| | | $results['timelineSharedFields'][$parentId] = []; |
| | | } |
| | | $temp = array_intersect($sharedFields, array_keys($data)); |
| | | $results['timelineSharedFields'][$parentId] = array_unique(array_merge($results['timelineSharedFields'][$parentId], $temp)); |
| | | } |
| | | } |
| | | } |
| | | $progress->advance(); |
| | | return $results; |
| | | } |
| | | |
| | | |
| | | protected function handleTerm(int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array |
| | | { |
| | | error_log('Handling term '.$ID.' with data: '.print_r($data, true)); |
| | | //Existing term update |
| | | if ($registrar->hasFeature('is_ownable') && (!JVB()->roles()->isOwner($this->userId, $ID) && !JVB()->roles()->isManager($this->userId, $ID))) { |
| | | error_log('Term is ownable. User does not own this term.'); |
| | | $progress->failItem($ID, 'No permission to modify this term'); |
| | | $results['errors'][$ID] = 'No permission'; |
| | | return $results; |
| | | } |
| | | |
| | | $result = $this->saveTermFields($ID, $data); |
| | | unset($data['content']); |
| | | if ($result) { |
| | | $results['success'][$ID] = $data; |
| | | } else { |
| | | $results['errors'][$ID] = 'Could not update term data'; |
| | | } |
| | | $progress->advance(); |
| | | return $results; |
| | | } |
| | | protected function handleUser(int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array |
| | | { |
| | | //Existing term update |
| | | if ($ID !== $this->userId || !user_can($this->userId, 'manage_options')) { |
| | | $progress->failItem($ID, 'No permission to modify this term'); |
| | | $results['errors'][$ID] = 'No permission'; |
| | | return $results; |
| | | } |
| | | |
| | | $result = $this->saveUserFields($ID, $data); |
| | | unset($data['content']); |
| | | if ($result) { |
| | | $results['success'][$ID] = $data; |
| | | } else { |
| | | $results['errors'][$ID] = 'Could not update post data'; |
| | | } |
| | | $progress->advance(); |
| | | return $results; |
| | | } |
| | | } |
| | |
| | | |
| | | public function execute(Operation $operation, Progress $progress): Result |
| | | { |
| | | error_log('Executing Content Term.... '); |
| | | // Extract taxonomy from operation type (e.g., "shop_update" -> "shop") |
| | | $data= $operation->requestData; |
| | | $taxonomy = $data['taxonomy']??false; |
| | |
| | | } |
| | | |
| | | // Update metadata |
| | | $meta->setAll($setData); |
| | | $results = $meta->save(); |
| | | $results = $meta->setAll($setData); |
| | | |
| | | if ($results) { |
| | | // Trigger any post-update actions (e.g., thumbnail generation) |
| | | do_action(BASE . "{$taxonomy}_updated", $termID, $userID, $setData); |
| | | |
| | | return Result::success([ |
| | | 'updated_fields' => array_keys($setData), |
| | | 'term_id' => $termID |
| | |
| | | error_log('Could not find a gallery upload field for post '.$ID); |
| | | } |
| | | |
| | | $meta->save(); |
| | | } |
| | | |
| | | |
| | |
| | | $meta->set($data['field_name'], implode(',', $allIds)); |
| | | } |
| | | |
| | | $meta->save(); |
| | | } |
| | | |
| | | private function updateFieldValue(array $data, array $results): void |
| | |
| | | $allIds = array_unique(array_merge($existingIds, $attachmentIds)); |
| | | |
| | | $meta->set($data['field_name'], implode(',', $allIds)); |
| | | $meta->save(); |
| | | } |
| | | |
| | | private function getMetaManager(array $data): ?Meta |
| | |
| | | } elseif (str_starts_with($mimeType, 'video/')) { |
| | | $meta = Meta::forPost($postId); |
| | | $meta->set('video', $attachmentId); |
| | | $meta->save(); |
| | | } else { |
| | | $meta = Meta::forPost($postId); |
| | | $existing = $meta->get('documents'); |
| | | $existingIds = !empty($existing) ? explode(',', $existing) : []; |
| | | $existingIds[] = $attachmentId; |
| | | $meta->set('documents', implode(',', $existingIds)); |
| | | $meta->save(); |
| | | } |
| | | } |
| | | |
| | |
| | | public array $config; |
| | | public bool $isDirty = false; |
| | | public bool $isValid = true; |
| | | public bool $isDefault = false; |
| | | public array $errors = []; |
| | | |
| | | public function __construct(string $name, mixed $value, array $config = []) |
| | |
| | | $this->value = $value; |
| | | $this->originalValue = $value; |
| | | $this->config = $config; |
| | | if (array_key_exists('wp', $config) && $config['wp'] === true) { |
| | | $this->isDefault = true; |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function set(mixed $value): self |
| | | { |
| | | $this->value = $value; |
| | | $this->isDirty = ($value !== $this->originalValue); |
| | | error_log('Checking if value is the same as old value: '.print_r($value, true)); |
| | | if ($value !== $this->value) { |
| | | error_log('Saving new value: '.print_r($value, true)); |
| | | $this->value = $value; |
| | | $this->isDirty = true; |
| | | } |
| | | return $this; |
| | | } |
| | | |
| | |
| | | */ |
| | | public function isWpDefault(): bool |
| | | { |
| | | return $this->config['_wp_default'] ?? false; |
| | | return $this->isDefault ?? false; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function isTaxonomy(): bool |
| | | { |
| | | return $this->type() === 'taxonomy' && !isset($this->config['taxonomy_type']); |
| | | return ($this->type() === 'taxonomy' || ($this->type() === 'selector' && isset($this->config['subtype']) && $this->config['subtype'] === 'taxonomy')) && !isset($this->config['isReference']); |
| | | } |
| | | |
| | | /** |
| | |
| | | namespace JVBase\meta; |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Post; |
| | | use WP_Term; |
| | | use WP_User; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | /** |
| | | * Main facade for meta operations |
| | | * Fluent API for getting/setting meta values with validation & sanitization |
| | | * |
| | | * Usage: |
| | | * $meta = Meta::forPost($id); |
| | | * $meta->price = 150; |
| | | * $meta->save(); |
| | | * |
| | | * Meta::forPost($id)->set('price', 150)->set('style', 'traditional')->save(); |
| | | */ |
| | | class Meta |
| | | { |
| | | /** |
| | | * @var string post, term, user, or options |
| | | */ |
| | | protected string $type; |
| | | /** |
| | | * @var string the full slug, with BASE |
| | | */ |
| | | protected string $slug; |
| | | |
| | | protected string $contentType; |
| | | protected Item $item; |
| | | protected Storage $storage; |
| | | protected Validator $validator; |
| | | protected Sanitizer $sanitizer; |
| | | protected array $fields; |
| | | protected WP_Post|WP_Term|WP_User|null $wpObject; |
| | | protected int|string $ID; |
| | | protected MetaTypeManager $typeManager; |
| | | protected static array $instances = ['post' => [],'term' => [], 'user'=>[],'options'=>[]]; |
| | | |
| | | protected bool $autoValidate = true; |
| | | protected bool $autoSanitize = true; |
| | | |
| | | /** @var array<string, callable[]> */ |
| | | protected array $onChangeCallbacks = []; |
| | | |
| | | /** @var array<string, callable> */ |
| | | protected array $computed = []; |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Factory Methods |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | protected array $defaults = ['post_thumbnail']; |
| | | /** |
| | | * Create Meta instance for a post |
| | | */ |
| | | public static function forPost(int $id): self |
| | | { |
| | | return new self($id, 'post'); |
| | | if (array_key_exists($id, self::$instances['post'])) { |
| | | return self::$instances['post'][$id]; |
| | | } |
| | | $new = new self($id, 'post'); |
| | | self::$instances['post'][$id] = $new; |
| | | return $new; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public static function forTerm(int $id): self |
| | | { |
| | | return new self($id, 'term'); |
| | | if (array_key_exists($id, self::$instances['term'])) { |
| | | return self::$instances['term'][$id]; |
| | | } |
| | | $new = new self($id, 'term'); |
| | | self::$instances['term'][$id] = $new; |
| | | return $new; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public static function forUser(int $id): self |
| | | { |
| | | return new self($id, 'user'); |
| | | if (array_key_exists($id, self::$instances['user'])) { |
| | | return self::$instances['user'][$id]; |
| | | } |
| | | $new = new self($id, 'user'); |
| | | self::$instances['user'][$id] = $new; |
| | | return $new; |
| | | } |
| | | |
| | | /** |
| | | * Create Meta instance for options |
| | | */ |
| | | public static function forOptions(?string $baseKey = null): self |
| | | public static function forOptions(?string $baseKey = 'ajv'): self |
| | | { |
| | | $instance = new self($baseKey, 'options'); |
| | | $instance->item->baseKey = $baseKey; |
| | | return $instance; |
| | | if (array_key_exists($baseKey, self::$instances['options'])) { |
| | | return self::$instances['options'][$baseKey]; |
| | | } |
| | | $new = new self($baseKey, 'options'); |
| | | self::$instances['options'][$baseKey] = $new; |
| | | return $new; |
| | | } |
| | | |
| | | /** |
| | | * Bulk load multiple posts with optional field preloading |
| | | * @return array<int, Meta> |
| | | */ |
| | | public static function bulkForPosts(array $ids, array $preloadFields = []): array |
| | | { |
| | | return self::bulkFor($ids, 'post', $preloadFields); |
| | | } |
| | | |
| | | /** |
| | | * Bulk load multiple terms with optional field preloading |
| | | * @return array<int, Meta> |
| | | */ |
| | | public static function bulkForTerms(array $ids, array $preloadFields = []): array |
| | | { |
| | | return self::bulkFor($ids, 'term', $preloadFields); |
| | | } |
| | | |
| | | /** |
| | | * Bulk load multiple users with optional field preloading |
| | | * @return array<int, Meta> |
| | | */ |
| | | public static function bulkForUsers(array $ids, array $preloadFields = []): array |
| | | { |
| | | return self::bulkFor($ids, 'user', $preloadFields); |
| | | } |
| | | |
| | | /** |
| | | * Generic bulk loader |
| | | * @return array<int, Meta> |
| | | */ |
| | | protected static function bulkFor(array $ids, string $type, array $preloadFields = []): array |
| | | { |
| | | if (empty($ids)) { |
| | | return []; |
| | | } |
| | | |
| | | $metas = []; |
| | | |
| | | // Create instances |
| | | foreach ($ids as $id) { |
| | | $metas[$id] = new self($id, $type); |
| | | } |
| | | |
| | | // Preload fields if specified |
| | | if (!empty($preloadFields)) { |
| | | self::bulkPreload($metas, $type, $preloadFields); |
| | | } |
| | | |
| | | return $metas; |
| | | } |
| | | |
| | | /** |
| | | * Bulk preload fields for multiple Meta instances |
| | | * @param Meta[] $metas |
| | | */ |
| | | protected static function bulkPreload(array $metas, string $objectType, array $fields): void |
| | | { |
| | | if (empty($metas) || empty($fields)) { |
| | | return; |
| | | } |
| | | |
| | | $ids = array_keys($metas); |
| | | $values = Storage::getBulkValues($ids, $objectType, $fields); |
| | | |
| | | // Distribute results to Meta instances |
| | | foreach ($values as $id => $fieldValues) { |
| | | if (!isset($metas[$id])) { |
| | | continue; |
| | | } |
| | | |
| | | $meta = $metas[$id]; |
| | | foreach ($fieldValues as $name => $value) { |
| | | $config = $meta->config($name) ?? ['type' => 'text']; |
| | | $field = new Field($name, $value, $config); |
| | | $meta->item()->setField($field); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Save multiple Meta instances efficiently |
| | | * @param Meta[] $metas |
| | | * @return array<int, bool> |
| | | */ |
| | | public static function saveBulk(array $metas, bool $updateTimestamp = true): array |
| | | { |
| | | // Validate all first |
| | | $invalid = []; |
| | | foreach ($metas as $id => $meta) { |
| | | if (!$meta->isValid()) { |
| | | $invalid[$id] = $meta->getErrors(); |
| | | } |
| | | } |
| | | |
| | | if (!empty($invalid)) { |
| | | JVB()->error()->log('meta', 'Bulk save has validation errors', [ |
| | | 'invalid_items' => $invalid |
| | | ], 'warning'); |
| | | } |
| | | |
| | | // Filter to only valid metas |
| | | $validMetas = array_filter($metas, fn($m) => $m->isValid()); |
| | | |
| | | // Check overrides before bulk save |
| | | foreach ($validMetas as $meta) { |
| | | foreach ($meta->item()->getDirtyFields() as $field) { |
| | | if ($meta->checkOverrides($field)) { |
| | | $field->markClean(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | $results = Storage::saveBulk($validMetas, $updateTimestamp); |
| | | |
| | | // Mark invalid ones as failed |
| | | foreach ($invalid as $id => $errors) { |
| | | $results[$id] = false; |
| | | } |
| | | |
| | | return $results; |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Constructor |
| | | // ───────────────────────────────────────────────────────────── |
| | | /*************************************************************** |
| | | * Constructor |
| | | ***************************************************************/ |
| | | |
| | | public function __construct(int|string|null $id, string $type) |
| | | { |
| | | $this->storage = new Storage(); |
| | | $this->validator = new Validator(); |
| | | $this->sanitizer = new Sanitizer(); |
| | | $this->typeManager = new MetaTypeManager(); |
| | | |
| | | $this->item = $this->buildItem($id, $type); |
| | | $this->type = $type; |
| | | $this->ID = $id; |
| | | $this->buildData($id, $type); |
| | | } |
| | | |
| | | protected function buildItem(int|string|null $id, string $type): Item |
| | | protected function buildData(int|string|null $id, string $type):void |
| | | { |
| | | $contentType = null; |
| | | $wpObject = null; |
| | | $this->wpObject = match($type) { |
| | | 'post' => get_post($id), |
| | | 'term' => get_term($id), |
| | | 'user', 'integrations' => get_userdata($id), |
| | | default => null |
| | | }; |
| | | $this->slug = match($type) { |
| | | 'post' => $this->wpObject->post_type, |
| | | 'term' => $this->wpObject->taxonomy, |
| | | 'user' => jvbUserRole($id), |
| | | default => null |
| | | }; |
| | | |
| | | if ($id && $type !== 'options') { |
| | | [$wpObject, $contentType] = match ($type) { |
| | | 'post' => [get_post($id), jvbNoBase(get_post_type($id))], |
| | | 'term' => [get_term($id), jvbNoBase(get_term($id)->taxonomy)], |
| | | 'user', 'integrations' => [get_user_by('id', $id), jvbUserRole($id)], |
| | | default => [null, null] |
| | | }; |
| | | } |
| | | |
| | | $item = new Item($id, $type, $contentType); |
| | | $item->wpObject = $wpObject; |
| | | $item->fieldConfigs = $this->loadFieldConfigs($contentType, $type); |
| | | |
| | | // Mark WP defaults in configs |
| | | $defaults = Item::WP_DEFAULTS[$type] ?? []; |
| | | foreach ($defaults as $name) { |
| | | if (!isset($item->fieldConfigs[$name])) { |
| | | $item->fieldConfigs[$name] = ['type' => 'text', '_wp_default' => true]; |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | $fields = $registrar ? $registrar->getFields() : []; |
| | | $meta = match($type) { |
| | | 'post' => get_post_meta($id), |
| | | 'term' => get_term_meta($id), |
| | | 'user' => get_user_meta($id), |
| | | default => [] |
| | | }; |
| | | $meta = array_map(fn($value) => maybe_unserialize($value[0]), $meta); |
| | | |
| | | foreach ($fields as $fieldName => $config) { |
| | | $fieldName = jvbNoBase($fieldName); |
| | | if ($this->wpObject && property_exists($this->wpObject, $fieldName)) { |
| | | $config['wp'] = true; |
| | | $value = $this->wpObject->$fieldName; |
| | | } else if (in_array($fieldName, $this->defaults)) { |
| | | $config['wp'] = true; |
| | | switch ($fieldName) { |
| | | case 'post_thumbnail': |
| | | $value = get_post_thumbnail_id($this->ID); |
| | | break; |
| | | } |
| | | } else { |
| | | $item->fieldConfigs[$name]['_wp_default'] = true; |
| | | $value = array_key_exists(BASE.$fieldName, $meta) ? $meta[BASE.$fieldName] : $config['default']??''; |
| | | switch ($config['type']) { |
| | | case 'taxonomy': |
| | | if (!$config['isReference']??true) { |
| | | $config['wp'] = true; |
| | | $value = implode(',', wp_get_post_terms($this->ID, jvbCheckBase($config['taxonomy']), ['fields' => 'ids'])); |
| | | } |
| | | break; |
| | | case 'selector': |
| | | if (array_key_exists('subtype', $config) && $config['subtype'] === 'taxonomy' && !$config['isReference']??true) { |
| | | $config['wp'] = true; |
| | | $value = implode(',',wp_get_post_terms($this->ID, jvbCheckBase($config['taxonomy']), ['fields' => 'ids'])); |
| | | } |
| | | break; |
| | | } |
| | | |
| | | } |
| | | $this->fields[$fieldName] = new Field($fieldName, $value, $config); |
| | | } |
| | | |
| | | return $item; |
| | | } |
| | | |
| | | protected function loadFieldConfigs(?string $contentType, string $objectType): array |
| | | { |
| | | if (!$contentType && $objectType !== 'options') { |
| | | return []; |
| | | } |
| | | |
| | | return Registrar::getFieldsFor($contentType??'options'); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | |
| | | |
| | | public function __isset(string $name): bool |
| | | { |
| | | return $this->item->hasField($name) || isset($this->computed[$name]); |
| | | return $this->item->hasField($name); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | |
| | | return $this->getByPath($name); |
| | | } |
| | | |
| | | // Check computed fields first |
| | | if (isset($this->computed[$name])) { |
| | | return ($this->computed[$name])($this); |
| | | } |
| | | |
| | | // Return from loaded field if exists |
| | | if ($field = $this->item->getField($name)) { |
| | | return $field->get(); |
| | | } |
| | | |
| | | // Load from storage |
| | | $value = $this->storage->get($this->item, $name); |
| | | $config = $this->item->getFieldConfig($name) ?? ['type' => 'text']; |
| | | |
| | | $field = new Field($name, $value, $config); |
| | | $this->item->setField($field); |
| | | |
| | | return $value; |
| | | return $this->fields[$name]->get(); |
| | | } |
| | | |
| | | /** |
| | | * Set a field value (validates & sanitizes by default) |
| | | */ |
| | | public function set(string $name, mixed $value): self |
| | | public function set(string $name, mixed $value, $autosave = true): self |
| | | { |
| | | // Handle repeater subfield path (e.g., "services:2:image") |
| | | if (str_contains($name, ':')) { |
| | | return $this->setByPath($name, $value); |
| | | } |
| | | |
| | | $config = $this->item->getFieldConfig($name); |
| | | |
| | | if (!$config) { |
| | | // Allow setting unknown fields with minimal config |
| | | $config = ['type' => 'text', 'name' => $name]; |
| | | $field = $this->fields[$name]??false; |
| | | if (!$field) { |
| | | error_log('No config found for field: '.$name); |
| | | return $this; |
| | | } |
| | | |
| | | // Validate |
| | | if ($this->autoValidate && !$this->validator->validate($value, $config)) { |
| | | $field = $this->item->getField($name) ?? new Field($name, $value, $config); |
| | | if (!$this->validator->validate($value, $field->config)) { |
| | | $field->addError("Validation failed for {$name}"); |
| | | $this->item->setField($field); |
| | | return $this; |
| | | } |
| | | |
| | | // Sanitize |
| | | if ($this->autoSanitize) { |
| | | $value = $this->sanitizer->sanitize($value, $config); |
| | | } |
| | | |
| | | // Get or create field |
| | | $field = $this->item->getField($name); |
| | | $oldValue = null; |
| | | |
| | | if ($field) { |
| | | $oldValue = $field->value; |
| | | $field->set($value); |
| | | } else { |
| | | // Load original to track dirty state |
| | | $original = $this->storage->get($this->item, $name); |
| | | $oldValue = $original; |
| | | $field = new Field($name, $original, $config); |
| | | $field->set($value); |
| | | $this->item->setField($field); |
| | | } |
| | | |
| | | // Fire change callbacks |
| | | if (isset($this->onChangeCallbacks[$name]) && $oldValue !== $value) { |
| | | foreach ($this->onChangeCallbacks[$name] as $callback) { |
| | | $callback($value, $oldValue, $this); |
| | | } |
| | | $value = $this->sanitizer->sanitize($value, $field->config); |
| | | $field->set($value); |
| | | if ($autosave && $field->isDirty) { |
| | | $this->save(); |
| | | } |
| | | |
| | | return $this; |
| | |
| | | public function getAll(array $fields = []): array |
| | | { |
| | | if (empty($fields) || $fields === ['all']) { |
| | | $fields = array_keys($this->item->fieldConfigs); |
| | | $fields = array_keys($this->fields); |
| | | } |
| | | $fields = array_filter($this->fields, function ($field) use ($fields) { |
| | | return in_array($field, $fields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | // Load all from storage |
| | | $values = $this->storage->getAll($this->item, $fields); |
| | | |
| | | // Create Field instances |
| | | foreach ($values as $name => $value) { |
| | | if (!$this->item->getField($name)) { |
| | | $config = $this->item->getFieldConfig($name) ?? ['type' => 'text']; |
| | | $this->item->setField(new Field($name, $value, $config)); |
| | | } |
| | | } |
| | | |
| | | return $values; |
| | | return array_map(function ($field) { |
| | | return $field->value; |
| | | }, $fields); |
| | | } |
| | | |
| | | /** |
| | | * Set multiple fields |
| | | */ |
| | | public function setAll(array $data): self |
| | | public function setAll(array $data):bool |
| | | { |
| | | foreach ($data as $name => $value) { |
| | | $this->set($name, $value); |
| | | error_log('Setting '.$name.' with value: '.print_r($value, true)); |
| | | $this->set($name, $value, false); |
| | | } |
| | | return $this; |
| | | return $this->save(); |
| | | } |
| | | |
| | | /** |
| | | * Save all dirty fields to database |
| | | */ |
| | | public function save(bool $updateTimestamp = true): bool |
| | | public function save(): bool |
| | | { |
| | | if (!$this->item->isValid()) { |
| | | JVB()->error()->log('meta', 'Cannot save: validation errors exist', [ |
| | | 'fields' => array_keys($this->item->getInvalidFields()) |
| | | ], 'warning'); |
| | | return false; |
| | | } |
| | | $dirtyFields = array_filter($this->fields, function ($field) { |
| | | return $field->isDirty; |
| | | }); |
| | | $defaults = array_filter($dirtyFields, function ($field) { |
| | | return $field->isDefault; |
| | | }); |
| | | $custom = array_filter($dirtyFields, function($field) { |
| | | return !$field->isDefault; |
| | | }); |
| | | |
| | | // Check for field overrides before saving |
| | | foreach ($this->item->getDirtyFields() as $field) { |
| | | if ($this->checkOverrides($field)) { |
| | | $field->markClean(); |
| | | $success = true; |
| | | |
| | | if (!empty($defaults)) { |
| | | $result = false; |
| | | switch ($this->type) { |
| | | case 'post': |
| | | //Deal with field exceptions, first, that cannot be set via wp_update_post |
| | | foreach ($defaults as $fieldName => $field) { |
| | | switch ($fieldName) { |
| | | case 'post_thumbnail': |
| | | $result = set_post_thumbnail($this->ID, $field->value); |
| | | unset($defaults[$fieldName]); |
| | | break; |
| | | } |
| | | //If it's a taxonomy, we use wp_set_object_terms |
| | | if ( |
| | | ($field->config['type'] === 'selector' && $field->config['subtype'] === 'taxonomy') |
| | | || $field->config['type'] === 'taxonomy') { |
| | | |
| | | $result = wp_set_object_terms($this->ID, array_map('absint', explode(',', $field->value)), jvbCheckBase($field->config['taxonomy'])); |
| | | unset($defaults[$fieldName]); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | if (!empty($defaults)) { |
| | | error_log('Remaining fields: '.print_r($defaults, true)); |
| | | $defaults = array_map(function ($field) { |
| | | return $field->value; |
| | | }, $defaults); |
| | | error_log('Remaining values to save: '.print_r($defaults, true)); |
| | | $data = array_merge([ |
| | | 'post_type' => $this->slug, |
| | | 'ID' => $this->ID |
| | | ], $defaults); |
| | | error_log('Updating post: '.print_r($data, true)); |
| | | $result = wp_update_post($data); |
| | | } |
| | | break; |
| | | case 'term': |
| | | $result = wp_update_term($this->ID, $this->slug, $defaults); |
| | | break; |
| | | case 'user': |
| | | $data = array_merge([ |
| | | 'ID' => $this->ID |
| | | ], $defaults); |
| | | $result = wp_update_user($data); |
| | | break; |
| | | } |
| | | if (!$result || is_wp_error($result)) { |
| | | $success = false; |
| | | } |
| | | } |
| | | if (!empty($custom)) { |
| | | $function = match ($this->type) { |
| | | 'post' => 'update_post_meta', |
| | | 'term' => 'update_term_meta', |
| | | 'user' => 'update_user_meta', |
| | | 'options', 'option' => 'update_option', |
| | | default => false, |
| | | }; |
| | | if (!$function) { |
| | | error_log('[Meta]::save() Invalid type, cannot save: '.$this->type); |
| | | return false; |
| | | } |
| | | foreach ($custom as $field) { |
| | | $result = $function($this->ID, BASE.$field->name, $field->value); |
| | | if (!$result) { |
| | | error_log('Problem saving field: '.$field->name.' with value: '.print_r($field->value, true)); |
| | | } |
| | | } |
| | | if ($this->type === 'term' && Registrar::getInstance($this->slug)->hasFeature('is_content')) { |
| | | update_term_meta($this->ID, BASE.'date_modified', date('Y-m-d H:i:s')); |
| | | } |
| | | } |
| | | |
| | | return $this->storage->save($this->item, $updateTimestamp); |
| | | |
| | | return $success; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function delete(string $name): bool |
| | | { |
| | | $result = $this->storage->delete($this->item, $name); |
| | | |
| | | if ($result && $field = $this->item->getField($name)) { |
| | | $field->set($this->getDefaultValue($name)); |
| | | $field->markClean(); |
| | | $field = $this->fields[$name]??false; |
| | | if (!$field) { |
| | | error_log('[Meta]::delete Could not delete field '.$name.': not registered'); |
| | | return true; |
| | | } |
| | | |
| | | return $result; |
| | | if ($field->isTaxonomy()) { |
| | | wp_set_object_terms($this->ID, [], $this->slug); |
| | | return true; |
| | | } |
| | | |
| | | return match ($this->type) { |
| | | 'post' => delete_post_meta($this->ID, BASE.$name), |
| | | 'term' => delete_term_meta($this->ID, BASE.$name), |
| | | 'user', 'integrations' => delete_user_meta($this->ID, BASE.$name), |
| | | 'options' => delete_option(BASE.$name), |
| | | default => false |
| | | }; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Delete multiple field values |
| | | */ |
| | |
| | | return $results; |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Repeater Access |
| | | // ───────────────────────────────────────────────────────────── |
| | | /***************************************************************** |
| | | * Repeater Access |
| | | *****************************************************************/ |
| | | |
| | | /** |
| | | * Get repeater accessor for fluent repeater operations |
| | |
| | | |
| | | protected function setByPath(string $path, mixed $value): self |
| | | { |
| | | error_log('Setting by path: '.$path.', with value: '.print_r($value, true)); |
| | | $parts = explode(':', $path, 3); |
| | | if (count($parts) !== 3) { |
| | | return $this; |
| | |
| | | // Utility Methods |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Get all dirty (changed) field values |
| | | */ |
| | | public function getDirty(): array |
| | | { |
| | | return array_map( |
| | | fn(Field $f) => $f->value, |
| | | $this->item->getDirtyFields() |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Check if any fields have changed |
| | | */ |
| | | public function isDirty(): bool |
| | | { |
| | | return $this->item->hasDirtyFields(); |
| | | } |
| | | |
| | | /** |
| | | * Discard all unsaved changes |
| | |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Get validation errors |
| | | */ |
| | | public function getErrors(): array |
| | | { |
| | | $errors = []; |
| | | foreach ($this->item->getInvalidFields() as $name => $field) { |
| | | $errors[$name] = $field->errors; |
| | | } |
| | | return $errors; |
| | | } |
| | | |
| | | /** |
| | | * Check if valid (no validation errors) |
| | | */ |
| | | public function isValid(): bool |
| | | { |
| | | return $this->item->isValid(); |
| | | } |
| | | |
| | | /** |
| | | * Disable auto-validation for bulk operations |
| | | */ |
| | | public function withoutValidation(): self |
| | | { |
| | | $this->autoValidate = false; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Disable auto-sanitization |
| | | */ |
| | | public function withoutSanitization(): self |
| | | { |
| | | $this->autoSanitize = false; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Re-enable validation and sanitization |
| | | */ |
| | | public function withDefaults(): self |
| | | { |
| | | $this->autoValidate = true; |
| | | $this->autoSanitize = true; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Get the underlying Item |
| | | */ |
| | | public function item(): Item |
| | | { |
| | | return $this->item; |
| | | } |
| | | |
| | | /** |
| | | * Get field configuration |
| | | */ |
| | | public function config(string $name): ?array |
| | | { |
| | | return $this->item->getFieldConfig($name); |
| | | return $this->fields[$name]->config??null; |
| | | } |
| | | |
| | | /** |
| | | * Get all field configurations |
| | | */ |
| | | public function configs(): array |
| | | { |
| | | return $this->item->fieldConfigs; |
| | | } |
| | | |
| | | /** |
| | | * Get item ID |
| | | */ |
| | | public function id(): int|string|null |
| | | { |
| | | return $this->item->id; |
| | | return $this->ID; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function objectType(): string |
| | | { |
| | | return $this->item->objectType; |
| | | return $this->type; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function contentType(): ?string |
| | | { |
| | | return $this->item->contentType; |
| | | return $this->contentType; |
| | | } |
| | | |
| | | /** |
| | | * Eager load all fields |
| | | */ |
| | | public function eager(): self |
| | | { |
| | | $this->getAll(); |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Convert loaded fields to array |
| | | */ |
| | | public function toArray(): array |
| | | { |
| | | return $this->item->toArray(); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Event Callbacks |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Register callback for field changes |
| | | */ |
| | | public function onChange(string $field, callable $callback): self |
| | | { |
| | | $this->onChangeCallbacks[$field][] = $callback; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Register computed/virtual field |
| | | */ |
| | | public function computed(string $name, callable $getter): self |
| | | { |
| | | $this->computed[$name] = $getter; |
| | | return $this; |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Protected Helpers |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\meta; |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | /** |
| | | * Main facade for meta operations |
| | | * Fluent API for getting/setting meta values with validation & sanitization |
| | | * |
| | | * Usage: |
| | | * $meta = Meta::forPost($id); |
| | | * $meta->price = 150; |
| | | * $meta->save(); |
| | | * |
| | | * Meta::forPost($id)->set('price', 150)->set('style', 'traditional')->save(); |
| | | */ |
| | | class MetaOld |
| | | { |
| | | protected Item $item; |
| | | protected Storage $storage; |
| | | protected Validator $validator; |
| | | protected Sanitizer $sanitizer; |
| | | protected MetaTypeManager $typeManager; |
| | | |
| | | protected bool $autoValidate = true; |
| | | protected bool $autoSanitize = true; |
| | | |
| | | /** @var array<string, callable[]> */ |
| | | protected array $onChangeCallbacks = []; |
| | | |
| | | /** @var array<string, callable> */ |
| | | protected array $computed = []; |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Factory Methods |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Create Meta instance for a post |
| | | */ |
| | | public static function forPost(int $id): self |
| | | { |
| | | return new self($id, 'post'); |
| | | } |
| | | |
| | | /** |
| | | * Create Meta instance for a term |
| | | */ |
| | | public static function forTerm(int $id): self |
| | | { |
| | | return new self($id, 'term'); |
| | | } |
| | | |
| | | /** |
| | | * Create Meta instance for a user |
| | | */ |
| | | public static function forUser(int $id): self |
| | | { |
| | | return new self($id, 'user'); |
| | | } |
| | | |
| | | /** |
| | | * Create Meta instance for options |
| | | */ |
| | | public static function forOptions(?string $baseKey = null): self |
| | | { |
| | | $instance = new self($baseKey, 'options'); |
| | | $instance->item->baseKey = $baseKey; |
| | | return $instance; |
| | | } |
| | | |
| | | /** |
| | | * Bulk load multiple posts with optional field preloading |
| | | * @return array<int, Meta> |
| | | */ |
| | | public static function bulkForPosts(array $ids, array $preloadFields = []): array |
| | | { |
| | | return self::bulkFor($ids, 'post', $preloadFields); |
| | | } |
| | | |
| | | /** |
| | | * Bulk load multiple terms with optional field preloading |
| | | * @return array<int, Meta> |
| | | */ |
| | | public static function bulkForTerms(array $ids, array $preloadFields = []): array |
| | | { |
| | | return self::bulkFor($ids, 'term', $preloadFields); |
| | | } |
| | | |
| | | /** |
| | | * Bulk load multiple users with optional field preloading |
| | | * @return array<int, Meta> |
| | | */ |
| | | public static function bulkForUsers(array $ids, array $preloadFields = []): array |
| | | { |
| | | return self::bulkFor($ids, 'user', $preloadFields); |
| | | } |
| | | |
| | | /** |
| | | * Generic bulk loader |
| | | * @return array<int, Meta> |
| | | */ |
| | | protected static function bulkFor(array $ids, string $type, array $preloadFields = []): array |
| | | { |
| | | if (empty($ids)) { |
| | | return []; |
| | | } |
| | | |
| | | $metas = []; |
| | | |
| | | // Create instances |
| | | foreach ($ids as $id) { |
| | | $metas[$id] = new self($id, $type); |
| | | } |
| | | |
| | | // Preload fields if specified |
| | | if (!empty($preloadFields)) { |
| | | self::bulkPreload($metas, $type, $preloadFields); |
| | | } |
| | | |
| | | return $metas; |
| | | } |
| | | |
| | | /** |
| | | * Bulk preload fields for multiple Meta instances |
| | | * @param Meta[] $metas |
| | | */ |
| | | protected static function bulkPreload(array $metas, string $objectType, array $fields): void |
| | | { |
| | | if (empty($metas) || empty($fields)) { |
| | | return; |
| | | } |
| | | |
| | | $ids = array_keys($metas); |
| | | $values = Storage::getBulkValues($ids, $objectType, $fields); |
| | | |
| | | // Distribute results to Meta instances |
| | | foreach ($values as $id => $fieldValues) { |
| | | if (!isset($metas[$id])) { |
| | | continue; |
| | | } |
| | | |
| | | $meta = $metas[$id]; |
| | | foreach ($fieldValues as $name => $value) { |
| | | $config = $meta->config($name) ?? ['type' => 'text']; |
| | | $field = new Field($name, $value, $config); |
| | | $meta->item()->setField($field); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Save multiple Meta instances efficiently |
| | | * @param Meta[] $metas |
| | | * @return array<int, bool> |
| | | */ |
| | | public static function saveBulk(array $metas, bool $updateTimestamp = true): array |
| | | { |
| | | // Validate all first |
| | | $invalid = []; |
| | | foreach ($metas as $id => $meta) { |
| | | if (!$meta->isValid()) { |
| | | $invalid[$id] = $meta->getErrors(); |
| | | } |
| | | } |
| | | |
| | | if (!empty($invalid)) { |
| | | JVB()->error()->log('meta', 'Bulk save has validation errors', [ |
| | | 'invalid_items' => $invalid |
| | | ], 'warning'); |
| | | } |
| | | |
| | | // Filter to only valid metas |
| | | $validMetas = array_filter($metas, fn($m) => $m->isValid()); |
| | | |
| | | // Check overrides before bulk save |
| | | foreach ($validMetas as $meta) { |
| | | foreach ($meta->item()->getDirtyFields() as $field) { |
| | | if ($meta->checkOverrides($field)) { |
| | | $field->markClean(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | $results = Storage::saveBulk($validMetas, $updateTimestamp); |
| | | |
| | | // Mark invalid ones as failed |
| | | foreach ($invalid as $id => $errors) { |
| | | $results[$id] = false; |
| | | } |
| | | |
| | | return $results; |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Constructor |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | public function __construct(int|string|null $id, string $type) |
| | | { |
| | | $this->storage = new Storage(); |
| | | $this->validator = new Validator(); |
| | | $this->sanitizer = new Sanitizer(); |
| | | $this->typeManager = new MetaTypeManager(); |
| | | |
| | | $this->item = $this->buildItem($id, $type); |
| | | } |
| | | |
| | | protected function buildItem(int|string|null $id, string $type): Item |
| | | { |
| | | $contentType = null; |
| | | $wpObject = null; |
| | | |
| | | if ($id && $type !== 'options') { |
| | | [$wpObject, $contentType] = match ($type) { |
| | | 'post' => [get_post($id), jvbNoBase(get_post_type($id))], |
| | | 'term' => [get_term($id), jvbNoBase(get_term($id)->taxonomy)], |
| | | 'user', 'integrations' => [get_user_by('id', $id), jvbUserRole($id)], |
| | | default => [null, null] |
| | | }; |
| | | } |
| | | |
| | | $item = new Item($id, $type, $contentType); |
| | | $item->wpObject = $wpObject; |
| | | $item->fieldConfigs = $this->loadFieldConfigs($contentType, $type); |
| | | |
| | | // Mark WP defaults in configs |
| | | $defaults = Item::WP_DEFAULTS[$type] ?? []; |
| | | foreach ($defaults as $name) { |
| | | if (!isset($item->fieldConfigs[$name])) { |
| | | $item->fieldConfigs[$name] = ['type' => 'text', '_wp_default' => true]; |
| | | } else { |
| | | $item->fieldConfigs[$name]['_wp_default'] = true; |
| | | } |
| | | } |
| | | |
| | | return $item; |
| | | } |
| | | |
| | | protected function loadFieldConfigs(?string $contentType, string $objectType): array |
| | | { |
| | | if (!$contentType && $objectType !== 'options') { |
| | | return []; |
| | | } |
| | | |
| | | return Registrar::getFieldsFor($contentType??'options'); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Magic Methods for Fluent Access |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | public function __get(string $name): mixed |
| | | { |
| | | return $this->get($name); |
| | | } |
| | | |
| | | public function __set(string $name, mixed $value): void |
| | | { |
| | | $this->set($name, $value); |
| | | } |
| | | |
| | | public function __isset(string $name): bool |
| | | { |
| | | return $this->item->hasField($name) || isset($this->computed[$name]); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Core API |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Get a field value |
| | | */ |
| | | public function get(string $name): mixed |
| | | { |
| | | // Handle repeater subfield path |
| | | if (str_contains($name, ':')) { |
| | | return $this->getByPath($name); |
| | | } |
| | | |
| | | // Check computed fields first |
| | | if (isset($this->computed[$name])) { |
| | | return ($this->computed[$name])($this); |
| | | } |
| | | |
| | | // Return from loaded field if exists |
| | | if ($field = $this->item->getField($name)) { |
| | | return $field->get(); |
| | | } |
| | | |
| | | // Load from storage |
| | | $value = $this->storage->get($this->item, $name); |
| | | $config = $this->item->getFieldConfig($name) ?? ['type' => 'text']; |
| | | |
| | | $field = new Field($name, $value, $config); |
| | | $this->item->setField($field); |
| | | |
| | | return $value; |
| | | } |
| | | |
| | | /** |
| | | * Set a field value (validates & sanitizes by default) |
| | | */ |
| | | public function set(string $name, mixed $value): self |
| | | { |
| | | // Handle repeater subfield path (e.g., "services:2:image") |
| | | if (str_contains($name, ':')) { |
| | | return $this->setByPath($name, $value); |
| | | } |
| | | |
| | | $config = $this->item->getFieldConfig($name); |
| | | |
| | | if (!$config) { |
| | | // Allow setting unknown fields with minimal config |
| | | $config = ['type' => 'text', 'name' => $name]; |
| | | } |
| | | |
| | | // Validate |
| | | if ($this->autoValidate && !$this->validator->validate($value, $config)) { |
| | | $field = $this->item->getField($name) ?? new Field($name, $value, $config); |
| | | $field->addError("Validation failed for {$name}"); |
| | | $this->item->setField($field); |
| | | return $this; |
| | | } |
| | | |
| | | // Sanitize |
| | | if ($this->autoSanitize) { |
| | | $value = $this->sanitizer->sanitize($value, $config); |
| | | } |
| | | |
| | | // Get or create field |
| | | $field = $this->item->getField($name); |
| | | $oldValue = null; |
| | | |
| | | if ($field) { |
| | | error_log('Stored field found'); |
| | | $oldValue = $field->value; |
| | | $field->set($value); |
| | | } else { |
| | | error_log('No stored field found. Creating a new one'); |
| | | // Load original to track dirty state |
| | | $original = $this->storage->get($this->item, $name); |
| | | $oldValue = $original; |
| | | $field = new Field($name, $original, $config); |
| | | $field->set($value); |
| | | $this->item->setField($field); |
| | | } |
| | | |
| | | // Fire change callbacks |
| | | if (isset($this->onChangeCallbacks[$name]) && $oldValue !== $value) { |
| | | foreach ($this->onChangeCallbacks[$name] as $callback) { |
| | | $callback($value, $oldValue, $this); |
| | | } |
| | | } |
| | | |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Get multiple fields |
| | | */ |
| | | public function getAll(array $fields = []): array |
| | | { |
| | | if (empty($fields) || $fields === ['all']) { |
| | | $fields = array_keys($this->item->fieldConfigs); |
| | | } |
| | | |
| | | // Load all from storage |
| | | $values = $this->storage->getAll($this->item, $fields); |
| | | |
| | | // Create Field instances |
| | | foreach ($values as $name => $value) { |
| | | if (!$this->item->getField($name)) { |
| | | $config = $this->item->getFieldConfig($name) ?? ['type' => 'text']; |
| | | $this->item->setField(new Field($name, $value, $config)); |
| | | } |
| | | } |
| | | |
| | | return $values; |
| | | } |
| | | |
| | | /** |
| | | * Set multiple fields |
| | | */ |
| | | public function setAll(array $data): self |
| | | { |
| | | error_log('Setting all Meta'); |
| | | foreach ($data as $name => $value) { |
| | | error_log('Setting '.$name.' with value: '.print_r($value, true)); |
| | | $this->set($name, $value); |
| | | } |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Save all dirty fields to database |
| | | */ |
| | | public function save(bool $updateTimestamp = true): bool |
| | | { |
| | | if (!$this->item->isValid()) { |
| | | JVB()->error()->log('meta', 'Cannot save: validation errors exist', [ |
| | | 'fields' => array_keys($this->item->getInvalidFields()) |
| | | ], 'warning'); |
| | | return false; |
| | | } |
| | | |
| | | // Check for field overrides before saving |
| | | foreach ($this->item->getDirtyFields() as $field) { |
| | | if ($this->checkOverrides($field)) { |
| | | $field->markClean(); |
| | | } |
| | | } |
| | | |
| | | return $this->storage->save($this->item, $updateTimestamp); |
| | | } |
| | | |
| | | /** |
| | | * Delete a field value |
| | | */ |
| | | public function delete(string $name): bool |
| | | { |
| | | $result = $this->storage->delete($this->item, $name); |
| | | |
| | | if ($result && $field = $this->item->getField($name)) { |
| | | $field->set($this->getDefaultValue($name)); |
| | | $field->markClean(); |
| | | } |
| | | |
| | | return $result; |
| | | } |
| | | |
| | | /** |
| | | * Delete multiple field values |
| | | */ |
| | | public function deleteAll(array $names): array |
| | | { |
| | | $results = []; |
| | | foreach ($names as $name) { |
| | | $results[$name] = $this->delete($name); |
| | | } |
| | | return $results; |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Repeater Access |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Get repeater accessor for fluent repeater operations |
| | | */ |
| | | public function repeater(string $name): Repeater |
| | | { |
| | | return new Repeater($this, $name); |
| | | } |
| | | |
| | | protected function setByPath(string $path, mixed $value): self |
| | | { |
| | | error_log('Setting by path: '.$path.', with value: '.print_r($value, true)); |
| | | $parts = explode(':', $path, 3); |
| | | if (count($parts) !== 3) { |
| | | return $this; |
| | | } |
| | | |
| | | [$repeaterName, $rowIndex, $subField] = $parts; |
| | | $this->repeater($repeaterName)->setField((int) $rowIndex, $subField, $value); |
| | | |
| | | return $this; |
| | | } |
| | | |
| | | protected function getByPath(string $path): mixed |
| | | { |
| | | $parts = explode(':', $path, 3); |
| | | if (count($parts) !== 3) { |
| | | return null; |
| | | } |
| | | |
| | | [$repeaterName, $rowIndex, $subField] = $parts; |
| | | return $this->repeater($repeaterName)->field((int) $rowIndex, $subField); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Utility Methods |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Get all dirty (changed) field values |
| | | */ |
| | | public function getDirty(): array |
| | | { |
| | | return array_map( |
| | | fn(Field $f) => $f->value, |
| | | $this->item->getDirtyFields() |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Check if any fields have changed |
| | | */ |
| | | public function isDirty(): bool |
| | | { |
| | | return $this->item->hasDirtyFields(); |
| | | } |
| | | |
| | | /** |
| | | * Discard all unsaved changes |
| | | */ |
| | | public function reset(): self |
| | | { |
| | | $this->item->resetAll(); |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Get validation errors |
| | | */ |
| | | public function getErrors(): array |
| | | { |
| | | $errors = []; |
| | | foreach ($this->item->getInvalidFields() as $name => $field) { |
| | | $errors[$name] = $field->errors; |
| | | } |
| | | return $errors; |
| | | } |
| | | |
| | | /** |
| | | * Check if valid (no validation errors) |
| | | */ |
| | | public function isValid(): bool |
| | | { |
| | | return $this->item->isValid(); |
| | | } |
| | | |
| | | /** |
| | | * Disable auto-validation for bulk operations |
| | | */ |
| | | public function withoutValidation(): self |
| | | { |
| | | $this->autoValidate = false; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Disable auto-sanitization |
| | | */ |
| | | public function withoutSanitization(): self |
| | | { |
| | | $this->autoSanitize = false; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Re-enable validation and sanitization |
| | | */ |
| | | public function withDefaults(): self |
| | | { |
| | | $this->autoValidate = true; |
| | | $this->autoSanitize = true; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Get the underlying Item |
| | | */ |
| | | public function item(): Item |
| | | { |
| | | return $this->item; |
| | | } |
| | | |
| | | /** |
| | | * Get field configuration |
| | | */ |
| | | public function config(string $name): ?array |
| | | { |
| | | return $this->item->getFieldConfig($name); |
| | | } |
| | | |
| | | /** |
| | | * Get all field configurations |
| | | */ |
| | | public function configs(): array |
| | | { |
| | | return $this->item->fieldConfigs; |
| | | } |
| | | |
| | | /** |
| | | * Get item ID |
| | | */ |
| | | public function id(): int|string|null |
| | | { |
| | | return $this->item->id; |
| | | } |
| | | |
| | | /** |
| | | * Get object type (post, term, user, options) |
| | | */ |
| | | public function objectType(): string |
| | | { |
| | | return $this->item->objectType; |
| | | } |
| | | |
| | | /** |
| | | * Get content type (tattoo, artist, etc) |
| | | */ |
| | | public function contentType(): ?string |
| | | { |
| | | return $this->item->contentType; |
| | | } |
| | | |
| | | /** |
| | | * Eager load all fields |
| | | */ |
| | | public function eager(): self |
| | | { |
| | | $this->getAll(); |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Convert loaded fields to array |
| | | */ |
| | | public function toArray(): array |
| | | { |
| | | return $this->item->toArray(); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Event Callbacks |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Register callback for field changes |
| | | */ |
| | | public function onChange(string $field, callable $callback): self |
| | | { |
| | | $this->onChangeCallbacks[$field][] = $callback; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Register computed/virtual field |
| | | */ |
| | | public function computed(string $name, callable $getter): self |
| | | { |
| | | $this->computed[$name] = $getter; |
| | | return $this; |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Protected Helpers |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * Check for field update overrides |
| | | */ |
| | | public function checkOverrides(Field $field): bool |
| | | { |
| | | $name = $field->name; |
| | | $type = $field->type(); |
| | | $value = $field->value; |
| | | |
| | | do_action('jvb_meta_update', $name, $value, $this->item->objectType); |
| | | |
| | | $overrides = [ |
| | | BASE . 'update_' . $name, |
| | | BASE . 'update_' . $type, |
| | | 'jvb_update_' . $name, |
| | | 'jvb_update_' . $type, |
| | | ]; |
| | | |
| | | foreach ($overrides as $override) { |
| | | if (function_exists($override)) { |
| | | $override($this->item->id, $value); |
| | | return true; |
| | | } |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Get default value for a field type |
| | | */ |
| | | protected function getDefaultValue(string $name): mixed |
| | | { |
| | | $config = $this->item->getFieldConfig($name); |
| | | $type = $config['type'] ?? 'text'; |
| | | |
| | | return match ($this->typeManager->getMetaType($type)) { |
| | | 'object', 'array' => [], |
| | | 'boolean' => false, |
| | | 'integer' => 0, |
| | | default => '', |
| | | }; |
| | | } |
| | | |
| | | } |
| | |
| | | public static function sanitize(mixed $value, array $field_config): mixed |
| | | { |
| | | $callback = static::getCallback($field_config); |
| | | |
| | | if (is_array($callback)) { |
| | | return call_user_func([static::class, $callback[1]], $value, $field_config); |
| | | } |
| | |
| | | return $sanitized; |
| | | } |
| | | |
| | | protected static function sanitizeSelector(string $value, array $config):string |
| | | protected static function sanitizeSelector(string|array $value, array $config):string |
| | | { |
| | | if (array_key_exists('type', $config)) { |
| | | return match ($config['type']) { |
| | | if (is_array($value)) { |
| | | $value = implode(',', $value); |
| | | } |
| | | if (array_key_exists('subtype', $config)) { |
| | | return match ($config['subtype']) { |
| | | 'user' => self::sanitizeUser($value, $config), |
| | | 'taxonomy'=> self::sanitizeTaxonomy($value, $config), |
| | | 'post' => self::sanitizePost($value, $config), |
| | |
| | | && ( |
| | | ($config['type'] ?? '') === 'taxonomy' |
| | | || (($config['type']??'') === 'selector' && ($config['subtype']??'') === 'taxonomy') |
| | | ) && !isset($config['taxonomy_type'])) { |
| | | ) && (!isset($config['taxonomy_type']) || !isset($config['isReference']))) { |
| | | $taxonomyFields[$name] = $config; |
| | | } else { |
| | | $metaFields[] = $name; |
| | |
| | | } |
| | | |
| | | if ($field->isTaxonomy()) { |
| | | error_log('Saving Taxonomy field with set_object_terms'); |
| | | return $this->saveTaxonomyField($item, $field); |
| | | } |
| | | |
| | | $metaKey = BASE . $field->name; |
| | | |
| | | return match ($item->objectType) { |
| | | 'post' => update_post_meta($item->id, $metaKey, $field->value) !== false, |
| | | 'term' => update_term_meta($item->id, $metaKey, $field->value) !== false, |
| | | 'user', 'integrations' => update_user_meta($item->id, $metaKey, $field->value) !== false, |
| | | $result = match ($item->objectType) { |
| | | 'post' => (bool)update_post_meta($item->id, $metaKey, $field->value), |
| | | 'term' => (bool)update_term_meta($item->id, $metaKey, $field->value), |
| | | 'user', 'integrations' => (bool)update_user_meta($item->id, $metaKey, $field->value), |
| | | 'options' => $this->saveOption($item, $field), |
| | | default => false |
| | | }; |
| | | |
| | | error_log('Result: '.print_r($result, true)); |
| | | return $result; |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | $this->wpdb->query('START TRANSACTION'); |
| | | |
| | | try { |
| | | foreach ($dirty as $field) { |
| | | if (!$this->saveField($item, $field)) { |
| | | error_log("Could not save field: {$field->name}"); |
| | | throw new Exception("Failed to save field: {$field->name}"); |
| | | } |
| | | $field->markClean(); |
| | |
| | | { |
| | | $taxonomy = jvbCheckBase($field->config['taxonomy']); |
| | | $value = $field->value; |
| | | |
| | | if (empty(trim((string)$value))) { |
| | | wp_set_object_terms($item->id, [], $taxonomy, false); |
| | | return true; |
| | |
| | | 'label' => 'Description', |
| | | ] |
| | | ]; |
| | | if ($this->registrar->args()['hierarchical']??false && $this->registrar->args()['hierarchical'] === true){ |
| | | if ($this->registrar->args()['hierarchical']??false){ |
| | | $fields['parent'] = [ |
| | | 'type' => 'taxonomy', |
| | | 'isReference' => true, |
| | |
| | | * Set this to true for the post type to be available in the block editor. |
| | | * @var bool |
| | | */ |
| | | public bool $show_in_rest; |
| | | public bool $show_in_rest = true; |
| | | /** |
| | | * To change the base URL of REST API route. Default is $post_type. |
| | | * @var string |
| | |
| | | |
| | | private static array $instances = []; |
| | | |
| | | protected string $based; |
| | | private function __construct(string $slug, string $singular, string $plural, string $type) { |
| | | $this->slug = $slug; |
| | | $this->based = jvbCheckBase($slug); |
| | | $this->type = $type; |
| | | $this->singular = $singular; |
| | | $this->plural = $plural; |
| | |
| | | return $this->slug; |
| | | } |
| | | |
| | | public function getBased():string |
| | | { |
| | | return $this->based; |
| | | } |
| | | |
| | | public function setTimeline(bool $set):self |
| | | { |
| | | $this->is_timeline = $set; |
| | |
| | | } |
| | | wp_reset_postdata(); |
| | | } |
| | | |
| | | add_filter('jvb_post_content_output', [$this, 'renderContent'], 20, 2); |
| | | |
| | | |
| | | //Add a date published and date modified fields, and auto-update them on term creation/modification |
| | | $this->fields()->addField('date_published', [ |
| | | 'type' => 'datetime', |
| | | 'label' => 'Published', |
| | | 'hidden' => true, |
| | | ]); |
| | | $this->fields()->addField('date_modified', [ |
| | | 'type' => 'datetime', |
| | | 'label' => 'Modified', |
| | | 'hidden' => true, |
| | | ]); |
| | | add_action('created_'.$this->based, [$this, 'addTermCreatedMeta']); |
| | | add_action('edited_'.$this->based, [$this, 'addTermUpdatedMeta']); |
| | | } |
| | | public function addTermCreatedMeta(int $termId):void |
| | | { |
| | | $meta = Meta::forTerm($termId); |
| | | $meta->set('date_published', date('Y-m-d H:i:s')); |
| | | } |
| | | public function handleContentTermMetaChange(int $meta_id, int $term_id, string $meta_key, $meta_value):void |
| | | { |
| | | $term = get_term($term_id); |
| | | $taxonomy = $term->taxonomy; |
| | | if ($taxonomy === $this->based && $meta_key !== BASE . 'date_modified') { |
| | | $meta = Meta::forTerm($term_id); |
| | | $meta->set('date_modified', date('Y-m-d H:i:s')); |
| | | } |
| | | |
| | | } |
| | | public function addTermUpdatedMeta(int $termId):void |
| | | { |
| | | $meta = Meta::forTerm($termId); |
| | | $meta->set('date_modified', date('Y-m-d H:i:s')); |
| | | } |
| | | public function renderContent(string $content, array $block):string |
| | | { |
| | | if (!is_page($this->page)) { |
| | |
| | | $meta = Meta::forTerm($item); |
| | | $slug = sanitize_title($meta->get('name')); |
| | | $item = sprintf( |
| | | '<li id="%s"><h3><a href="%s">%s</a></h3><p>%s</p><ul>', |
| | | '<li id="%s"><h2><a href="%s">%s</a></h2><p>%s</p><ul class="item-grid">', |
| | | $slug, |
| | | get_term_link($item, jvbCheckBase($this->slug))??'', |
| | | $meta->get('name'), |
| | |
| | | $img = $postMeta->get('post_thumbnail'); |
| | | $img = !empty($img) ? jvbFormatImage((int)$img, 'tiny', 'medium') : ''; |
| | | $item .= sprintf( |
| | | '<li id="%s"><h4><a href="%s">%s</a></h4>%s</li>', |
| | | '<li id="%s" class="item"><h3><a href="%s">%s</a></h3>%s</li>', |
| | | $slug.'-'.sanitize_title(get_the_title($ID)), |
| | | get_the_permalink($ID), |
| | | $postMeta->get('post_title'), |
| | |
| | | * Whether to include the taxonomy in the REST API. Set this to true for the taxonomy to be available in the block editor. |
| | | * @var bool |
| | | */ |
| | | public bool $show_in_rest; |
| | | public bool $show_in_rest = true; |
| | | /** |
| | | * To change the base url of REST API route. Default is $taxonomy. |
| | | * @var string |
| | |
| | | $meta = Meta::forPost($ID); |
| | | $config = $this->getConfig(); |
| | | |
| | | $class = $this->classFromConfig($config, $meta); |
| | | $class = JVB()->schemaHelper()::classFromConfig($config, $meta); |
| | | |
| | | $class->setAuthor(JVB()->seo()->getCreator(true)); |
| | | return $class->outputSchema(); |
| | |
| | | $ID, |
| | | function() use ($ID) { |
| | | $action = BASE.ucfirst($this->slug).'Schema'; |
| | | $config = get_option($action, apply_filters($action, $this->defaultSchema)); |
| | | $config = JVB()->schemaHelper()::schema($action); |
| | | |
| | | if (!array_key_exists('type', $config)) { |
| | | $config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage'; |
| | | } |
| | |
| | | error_log('No class found for archive schema output: '.$config['type']); |
| | | return []; |
| | | } |
| | | $class = $this->classFromConfig($config); |
| | | $class = JVB()->schemaHelper()::classFromConfig($config); |
| | | |
| | | $class->setIsPartOf(get_home_url().'/#website'); |
| | | $itemList = new render\Thing\Intangible\ItemList\ItemList(); |
| | |
| | | $this->slug, |
| | | function() { |
| | | $action = BASE.ucfirst($this->slug).'Archive'; |
| | | $config = get_option($action, apply_filters($action, $this->defaultArchive)); |
| | | $config = JVB()->schemaHelper()->archive($this->slug); |
| | | if (!array_key_exists('type', $config)) { |
| | | $config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage'; |
| | | } |
| | |
| | | $obj = get_queried_object(); |
| | | $meta = (property_exists($obj, 'taxonomy')) ? Meta::forTerm($obj->term_id) : null; |
| | | |
| | | $class = $this->classFromConfig($config, $meta); |
| | | $class = JVB()->schemaHelper()::classFromConfig($config, $meta); |
| | | |
| | | $class->setIsPartOf(get_home_url().'/#website'); |
| | | $itemList = new render\Thing\Intangible\ItemList\ItemList(); |
| | |
| | | $meta = null; |
| | | } |
| | | $config = $this->getConfig('archive'); |
| | | $class = $this->classFromConfig($config, $meta); |
| | | $class = JVB()->schemaHelper()::classFromConfig($config, $meta); |
| | | $class->delete('about'); |
| | | |
| | | switch ($type) { |
| | |
| | | error_log('[SEO]Schema::getConfig Invalid type: '.$type); |
| | | return []; |
| | | } |
| | | $action = BASE.ucfirst($this->slug).ucfirst($type); |
| | | $default = 'default'.ucfirst($type); |
| | | return get_option($action, apply_filters($action, $this->$default)); |
| | | return JVB()->schemaHelper()::getConfig($this->slug, $type); |
| | | } |
| | | |
| | | public function define(string $property, string $value):void |
| | |
| | | $config = $this->getConfig('meta'); |
| | | $meta = Meta::forPost(get_the_ID()); |
| | | $title = Resolver::resolve($config['name'], $meta); |
| | | } elseif (is_post_type_archive($based) || is_tax($based)) { |
| | | } elseif (is_post_type_archive($based) ) { |
| | | $config = $this->getConfig('archive'); |
| | | $title = $config['name']; |
| | | } elseif (is_tax($based)) { |
| | | $config = $this->getConfig('archive'); |
| | | $meta = Meta::forTerm(get_queried_object_id()); |
| | | $title = Resolver::resolve($config['name'], $meta); |
| | | } |
| | | return $title; |
| | | } |
| | | |
| | | protected function classFromConfig(array $config, ?Meta $meta = null):mixed |
| | | { |
| | | if (!array_key_exists('type', $config)) { |
| | | error_log('[Schema]::classFromConfig No class defined in config: '.print_r($config, true)); |
| | | return false; |
| | | } |
| | | $className = $config['type']; |
| | | unset($config['type']); |
| | | $class = new $className(); |
| | | |
| | | foreach ($config as $property=>$value) { |
| | | if (is_array ($value)) { |
| | | $value = $this->classFromConfig($value, $meta); |
| | | } |
| | | $method = 'set'.ucfirst($property); |
| | | if (!method_exists($class, $method)) { |
| | | error_log('[Schema]::classFromConfig - method: '.$method.' does not exist in class: '.$className); |
| | | continue; |
| | | } |
| | | if (is_string($value) && str_contains($value, '{{')) { |
| | | $value = Resolver::resolveForSchema($property, $value, $config, $meta); |
| | | } |
| | | if (!empty($value)) { |
| | | $class->$method($value); |
| | | } |
| | | } |
| | | return $class; |
| | | } |
| | | } |
| | |
| | | public function getConfig():array{ |
| | | $config = get_object_vars($this); |
| | | |
| | | $config = array_map(function ($item) { |
| | | return array_map(function ($item) { |
| | | if (is_a($item, Field::class)) { |
| | | return $item->getConfig(); |
| | | } else if (is_array($item)) { |
| | | $temp = []; |
| | | foreach ($item as $v) { |
| | | foreach ($item as $k => $v) { |
| | | if (is_a($v, Field::class)) { |
| | | $temp[] = $v->getConfig(); |
| | | $temp[$k] = $v->getConfig(); |
| | | } else { |
| | | $temp[] = $v; |
| | | $temp[$k] = $v; |
| | | } |
| | | } |
| | | return $temp; |
| | |
| | | return $item; |
| | | } |
| | | }, $config); |
| | | |
| | | return $config; |
| | | } |
| | | } |
| | |
| | | namespace JVBase\rest; |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\utility\Features; |
| | | use WP_REST_Request; |
| | |
| | | // CACHE MANAGEMENT |
| | | // ========================================================================= |
| | | |
| | | protected function checkCache(string $key, $request):WP_REST_Response|false |
| | | { |
| | | // Check HTTP cache headers with the specific content type |
| | | $cache_check = $this->checkHeaders($request, $key); |
| | | if ($cache_check) { |
| | | return $cache_check; |
| | | } |
| | | |
| | | $cache = $this->cache->get($key); |
| | | if ($cache) { |
| | | $response = Response::success($cache); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | | return false; |
| | | } |
| | | /** |
| | | * Check request headers for conditional caching (ETag, If-Modified-Since) |
| | | */ |
| | |
| | | ***************************************************************************/ |
| | | protected function isTimeline($args, $data):bool |
| | | { |
| | | if (!array_key_exists('post_type', $args)) { |
| | | return false; |
| | | } |
| | | $post_types = is_array($args['post_type']) ? $args['post_type'] : [$args['post_type']]; |
| | | $hasTimeline = array_map(function($item) { return jvbCheckBase($item); },Registrar::getFeatured('is_timeline', 'post')); |
| | | return !empty(array_intersect($post_types, $hasTimeline)); |
| | |
| | | delete_user_meta($user_id, BASE . 'session_fingerprint'); |
| | | delete_user_meta($user_id, BASE . 'session_timestamp'); |
| | | } |
| | | |
| | | /******************************* |
| | | * META HELPERS |
| | | *******************************/ |
| | | public function getFieldsOfType(array $fields, string|array $type, Meta $meta, array $subType = []):array |
| | | { |
| | | $gotFields = []; |
| | | if (is_string($type)) { |
| | | $type = [$type]; |
| | | } |
| | | foreach ($fields as $field => $value) { |
| | | //Skip empty values |
| | | if (empty($value)) { |
| | | continue; |
| | | } |
| | | $config = $meta->config($field); |
| | | if (in_array($config['type'], ['group', 'repeater', 'tagList'])) { |
| | | foreach ($config['fields'] as $subfield => $subConfig) { |
| | | if (is_numeric($subfield) && array_key_exists('name', $subConfig)) { |
| | | $subfield = $subConfig['name']; |
| | | } |
| | | if (is_numeric($subfield)) continue; |
| | | if (array_key_exists('type', $subConfig) && in_array($subConfig['type'], $type)) { |
| | | $gotFields[] = $field.':'.$subfield; |
| | | } |
| | | } |
| | | } elseif (in_array($config['type'], $type)) { |
| | | $gotFields[] = $field; |
| | | } else if ((!empty($subType) && in_array($config['type'], array_keys($subType)) && in_array($config['subtype'], array_values($subType)))) { |
| | | $gotFields[] = $field; |
| | | } |
| | | } |
| | | return $gotFields; |
| | | } |
| | | |
| | | |
| | | protected function extractImages(array $fields, Meta $meta):array |
| | | { |
| | | $images = []; |
| | | $get = $this->getFieldsOfType($fields, ['upload', 'gallery','image'], $meta); |
| | | if (!empty($get)) { |
| | | $baseFields = array_map(function($fieldName) { |
| | | return (str_contains($fieldName, ':')) ? strtok($fieldName, ':') : $fieldName; |
| | | }, $get); |
| | | |
| | | $temp = array_map( |
| | | function($item) { |
| | | return explode(':', $item); |
| | | }, |
| | | array_filter($get, function($fieldName) { |
| | | return str_contains($fieldName, ':'); |
| | | }) |
| | | ); |
| | | $complex = []; |
| | | foreach ($temp as $tmp) { |
| | | $complex[$tmp[0]] = $tmp[1]; |
| | | } |
| | | |
| | | $fields = array_filter($fields, function ($field) use ($baseFields) { |
| | | return in_array($field, $baseFields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | foreach ($fields as $fieldName => $value) { |
| | | //Check if it's a complex field |
| | | if (array_key_exists($fieldName, $complex)) { |
| | | $check = $complex[$fieldName]; |
| | | foreach ($value as $row) { |
| | | foreach ($row as $fName => $fValue) { |
| | | if ($fName === $check && !empty($fValue)) { |
| | | $images = $this->addImages($fValue, $images); |
| | | } |
| | | } |
| | | } |
| | | } else { |
| | | $images = $this->addImages($value, $images); |
| | | } |
| | | } |
| | | } |
| | | return $images; |
| | | } |
| | | public function addImages(string $imgs, array $images):array |
| | | { |
| | | $temp = explode(',', $imgs); |
| | | foreach ($temp as $img) { |
| | | if (is_numeric($img) && !array_key_exists($img, $images) && $img > 0) { |
| | | $images[$img] = jvbImageData((int)$img); |
| | | } |
| | | } |
| | | return $images; |
| | | } |
| | | |
| | | protected function extractTerms(array $fields, Meta $meta):array |
| | | { |
| | | $terms = []; |
| | | $get = $this->getFieldsOfType($fields, ['taxonomy'], $meta, ['selector' => 'taxonomy']); |
| | | if (!empty($get)) { |
| | | $baseFields = array_map(function($fieldName) { |
| | | return (str_contains($fieldName, ':')) ? strtok($fieldName, ':') : $fieldName; |
| | | }, $get); |
| | | |
| | | $complex = array_map( |
| | | function($item) { |
| | | return explode(':', $item); |
| | | }, |
| | | array_filter($get, function($fieldName) { |
| | | return str_contains($fieldName, ':'); |
| | | }) |
| | | ); |
| | | |
| | | $fields = array_filter($fields, function ($field) use ($baseFields) { |
| | | return in_array($field, $baseFields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | foreach ($fields as $fieldName => $value) { |
| | | $config = $meta->config($fieldName); |
| | | //Check if it's a complex field |
| | | if (array_key_exists($fieldName, $complex)) { |
| | | foreach ($value as $row) { |
| | | foreach ($row as $fName => $fValue) { |
| | | if (in_array($fName, $complex[$fieldName])) { |
| | | $terms = $this->addTerms($fValue, $terms, $config); |
| | | } |
| | | } |
| | | } |
| | | } else { |
| | | |
| | | $terms = $this->addTerms($value, $terms, $config); |
| | | } |
| | | } |
| | | } |
| | | return $terms; |
| | | |
| | | } |
| | | |
| | | protected function addTerms(string $value, array $terms, array $config):array |
| | | { |
| | | $taxonomy = jvbNoBase($config['taxonomy']); |
| | | if (empty($value)) { |
| | | return $terms; |
| | | } |
| | | $ids = array_map('absint', explode(',',$value)); |
| | | $cache = Cache::for('term_data')->connect('taxonomy'); |
| | | $cache->flush(); |
| | | if (!array_key_exists($taxonomy, $terms)) { |
| | | $terms[$taxonomy] = []; |
| | | $registrar = Registrar::getInstance($taxonomy); |
| | | $terms[$taxonomy]['icon'] = $registrar ? $registrar->getIcon() : jvbDefaultIcon();; |
| | | } |
| | | foreach ($ids as $id) { |
| | | $data = $cache->remember( |
| | | $id, |
| | | function () use ($id, $taxonomy) { |
| | | $term = get_term($id, $taxonomy); |
| | | if ($term && !is_wp_error($term)) { |
| | | return [ |
| | | 'id' => $term->term_id, |
| | | 'name' => $term->name, |
| | | 'slug' => $term->slug, |
| | | 'parent' => $term->parent, |
| | | 'path' => JVB()->routes('term')->getTermPath($term->term_id, $term->name, $taxonomy), |
| | | 'taxonomy' => jvbNoBase($term->taxonomy), |
| | | 'count' => $term->count, |
| | | ]; |
| | | } |
| | | return []; |
| | | } |
| | | ); |
| | | if (!empty($data)) { |
| | | $terms[$taxonomy][$id] = $data; |
| | | } |
| | | } |
| | | return $terms; |
| | | } |
| | | } |
| | |
| | | use WP_Query; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Term; |
| | | use WP_Term_Query; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | |
| | | $this->cache->flush(); |
| | | } |
| | | $this->cache->connect('post', true); |
| | | $this->cache->connect('term', true); |
| | | add_action('init', [$this, 'registerContentExecutors'], 5); |
| | | } |
| | | |
| | |
| | | unset($data['id']); |
| | | |
| | | error_log('[CONTENT]:'.print_r($data, true)); |
| | | |
| | | $queue = JVB()->queue(); |
| | | $queue->queueOperation( |
| | | 'content_update', |
| | | $user_id, |
| | | $data, |
| | | [ |
| | | 'count' => $count, |
| | | 'chunk_key' => 'posts', |
| | | 'chunk_size' => 10, |
| | | 'operation_id' => $operationId |
| | | ] |
| | | ); |
| | |
| | | public function getContent(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $params = $request->get_params(); |
| | | error_log('getContent params: '.print_r($params, true)); |
| | | error_log('getContent::params '.print_r($params, true)); |
| | | |
| | | $registrar = Registrar::getInstance($params['content']); |
| | | switch ($registrar->getType()) { |
| | | case 'term': |
| | | return $this->getTerms($request, $params, $registrar); |
| | | case 'user': |
| | | //TODO maybe do something? |
| | | break; |
| | | case 'post': |
| | | return $this->getPosts($request, $params, $registrar); |
| | | } |
| | | |
| | | return $this->error('Something went wrong, this does not appear to have a proper content type'); |
| | | } |
| | | |
| | | public function getPosts(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response |
| | | { |
| | | $user_id = $params['user']; |
| | | |
| | | $post_status = $params['status']; |
| | |
| | | } |
| | | $post_type = str_replace('-', '_', jvbCheckBase($params['content'])); |
| | | |
| | | |
| | | // Build query args |
| | | $args = [ |
| | | 'post_type' => $post_type, |
| | |
| | | 'author' => $user_id, |
| | | 'post_status' => $post_status |
| | | ]; |
| | | //Only top level posts for timeline types |
| | | $registrar = Registrar::getInstance($post_type); |
| | | |
| | | if ($registrar && $registrar->hasFeature('is_timeline')) { |
| | | |
| | | //Only top level posts for timeline types |
| | | if ($registrar?->hasFeature('is_timeline')) { |
| | | $args['post_parent'] = 0; |
| | | } |
| | | |
| | | //Calendar filters |
| | | if ($registrar && $registrar->hasFeature('is_calendar')) { |
| | | if ($registrar?->hasFeature('is_calendar')) { |
| | | $args = $this->applyCalendarFilters($args, $params); |
| | | } |
| | | $taxonomies = array_filter($params, function ($param) { |
| | |
| | | $args['s'] = sanitize_text_field($params['search']); |
| | | } |
| | | |
| | | $key = $this->cache->generateKey($args); |
| | | $cached = $this->checkCache($key, $request); |
| | | if ($cached) { |
| | | return $cached; |
| | | } |
| | | |
| | | $this->post_type = jvbCheckBase($params['content'] ?? $params['type']); |
| | | |
| | | if (array_key_exists('s', $args)) { |
| | | $args = $this->applySearchFilters($args, $params); |
| | | } |
| | | |
| | | // Run query |
| | | $query = new WP_Query($args); |
| | | |
| | | $registrar = Registrar::getInstance($this->post_type); |
| | | $this->fields = $registrar->getFields()??[]; |
| | | $this->taxonomies = $this->getTaxonomies($this->post_type); |
| | | |
| | | $posts = array_map([$this, 'preparePost'], $query->posts); |
| | | |
| | | $data = [ |
| | | 'items' => $posts, |
| | | 'total' => $query->found_posts, |
| | | 'total_pages' => $query->max_num_pages, |
| | | 'has_more' => $args['paged']??1 < $query->max_num_pages, |
| | | ]; |
| | | |
| | | |
| | | $this->cache->set($key, $data); |
| | | |
| | | $response = Response::success($data); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | | public function getTerms(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response |
| | | { |
| | | // Build query args |
| | | $args = [ |
| | | 'taxonomy' => jvbCheckBase($params['content']), |
| | | 'number' => $params['per_page'] ?? 30, |
| | | 'orderby' => 'name', |
| | | 'order' => 'DESC', |
| | | 'hide_empty' => false, |
| | | ]; |
| | | $paged = $params['page']??1; |
| | | $args['page'] = $paged; |
| | | if ($paged > 1) { |
| | | $args['offset'] = ($paged-1) * $args['number']; |
| | | } |
| | | |
| | | //TODO |
| | | // if (array_key_exists('taxonomies', $params)) { |
| | | // $args = $this->applyTaxonomyFilters($args, $params); |
| | | // } |
| | | // if (array_key_exists('date-filter', $params) || array_key_exists('dateFrom', $params)) { |
| | | // $args = $this->applyDateFilters($args, $params); |
| | | // } |
| | | if (array_key_exists('orderby', $params) || array_key_exists('order', $params)) { |
| | | $args = $this->applyOrderFilters($args, $params); |
| | | } |
| | | |
| | | if (array_key_exists('search', $params)) { |
| | | $args['s'] = sanitize_text_field($params['search']); |
| | | } |
| | | |
| | | $key = $this->cache->generateKey($args); |
| | | // Check HTTP cache headers with the specific content type |
| | |
| | | $response = Response::success($cache); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | | |
| | | $this->post_type = jvbCheckBase($params['content'] ?? $params['type']); |
| | | // Only expand search to taxonomies if we're actually going to query |
| | | if (array_key_exists('s', $args)) { |
| | |
| | | } |
| | | |
| | | // Run query |
| | | $query = new WP_Query($args); |
| | | $query = new WP_Term_Query($args); |
| | | |
| | | $registrar = Registrar::getInstance(str_replace('-', '_', $this->post_type)); |
| | | $this->fields = $registrar->getFields()??[]; |
| | | |
| | | $this->taxonomies = $this->getTaxonomies($this->post_type); |
| | | $posts = array_map([$this, 'prepareItem'], $query->posts); |
| | | |
| | | $terms = $query->get_terms(); |
| | | $data = [ |
| | | 'items' => $posts, |
| | | 'total' => $query->found_posts, |
| | | 'total_pages' => $query->max_num_pages, |
| | | 'has_more' => $args['paged']??1 < $query->max_num_pages, |
| | | 'total' => 0, |
| | | 'total_pages' => 0, |
| | | 'has_more' => false |
| | | ]; |
| | | |
| | | if (!is_wp_error($terms) && !empty($terms)) |
| | | { |
| | | $total = get_terms([ |
| | | 'taxonomy' => $args['taxonomy'], |
| | | 'hide_empty' => false, |
| | | 'fields' => 'count' |
| | | ]); |
| | | $data['total'] = $total; |
| | | $data['total_pages'] = max($total/$args['number'], 1); |
| | | $data['has_more'] = ($args['page'] * $args['number']) < $total; |
| | | } else { |
| | | $terms = []; |
| | | } |
| | | |
| | | $this->fields = $registrar->getFields()??[]; |
| | | |
| | | $this->taxonomies = []; |
| | | $data['items'] =array_map([$this, 'prepareTerm'], $terms); |
| | | |
| | | $this->cache->set($key, $data); |
| | | |
| | |
| | | protected function getTaxonomies(string $content): array |
| | | { |
| | | $registrar = Registrar::getInstance($content); |
| | | if (!$registrar) { |
| | | if (!$registrar || $registrar->getType()!== 'post') { |
| | | return []; |
| | | } |
| | | $out = []; |
| | |
| | | * |
| | | * @return array |
| | | */ |
| | | protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true): array |
| | | protected function preparePost(WP_Post $post, bool $skip = false, bool $fields = true): array |
| | | { |
| | | $registrar = Registrar::getInstance($post->post_type); |
| | | if (!$skip && $registrar && $registrar->hasFeature('is_timeline')) { |
| | |
| | | 'images' => [], |
| | | ]; |
| | | |
| | | // Add taxonomy terms |
| | | foreach ($this->taxonomies as $taxonomy => $options) { |
| | | $tax = str_replace(BASE, '', $taxonomy); |
| | | $terms = wp_get_object_terms( |
| | | $post->ID, |
| | | $taxonomy, |
| | | ['fields' => 'id=>name'] |
| | | ); |
| | | $data['taxonomies'][$tax] = [ |
| | | 'terms' => (is_wp_error($terms)) ? [] : $terms, |
| | | 'name' => $options['label'], |
| | | 'icon' => $tax |
| | | ]; |
| | | } |
| | | |
| | | $images = $this->extractImages(); |
| | | |
| | | |
| | | $images = $this->extractImages($fields, $this->meta); |
| | | if (!empty($images)) { |
| | | $data['images'] = $images; |
| | | } |
| | | |
| | | |
| | | $taxonomies = $this->extractTerms($fields, $this->meta); |
| | | if (!empty($taxonomies)) { |
| | | $data['taxonomies'] = $taxonomies; |
| | | } |
| | | return $data; |
| | | } |
| | | |
| | | protected function extractImages(array $fields = []): array |
| | | /** |
| | | * @param WP_Term $post the post object |
| | | * |
| | | * @return array |
| | | */ |
| | | protected function prepareTerm(WP_Term $post, bool $fields = true): array |
| | | { |
| | | //Extract images |
| | | $images = []; |
| | | $get = []; |
| | | $fields = (empty($fields)) ? $this->fields : $fields; |
| | | $get = $this->getUploadFields($fields); |
| | | error_log('Upload fields: '.print_r($get, true)); |
| | | $registrar = Registrar::getInstance($post->taxonomy); |
| | | |
| | | if (!empty($get)) { |
| | | $actualGet = array_map(function($fieldName) { |
| | | return (str_contains($fieldName, ':')) ? strtok($fieldName, ':') : $fieldName; |
| | | }, $get); |
| | | $complex = array_map(function($item) { |
| | | return explode(':', $item); |
| | | },array_filter($get, function($fieldName) { |
| | | return str_contains($fieldName, ':'); |
| | | })); |
| | | $this->meta = Meta::forTerm($post->term_id); |
| | | $fields = ($fields) ? $this->meta->getAll() : []; |
| | | $data = [ |
| | | 'id' => $post->term_id, |
| | | 'title' => $post->name, |
| | | 'date' => $fields['created_date']??'', |
| | | 'modified' => $fields['modified_date']??'', |
| | | 'thumbnail' => '', |
| | | 'icon' => $registrar->getIcon(), |
| | | 'taxonomies' => [], |
| | | 'fields' => $fields, |
| | | 'images' => [], |
| | | ]; |
| | | |
| | | $allImages = $this->meta->getAll($actualGet); |
| | | foreach ($allImages as $k => $imgs) { |
| | | //It's a complex field |
| | | if (is_array($imgs)) { |
| | | foreach ($imgs as $row) { |
| | | foreach ($row as $fName => $fValue) { |
| | | foreach ($complex as $c) { |
| | | if (in_array($k, $c)) { |
| | | foreach ($c as $complexField) { |
| | | if ($complexField === $fName) { |
| | | $images = $this->addImages($fValue, $images); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } else { |
| | | $images = $this->addImages($imgs, $images); |
| | | } |
| | | } |
| | | } |
| | | return $images; |
| | | } |
| | | public function addImages(string $imgs, array $images):array |
| | | { |
| | | $temp = explode(',', $imgs); |
| | | foreach ($temp as $img) { |
| | | if (is_numeric($img) && !array_key_exists($img, $images)) { |
| | | $images[$img] = jvbImageData((int)$img); |
| | | } |
| | | } |
| | | return $images; |
| | | } |
| | | public function getUploadFields($fields):array |
| | | { |
| | | $get = []; |
| | | foreach ($fields as $field => $config) { |
| | | if (in_array($config['type'], ['group', 'repeater'])) { |
| | | $nestedUploads = $this->getUploadFields($config['fields']); |
| | | foreach ($nestedUploads as $nested) { |
| | | $get[] = $field.':'.$nested; |
| | | } |
| | | } elseif ($config['type'] === 'upload' || $config['type'] === 'gallery' || $config['type'] === 'image' ) { |
| | | $get[] = $field; |
| | | } |
| | | $images = $this->extractImages($fields, $this->meta); |
| | | if (!empty($images)) { |
| | | $data['images'] = $images; |
| | | } |
| | | |
| | | return $get; |
| | | $taxonomies = $this->extractTerms($fields, $this->meta); |
| | | if (!empty($taxonomies)) { |
| | | $data['taxonomies'] = $taxonomies; |
| | | } |
| | | |
| | | error_log('Term data: '.print_r($data, true)); |
| | | return $data; |
| | | } |
| | | |
| | | |
| | | public function formatTimeline(WP_Post $post): array |
| | | { |
| | | $item = $this->prepareItem($post, true, false); |
| | | $item = $this->preparePost($post, true, false); |
| | | //Step 1: Get the fields that apply to all posts |
| | | $mainMeta = Meta::forPost($post->ID); |
| | | $item['fields'] = $mainMeta->getAll($this->timelineSharedFields); |
| | |
| | | $m = $meta->set($key, $value); |
| | | $result[$key] = $m; |
| | | } |
| | | $meta->save(); |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | |
| | | } |
| | | $this->cache->flush(); |
| | | } |
| | | $tempMeta->save(); |
| | | $meta->save(); |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => $results, |
| | |
| | | } |
| | | |
| | | $meta->setAll($setData); |
| | | $results = $meta->save(); |
| | | |
| | | // $results = []; |
| | | // |
| | |
| | | |
| | | |
| | | $userMeta->set($uMeta, $shops); |
| | | $userMeta->save(); |
| | | |
| | | $artistMeta->set($aMeta, $shops); |
| | | $artistMeta->save(); |
| | | |
| | | $owners = $shopMeta->get($sMeta); |
| | | $owners = ($owners === '') ? [] : explode(',', $shops); |
| | |
| | | } |
| | | $owners = implode(',', $owners); |
| | | $shopMeta->set($sMeta, $owners); |
| | | $shopMeta->save(); |
| | | return true; |
| | | } |
| | | } |
| | |
| | | { |
| | | $data = $request->get_params(); |
| | | $args = []; |
| | | $registrar = Registrar::getInstance($data['content']??''); |
| | | |
| | | foreach ($data as $key => $value) { |
| | | switch ($key) { |
| | | case 'depends_on': |
| | |
| | | case 'item_id': |
| | | if (is_numeric($value)) { |
| | | $args['item_id'] = absint($value); |
| | | if (!array_key_exists('post_id', $args)) { |
| | | $args['post_id'] = absint($value); |
| | | if ($registrar) { |
| | | switch ($registrar->getType()) { |
| | | case 'post': |
| | | $args['post_id'] = absint($value); |
| | | break; |
| | | case 'term': |
| | | $args['term_id'] = absint($value); |
| | | break; |
| | | case 'user': |
| | | $args['user_id'] = absint($value); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | break; |
| | | // Post Type/Taxonomy |
| | | case 'content': |
| | | $key = str_replace('-', '_', $key); |
| | | if ($value === 'options' || array_key_exists($value, Registrar::getRegistered('post')) || Registrar::getInstance($key)->hasFeature('is_content')??false) { |
| | | $value = str_replace('-', '_', $value); |
| | | if ($value === 'options' || $registrar) { |
| | | $args['content'] = $value; |
| | | } |
| | | break; |
| | |
| | | |
| | | // Update with comma-separated string |
| | | $meta->set($data['field_name'], implode(',', $all_ids)); |
| | | $meta->save(); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function addTaxonomyFilter(array $taxonomies, ?string $limit = null): self { |
| | | foreach($taxonomies as $taxonomy) { |
| | | error_log('Fetchinig taxonomy: '.print_r($taxonomy, true)); |
| | | $registrar = Registrar::getInstance($taxonomy); |
| | | |
| | | if ($registrar) { |
| | |
| | | <input type="hidden" name="content" value="<?=$this->dataType?>" /> |
| | | <div class="fields"> |
| | | <?php |
| | | echo Form::render('post_status', '', $this->getStatusFieldConfig('edit-')); |
| | | if (!empty($this->statuses)) { |
| | | echo Form::render('post_status', '', $this->getStatusFieldConfig('edit-')); |
| | | } |
| | | |
| | | |
| | | |
| | | if (!empty($this->sections)) { |
| | |
| | | |
| | | if ($addLink) { |
| | | if (!$postSlug) { |
| | | global $post; |
| | | $postSlug = $post->post_name; |
| | | if (is_singular()) { |
| | | global $post; |
| | | $postSlug = $post->post_name; |
| | | }else if (is_tax()) { |
| | | $tax = get_queried_object(); |
| | | $postSlug = jvbNoBase($tax->taxonomy); |
| | | }elseif (is_post_type_archive()) { |
| | | $obj = get_queried_object(); |
| | | $postSlug = jvbNoBase($obj->post_type); |
| | | } |
| | | |
| | | } |
| | | $full = wp_get_attachment_image_src($ID, 'full'); |
| | | |
| | |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\managers\IconsManager; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | use JVBase\utility\Features; |
| | |
| | | }, 10, 3); |
| | | |
| | | |
| | | const JVB_LOCAL = 'northeh.test'; |
| | | |
| | | function jvbIgnoredPostTypes():array |
| | | { |
| | | return [BASE.'directory', BASE.'dash', 'attachment', 'revision', 'nav_menu_item']; |
| | |
| | | wp_enqueue_script('jvb-page-nav'); |
| | | } |
| | | |
| | | if (has_block('jvb/summaryBlock')) { |
| | | wp_enqueue_script('jvb-page-nav'); |
| | | } |
| | | |
| | | // Only load on single shop pages or other relevant pages |
| | | if (is_tax(BASE.'shop') || |
| | | is_singular(BASE.'partner')) { |
| | |
| | | //add_action('wp_head', 'jvbDumpIt'); |
| | | function jvbDumpIt() |
| | | { |
| | | |
| | | } |
| | | |
| | | add_action('after_setup_theme', 'jvbImageSize'); |
| | |
| | | $meta = Meta::forPost($current->ID); |
| | | $artist = jvbContentFromUser((int)$current->post_author); |
| | | |
| | | $registrar = Registrar::getInstance($current->post_type)); |
| | | $registrar = Registrar::getInstance($current->post_type); |
| | | $sections = []; |
| | | if ($registrar) { |
| | | $sections = $registrar->getSections(); |